前言
? ? ? ?兩個進程如果要進行通訊最基本的一個前提就是能夠唯一的標識一個進程,在本地進程通訊中我們可以使用 PID 來唯一標識一個進程,但 PID 只在本地是唯一的,網絡中兩個進程 PID 沖突幾率很大,這時我們就需要通過其他手段來唯一標識網絡中的進程了,我們知道 IP 層的 ip 地址可以唯一標示主機,而 TCP 層協議和端口號結合就可以唯一標示主機的一個進程了。
能夠唯一標示網絡中的進程后,它們就可以利用 Socket 進行通信了,什么是 Socket 呢?我們經常把 Socket 翻譯為套接字(為什么翻譯成套接字),Socket 是在應用層和傳輸層之間的一個抽象層,它把 TCP/IP 層復雜的操作抽象為幾個簡單的接口供應用層調用,從而實現進程在網絡中通信。
相關類
? ? ? ?這里提到的 Socket 為廣義上的 Socket 編程,它可以基于 TCP 或者 UDP 實現,Java為 Socket 編程封裝了幾個重要的類,如下:
Socket (TCP)
? ? ? Socket 類實現了一個客戶端 Socket,作為兩臺機器通信的終端,默認采用的傳輸層協議為 TCP 可靠傳輸協議。Socket 類除了構造函數返回一個 socket 外,還提供了 connect , getOutputStream, getInputStream 和 close 方法。connect 方法用于請求一個 socket 連接,getOutputStream 用于獲得寫 socket的輸出流,getInputStream 用于獲得讀 socket 的輸入流,close 方法用于關閉一個流。
DatagramSocket (UDP)
? ? ? ?DatagramSocket 類實現了一個發送和接收數據報的 socket,傳輸層協議使用 UDP,不能保證數據報的可靠傳輸。DataGramSocket 主要有 send, receive 和 close 三個方法。send 用于發送一個數據報,Java 提供了 DatagramPacket 對象用來表達一個數據報。receive 用于接收一個數據報,調用該方法后,一直阻塞接收到直到數據報或者超時。close 是關閉一個 socket。
ServerSocket
? ? ? ?ServerSocket 類實現了一個服務器 socket,一個服務器 socke t等待客戶端網絡請求,然后基于這些請求執行操作,并返回給請求者一個結果。ServerSocket 提供了 bind、accept 和 close 三個方法。bind 方法為ServerSocket 綁定一個IP地址和端口,并開始監聽該端口。accept 方法為 ServerSocket 接受請求并返回一個 Socket 對象,accept 方法調用后,將一直阻塞直到有請求到達。close 方法關閉一個 ServerSocket 對象。
SocketAddress
? ? ? ?SocketAddress 提供了一個 socket 地址,不關心傳輸層協議。這是一個虛類,由子類來具體實現功能、綁定傳輸協議。它提供了一個不可變的對象,被 socket 用來綁定、連接或者返回數值。
InetSocketAddress
? ? ? ?InetSocketAddress 實現了IP地址的 SocketAddress,也就是有 IP 地址和端口號表達 Socket 地址。如果不制定具體的 IP 地址和端口號,那么 IP 地址默認為本機地址,端口號隨機選擇一個。
DatagramPacket(UDP)
? ? ? ? DatagramSocket 是面向數據報 socket 通信的一個可選通道。數據報通道不是對網絡數據報 socket 通信的完全抽象。socket通信的控制由DatagramSocket 對象實現。DatagramPacket 需要與 DatagramSocket 配合使用才能完成基于數據報的 socket 通信。
基于TCP的 Socket
? ? ? ?基于 TCP 的 Socket可以實現客戶端—服務器間的雙向實時通信。上面提到的 java.NET包中定義的兩個類 Socket 和 ServerSocket,分別用來實現雙向連接的 client 和 server 端。
實現
客戶端連接:demo
客戶端發送:消息給服務端
服務端代碼:
'''
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("服務器已啟動...");
? ? ? ? ? ? ? ? ? ? ? ? ?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("成功連接服務器"+"(服務器發送)");
System.out.println("成功連接服務器");
}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("客戶端請求斷開連接");
printWriter.println("服務端斷開連接"+"(服務器發送)");
mList.remove(socket);
in.close();
socket.close();
break;
}else{
sendMsg ="我已接收:"+ receiveMsg +"(服務器發送)";
printWriter.println(sendMsg);
}
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
'''
服務端使用線程池實現多客戶端連接,server.accept() 表示等待客戶端連接,當有客戶端連接時新建一個線程去處理,其中涉及到的方法之前都提到過,不再贅述。
客戶端代碼:
'''
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();
}
}
}
'''
客戶端同樣使用了線程池進行管理,把連接和發送分割為兩個 Runnable 易于調用,當發送 “0” 且服務端收到時關閉連接。
okio 實現
到這里一個簡單的 Socket 通信就完成了,其中對于 Socket 的信息流使用的是 java.io,之前學習 okio 時,了解到 okio 可以替代 java.io,okio是一個由square公司開發的開源庫,它彌補了Java.io和java.nio的不足,能夠更方便快速的讀取、存儲和處理數據(了解更多請點擊Okio源碼分析),下面就嘗試用 okio 替換 java.io。
直接上代碼:
服務端代碼:
'''
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("服務器已啟動...");
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="成功連接服務器"+"(服務器發送)";
mSink.writeUtf8(sendMsg+"\n");
mSink.flush();
System.out.println("成功連接服務器");
}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("客戶端請求斷開連接");
mSink.writeUtf8("服務端斷開連接"+"(服務器發送)");
mSink.flush();
mList.remove(socket);
socket.close();
break;
}else{
sendMsg ="我已接收:"+ receiveMsg +"(服務器發送)";
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();
}
}
}
'''
這里有一個很坑的地方:
mSink.writeUtf8(this.msg+"\n");
mSink.flush();
起初沒有加 “\n” 時,調用 flush 方法后消息是無法發送成功的,除非調用 sink.close 方法后才會發送成功,但是我們不能每發送一次就 close 掉,對比 printWriter.println 方法,嘗試加上一個換行符,果真發送成功。
總結
android有兩種通信方式,一種是常用的基于 HTTP 協議方式,另一種就是基于 TCP/UDP 協議的 Socket 方式。雖然大部分需求都可通過 HTTP 實現,實現起來也較為簡單,但某些情景下需要使用 Socket 方式,這時永遠不要放棄去使用最佳的工具來解決問題的機會。本文主要通過 Socket 實現了 Android 基于 TCP 協議的通信,后面將 Socket 的輸入輸出流處理由 java.io 替換為 Okio 實現,雖然說 Okio 彌補了Java.io和 java.nio 的不足,能夠更方便快速的讀取、存儲和處理數據,但是實際性能并沒測試過,這里主要是為了復習一下 Okio 的使用,另外就是在Okio源碼分析中沒有涉及到 Socket 的內容,這里正好填補一下知識漏洞。