Android 進階12:進程通信之 Socket (順便回顧 TCP UDP)

  • 不要害怕困難,這是你進步的機會!

讀完本文你將瞭解:

前面幾篇文章我們介紹了 AIDLBinderMessenger 以及 ContentProvider 實現進程通信的方式,這篇文章將介紹“使用 Socket 進行跨進程通信”。

在介紹 Socket 之前我們先來回顧一下網絡基礎知識,有的知識需要經常回顧一下加深印象。

OSI 七層網絡模型

爲了使不同廠家生產的計算機可以相互通信,建立更大範圍的計算機網絡,國際標準化組織(ISO)在 1978 年提出了“開放系統互聯參考模型”,即 OSI/RM 模型(Open System Interconnection/Reference Model)。

OSI 模型將計算機網絡體系結構的通信協議劃分爲七層,每一層都建立在它的下層之上,同時向它的上一層提供一定服務。上層只管調用下層提供的服務,而不用關心具體實現細節,有些類似我們開發中對外暴露接口隱藏實現的思想。

七層模型自下而上分別爲:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層、應用層。其中低四層完成數據傳輸,高三層面向用戶。

各層的功能見下圖(圖片來自 維基百科):

shixinzhang

TCP/IP 四層模型

由於 OSI/RM 模型過於複雜難以實現,現實中廣泛使用的是 TCP/IP 模型。

TCP/IP 是一個協議集,是由 ARPA ( Advanced Research Projects Agency Network 高等研究計劃署網絡 ) 於 1977 到 1979 年推出的一種網絡體系結構和協議規範。

隨着 Internet 的發展,TCP/IP 得到進一步的研究和推廣,成爲 Internet 上的 “通用模型”。

TCP/IP 模型在 OSI 模型的基礎上進行了簡化,變成了四層,從下到上分別爲:網絡接口層、網絡層、傳輸層、應用層。與 OSI 體系結構對比如下:

這裏寫圖片描述

可以看到,TCP/IP 模型 的網絡接口層對應 OSI 模型的物理層、數據鏈路層,應用層對應會話層、表示層和應用層每一層的功能如下:

  • 應用層:應用程序爲了訪問網絡所使用的一層
    • 數據以應用內部使用的格式進行傳送,然後被編碼成標準協議的格式
    • 比如萬維網使用的 HTTP 協議,傳輸文件的 FTP 協議等等
  • 傳輸層:響應來自應用層的請求,並向網絡層發出服務請求
    • 提供兩臺主機之間的數據傳輸,通常用於端到端連接、流量控制或者錯誤恢復
    • 最重要的兩個協議就是 TCP 和 UDP
  • 網絡層:提供端到端的數據包交付
    • 負責數據包從源發送到目的地
    • 任務包括網絡路由、差錯控制和 IP 編制等
    • 重要的協議有 IP、ICMP 等
  • 網絡接口層:負責通過網絡發送和接受 IP 數據包

每一層包括的協議如下圖:

這裏寫圖片描述

Socket 作爲應用層和傳輸層之間的橋樑,與之關係最大的兩個協議就是傳輸層中的 TCP 和 UDP協議。

這裏寫圖片描述
(圖片來自:http://www.jianshu.com/p/089fb79e308b

Socket 分爲流式套接字和用戶數據報套接字,分別使用傳輸層中的 TCP 和 UDP 協議。

TCP 協議

TCP (Transmission Control Protocol 傳輸控制協議),是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。

TCP 協議被認爲是穩定的協議,因爲它有以下特點:

  • 面向連接,“三次握手”
  • 雙向通信
  • 保證數據按序發送,按序到達
  • 超時重傳

要使用 TCP 傳輸數據,必須先建立連接,傳輸完成後釋放連接。分別對應常說的“三次握手”、“四次揮手”。

TCP 的三次握手

在 socket 編程中,客戶端執行 connect() 時。將觸發三次握手。

TCP 的三次握手流程圖如下:

這裏寫圖片描述

解釋如下:

  1. 客戶端發送一個建立 C 到 S 連接的請求報文,其中同步標誌位(SYN)置 1。然後進入 SYN_SEND 狀態,等待服務端確認
  2. 服務端返回確認數據報文,將 ACK 置爲 1,同時也將 SYN 置爲 1,請求建立 S 到 C 的連接
  3. 客戶端返回確認數據報文,ACK 遞增,這時雙方連接建立成功

雙向連接都建立成功後就可以收發數據了。

爲什麼是三次呢?

爲了防止已經失效的連接請求報文突然又傳送到服務端,因而產生錯誤。
減小因延遲高擁塞大對報文傳輸的影響。

在這三次握手過程中,任何一次未收到對面回覆都要重發,保證請求報文的及時性。

建立連接是需要耗費資源的,就像打電話一樣,只有在雙方都確認後纔等待通話,只要有一方沒有及時響應就掛斷,而不是一方確認後就等着,這樣會浪費資源,甚至可能導致其他問題。

一副圖簡化理解三次握手:

這裏寫圖片描述

TCP 的四次揮手

TCP 協議中,在通信結束後,需要斷開連接,這需要通過四次揮手,客戶端或服務器均可主動發起,主動的一方先斷開

在 socket 編程中,任何一方執行 close() 操作即可產生揮手操作。

流程圖如下 (圖片來自:http://blog.csdn.net/sszgg2006/article/details/56015112):

這裏寫圖片描述

解釋如下:

  1. 客戶端 C 發送 FIN 的報文,表示沒有數據要發送給服務端了,請求關閉 C 到 S 的連接
  2. 服務端確認這個報文,發回一個 ACK,關閉它的 Receive 通道;客戶端收到 ACK 後關閉它的 Send 通道
  3. 服務端 S 發出 FIN ,表示沒有數據發送給客戶端了,請求斷開連接
  4. 客戶端確認這個報文,發回 ACK,等待 2MSL 後關閉 Receive 通道;S 收到後關閉 Send 通道

注意第三步,S 發出 FIN 後還沒有斷開!

爲什麼是四次呢?

TCP 連接是全雙工的,每一端都可以同時發送和接受數據,關閉的時候兩端都要關閉各自兩個方向的通道,總共相當於要關閉四個。

(假設以客戶端先發起斷開請求)

  • 在客戶端發送 FIN 報文時,僅代表客戶端沒有數據發送了
  • 這時服務端可能還是有數據要發送,因此不會馬上關閉服務端到客戶端的發送通道,而是先回答 ACK “哦知道了,我先不接收你的數據,你先斷了發送通道吧”;客戶端收到服務端的確認消息後,斷開到服務端的發送通道
  • 等服務端沒有數據發送時,向客戶端發送 FIN 報文,說“我沒啥發的了,請求斷開”
  • 客戶端收到後回覆 “好的你斷吧”,同時斷開到服務端的接受通道;服務端得到確認後斷開到客戶端的發送通道

至此,四個通道全部關閉。

第四步客戶端爲什麼要等待 2MSL?

首先,MSL(Maximum Segment Life),是 TCP 對 TCP Segment 生存時間的限制。

客戶端在發出確認服務端關閉的 ACK 後,它沒有辦法知道對方是否收到這個消息,於是需要等待一段時間,如果服務端沒有收到關閉的消息後會重新發出 FIN 報文,這樣客戶端就知道自己上條消息丟了,需要再發一次;如果等待的這段時間沒有在收到 FIN 的重發報文,說明它的確已經收到斷開的消息並且已經斷開了。

這個等待時間至少是:客戶端的 timeout + FIN 的傳輸時間,爲了保證可靠,採用更加保守的等待時間 2MSL。

UDP 協議

UDP 協議沒有 TCP 協議穩定,因爲它不建立連接,也不按順序發送,可能會出現丟包現象,使傳輸的數據出錯。

但是有得就有失,UDP 的效率更高,因爲 UDP 頭包含很少的字節,比 TCP 負載消耗少,同時也可以實現雙向通信,不管消息送達的準確率,只負責無腦發送。

UDP 服務於很多知名應用層協議,比如 NFS(網絡文件系統)、SNMP(簡單網絡管理協議)

UDP 一般多用於 IP 電話、網絡視頻等容錯率強的場景。

Socket 簡介

TCP 或者 UDP 的報文中,除了數據本身還包含了包的信息,比如目的地址和端口,包的源地址和端口,以及其他附加校驗信息。

由於包的長度有限,在傳輸的過程中還需要拆包,到達目的地後再重新組合。

如果有丟失或者損壞的包還需要重傳,有的在到達目的地後還需要重新排序。

這些工作是複雜且與業務無關的,Socket 爲我們封裝了這些處理工作。

Socket 被稱爲“套接字”,它把複雜的 TCP/IP 協議簇隱藏在背後,爲用戶提供簡單的客戶端到服務端接口,讓我們感覺這邊輸入數據,那邊就直接收到了數據,像一個“管道”一樣。

這裏寫圖片描述

Socket 的基本操作

Socket 的基本操作有以下幾部分:

  1. 連接遠程機器
  2. 發送數據
  3. 接收數據
  4. 關閉連接
  5. 綁定端口
  6. 監聽到達數據
  7. 在綁定的端口上接受來自遠程機器的連接

要實現客戶端與服務端的通信,雙方都需要實例化一個 Socket。

在 Java 中,客戶端可以實現上面的 1、2、3、4、,服務端實現 5、6、7.

Java.net 中爲我們提供了使用 TCP、UDP 通信的兩種 Socket:

  • ServerSocket:流套接字,TCP
  • DatagramSocket:數據報套接字,UDP

使用 TCP 通信的 Socket 流程

服務端:

  1. 調用 ServerSocket(int port) 創建一個 ServerSocket,綁定到指定端口
  2. 調用 accept() 監聽連接請求,如果客戶端請求連接則接受,返回通信套接字
  3. 調用 Socket 類的 getOutputStream()getInputStream() 獲取輸出和輸入流,進行網絡數據的收發
  4. 關閉套接字

客戶端:

  1. 調用 Socket() 創建一個流套接字,連接到服務端
  2. 調用 Socket 類的 getOutputStream()getInputStream() 獲取輸出和輸入流,進行網絡數據的收發
  3. 關閉套接字

使用 UDP 通信的 Socket 流程

服務端:

  1. 調用 DatagramSocket(int port) 創建一個數據報套接字,綁定到指定端口
  2. 調用 DatagramPacket(byte[] buf, int length) 建立一個字節數組,以接受 UDP 包
  3. 調用 DatagramSocketreceive() 接收 UDP 包
  4. 調用 DatagramSocket.send() 發送 UDP 包
  5. 關閉數據報套接字

客戶端:

  1. 調用 DatagramSocket() 創建一個數據報套接字
  2. 調用 DatagramPacket(byte buf[], int offset, int length,InetAddress address, int port) 建立要發送的 UDP 包
  3. 調用 DatagramSocketreceive() 接收 UDP 包
  4. 調用 DatagramSocket.send() 發送 UDP 包
  5. 關閉數據報套接字

這裏我們舉一個 使用 TCP 通信的 Socket 例子,使用 UDP 通信時客戶端和服務端非常相似,可以看這篇文章:http://blog.csdn.net/mad1989/article/details/9147661

使用 TCP 通信的 Socket 實現跨進程聊天

我們使用流套接字實現一個跨進程聊天程序。

創建服務端 TCPServerService

public class TCPServerService extends BaseService {

    private final String TAG = this.getClass().getSimpleName();

    private boolean mIsServiceDisconnected;

    @Override
    public void onCreate() {
        super.onCreate();
        LogUtils.d(TAG, "服務已 create");
        new Thread(new TCPServer()).start();    //新開一個線程開啓 Socket
    }


    private class TCPServer implements Runnable {
        @Override
        public void run() {
            ServerSocket serverSocket;
            try {
                serverSocket = new ServerSocket(ConfigHelper.TEST_SOCKET_PORT);
                LogUtils.d(TAG, "TCP 服務已創建");
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("TCP 服務端創建失敗");
                return;
            }

            while (!mIsServiceDisconnected) {
                try {
                    Socket client = serverSocket.accept();  //接受客戶端消息,阻塞直到收到消息
                    //我這裏使用了線程池,也可以直接新建一個線程
//                    new Thread(responseClient(client)).start();    
                    ThreadPoolManager.getInstance()
                            .addTask(responseClient(client));    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //在這裏接受和回覆客戶端消息
    private Runnable responseClient(final Socket client) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    //接受消息
                    BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                    //回覆消息
                    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
                    out.println("服務端已連接 *****");

                    while (!mIsServiceDisconnected) {
                        String inputStr = in.readLine();
                        LogUtils.i(TAG, "收到客戶端的消息:" + inputStr);
                        if (TextUtils.isEmpty(inputStr)) {
                            LogUtils.i(TAG, "收到消息爲空,客戶端斷開連接 ***");
                            break;
                        }
                        out.println("你這句【" + inputStr + "】非常有道理啊!");
                    }
                    out.close();
                    in.close();
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
    }

    @Override
    public void onDestroy() {
        mIsServiceDisconnected = true;
        super.onDestroy();
    }
}

然後在 AndroidManifest.xml 文件中聲明 Service,放到另外一個進程:

<service
    android:name=".service.TCPServerService"
    android:exported="true"
    android:process=":socket"/>

在客戶端中建立連接,收發數據

這裏的客戶端就是我們的 Activity。

佈局代碼:

<merge xmlns:tools="http://schemas.android.com/tools"
       xmlns:android="http://schemas.android.com/apk/res/android">


    <TextView
        android:id="@+id/tv_socket_message"
        android:layout_width="match_parent"
        android:layout_height="300dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginBottom="50dp"
        android:background="#efefef"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_client_socket"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/bt_send_socket"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:enabled="false"
            android:text="向服務器發消息"/>
    </LinearLayout>
</merge>

然後 include 到 Activity 的佈局文件中。

Activity 代碼:


/**
 * 處理 Socket 線程切換
 */
@SuppressWarnings("HandlerLeak")
public class SocketHandler extends Handler {
    public static final int CODE_SOCKET_CONNECT = 1;
    public static final int CODE_SOCKET_MSG = 2;

    @Override
    public void handleMessage(final Message msg) {
        switch (msg.what) {
            case CODE_SOCKET_CONNECT:
                mBtSendSocket.setEnabled(true);
                break;
            case CODE_SOCKET_MSG:
                mTvSocketMessage.setText(mTvSocketMessage.getText() + (String) msg.obj);
                break;
        }
    }
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_ipc);
    ButterKnife.bind(this);
    bindSocketService();
}

private void bindSocketService() {
    //啓動服務端
    Intent intent = new Intent(this, TCPServerService.class);
    startService(intent);

    mSocketHandler = new SocketHandler();
    new Thread(new Runnable() {    //新開一個線程連接、接收數據
        @Override
        public void run() {
            try {
                connectSocketServer();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private Socket mClientSocket;
private PrintWriter mPrintWriter;
private SocketHandler mSocketHandler;

/**
 * 通過 Socket 連接服務端
 */
private void connectSocketServer() throws IOException {
    Socket socket = null;
    while (socket == null) {    //選擇在循環中連接是因爲有時請求連接時服務端還沒創建,需要重試
        try {
            socket = new Socket("localhost", ConfigHelper.TEST_SOCKET_PORT);
            mClientSocket = socket;
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
        } catch (IOException e) {
            SystemClock.sleep(1_000);
        }
    }

    //連接成功
    mSocketHandler.sendEmptyMessage(SocketHandler.CODE_SOCKET_CONNECT);

    //獲取輸入流
    BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    while (!isFinishing()) {    //死循環監聽服務端發送的數據
        final String msg = in.readLine();    
        if (!TextUtils.isEmpty(msg)) {
            //數據傳到 Handler 中展示
            mSocketHandler.obtainMessage(SocketHandler.CODE_SOCKET_MSG,
                    "\n" + DateUtils.getCurrentTime() + "\nserver : " + msg)
                    .sendToTarget();
        }
        SystemClock.sleep(1_000);
    }

    System.out.println("Client quit....");
    mPrintWriter.close();
    in.close();
    socket.close();
}

@OnClick(R.id.bt_send_socket)
public void sendMsgToSocketServer() {
    final String msg = mEtClientSocket.getText().toString();
    if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
        //發送數據,這裏注意要在線程中發送,不能在主線程進行網絡請求,不然就會報錯
        ThreadPoolManager.getInstance().addTask(new Runnable() {
            @Override
            public void run() {
                mPrintWriter.println(msg);
            }
        });
        mEtClientSocket.setText("");
        mTvSocketMessage.setText(mTvSocketMessage.getText() + "\n" + DateUtils.getCurrentTime() + "\nclient : " + msg);
    }
}

運行結果

這裏寫圖片描述

代碼地址

Thanks

《計算機網絡》
《Android 開發藝術探索》
《深入理解 Android 網絡編程》
http://www.cnblogs.com/BlueTzar/articles/811160.html
http://blog.csdn.net/mad1989/article/details/9147661
http://www.jianshu.com/p/089fb79e308b#
http://blog.csdn.net/sszgg2006/article/details/56015112
https://www.zhihu.com/question/36930631

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章