前言
? ? ? ?兩個(gè)進(jìn)程如果要進(jìn)行通訊最基本的一個(gè)前提就是能夠唯一的標(biāo)識(shí)一個(gè)進(jìn)程掸屡,在本地進(jìn)程通訊中我們可以使用 PID 來(lái)唯一標(biāo)識(shí)一個(gè)進(jìn)程,但 PID 只在本地是唯一的规哲,網(wǎng)絡(luò)中兩個(gè)進(jìn)程 PID 沖突幾率很大,這時(shí)我們就需要通過(guò)其他手段來(lái)唯一標(biāo)識(shí)網(wǎng)絡(luò)中的進(jìn)程了,我們知道 IP 層的 ip 地址可以唯一標(biāo)示主機(jī)谣蠢,而 TCP 層協(xié)議和端口號(hào)結(jié)合就可以唯一標(biāo)示主機(jī)的一個(gè)進(jìn)程了。
能夠唯一標(biāo)示網(wǎng)絡(luò)中的進(jìn)程后,它們就可以利用 Socket 進(jìn)行通信了眉踱,什么是 Socket 呢挤忙?我們經(jīng)常把 Socket 翻譯為套接字(為什么翻譯成套接字),Socket 是在應(yīng)用層和傳輸層之間的一個(gè)抽象層谈喳,它把 TCP/IP 層復(fù)雜的操作抽象為幾個(gè)簡(jiǎn)單的接口供應(yīng)用層調(diào)用册烈,從而實(shí)現(xiàn)進(jìn)程在網(wǎng)絡(luò)中通信。
相關(guān)類
? ? ? ?這里提到的 Socket 為廣義上的 Socket 編程婿禽,它可以基于 TCP 或者 UDP 實(shí)現(xiàn)赏僧,Java為 Socket 編程封裝了幾個(gè)重要的類,如下:
Socket (TCP)
? ? ? Socket 類實(shí)現(xiàn)了一個(gè)客戶端 Socket扭倾,作為兩臺(tái)機(jī)器通信的終端淀零,默認(rèn)采用的傳輸層協(xié)議為 TCP 可靠傳輸協(xié)議。Socket 類除了構(gòu)造函數(shù)返回一個(gè) socket 外膛壹,還提供了 connect , getOutputStream, getInputStream 和 close 方法驾中。connect 方法用于請(qǐng)求一個(gè) socket 連接,getOutputStream 用于獲得寫 socket的輸出流模聋,getInputStream 用于獲得讀 socket 的輸入流哀卫,close 方法用于關(guān)閉一個(gè)流。
DatagramSocket (UDP)
? ? ? ?DatagramSocket 類實(shí)現(xiàn)了一個(gè)發(fā)送和接收數(shù)據(jù)報(bào)的 socket撬槽,傳輸層協(xié)議使用 UDP此改,不能保證數(shù)據(jù)報(bào)的可靠傳輸。DataGramSocket 主要有 send, receive 和 close 三個(gè)方法侄柔。send 用于發(fā)送一個(gè)數(shù)據(jù)報(bào)共啃,Java 提供了 DatagramPacket 對(duì)象用來(lái)表達(dá)一個(gè)數(shù)據(jù)報(bào)。receive 用于接收一個(gè)數(shù)據(jù)報(bào)暂题,調(diào)用該方法后移剪,一直阻塞接收到直到數(shù)據(jù)報(bào)或者超時(shí)。close 是關(guān)閉一個(gè) socket薪者。
ServerSocket
? ? ? ?ServerSocket 類實(shí)現(xiàn)了一個(gè)服務(wù)器 socket纵苛,一個(gè)服務(wù)器 socke t等待客戶端網(wǎng)絡(luò)請(qǐng)求,然后基于這些請(qǐng)求執(zhí)行操作言津,并返回給請(qǐng)求者一個(gè)結(jié)果攻人。ServerSocket 提供了 bind、accept 和 close 三個(gè)方法悬槽。bind 方法為ServerSocket 綁定一個(gè)IP地址和端口怀吻,并開(kāi)始監(jiān)聽(tīng)該端口。accept 方法為 ServerSocket 接受請(qǐng)求并返回一個(gè) Socket 對(duì)象初婆,accept 方法調(diào)用后蓬坡,將一直阻塞直到有請(qǐng)求到達(dá)猿棉。close 方法關(guān)閉一個(gè) ServerSocket 對(duì)象。
SocketAddress
? ? ? ?SocketAddress 提供了一個(gè) socket 地址屑咳,不關(guān)心傳輸層協(xié)議萨赁。這是一個(gè)虛類,由子類來(lái)具體實(shí)現(xiàn)功能兆龙、綁定傳輸協(xié)議杖爽。它提供了一個(gè)不可變的對(duì)象,被 socket 用來(lái)綁定详瑞、連接或者返回?cái)?shù)值掂林。
InetSocketAddress
? ? ? ?InetSocketAddress 實(shí)現(xiàn)了IP地址的 SocketAddress,也就是有 IP 地址和端口號(hào)表達(dá) Socket 地址坝橡。如果不制定具體的 IP 地址和端口號(hào)泻帮,那么 IP 地址默認(rèn)為本機(jī)地址,端口號(hào)隨機(jī)選擇一個(gè)计寇。
DatagramPacket(UDP)
? ? ? ? DatagramSocket 是面向數(shù)據(jù)報(bào) socket 通信的一個(gè)可選通道锣杂。數(shù)據(jù)報(bào)通道不是對(duì)網(wǎng)絡(luò)數(shù)據(jù)報(bào) socket 通信的完全抽象。socket通信的控制由DatagramSocket 對(duì)象實(shí)現(xiàn)番宁。DatagramPacket 需要與 DatagramSocket 配合使用才能完成基于數(shù)據(jù)報(bào)的 socket 通信元莫。
基于TCP的 Socket
? ? ? ?基于 TCP 的 Socket可以實(shí)現(xiàn)客戶端—服務(wù)器間的雙向?qū)崟r(shí)通信。上面提到的 java.NET包中定義的兩個(gè)類 Socket 和 ServerSocket蝶押,分別用來(lái)實(shí)現(xiàn)雙向連接的 client 和 server 端踱蠢。
實(shí)現(xiàn)
客戶端連接:demo
客戶端發(fā)送:消息給服務(wù)端
服務(wù)端代碼:
'''
public class SocketTest {
? ? ? ? ? private static final int PORT =9999;
? ? ? ? ? private List mList =newArrayList();
? ? ? ? ? private ServerSocket server =null;
? ? ? ? ? private ExecutorService mExecutorService =null;
? ? ? ? ? private String receiveMsg;
? ? ? ? ? private String sendMsg;
? ? ? ? ? public static void main(String[] args) {
? ? ? ? ? ? ? ? ? ? ? newSocketTest();
? ? ? ? ? }
? ? ? ? ?public Socket Test() {
? ? ? ? ? ? ? ? ? ?try{
? ? ? ? ? ? ? ? ? ? ? ? ?server =newServerSocket(PORT);
? ? ? ? ? ? ? ? ? ? ? ? ?mExecutorService = Executors.newCachedThreadPool();
? ? ? ? ? ? ? ? ? ? ? ? ?System.out.println("服務(wù)器已啟動(dòng)...");
? ? ? ? ? ? ? ? ? ? ? ? ?Socket client =null;
? ? ? ? ? ? ? ? ? ? ? ? ?while(true) {
? ? ? ? ? ? ? ? ? ? ? ? ?client = server.accept();
? ? ? ? ? ? ? ? ? ? ? ? ?mList.add(client);
? ? ? ? ? ? ? ? ? ? ? ? ?mExecutorService.execute(new Service(client));
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ?}catch(Exception e) {
? ? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? ? }
? ? ? }
class Service implements Runnable {
private Socket socket;
private BufferedReader in=null;
private PrintWriter printWriter=null;
public Service(Socket socket) {
this.socket = socket;try{
printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter( socket.getOutputStream(),"UTF-8")),true);
in=new BufferedReader(new InputStreamReader(
socket.getInputStream(),"UTF-8"));
printWriter.println("成功連接服務(wù)器"+"(服務(wù)器發(fā)送)");
System.out.println("成功連接服務(wù)器");
}catch(IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try{
while(true) {
if((receiveMsg =in.readLine())!=null) {
System.out.println("receiveMsg:"+receiveMsg);
if(receiveMsg.equals("0")) {
System.out.println("客戶端請(qǐng)求斷開(kāi)連接");
printWriter.println("服務(wù)端斷開(kāi)連接"+"(服務(wù)器發(fā)送)");
mList.remove(socket);
in.close();
socket.close();
break;
}else{
sendMsg ="我已接收:"+ receiveMsg +"(服務(wù)器發(fā)送)";
printWriter.println(sendMsg);
}
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
'''
服務(wù)端使用線程池實(shí)現(xiàn)多客戶端連接,server.accept() 表示等待客戶端連接棋电,當(dāng)有客戶端連接時(shí)新建一個(gè)線程去處理茎截,其中涉及到的方法之前都提到過(guò),不再贅述赶盔。
客戶端代碼:
'''
public class SocketActivity extends AppCompatActivity{
private EditText mEditText;
private TextView mTextView;
private static final String TAG ="TAG";
private static final String HOST ="192.168.23.1";
private static final int PORT =9999;
private PrintWriter printWriter;
private BufferedReader in;
private ExecutorService mExecutorService =null;
private String receiveMsg;
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mEditText = (EditText) findViewById(R.id.editText);
mTextView = (TextView) findViewById(R.id.textView);
mExecutorService = Executors.newCachedThreadPool();
}
public void connect(View view) {
mExecutorService.execute(newconnectService());
}
public void send(View view) {
String sendMsg = mEditText.getText().toString();
mExecutorService.execute(newsendService(sendMsg));
}
public void disconnect(View view) {
mExecutorService.execute(newsendService("0"));
}
private class sendService implements Runnable{
privateString msg;
sendService(String msg) {
this.msg = msg;
}
@Override
public void run() {
printWriter.println(this.msg);
}
}
private class connectService implements Runnable{
@Override
public void run() {try{
Socket socket =newSocket(HOST, PORT);
socket.setSoTimeout(60000);
printWriter =newPrintWriter(new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(),"UTF-8")),true);
in =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
receiveMsg();
}catch(Exception e) {
Log.e(TAG, ("connectService:"+ e.getMessage()));
}
}
}
private void receiveMsg() {
try{
while(true) {
if((receiveMsg = in.readLine()) !=null) {
Log.d(TAG,"receiveMsg:"+ receiveMsg);
runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(receiveMsg +"\n\n"+ mTextView.getText());
}
});
}
}
}catch(IOException e) {
Log.e(TAG,"receiveMsg: ");
e.printStackTrace();
}
}
}
'''
客戶端同樣使用了線程池進(jìn)行管理企锌,把連接和發(fā)送分割為兩個(gè) Runnable 易于調(diào)用,當(dāng)發(fā)送 “0” 且服務(wù)端收到時(shí)關(guān)閉連接于未。
okio 實(shí)現(xiàn)
到這里一個(gè)簡(jiǎn)單的 Socket 通信就完成了撕攒,其中對(duì)于 Socket 的信息流使用的是 java.io,之前學(xué)習(xí) okio 時(shí)烘浦,了解到 okio 可以替代 java.io抖坪,okio是一個(gè)由square公司開(kāi)發(fā)的開(kāi)源庫(kù),它彌補(bǔ)了Java.io和java.nio的不足谎倔,能夠更方便快速的讀取柳击、存儲(chǔ)和處理數(shù)據(jù)(了解更多請(qǐng)點(diǎn)擊Okio源碼分析),下面就嘗試用 okio 替換 java.io片习。
直接上代碼:
服務(wù)端代碼:
'''
public class SocketTest {
private static final int PORT =9999;
private List mList =newArrayList();
private ServerSocket server =null;
private ExecutorService mExecutorService =null;
private String receiveMsg;
private String sendMsg;
public static void main(String[] args) {
newSocketTest();
}
public SocketTest() {
try{
server =new ServerSocket(PORT);
mExecutorService = Executors.newCachedThreadPool();
System.out.println("服務(wù)器已啟動(dòng)...");
Socket client =null;
while(true) {
client = server.accept();
mList.add(client);
mExecutorService.execute(new Service(client));
}
}catch(Exception e) {
e.printStackTrace();
}
}
class Service implements Runnable {
private Socket socket;
private BufferedSink mSink;
private BufferedSource mSource;
public Service(Socket socket) {
this.socket = socket;
try{
mSink = Okio.buffer(Okio.sink(socket));
mSource = Okio.buffer(Okio.source(socket));
sendMsg="成功連接服務(wù)器"+"(服務(wù)器發(fā)送)";
mSink.writeUtf8(sendMsg+"\n");
mSink.flush();
System.out.println("成功連接服務(wù)器");
}catch(IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try{
while(true) {
for(String receiveMsg; (receiveMsg = mSource
.readUtf8Line()) !=null;) {
System.out.println("receiveMsg:"+ receiveMsg);
if(receiveMsg.equals("0")) {
System.out.println("客戶端請(qǐng)求斷開(kāi)連接");
mSink.writeUtf8("服務(wù)端斷開(kāi)連接"+"(服務(wù)器發(fā)送)");
mSink.flush();
mList.remove(socket);
socket.close();
break;
}else{
sendMsg ="我已接收:"+ receiveMsg +"(服務(wù)器發(fā)送)";
mSink.writeUtf8(sendMsg+"\n");
mSink.flush();
}
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
'''
客戶端代碼:
'''
public class SocketActivity extends AppCompatActivity{
private EditText mEditText;
private TextView mTextView;
private static final String TAG ="TAG";
private static final String HOST ="192.168.23.1";
private static final int PORT =9999;
private BufferedSink mSink;
private BufferedSource mSource;
private ExecutorService mExecutorService =null;
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mEditText = (EditText) findViewById(R.id.editText);
mTextView = (TextView) findViewById(R.id.textView);
mExecutorService = Executors.newCachedThreadPool();
}publicvoidconnect(View view) {
mExecutorService.execute(new connectService());
}
public void send(View view) {
String sendMsg = mEditText.getText().toString();
mExecutorService.execute(new sendService(sendMsg));
}
public void disconnect(View view) {
mExecutorService.execute(new sendService("0"));
}
private class sendService implements Runnable{
private String msg;
sendService(String msg) {
this.msg = msg;
}
@Override
public void run() {
try{
mSink.writeUtf8(this.msg+"\n");
mSink.flush();
}catch(IOException e) {
e.printStackTrace();
}
}
}
private class connectService implements Runnable{
@Override
public void run() {
try{
Socket socket =newSocket(HOST, PORT);
mSink = Okio.buffer(Okio.sink(socket));
mSource = Okio.buffer(Okio.source(socket));
receiveMsg();
}catch(Exception e) {
Log.e(TAG, ("connectService:"+ e.getMessage()));
}
}
}
private void receiveMsg() {
try{
while(true) {
for(String receiveMsg; (receiveMsg = mSource.readUtf8Line()) !=null; ) {
Log.d(TAG,"receiveMsg:"+ receiveMsg);finalString finalReceiveMsg = receiveMsg;
runOnUiThread(new Runnable() {
@Override
public void run() {
mTextView.setText(finalReceiveMsg +"\n\n"+ mTextView.getText());
}
});
}
}
}catch(IOException e) {
Log.e(TAG,"receiveMsg: ");
e.printStackTrace();
}
}
}
'''
這里有一個(gè)很坑的地方:
mSink.writeUtf8(this.msg+"\n");
mSink.flush();
起初沒(méi)有加 “\n” 時(shí)捌肴,調(diào)用 flush 方法后消息是無(wú)法發(fā)送成功的,除非調(diào)用 sink.close 方法后才會(huì)發(fā)送成功藕咏,但是我們不能每發(fā)送一次就 close 掉状知,對(duì)比 printWriter.println 方法,嘗試加上一個(gè)換行符孽查,果真發(fā)送成功饥悴。
總結(jié)
android有兩種通信方式,一種是常用的基于 HTTP 協(xié)議方式盲再,另一種就是基于 TCP/UDP 協(xié)議的 Socket 方式西设。雖然大部分需求都可通過(guò) HTTP 實(shí)現(xiàn),實(shí)現(xiàn)起來(lái)也較為簡(jiǎn)單答朋,但某些情景下需要使用 Socket 方式贷揽,這時(shí)永遠(yuǎn)不要放棄去使用最佳的工具來(lái)解決問(wèn)題的機(jī)會(huì)。本文主要通過(guò) Socket 實(shí)現(xiàn)了 Android 基于 TCP 協(xié)議的通信梦碗,后面將 Socket 的輸入輸出流處理由 java.io 替換為 Okio 實(shí)現(xiàn)禽绪,雖然說(shuō) Okio 彌補(bǔ)了Java.io和 java.nio 的不足,能夠更方便快速的讀取洪规、存儲(chǔ)和處理數(shù)據(jù)印屁,但是實(shí)際性能并沒(méi)測(cè)試過(guò),這里主要是為了復(fù)習(xí)一下 Okio 的使用斩例,另外就是在Okio源碼分析中沒(méi)有涉及到 Socket 的內(nèi)容雄人,這里正好填補(bǔ)一下知識(shí)漏洞。