網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接

本文原做者:「水晶蝦餃」,原文由「玉剛說」寫做平臺提供寫做贊助,原文版權歸「玉剛說」微信公衆號全部,即時通信網收錄時有改動。php

一、引言

好多小白初次接觸即時通信(好比:IM或者消息推送應用)時,老是不能理解Web短鏈接(就是最多見的HTTP通訊了)跟長鏈接(主要指TCP、UDP協議實現的socket通訊,固然HTML5裏的Websocket協議也是長鏈接)的區別,致使寫即時通信這類系統代碼時每每找不到最佳實踐,搞的一臉蒙逼。html

本篇咱們先簡單瞭解一下 TCP/IP,而後經過實現一個 echo 服務器來學習 Java 的 Socket API。最後咱們聊聊偏高級一點點的 socket 長鏈接和協議設計。java

另外,本系列文章的前2篇《網絡編程懶人入門(一):快速理解網絡通訊協議(上篇)》、《網絡編程懶人入門(二):快速理解網絡通訊協議(下篇)》快速介紹了網絡基本通訊協議及理論基礎,若是您對網絡基礎毫無概念,則請務必首先閱讀完這2篇文章。本系列的第3篇文章《網絡編程懶人入門(三):快速理解TCP協議一篇就夠》有助於您快速理解TCP協議理論的方方面面,建議也能夠讀一讀。面試

TCP 是互聯網的核心協議之一,鑑於它的重要性,但願經過閱讀上面介紹的幾篇理論文章,再針對本文的動手實踐,能真正加深您對TCP協議的理解。數據庫

若是您正打算系統地學習即時通信開發,在讀完本文後,建議您能夠詳細閱讀《新手入門一篇就夠:從零開發移動端IM》。編程

學習交流:api

- 即時通信開發交流3羣:185926912[推薦]安全

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM服務器

(本文同步發佈於:http://www.52im.net/thread-1722-1-1.html)微信

二、系列文章

本文是系列文章中的第8篇,本系列文章的大綱以下:

網絡編程懶人入門(一):快速理解網絡通訊協議(上篇)

網絡編程懶人入門(二):快速理解網絡通訊協議(下篇)

網絡編程懶人入門(三):快速理解TCP協議一篇就夠

網絡編程懶人入門(四):快速理解TCP和UDP的差別

網絡編程懶人入門(五):快速理解爲何說UDP有時比TCP更有優點

網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門

網絡編程懶人入門(七):深刻淺出,全面理解HTTP協議

網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接》(本文)

若是您以爲本系列文章過於基礎,您可直接閱讀《鮮爲人知的網絡編程》系列文章,該系列目錄以下:

鮮爲人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)

鮮爲人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)

鮮爲人知的網絡編程(三):關閉TCP鏈接時爲何會TIME_WAIT、CLOSE_WAIT

鮮爲人知的網絡編程(四):深刻研究分析TCP的異常關閉

鮮爲人知的網絡編程(五):UDP的鏈接性和負載均衡

鮮爲人知的網絡編程(六):深刻地理解UDP協議並用好它

若是您對服務端高性能網絡編程感興趣,能夠閱讀如下系列文章:

高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少

高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題

高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了

高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索

關於移動端網絡特性及優化手段的總結性文章請見:

現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障

移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」

移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結

三、參考資料

TCP/IP詳解 - 第11章·UDP:用戶數據報協議

TCP/IP詳解 - 第17章·TCP:傳輸控制協議

TCP/IP詳解 - 第18章·TCP鏈接的創建與終止

TCP/IP詳解 - 第21章·TCP的超時與重傳

通俗易懂-深刻理解TCP協議(上):理論基礎

通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理

理論經典:TCP協議的3次握手與4次揮手過程詳解

理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程

計算機網絡通信協議關係圖(中文珍藏版)

高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少

高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題

高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了

高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索

簡述傳輸層協議TCP和UDP的區別

爲何QQ用的是UDP協議而不是TCP協議?

移動端即時通信協議選擇:UDP仍是TCP?

四、TCP/IP 協議簡介

TCP/IP協議族是互聯網最重要的基礎設施之一,若有興趣瞭解TCP/IP的貢獻,能夠讀一讀此文:《技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)》,本文因篇幅緣由僅做簡要介紹。

4.1IP協議

首先咱們看 IP(Internet Protocol)協議。IP 協議提供了主機和主機間的通訊。

爲了完成不一樣主機的通訊,咱們須要某種方式來惟一標識一臺主機,這個標識,就是著名的IP地址。經過IP地址,IP 協議就可以幫咱們把一個數據包發送給對方。

4.2TCP協議

前面咱們說過,IP 協議提供了主機和主機間的通訊。TCP 協議在 IP 協議提供的主機間通訊功能的基礎上,完成這兩個主機上進程對進程的通訊。

有了 IP,不一樣主機就可以交換數據。可是,計算機收到數據後,並不知道這個數據屬於哪一個進程(簡單講,進程就是一個正在運行的應用程序)。TCP 的做用就在於,讓咱們可以知道這個數據屬於哪一個進程,從而完成進程間的通訊。

爲了標識數據屬於哪一個進程,咱們給須要進行 TCP 通訊的進程分配一個惟一的數字來標識它。這個數字,就是咱們常說的端口號。

TCP 的全稱是 Transmission Control Protocol,你們對它說得最多的,大概就是面向鏈接的特性了。之因此說它是有鏈接的,是說在進行通訊前,通訊雙方須要先通過一個三次握手的過程。三次握手完成後,鏈接便創建了。這時候咱們才能夠開始發送/接收數據。(與之相對的是 UDP,不須要通過握手,就能夠直接發送數據)。

下面咱們簡單瞭解一下三次握手的過程:

 

首先,客戶向服務端發送一個 SYN,假設此時 sequence number 爲 x。這個 x 是由操做系統根據必定的規則生成的,不妨認爲它是一個隨機數;

服務端收到 SYN 後,會向客戶端再發送一個 SYN,此時服務器的 seq number = y。與此同時,會 ACK x+1,告訴客戶端「已經收到了 SYN,能夠發送數據了」;

客戶端收到服務器的 SYN 後,回覆一個 ACK y+1,這個 ACK 則是告訴服務器,SYN 已經收到,服務器能夠發送數據了。

通過這 3 步,TCP 鏈接就創建了,這裏須要注意的有三點:

鏈接是由客戶端主動發起的;

在第 3 步客戶端向服務器回覆 ACK 的時候,TCP 協議是容許咱們攜帶數據的。之因此作不到,是 API 的限制致使的;

TCP 協議還容許 「四次握手」 的發生,一樣的,因爲 API 的限制,這個極端的狀況並不會發生。

TCP/IP 相關的理論知識咱們就先了解到這裏,若是對TCP的3次握手和4次揮手還不太理解,那就詳細讀讀如下文章:

通俗易懂-深刻理解TCP協議(上):理論基礎

通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理

理論經典:TCP協議的3次握手與4次揮手過程詳解

理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程

關於 TCP,還有諸如可靠性、流量控制、擁塞控制等很是有趣的特性。強烈推薦讀者看一看 Richard 的名著《TCP/IP 詳解 - 卷1》(注意,是第1版,不是第2版)。

 

▲ 網絡編程理論經典《TCP/IP 詳解 - 卷1》(在線閱讀版點此進入

另外,TCP/IP協議實際上是一個龐大的協議族,《計算機網絡通信協議關係圖(中文珍藏版)》一文中爲您清晰展示了這個協議族之間的關係,頗有收藏價值,建議務必讀一讀。

 

▲ TCP/IP協議族圖(高清原圖點此進入

下面咱們看一些偏實戰的東西。

五、Socket 基本用法

Socket 是 TCP 層的封裝,經過 socket,咱們就能進行 TCP 通訊。

在 Java 的 SDK 中,socket 的共有兩個接口:用於監聽客戶鏈接的 ServerSocket 和用於通訊的 Socket

使用 socket 的步驟以下:

1)建立 ServerSocket 並監聽客戶鏈接;

2)使用 Socket 鏈接服務端;

3)經過 Socket.getInputStream()/getOutputStream() 獲取輸入輸出流進行通訊。

下面,咱們經過實現一個簡單的 echo 服務來學習 socket 的使用。所謂的 echo 服務,就是客戶端向服務端寫入任意數據,服務器都將數據原封不動地寫回給客戶端。

5.1第一步:建立 ServerSocket 並監聽客戶鏈接

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

5.2第二步:使用 Socket 鏈接服務端

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

5.3第三步:經過 socket.getInputStream()/getOutputStream() 獲取輸入/輸出流進行通訊

首先,咱們來實現服務端:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

能夠看到,服務端的實現其實很簡單,咱們不停地讀取輸入數據,而後寫回給客戶端。

下面咱們看看客戶端:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

客戶端會稍微複雜一點點,在讀取用戶輸入的同時,咱們又想讀取服務器的響應。因此,這裏建立了一個線程來讀服務器的響應。

不熟悉 lambda 的讀者,能夠把Thread readerThread = new Thread(this::readResponse) 換成下面這個代碼:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

在客戶端,咱們會看到,輸入的全部字符都打印了出來。

5.4最後須要注意的有幾點

1)在上面的代碼中,咱們全部的異常都沒有處理。實際應用中,在發生異常時,須要關閉 socket,並根據實際業務作一些錯誤處理工做;

2)在客戶端,咱們沒有中止 readThread。實際應用中,咱們能夠經過關閉 socket 來讓線程從阻塞讀中返回。推薦讀者閱讀《Java併發編程實戰》;

3)咱們的服務端只處理了一個客戶鏈接。若是須要同時處理多個客戶端,能夠建立線程來處理請求。這個做爲練習留給讀者來徹底。

六、Socket、ServerSocket 傻傻分不清楚

在進入這一節的主題前,讀者不妨先考慮一個問題:在上一節的實例中,咱們運行 echo 服務後,在客戶端鏈接成功時,一個有多少個 socket 存在?

答案是 3 個 socket:客戶端一個,服務端有兩個。跟這個問題的答案直接關聯的是本節的主題——Socket 和 ServerSocket 的區別是什麼。

眼尖的讀者,可能會注意到在上一節我是這樣描述他們的:

在 Java 的 SDK 中,socket 的共有兩個接口:用於監聽客戶鏈接的 ServerSocket 和用於通訊的 Socket。

注意:我只說 ServerSocket 是用於監聽客戶鏈接,而沒有說它也能夠用來通訊。下面咱們來詳細瞭解一下他們的區別。

注:如下描述使用的是 UNIX/Linux 系統的 API。

首先,咱們建立 ServerSocket 後,內核會建立一個 socket。這個 socket 既能夠拿來監聽客戶鏈接,也能夠鏈接遠端的服務。因爲 ServerSocket 是用來監聽客戶鏈接的,緊接着它就會對內核建立的這個 socket 調用 listen 函數。這樣一來,這個 socket 就成了所謂的 listening socket,它開始監聽客戶的鏈接。

接下來,咱們的客戶端建立一個 Socket,一樣的,內核也建立一個 socket 實例。內核建立的這個 socket 跟 ServerSocket 一開始建立的那個沒有什麼區別。不一樣的是,接下來 Socket 會對它執行 connect,發起對服務端的鏈接。前面咱們說過,socket API 實際上是 TCP 層的封裝,因此 connect 後,內核會發送一個 SYN 給服務端。

如今,咱們切換角色到服務端。服務端的主機在收到這個 SYN 後,會建立一個新的 socket,這個新建立的 socket 跟客戶端繼續執行三次握手過程。

三次握手完成後,咱們執行的 serverSocket.accept() 會返回一個 Socket 實例,這個 socket 就是上一步內核自動幫咱們建立的。

因此說:在一個客戶端鏈接的狀況下,其實有 3 個 socket。

關於內核自動建立的這個 socket,還有一個頗有意思的地方。它的端口號跟 ServerSocket 是一毛同樣的。咦!!不是說,一個端口只能綁定一個 socket 嗎?其實這個說法並不夠準確。

前面我說的TCP 經過端口號來區分數據屬於哪一個進程的說法,在 socket 的實現裏須要改一改。Socket 並不只僅使用端口號來區別不一樣的 socket 實例,而是使用 這個四元組。

在上面的例子中,咱們的 ServerSocket 長這樣:<*:*, *:9877>。意思是,能夠接受任何的客戶端,和本地任何 IP。

accept 返回的 Socket 則是這樣:<127.0.0.1:xxxx, 127.0.0.1:9877>。其中,xxxx 是客戶端的端口號。

若是數據是發送給一個已鏈接的 socket,內核會找到一個徹底匹配的實例,因此數據準確發送給了對端。

若是是客戶端要發起鏈接,這時候只有 <*:*, *:9877> 會匹配成功,因此 SYN 也準確發送給了監聽套接字。

Socket/ServerSocket 的區別咱們就講到這裏。若是讀者以爲不過癮,能夠參考《TCP/IP 詳解》卷1、卷2。

七、Socket 「長」鏈接的實現

7.1背景知識

Socket 長鏈接,指的是在客戶和服務端之間保持一個 socket 鏈接長時間不斷開。

比較熟悉 Socket 的讀者,可能知道有這樣一個 API:

1socket.setKeepAlive(true);

嗯……keep alive,「保持活着」,這個應該就是讓 TCP 不斷開的意思。那麼,咱們要實現一個 socket 的長鏈接,只須要這一個調用便可。

遺憾的是,生活並不老是那麼美好。對於 4.4BSD 的實現來講,Socket 的這個 keep alive 選項若是打開而且兩個小時內沒有通訊,那麼底層會發一個心跳,看看對方是否是還活着。

注意:兩個小時纔會發一次。也就是說,在沒有實際數據通訊的時候,我把網線拔了,你的應用程序要通過兩個小時纔會知道。

這個話題,對於即時通信的老手來講,也就是常常討論的「網絡鏈接心跳保活」這個話題了,感興趣的話能夠讀一讀《聊聊iOS中網絡編程長鏈接的那些事》、《爲什麼基於TCP協議的移動端IM仍然須要心跳保活機制?》、《微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)》、《Android端消息推送總結:實現原理、心跳保活、遇到的問題等》。

在說明若是實現長鏈接前,咱們先來理一理咱們面臨的問題。

假定如今有一對已經鏈接的 socket,在如下狀況發生時候,socket 將再也不可用:

1)某一端關閉是 socket(這不是廢話嗎):主動關閉的一方會發送 FIN,通知對方要關閉 TCP 鏈接。在這種狀況下,另外一端若是去讀 socket,將會讀到 EoF(End of File)。因而咱們知道對方關閉了 socket;

2)應用程序奔潰:此時 socket 會由內核關閉,結果跟狀況1同樣;

3)系統奔潰:這時候系統是來不及發送 FIN 的,由於它已經跪了。此時對方沒法得知這一狀況。對方在嘗試讀取數據時,最後會返回 read time out。若是寫數據,則是 host unreachable 之類的錯誤。

4)電纜被挖斷、網線被拔:跟狀況3差很少,若是沒有對 socket 進行讀寫,兩邊都不知道發生了事故。跟狀況3不一樣的是,若是咱們把網線接回去,socket 依舊能夠正常使用。

在上面的幾種情形中,有一個共同點就是,只要去讀、寫 socket,只要 socket 鏈接不正常,咱們就可以知道。基於這一點,要實現一個 socket 長鏈接,咱們須要作的就是不斷地給對方寫數據,而後讀取對方的數據,也就是所謂的心跳。只要心還在跳,socket 就是活的。寫數據的間隔,須要根據實際的應用需求來決定。

心跳包不是實際的業務數據,根據通訊協議的不一樣,須要作不一樣的處理。

比方說,咱們使用 JSON 進行通訊,那麼,能夠爲協議包加一個 type 字段,表面這個 JSON 是心跳仍是業務數據:

{

    "type": 0,  // 0 表示心跳

    // ...

}

使用二進制協議的狀況相似。要求就是,咱們可以區別一個數據包是心跳仍是真實數據。這樣,咱們便實現了一個 socket 長鏈接。

7.2實現示例

這一小節咱們一塊兒來實現一個帶長鏈接的 Android echo 客戶端。完整的代碼能夠在本文末尾的附件找到。

首先了接口部分:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

咱們這個支持長鏈接的類就叫 LongLiveSocket 好了。若是在 socket 斷開後須要重連,只須要在對應的接口裏面返回 true 便可(在真實場景裏,咱們還須要讓客戶設置重連的等待時間,還有讀寫、鏈接的 timeout等。爲了簡單,這裏就直接不支持了。

另外須要注意的一點是,若是要作一個完整的庫,須要同時提供阻塞式和回調式API。一樣因爲篇幅緣由,這裏直接省掉了。

下面咱們直接看實現:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

下面是咱們新實現的 EchoClient:

(因代碼太長,爲保證文章體驗已在本文中刪除,如需查看代碼請至連接:http://www.52im.net/thread-1722-1-1.html)

就這樣,一個帶 socket 長鏈接的客戶端就完成了。剩餘代碼跟咱們這裏的主題沒有太大關係,感興趣的讀者能夠看看文末附件裏的源碼或者本身完成這個例子。

下面是一些輸出示例:

03:54:55.583 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:00.588 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:05.594 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:09.638 12691-12710/com.example.echoD/EchoClient: onSuccess:

03:55:09.639 12691-12713/com.example.echoI/EchoClient: EchoClient: received: hello

03:55:10.595 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:14.652 12691-12710/com.example.echoD/EchoClient: onSuccess:

03:55:14.654 12691-12713/com.example.echoI/EchoClient: EchoClient: received: echo

03:55:15.596 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:20.597 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

03:55:25.602 12691-12713/com.example.echoI/LongLiveSocket: readResponse: heart beat received

最後須要說明的是,若是想節省資源,在有客戶發送數據的時候能夠省略 heart beat。

咱們對讀出錯時候的處理,可能也存在一些爭議。讀出錯後,咱們只是關閉了 socket。socket 須要等到下一次寫動做發生時,纔會從新鏈接。實際應用中,若是這是一個問題,在讀出錯後能夠直接開始重連。這種狀況下,還須要一些額外的同步,避免重複建立 socket。heart beat timeout 的狀況相似。

八、跟 TCP/IP 學協議設計

若是僅僅是爲了使用是 socket,咱們大能夠不去理會協議的細節。之因此推薦你們去看一看《TCP/IP 詳解》,是由於它們有太多值得學習的地方。不少咱們工做中遇到的問題,均可以在這裏找到答案。

如下每個小節的標題都是一個小問題,建議讀者獨立思考一下,再繼續往下看。

8.1協議版本如何升級?

有這麼一句流行的話:這個世界惟一不變的,就是變化。當咱們對協議版本進行升級的時候,正確識別不一樣版本的協議對軟件的兼容很是重要。那麼,咱們如何設計協議,纔可以爲未來的版本升級作準備呢?

答案能夠在 IP 協議找到。

IP 協議的第一個字段叫 version,目前使用的是 4 或 6,分別表示 IPv4 和 IPv6。因爲這個字段在協議的開頭,接收端收到數據後,只要根據第一個字段的值就可以判斷這個數據包是 IPv4 仍是 IPv6。

再強調一下,這個字段在兩個版本的IP協議都位於第一個字段,爲了作兼容處理,對應的這個字段必須位於同一位置。文本協議(如,JSON、HTML)的狀況相似。

8.2如何發送不定長數據的數據包?

舉個例子,咱們用微信發送一條消息。這條消息的長度是不肯定的,而且每條消息都有它的邊界。咱們如何來處理這個邊界呢?

仍是同樣,看看 IP。IP 的頭部有個 header length 和 data length 兩個字段。經過添加一個 len 域,咱們就可以把數據根據應用邏輯分開。

跟這個相對的,還有另外一個方案,那就是在數據的末尾放置終止符。比方說,想 C 語言的字符串那樣,咱們在每一個數據的末尾放一個 \0 做爲終止符,用以標識一條消息的尾部。這個方法帶來的問題是,用戶的數據也可能存在 \0。此時,咱們就須要對用戶的數據進行轉義。比方說,把用戶數據的全部 \0 都變成 \0\0。讀消息的過程總,若是遇到 \0\0,那它就表明 \0,若是隻有一個 \0,那就是消息尾部。

使用 len 字段的好處是,咱們不須要對數據進行轉義。讀取數據的時候,只要根據 len 字段,一次性把數據都讀進來就好,效率會更高一些。

終止符的方案雖然要求咱們對數據進行掃描,可是若是咱們可能從任意地方開始讀取數據,就須要這個終止符來肯定哪裏纔是消息的開頭了。

固然,這兩個方法不是互斥的,能夠一塊兒使用。

8.3上傳多個文件,只有全部文件都上傳成功時纔算成功

如今咱們有一個需求,須要一次上傳多個文件到服務器,只有在全部文件都上傳成功的狀況下,纔算成功。咱們該如何來實現呢?

IP 在數據報過大的時候,會把一個數據報拆分紅多個,並設置一個 MF (more fragments)位,表示這個包只是被拆分後的數據的一部分。

好,咱們也學一學 IP。這裏,咱們能夠給每一個文件從 0 開始編號。上傳文件的同時,也攜帶這個編號,並額外附帶一個 MF 標誌。除了編號最大的文件,全部文件的 MF 標誌都置位。由於 MF 沒有置位的是最後一個文件,服務器就能夠根據這個得出總共有多少個文件。

另外一種不使用 MF 標誌的方法是,咱們在上傳文件前,就告訴服務器總共有多少個文件。

若是讀者對數據庫比較熟悉,學數據庫用事務來處理,也是能夠的。這裏就不展開討論了。

8.4如何保證數據的有序性?

這裏講一個我曾經遇到過的面試題。如今有一個任務隊列,多個工做線程從中取出任務並執行,執行結果放到一個結果隊列中。先要求,放入結果隊列的時候,順序順序須要跟從工做隊列取出時的同樣(也就是說,先取出的任務,執行結果須要先放入結果隊列)。

咱們看看 TCP/IP 是怎麼處理的。IP 在發送數據的時候,不一樣數據報到達對端的時間是不肯定的,後面發送的數據有可能較先到達。TCP 爲了解決這個問題,給所發送數據的每一個字節都賦了一個序列號,經過這個序列號,TCP 就可以把數據按原順序從新組裝。

同樣,咱們也給每一個任務賦一個值,根據進入工做隊列的順序依次遞增。工做線程完成任務後,在將結果放入結果隊列前,先檢查要放入對象的寫一個序列號是否是跟本身的任務相同,若是不一樣,這個結果就不能放進去。此時,最簡單的作法是等待,知道下一個能夠放入隊列的結果是本身所執行的那一個。可是,這個線程就沒辦法繼續處理任務了。

更好的方法是,咱們維護多一個結果隊列的緩衝,這個緩衝裏面的數據按序列號從小到大排序。

工做線程要將結果放入,有兩種可能:

1)剛剛完成的任務恰好是下一個,將這個結果放入隊列。而後從緩衝的頭部開始,將全部能夠放入結果隊列的數據都放進去;

2)所完成的任務不能放入結果隊列,這個時候就插入結果隊列。而後,跟上一種狀況同樣,須要檢查緩衝。

若是測試代表,這個結果緩衝的數據很少,那麼使用普通的鏈表就能夠。若是數據比較多,可使用一個最小堆。

8.5如何保證對方收到了消息?

咱們說,TCP 提供了可靠的傳輸。這樣不就可以保證對方收到消息了嗎?

很遺憾,其實不能。在咱們往 socket 寫入的數據,只要對端的內核收到後,就會返回 ACK,此時,socket 就認爲數據已經寫入成功。然而要注意的是,這裏只是對方所運行的系統的內核成功收到了數據,並不表示應用程序已經成功處理了數據。

解決辦法仍是同樣,咱們學 TCP,添加一個應用層的 APP ACK。應用接收到消息並處理成功後,發送一個 APP ACK 給對方。

有了 APP ACK,咱們須要處理的另外一個問題是,若是對方真的沒有收到,須要怎麼作?

TCP 發送數據的時候,消息同樣可能丟失。TCP 發送數據後,若是長時間沒有收到對方的 ACK,就假設數據已經丟失,並從新發送。

咱們也同樣,若是長時間沒有收到 APP ACK,就假設數據丟失,從新發送一個。

關於數據送達保證和應應答機制,如下文章進行了詳細討論:

IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

IM消息送達保證機制實現(二):保證離線消息的可靠投遞

IM羣聊消息如此複雜,如何保證不丟不重?

從客戶端的角度來談談移動端IM的消息可靠性和送達機制

九、源碼附件下載

請從連接:http://www.52im.net/thread-1722-1-1.html 中下載。

附錄:更多網絡編程資料

技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)

UDP中一個包的大小最大能多大?

Java新一代網絡編程模型AIO原理及Linux系統AIO介紹

NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示

NIO框架入門(二):服務端基於MINA2的UDP雙向通訊Demo演示

NIO框架入門(三):iOS與MINA二、Netty4的跨平臺UDP雙向通訊實戰

NIO框架入門(四):Android與MINA二、Netty4的跨平臺UDP雙向通訊實戰

P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介

P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解

P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解

通俗易懂:快速理解P2P技術中的NAT穿透原理

>> 更多同類文章 ……

(本文同步發佈於:http://www.52im.net/thread-1722-1-1.html)

相關文章
相關標籤/搜索