Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          本文原作者:“水晶蝦餃”,原文由“玉剛說(shuō)”寫(xiě)作平臺(tái)提供寫(xiě)作贊助,原文版權(quán)歸“玉剛說(shuō)”微信公眾號(hào)所有,即時(shí)通訊網(wǎng)收錄時(shí)有改動(dòng)。

          1、引言

          好多小白初次接觸即時(shí)通訊(比如:IM或者消息推送應(yīng)用)時(shí),總是不能理解Web短連接(就是最常見(jiàn)的HTTP通信了)跟長(zhǎng)連接(主要指TCP、UDP協(xié)議實(shí)現(xiàn)的socket通信,當(dāng)然HTML5里的Websocket協(xié)議也是長(zhǎng)連接)的區(qū)別,導(dǎo)致寫(xiě)即時(shí)通訊這類(lèi)系統(tǒng)代碼時(shí)往往找不到最佳實(shí)踐,搞的一臉蒙逼。

          本篇我們先簡(jiǎn)單了解一下 TCP/IP,然后通過(guò)實(shí)現(xiàn)一個(gè) echo 服務(wù)器來(lái)學(xué)習(xí) Java 的 Socket API。最后我們聊聊偏高級(jí)一點(diǎn)點(diǎn)的 socket 長(zhǎng)連接和協(xié)議設(shè)計(jì)。

          另外,本系列文章的前2篇《網(wǎng)絡(luò)編程懶人入門(mén)(一):快速理解網(wǎng)絡(luò)通信協(xié)議(上篇)》、《網(wǎng)絡(luò)編程懶人入門(mén)(二):快速理解網(wǎng)絡(luò)通信協(xié)議(下篇)》快速介紹了網(wǎng)絡(luò)基本通信協(xié)議及理論基礎(chǔ),如果您對(duì)網(wǎng)絡(luò)基礎(chǔ)毫無(wú)概念,則請(qǐng)務(wù)必首先閱讀完這2篇文章。本系列的第3篇文章《網(wǎng)絡(luò)編程懶人入門(mén)(三):快速理解TCP協(xié)議一篇就夠》有助于您快速理解TCP協(xié)議理論的方方面面,建議也可以讀一讀。

          TCP 是互聯(lián)網(wǎng)的核心協(xié)議之一,鑒于它的重要性,希望通過(guò)閱讀上面介紹的幾篇理論文章,再針對(duì)本文的動(dòng)手實(shí)踐,能真正加深您對(duì)TCP協(xié)議的理解。

          如果您正打算系統(tǒng)地學(xué)習(xí)即時(shí)通訊開(kāi)發(fā),在讀完本文后,建議您可以詳細(xì)閱讀《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》。

          學(xué)習(xí)交流:

          - 即時(shí)通訊開(kāi)發(fā)交流3群:185926912[推薦]

          - 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM

          (本文同步發(fā)布于:http://www.52im.net/thread-1722-1-1.html)

          2、系列文章

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

          網(wǎng)絡(luò)編程懶人入門(mén)(一):快速理解網(wǎng)絡(luò)通信協(xié)議(上篇)

          網(wǎng)絡(luò)編程懶人入門(mén)(二):快速理解網(wǎng)絡(luò)通信協(xié)議(下篇)

          網(wǎng)絡(luò)編程懶人入門(mén)(三):快速理解TCP協(xié)議一篇就夠

          網(wǎng)絡(luò)編程懶人入門(mén)(四):快速理解TCP和UDP的差異

          網(wǎng)絡(luò)編程懶人入門(mén)(五):快速理解為什么說(shuō)UDP有時(shí)比TCP更有優(yōu)勢(shì)

          網(wǎng)絡(luò)編程懶人入門(mén)(六):史上最通俗的集線器、交換機(jī)、路由器功能原理入門(mén)

          網(wǎng)絡(luò)編程懶人入門(mén)(七):深入淺出,全面理解HTTP協(xié)議

          網(wǎng)絡(luò)編程懶人入門(mén)(八):手把手教你寫(xiě)基于TCP的Socket長(zhǎng)連接》(本文)

          如果您覺(jué)得本系列文章過(guò)于基礎(chǔ),您可直接閱讀《不為人知的網(wǎng)絡(luò)編程》系列文章,該系列目錄如下:

          不為人知的網(wǎng)絡(luò)編程(一):淺析TCP協(xié)議中的疑難雜癥(上篇)

          不為人知的網(wǎng)絡(luò)編程(二):淺析TCP協(xié)議中的疑難雜癥(下篇)

          不為人知的網(wǎng)絡(luò)編程(三):關(guān)閉TCP連接時(shí)為什么會(huì)TIME_WAIT、CLOSE_WAIT

          不為人知的網(wǎng)絡(luò)編程(四):深入研究分析TCP的異常關(guān)閉

          不為人知的網(wǎng)絡(luò)編程(五):UDP的連接性和負(fù)載均衡

          不為人知的網(wǎng)絡(luò)編程(六):深入地理解UDP協(xié)議并用好它

          如果您對(duì)服務(wù)端高性能網(wǎng)絡(luò)編程感興趣,可以閱讀以下系列文章:

          高性能網(wǎng)絡(luò)編程(一):?jiǎn)闻_(tái)服務(wù)器并發(fā)TCP連接數(shù)到底可以有多少

          高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問(wèn)題

          高性能網(wǎng)絡(luò)編程(三):下一個(gè)10年,是時(shí)候考慮C10M并發(fā)問(wèn)題了

          高性能網(wǎng)絡(luò)編程(四):從C10K到C10M高性能網(wǎng)絡(luò)應(yīng)用的理論探索

          關(guān)于移動(dòng)端網(wǎng)絡(luò)特性及優(yōu)化手段的總結(jié)性文章請(qǐng)見(jiàn):

          現(xiàn)代移動(dòng)端網(wǎng)絡(luò)短連接的優(yōu)化手段總結(jié):請(qǐng)求速度、弱網(wǎng)適應(yīng)、安全保障

          移動(dòng)端IM開(kāi)發(fā)者必讀(一):通俗易懂,理解移動(dòng)網(wǎng)絡(luò)的“弱”和“慢”

          移動(dòng)端IM開(kāi)發(fā)者必讀(二):史上最全移動(dòng)弱網(wǎng)絡(luò)優(yōu)化方法總結(jié)

          3、參考資料

          TCP/IP詳解 - 第11章·UDP:用戶數(shù)據(jù)報(bào)協(xié)議

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

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

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

          通俗易懂-深入理解TCP協(xié)議(上):理論基礎(chǔ)

          通俗易懂-深入理解TCP協(xié)議(下):RTT、滑動(dòng)窗口、擁塞處理

          理論經(jīng)典:TCP協(xié)議的3次握手與4次揮手過(guò)程詳解

          理論聯(lián)系實(shí)際:Wireshark抓包分析TCP 3次握手、4次揮手過(guò)程

          計(jì)算機(jī)網(wǎng)絡(luò)通訊協(xié)議關(guān)系圖(中文珍藏版)

          高性能網(wǎng)絡(luò)編程(一):?jiǎn)闻_(tái)服務(wù)器并發(fā)TCP連接數(shù)到底可以有多少

          高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問(wèn)題

          高性能網(wǎng)絡(luò)編程(三):下一個(gè)10年,是時(shí)候考慮C10M并發(fā)問(wèn)題了

          高性能網(wǎng)絡(luò)編程(四):從C10K到C10M高性能網(wǎng)絡(luò)應(yīng)用的理論探索

          簡(jiǎn)述傳輸層協(xié)議TCP和UDP的區(qū)別

          為什么QQ用的是UDP協(xié)議而不是TCP協(xié)議?

          移動(dòng)端即時(shí)通訊協(xié)議選擇:UDP還是TCP?

          4、TCP/IP 協(xié)議簡(jiǎn)介

          TCP/IP協(xié)議族是互聯(lián)網(wǎng)最重要的基礎(chǔ)設(shè)施之一,如有興趣了解TCP/IP的貢獻(xiàn),可以讀一讀此文:《技術(shù)往事:改變世界的TCP/IP協(xié)議(珍貴多圖、手機(jī)慎點(diǎn))》,本文因篇幅原因僅作簡(jiǎn)要介紹。

          4.1IP協(xié)議

          首先我們看 IP(Internet Protocol)協(xié)議。IP 協(xié)議提供了主機(jī)和主機(jī)間的通信。

          為了完成不同主機(jī)的通信,我們需要某種方式來(lái)唯一標(biāo)識(shí)一臺(tái)主機(jī),這個(gè)標(biāo)識(shí),就是著名的IP地址。通過(guò)IP地址,IP 協(xié)議就能夠幫我們把一個(gè)數(shù)據(jù)包發(fā)送給對(duì)方。

          4.2TCP協(xié)議

          前面我們說(shuō)過(guò),IP 協(xié)議提供了主機(jī)和主機(jī)間的通信。TCP 協(xié)議在 IP 協(xié)議提供的主機(jī)間通信功能的基礎(chǔ)上,完成這兩個(gè)主機(jī)上進(jìn)程對(duì)進(jìn)程的通信。

          有了 IP,不同主機(jī)就能夠交換數(shù)據(jù)。但是,計(jì)算機(jī)收到數(shù)據(jù)后,并不知道這個(gè)數(shù)據(jù)屬于哪個(gè)進(jìn)程(簡(jiǎn)單講,進(jìn)程就是一個(gè)正在運(yùn)行的應(yīng)用程序)。TCP 的作用就在于,讓我們能夠知道這個(gè)數(shù)據(jù)屬于哪個(gè)進(jìn)程,從而完成進(jìn)程間的通信。

          為了標(biāo)識(shí)數(shù)據(jù)屬于哪個(gè)進(jìn)程,我們給需要進(jìn)行 TCP 通信的進(jìn)程分配一個(gè)唯一的數(shù)字來(lái)標(biāo)識(shí)它。這個(gè)數(shù)字,就是我們常說(shuō)的端口號(hào)。

          TCP 的全稱是 Transmission Control Protocol,大家對(duì)它說(shuō)得最多的,大概就是面向連接的特性了。之所以說(shuō)它是有連接的,是說(shuō)在進(jìn)行通信前,通信雙方需要先經(jīng)過(guò)一個(gè)三次握手的過(guò)程。三次握手完成后,連接便建立了。這時(shí)候我們才可以開(kāi)始發(fā)送/接收數(shù)據(jù)。(與之相對(duì)的是 UDP,不需要經(jīng)過(guò)握手,就可以直接發(fā)送數(shù)據(jù))。

          下面我們簡(jiǎn)單了解一下三次握手的過(guò)程:

          首先,客戶向服務(wù)端發(fā)送一個(gè) SYN,假設(shè)此時(shí) sequence number 為 x。這個(gè) x 是由操作系統(tǒng)根據(jù)一定的規(guī)則生成的,不妨認(rèn)為它是一個(gè)隨機(jī)數(shù);

          服務(wù)端收到 SYN 后,會(huì)向客戶端再發(fā)送一個(gè) SYN,此時(shí)服務(wù)器的 seq number = y。與此同時(shí),會(huì) ACK x+1,告訴客戶端“已經(jīng)收到了 SYN,可以發(fā)送數(shù)據(jù)了”;

          客戶端收到服務(wù)器的 SYN 后,回復(fù)一個(gè) ACK y+1,這個(gè) ACK 則是告訴服務(wù)器,SYN 已經(jīng)收到,服務(wù)器可以發(fā)送數(shù)據(jù)了。

          經(jīng)過(guò)這 3 步,TCP 連接就建立了,這里需要注意的有三點(diǎn):

          連接是由客戶端主動(dòng)發(fā)起的;

          在第 3 步客戶端向服務(wù)器回復(fù) ACK 的時(shí)候,TCP 協(xié)議是允許我們攜帶數(shù)據(jù)的。之所以做不到,是 API 的限制導(dǎo)致的;

          TCP 協(xié)議還允許 “四次握手” 的發(fā)生,同樣的,由于 API 的限制,這個(gè)極端的情況并不會(huì)發(fā)生。

          TCP/IP 相關(guān)的理論知識(shí)我們就先了解到這里,如果對(duì)TCP的3次握手和4次揮手還不太理解,那就詳細(xì)讀讀以下文章:

          通俗易懂-深入理解TCP協(xié)議(上):理論基礎(chǔ)

          通俗易懂-深入理解TCP協(xié)議(下):RTT、滑動(dòng)窗口、擁塞處理

          理論經(jīng)典:TCP協(xié)議的3次握手與4次揮手過(guò)程詳解

          理論聯(lián)系實(shí)際:Wireshark抓包分析TCP 3次握手、4次揮手過(guò)程

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

          ▲ 網(wǎng)絡(luò)編程理論經(jīng)典《TCP/IP 詳解 - 卷1》(在線閱讀版點(diǎn)此進(jìn)入

          另外,TCP/IP協(xié)議其實(shí)是一個(gè)龐大的協(xié)議族,《計(jì)算機(jī)網(wǎng)絡(luò)通訊協(xié)議關(guān)系圖(中文珍藏版)》一文中為您清晰展現(xiàn)了這個(gè)協(xié)議族之間的關(guān)系,很有收藏價(jià)值,建議務(wù)必讀一讀。

          ▲ TCP/IP協(xié)議族圖(高清原圖點(diǎn)此進(jìn)入

          下面我們看一些偏實(shí)戰(zhàn)的東西。

          5、Socket 基本用法

          Socket 是 TCP 層的封裝,通過(guò) socket,我們就能進(jìn)行 TCP 通信。

          在 Java 的 SDK 中,socket 的共有兩個(gè)接口:用于監(jiān)聽(tīng)客戶連接的 ServerSocket 和用于通信的 Socket

          使用 socket 的步驟如下:

          1)創(chuàng)建 ServerSocket 并監(jiān)聽(tīng)客戶連接;

          2)使用 Socket 連接服務(wù)端;

          3)通過(guò) Socket.getInputStream()/getOutputStream() 獲取輸入輸出流進(jìn)行通信。

          下面,我們通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 echo 服務(wù)來(lái)學(xué)習(xí) socket 的使用。所謂的 echo 服務(wù),就是客戶端向服務(wù)端寫(xiě)入任意數(shù)據(jù),服務(wù)器都將數(shù)據(jù)原封不動(dòng)地寫(xiě)回給客戶端。

          5.1第一步:創(chuàng)建 ServerSocket 并監(jiān)聽(tīng)客戶連接

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          5.2第二步:使用 Socket 連接服務(wù)端

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          5.3第三步:通過(guò) socket.getInputStream()/getOutputStream() 獲取輸入/輸出流進(jìn)行通信

          首先,我們來(lái)實(shí)現(xiàn)服務(wù)端:

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          可以看到,服務(wù)端的實(shí)現(xiàn)其實(shí)很簡(jiǎn)單,我們不停地讀取輸入數(shù)據(jù),然后寫(xiě)回給客戶端。

          下面我們看看客戶端:

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          客戶端會(huì)稍微復(fù)雜一點(diǎn)點(diǎn),在讀取用戶輸入的同時(shí),我們又想讀取服務(wù)器的響應(yīng)。所以,這里創(chuàng)建了一個(gè)線程來(lái)讀服務(wù)器的響應(yīng)。

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

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          在客戶端,我們會(huì)看到,輸入的所有字符都打印了出來(lái)。

          5.4最后需要注意的有幾點(diǎn)

          1)在上面的代碼中,我們所有的異常都沒(méi)有處理。實(shí)際應(yīng)用中,在發(fā)生異常時(shí),需要關(guān)閉 socket,并根據(jù)實(shí)際業(yè)務(wù)做一些錯(cuò)誤處理工作;

          2)在客戶端,我們沒(méi)有停止 readThread。實(shí)際應(yīng)用中,我們可以通過(guò)關(guān)閉 socket 來(lái)讓線程從阻塞讀中返回。推薦讀者閱讀《Java并發(fā)編程實(shí)戰(zhàn)》;

          3)我們的服務(wù)端只處理了一個(gè)客戶連接。如果需要同時(shí)處理多個(gè)客戶端,可以創(chuàng)建線程來(lái)處理請(qǐng)求。這個(gè)作為練習(xí)留給讀者來(lái)完全。

          6、Socket、ServerSocket 傻傻分不清楚

          在進(jìn)入這一節(jié)的主題前,讀者不妨先考慮一個(gè)問(wèn)題:在上一節(jié)的實(shí)例中,我們運(yùn)行 echo 服務(wù)后,在客戶端連接成功時(shí),一個(gè)有多少個(gè) socket 存在?

          答案是 3 個(gè) socket:客戶端一個(gè),服務(wù)端有兩個(gè)。跟這個(gè)問(wèn)題的答案直接關(guān)聯(lián)的是本節(jié)的主題——Socket 和 ServerSocket 的區(qū)別是什么。

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

          在 Java 的 SDK 中,socket 的共有兩個(gè)接口:用于監(jiān)聽(tīng)客戶連接的 ServerSocket 和用于通信的 Socket。

          注意:我只說(shuō) ServerSocket 是用于監(jiān)聽(tīng)客戶連接,而沒(méi)有說(shuō)它也可以用來(lái)通信。下面我們來(lái)詳細(xì)了解一下他們的區(qū)別。

          注:以下描述使用的是 UNIX/Linux 系統(tǒng)的 API。

          首先,我們創(chuàng)建 ServerSocket 后,內(nèi)核會(huì)創(chuàng)建一個(gè) socket。這個(gè) socket 既可以拿來(lái)監(jiān)聽(tīng)客戶連接,也可以連接遠(yuǎn)端的服務(wù)。由于 ServerSocket 是用來(lái)監(jiān)聽(tīng)客戶連接的,緊接著它就會(huì)對(duì)內(nèi)核創(chuàng)建的這個(gè) socket 調(diào)用 listen 函數(shù)。這樣一來(lái),這個(gè) socket 就成了所謂的 listening socket,它開(kāi)始監(jiān)聽(tīng)客戶的連接。

          接下來(lái),我們的客戶端創(chuàng)建一個(gè) Socket,同樣的,內(nèi)核也創(chuàng)建一個(gè) socket 實(shí)例。內(nèi)核創(chuàng)建的這個(gè) socket 跟 ServerSocket 一開(kāi)始創(chuàng)建的那個(gè)沒(méi)有什么區(qū)別。不同的是,接下來(lái) Socket 會(huì)對(duì)它執(zhí)行 connect,發(fā)起對(duì)服務(wù)端的連接。前面我們說(shuō)過(guò),socket API 其實(shí)是 TCP 層的封裝,所以 connect 后,內(nèi)核會(huì)發(fā)送一個(gè) SYN 給服務(wù)端。

          現(xiàn)在,我們切換角色到服務(wù)端。服務(wù)端的主機(jī)在收到這個(gè) SYN 后,會(huì)創(chuàng)建一個(gè)新的 socket,這個(gè)新創(chuàng)建的 socket 跟客戶端繼續(xù)執(zhí)行三次握手過(guò)程。

          三次握手完成后,我們執(zhí)行的 serverSocket.accept() 會(huì)返回一個(gè) Socket 實(shí)例,這個(gè) socket 就是上一步內(nèi)核自動(dòng)幫我們創(chuàng)建的。

          所以說(shuō):在一個(gè)客戶端連接的情況下,其實(shí)有 3 個(gè) socket。

          關(guān)于內(nèi)核自動(dòng)創(chuàng)建的這個(gè) socket,還有一個(gè)很有意思的地方。它的端口號(hào)跟 ServerSocket 是一毛一樣的。咦!!不是說(shuō),一個(gè)端口只能綁定一個(gè) socket 嗎?其實(shí)這個(gè)說(shuō)法并不夠準(zhǔn)確。

          前面我說(shuō)的TCP 通過(guò)端口號(hào)來(lái)區(qū)分?jǐn)?shù)據(jù)屬于哪個(gè)進(jìn)程的說(shuō)法,在 socket 的實(shí)現(xiàn)里需要改一改。Socket 并不僅僅使用端口號(hào)來(lái)區(qū)別不同的 socket 實(shí)例,而是使用 這個(gè)四元組。

          在上面的例子中,我們的 ServerSocket 長(zhǎng)這樣:<*:*, *:9877>。意思是,可以接受任何的客戶端,和本地任何 IP。

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

          如果數(shù)據(jù)是發(fā)送給一個(gè)已連接的 socket,內(nèi)核會(huì)找到一個(gè)完全匹配的實(shí)例,所以數(shù)據(jù)準(zhǔn)確發(fā)送給了對(duì)端。

          如果是客戶端要發(fā)起連接,這時(shí)候只有 <*:*, *:9877> 會(huì)匹配成功,所以 SYN 也準(zhǔn)確發(fā)送給了監(jiān)聽(tīng)套接字。

          Socket/ServerSocket 的區(qū)別我們就講到這里。如果讀者覺(jué)得不過(guò)癮,可以參考《TCP/IP 詳解》卷1、卷2。

          7、Socket “長(zhǎng)”連接的實(shí)現(xiàn)

          7.1背景知識(shí)

          Socket 長(zhǎng)連接,指的是在客戶和服務(wù)端之間保持一個(gè) socket 連接長(zhǎng)時(shí)間不斷開(kāi)。

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

          1socket.setKeepAlive(true);

          嗯……keep alive,“保持活著”,這個(gè)應(yīng)該就是讓 TCP 不斷開(kāi)的意思。那么,我們要實(shí)現(xiàn)一個(gè) socket 的長(zhǎng)連接,只需要這一個(gè)調(diào)用即可。

          遺憾的是,生活并不總是那么美好。對(duì)于 4.4BSD 的實(shí)現(xiàn)來(lái)說(shuō),Socket 的這個(gè) keep alive 選項(xiàng)如果打開(kāi)并且兩個(gè)小時(shí)內(nèi)沒(méi)有通信,那么底層會(huì)發(fā)一個(gè)心跳,看看對(duì)方是不是還活著。

          注意:兩個(gè)小時(shí)才會(huì)發(fā)一次。也就是說(shuō),在沒(méi)有實(shí)際數(shù)據(jù)通信的時(shí)候,我把網(wǎng)線拔了,你的應(yīng)用程序要經(jīng)過(guò)兩個(gè)小時(shí)才會(huì)知道。

          這個(gè)話題,對(duì)于即時(shí)通訊的老手來(lái)說(shuō),也就是經(jīng)常討論的“網(wǎng)絡(luò)連接心跳保活”這個(gè)話題了,感興趣的話可以讀一讀《聊聊iOS中網(wǎng)絡(luò)編程長(zhǎng)連接的那些事》、《為何基于TCP協(xié)議的移動(dòng)端IM仍然需要心跳保活機(jī)制?》、《微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)保活實(shí)戰(zhàn)分享(網(wǎng)絡(luò)保活篇)》、《Android端消息推送總結(jié):實(shí)現(xiàn)原理、心跳保活、遇到的問(wèn)題等》。

          在說(shuō)明如果實(shí)現(xiàn)長(zhǎng)連接前,我們先來(lái)理一理我們面臨的問(wèn)題。

          假定現(xiàn)在有一對(duì)已經(jīng)連接的 socket,在以下情況發(fā)生時(shí)候,socket 將不再可用:

          1)某一端關(guān)閉是 socket(這不是廢話嗎):主動(dòng)關(guān)閉的一方會(huì)發(fā)送 FIN,通知對(duì)方要關(guān)閉 TCP 連接。在這種情況下,另一端如果去讀 socket,將會(huì)讀到 EoF(End of File)。于是我們知道對(duì)方關(guān)閉了 socket;

          2)應(yīng)用程序奔潰:此時(shí) socket 會(huì)由內(nèi)核關(guān)閉,結(jié)果跟情況1一樣;

          3)系統(tǒng)奔潰:這時(shí)候系統(tǒng)是來(lái)不及發(fā)送 FIN 的,因?yàn)樗呀?jīng)跪了。此時(shí)對(duì)方無(wú)法得知這一情況。對(duì)方在嘗試讀取數(shù)據(jù)時(shí),最后會(huì)返回 read time out。如果寫(xiě)數(shù)據(jù),則是 host unreachable 之類(lèi)的錯(cuò)誤。

          4)電纜被挖斷、網(wǎng)線被拔:跟情況3差不多,如果沒(méi)有對(duì) socket 進(jìn)行讀寫(xiě),兩邊都不知道發(fā)生了事故。跟情況3不同的是,如果我們把網(wǎng)線接回去,socket 依舊可以正常使用。

          在上面的幾種情形中,有一個(gè)共同點(diǎn)就是,只要去讀、寫(xiě) socket,只要 socket 連接不正常,我們就能夠知道。基于這一點(diǎn),要實(shí)現(xiàn)一個(gè) socket 長(zhǎng)連接,我們需要做的就是不斷地給對(duì)方寫(xiě)數(shù)據(jù),然后讀取對(duì)方的數(shù)據(jù),也就是所謂的心跳。只要心還在跳,socket 就是活的。寫(xiě)數(shù)據(jù)的間隔,需要根據(jù)實(shí)際的應(yīng)用需求來(lái)決定。

          心跳包不是實(shí)際的業(yè)務(wù)數(shù)據(jù),根據(jù)通信協(xié)議的不同,需要做不同的處理。

          比方說(shuō),我們使用 JSON 進(jìn)行通信,那么,可以為協(xié)議包加一個(gè) type 字段,表面這個(gè) JSON 是心跳還是業(yè)務(wù)數(shù)據(jù):

          {

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

              // ...

          }

          使用二進(jìn)制協(xié)議的情況類(lèi)似。要求就是,我們能夠區(qū)別一個(gè)數(shù)據(jù)包是心跳還是真實(shí)數(shù)據(jù)。這樣,我們便實(shí)現(xiàn)了一個(gè) socket 長(zhǎng)連接。

          7.2實(shí)現(xiàn)示例

          這一小節(jié)我們一起來(lái)實(shí)現(xiàn)一個(gè)帶長(zhǎng)連接的 Android echo 客戶端。完整的代碼可以在本文末尾的附件找到。

          首先了接口部分:

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          我們這個(gè)支持長(zhǎng)連接的類(lèi)就叫 LongLiveSocket 好了。如果在 socket 斷開(kāi)后需要重連,只需要在對(duì)應(yīng)的接口里面返回 true 即可(在真實(shí)場(chǎng)景里,我們還需要讓客戶設(shè)置重連的等待時(shí)間,還有讀寫(xiě)、連接的 timeout等。為了簡(jiǎn)單,這里就直接不支持了。

          另外需要注意的一點(diǎn)是,如果要做一個(gè)完整的庫(kù),需要同時(shí)提供阻塞式和回調(diào)式API。同樣由于篇幅原因,這里直接省掉了。

          下面我們直接看實(shí)現(xiàn):

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          下面是我們新實(shí)現(xiàn)的 EchoClient:

          (因代碼太長(zhǎng),為保證文章體驗(yàn)已在本文中刪除,如需查看代碼請(qǐng)至鏈接:http://www.52im.net/thread-1722-1-1.html)

          就這樣,一個(gè)帶 socket 長(zhǎng)連接的客戶端就完成了。剩余代碼跟我們這里的主題沒(méi)有太大關(guān)系,感興趣的讀者可以看看文末附件里的源碼或者自己完成這個(gè)例子。

          下面是一些輸出示例:

          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

          最后需要說(shuō)明的是,如果想節(jié)省資源,在有客戶發(fā)送數(shù)據(jù)的時(shí)候可以省略 heart beat。

          我們對(duì)讀出錯(cuò)時(shí)候的處理,可能也存在一些爭(zhēng)議。讀出錯(cuò)后,我們只是關(guān)閉了 socket。socket 需要等到下一次寫(xiě)動(dòng)作發(fā)生時(shí),才會(huì)重新連接。實(shí)際應(yīng)用中,如果這是一個(gè)問(wèn)題,在讀出錯(cuò)后可以直接開(kāi)始重連。這種情況下,還需要一些額外的同步,避免重復(fù)創(chuàng)建 socket。heart beat timeout 的情況類(lèi)似。

          8、跟 TCP/IP 學(xué)協(xié)議設(shè)計(jì)

          如果僅僅是為了使用是 socket,我們大可以不去理會(huì)協(xié)議的細(xì)節(jié)。之所以推薦大家去看一看《TCP/IP 詳解》,是因?yàn)樗鼈冇刑嘀档脤W(xué)習(xí)的地方。很多我們工作中遇到的問(wèn)題,都可以在這里找到答案。

          以下每一個(gè)小節(jié)的標(biāo)題都是一個(gè)小問(wèn)題,建議讀者獨(dú)立思考一下,再繼續(xù)往下看。

          8.1協(xié)議版本如何升級(jí)?

          有這么一句流行的話:這個(gè)世界唯一不變的,就是變化。當(dāng)我們對(duì)協(xié)議版本進(jìn)行升級(jí)的時(shí)候,正確識(shí)別不同版本的協(xié)議對(duì)軟件的兼容非常重要。那么,我們?nèi)绾卧O(shè)計(jì)協(xié)議,才能夠?yàn)閷?lái)的版本升級(jí)做準(zhǔn)備呢?

          答案可以在 IP 協(xié)議找到。

          IP 協(xié)議的第一個(gè)字段叫 version,目前使用的是 4 或 6,分別表示 IPv4 和 IPv6。由于這個(gè)字段在協(xié)議的開(kāi)頭,接收端收到數(shù)據(jù)后,只要根據(jù)第一個(gè)字段的值就能夠判斷這個(gè)數(shù)據(jù)包是 IPv4 還是 IPv6。

          再?gòu)?qiáng)調(diào)一下,這個(gè)字段在兩個(gè)版本的IP協(xié)議都位于第一個(gè)字段,為了做兼容處理,對(duì)應(yīng)的這個(gè)字段必須位于同一位置。文本協(xié)議(如,JSON、HTML)的情況類(lèi)似。

          8.2如何發(fā)送不定長(zhǎng)數(shù)據(jù)的數(shù)據(jù)包?

          舉個(gè)例子,我們用微信發(fā)送一條消息。這條消息的長(zhǎng)度是不確定的,并且每條消息都有它的邊界。我們?nèi)绾蝸?lái)處理這個(gè)邊界呢?

          還是一樣,看看 IP。IP 的頭部有個(gè) header length 和 data length 兩個(gè)字段。通過(guò)添加一個(gè) len 域,我們就能夠把數(shù)據(jù)根據(jù)應(yīng)用邏輯分開(kāi)。

          跟這個(gè)相對(duì)的,還有另一個(gè)方案,那就是在數(shù)據(jù)的末尾放置終止符。比方說(shuō),想 C 語(yǔ)言的字符串那樣,我們?cè)诿總€(gè)數(shù)據(jù)的末尾放一個(gè) \0 作為終止符,用以標(biāo)識(shí)一條消息的尾部。這個(gè)方法帶來(lái)的問(wèn)題是,用戶的數(shù)據(jù)也可能存在 \0。此時(shí),我們就需要對(duì)用戶的數(shù)據(jù)進(jìn)行轉(zhuǎn)義。比方說(shuō),把用戶數(shù)據(jù)的所有 \0 都變成 \0\0。讀消息的過(guò)程總,如果遇到 \0\0,那它就代表 \0,如果只有一個(gè) \0,那就是消息尾部。

          使用 len 字段的好處是,我們不需要對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)義。讀取數(shù)據(jù)的時(shí)候,只要根據(jù) len 字段,一次性把數(shù)據(jù)都讀進(jìn)來(lái)就好,效率會(huì)更高一些。

          終止符的方案雖然要求我們對(duì)數(shù)據(jù)進(jìn)行掃描,但是如果我們可能從任意地方開(kāi)始讀取數(shù)據(jù),就需要這個(gè)終止符來(lái)確定哪里才是消息的開(kāi)頭了。

          當(dāng)然,這兩個(gè)方法不是互斥的,可以一起使用。

          8.3上傳多個(gè)文件,只有所有文件都上傳成功時(shí)才算成功

          現(xiàn)在我們有一個(gè)需求,需要一次上傳多個(gè)文件到服務(wù)器,只有在所有文件都上傳成功的情況下,才算成功。我們?cè)撊绾蝸?lái)實(shí)現(xiàn)呢?

          IP 在數(shù)據(jù)報(bào)過(guò)大的時(shí)候,會(huì)把一個(gè)數(shù)據(jù)報(bào)拆分成多個(gè),并設(shè)置一個(gè) MF (more fragments)位,表示這個(gè)包只是被拆分后的數(shù)據(jù)的一部分。

          好,我們也學(xué)一學(xué) IP。這里,我們可以給每個(gè)文件從 0 開(kāi)始編號(hào)。上傳文件的同時(shí),也攜帶這個(gè)編號(hào),并額外附帶一個(gè) MF 標(biāo)志。除了編號(hào)最大的文件,所有文件的 MF 標(biāo)志都置位。因?yàn)?MF 沒(méi)有置位的是最后一個(gè)文件,服務(wù)器就可以根據(jù)這個(gè)得出總共有多少個(gè)文件。

          另一種不使用 MF 標(biāo)志的方法是,我們?cè)谏蟼魑募埃透嬖V服務(wù)器總共有多少個(gè)文件。

          如果讀者對(duì)數(shù)據(jù)庫(kù)比較熟悉,學(xué)數(shù)據(jù)庫(kù)用事務(wù)來(lái)處理,也是可以的。這里就不展開(kāi)討論了。

          8.4如何保證數(shù)據(jù)的有序性?

          這里講一個(gè)我曾經(jīng)遇到過(guò)的面試題。現(xiàn)在有一個(gè)任務(wù)隊(duì)列,多個(gè)工作線程從中取出任務(wù)并執(zhí)行,執(zhí)行結(jié)果放到一個(gè)結(jié)果隊(duì)列中。先要求,放入結(jié)果隊(duì)列的時(shí)候,順序順序需要跟從工作隊(duì)列取出時(shí)的一樣(也就是說(shuō),先取出的任務(wù),執(zhí)行結(jié)果需要先放入結(jié)果隊(duì)列)。

          我們看看 TCP/IP 是怎么處理的。IP 在發(fā)送數(shù)據(jù)的時(shí)候,不同數(shù)據(jù)報(bào)到達(dá)對(duì)端的時(shí)間是不確定的,后面發(fā)送的數(shù)據(jù)有可能較先到達(dá)。TCP 為了解決這個(gè)問(wèn)題,給所發(fā)送數(shù)據(jù)的每個(gè)字節(jié)都賦了一個(gè)序列號(hào),通過(guò)這個(gè)序列號(hào),TCP 就能夠把數(shù)據(jù)按原順序重新組裝。

          一樣,我們也給每個(gè)任務(wù)賦一個(gè)值,根據(jù)進(jìn)入工作隊(duì)列的順序依次遞增。工作線程完成任務(wù)后,在將結(jié)果放入結(jié)果隊(duì)列前,先檢查要放入對(duì)象的寫(xiě)一個(gè)序列號(hào)是不是跟自己的任務(wù)相同,如果不同,這個(gè)結(jié)果就不能放進(jìn)去。此時(shí),最簡(jiǎn)單的做法是等待,知道下一個(gè)可以放入隊(duì)列的結(jié)果是自己所執(zhí)行的那一個(gè)。但是,這個(gè)線程就沒(méi)辦法繼續(xù)處理任務(wù)了。

          更好的方法是,我們維護(hù)多一個(gè)結(jié)果隊(duì)列的緩沖,這個(gè)緩沖里面的數(shù)據(jù)按序列號(hào)從小到大排序。

          工作線程要將結(jié)果放入,有兩種可能:

          1)剛剛完成的任務(wù)剛好是下一個(gè),將這個(gè)結(jié)果放入隊(duì)列。然后從緩沖的頭部開(kāi)始,將所有可以放入結(jié)果隊(duì)列的數(shù)據(jù)都放進(jìn)去;

          2)所完成的任務(wù)不能放入結(jié)果隊(duì)列,這個(gè)時(shí)候就插入結(jié)果隊(duì)列。然后,跟上一種情況一樣,需要檢查緩沖。

          如果測(cè)試表明,這個(gè)結(jié)果緩沖的數(shù)據(jù)不多,那么使用普通的鏈表就可以。如果數(shù)據(jù)比較多,可以使用一個(gè)最小堆。

          8.5如何保證對(duì)方收到了消息?

          我們說(shuō),TCP 提供了可靠的傳輸。這樣不就能夠保證對(duì)方收到消息了嗎?

          很遺憾,其實(shí)不能。在我們往 socket 寫(xiě)入的數(shù)據(jù),只要對(duì)端的內(nèi)核收到后,就會(huì)返回 ACK,此時(shí),socket 就認(rèn)為數(shù)據(jù)已經(jīng)寫(xiě)入成功。然而要注意的是,這里只是對(duì)方所運(yùn)行的系統(tǒng)的內(nèi)核成功收到了數(shù)據(jù),并不表示應(yīng)用程序已經(jīng)成功處理了數(shù)據(jù)。

          解決辦法還是一樣,我們學(xué) TCP,添加一個(gè)應(yīng)用層的 APP ACK。應(yīng)用接收到消息并處理成功后,發(fā)送一個(gè) APP ACK 給對(duì)方。

          有了 APP ACK,我們需要處理的另一個(gè)問(wèn)題是,如果對(duì)方真的沒(méi)有收到,需要怎么做?

          TCP 發(fā)送數(shù)據(jù)的時(shí)候,消息一樣可能丟失。TCP 發(fā)送數(shù)據(jù)后,如果長(zhǎng)時(shí)間沒(méi)有收到對(duì)方的 ACK,就假設(shè)數(shù)據(jù)已經(jīng)丟失,并重新發(fā)送。

          我們也一樣,如果長(zhǎng)時(shí)間沒(méi)有收到 APP ACK,就假設(shè)數(shù)據(jù)丟失,重新發(fā)送一個(gè)。

          關(guān)于數(shù)據(jù)送達(dá)保證和應(yīng)應(yīng)答機(jī)制,以下文章進(jìn)行了詳細(xì)討論:

          IM消息送達(dá)保證機(jī)制實(shí)現(xiàn)(一):保證在線實(shí)時(shí)消息的可靠投遞

          IM消息送達(dá)保證機(jī)制實(shí)現(xiàn)(二):保證離線消息的可靠投遞

          IM群聊消息如此復(fù)雜,如何保證不丟不重?

          從客戶端的角度來(lái)談?wù)勔苿?dòng)端IM的消息可靠性和送達(dá)機(jī)制

          9、源碼附件下載

          請(qǐng)從鏈接:http://www.52im.net/thread-1722-1-1.html 中下載。

          附錄:更多網(wǎng)絡(luò)編程資料

          技術(shù)往事:改變世界的TCP/IP協(xié)議(珍貴多圖、手機(jī)慎點(diǎn))

          UDP中一個(gè)包的大小最大能多大?

          Java新一代網(wǎng)絡(luò)編程模型AIO原理及Linux系統(tǒng)AIO介紹

          NIO框架入門(mén)(一):服務(wù)端基于Netty4的UDP雙向通信Demo演示

          NIO框架入門(mén)(二):服務(wù)端基于MINA2的UDP雙向通信Demo演示

          NIO框架入門(mén)(三):iOS與MINA2、Netty4的跨平臺(tái)UDP雙向通信實(shí)戰(zhàn)

          NIO框架入門(mén)(四):Android與MINA2、Netty4的跨平臺(tái)UDP雙向通信實(shí)戰(zhàn)

          P2P技術(shù)詳解(一):NAT詳解——詳細(xì)原理、P2P簡(jiǎn)介

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

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

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

          >> 更多同類(lèi)文章 ……

          (本文同步發(fā)布于:http://www.52im.net/thread-1722-1-1.html)



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開(kāi)發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 乐业县| 黄梅县| 辽阳县| 平远县| 资溪县| 凤冈县| 天台县| 海南省| 阿合奇县| 霞浦县| 巫山县| 云林县| 寻甸| 集贤县| 邯郸县| 乃东县| 富顺县| 高安市| 伊春市| 南开区| 甘孜县| 永城市| 开化县| 曲阳县| 信阳市| 临潭县| 崇州市| 久治县| 高平市| 建始县| 托里县| 老河口市| 区。| 吉林省| 姚安县| 隆子县| 岗巴县| 西充县| 马山县| 三亚市| 许昌县|