鮮爲人知的網絡編程(十二):完全搞懂TCP協議層的KeepAlive保活機制

文中引用了參考資料中的部份內容,本文參考資料詳見文末「參考資料」一節,感謝資料分享者。html

一、引言

對於IM開發者而言,網絡保活這件事再熟悉不過了,好比這是我最近一篇有關網絡保活話題文章《一文讀懂即時通信應用中的網絡心跳包機制:做用、原理、實現思路等》,以及我分享的大量代碼實戰編碼中也都必需要考慮這個問題的實現,好比最近的這篇《跟着源碼學IM(五):正確理解IM長鏈接、心跳及重連機制,並動手實現》。java

對於IM這種應用而言,應用層的網絡保活的最直接辦法就是心跳機制,好比主流的IM裏有微信、QQ、釘釘、易信等等,可能代碼實現細節有所差別,但理論上無一例外都是這樣實現。(PS:沒錯,當初微信跟運營商間的「信令危機」就是跟這個有關linux

所謂的網絡心跳,一般是客戶端每隔一小段時間向服務器發送一個數據包(即心跳包),通知服務器本身仍然在線(心跳包中同時可能傳輸一些必要的數據)。發送心跳包,從通訊層面來講就是爲了保持長鏈接,至於這個包的內容,是沒有什麼特別規定的,但在移動端IM中爲了省流量,通常都是很小的包(好比某些第3方的IM云爲了說明心跳不費流量,號稱1字節的心跳包)。程序員

但常常有人會問到,既然TCP協議自己有KeepAlive保活這個東西(見:《TCP/IP詳解 卷1 - 第23章·TCP的保活定時器),爲何還要自已在應用層去實現網絡保活/心跳機制呢?編程

沒錯,一般面視即時通信/IM方面的程序員時,這幾乎是必提問題!瀏覽器

要解答這個問題,我一般建議看看《爲何說基於TCP的移動端IM仍然須要心跳保活?》這篇。但限於篇幅,該篇並無深刻探討TCP協議自己的KeepAlive機制,因此此次借本文想把TCP協議的KeepAlive保活機制給詳細的整理出來,以便你們能深刻其中一窺究竟。安全

二、系列文章

本文是系列文章中的第12篇,本系列文章的大綱以下:服務器

三、TCP KeepAlive的初衷

採用TCP鏈接的C/S模式應用中,當鏈接的雙方在鏈接空閒狀態時,若是任意一方意外崩潰、當機、網線斷開或路由器故障,另外一方沒法得知TCP鏈接已經失效。微信

那麼,鏈接的另外一方並不知道對端的狀況,它會一直維護這個鏈接。而做爲「服務端」來講,長時間的積累會致使很是多的半打開鏈接,形成端系統資源的消耗和浪費,且有可能致使在一個無效的數據鏈路層面發送業務數據,結果就是發送失敗。網絡

因此各端要作到快速感知失敗,減小無效連接操做,這就有了TCP的KeepAlive保活探測機制。

PS:這樣寬泛的說TCP的KeepAlive機制的必要性,貌似還不是頗有說服力,下節將帶着具體的例子深刻分析。

四、從NAT角度更具體地理解TCP KeepAlive的必要性

講到TCP的KeepAlive的必要性,多數文章都是像上節這樣比較籠統的進行說明,但對於愛刨根問底的開發者來講,這還遠遠不夠。

本節將以路由器的NAT機制這個角度來具體分析TCP協議的造物主們設計KeepAlive機制的必要性。

4.1 從NAT原理講起

狹義上,NAT分爲SNAT(原地址轉換)和DNAT(目標地址轉換),關於DNAT,有興趣的同窗能夠自行查閱,這裏只討論SNAT。

咱們都知道,路由器的最基本功能是對第三層(網絡層)上的IP報文進行轉發。實際上,路由器還有很關鍵的一個功能,這即是NAT。特別是對於ISP對普通用戶鏈路上的路由器,NAT功能尤其重要。

爲何要使用NAT?

緣由很簡單:IPv4地址很是稀缺。上網需求龐大,這使得ISP不可能爲每個入網用戶都提供一個獨立的公網IP,所以一般狀況下,ISP會把用戶接入局域網,使得多個用戶共享同一個公網IP,而每個用戶各分得一個局域網內網IP。而鏈接公網和局域網的這臺路由器,稱之爲網關(gateway),NAT的過程就發生在這臺網關路由器上。

PS:P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》這篇文章有助於更深刻的理解NAT原理。

4.2 三層地址轉換

局域網內的主機向公網發出的網絡層IP報文,將經由網關被轉發至公網,而在該轉發過程當中發生了地址轉換。網關將該IP報文中的 源IP地址 從」該主機的內網IP」修改成」網關的公網IP」。

好比:局域網主機得到的內網IP爲192.168.1.100,網關的公網IP爲210.177.63.2,局域網主機向公網目標主機發出的IP報文中,源IP字段數據爲192.168.1.100,在通過網關時,該字段數據將被修改成210.177.63.2。

爲何要這麼作,相信你們已經猜到了:公網上的目標主機在收到這個IP報文後,須要知道這個IP報文的來源地址,並向該來源地址發送響應報文,但若是不通過NAT,目標主機拿到的來源地址是192.168.1.100,這顯然是一個公網上不可訪問到的私有地址,目標主機沒法將響應報文發送到正確的來源主機上。開啓了NAT以後,IP報文的來源地址被網關修改成210.177.63.2,這是一個公網地址,目標主機將向這個地址(即網關路由器的公網地址)發送響應報文。

可是請注意:若是這個IP報文的數據段不含傳輸層協議報文,而是一個pure的網絡層packet,來自目標主機的響應報文是不能被網關準確轉發到多臺局域網主機中的其中一臺的。

PS:ICMP報文除外,其報頭中有Identifier字段用於標識不一樣的主機或進程,網關在處理Identifier時相似於下面提到的運輸層端口。

4.3 傳輸層端口轉換表

在三層地址轉換中,咱們能夠保證局域網內主機向公網發出的IP報文能順利到達目的主機,可是從目的主機返回的IP報文卻不能準確送至指定局域網主機(咱們不能讓網關把IP報文廣播至所有局域網主機,由於這樣必然會帶來安全和性能問題)。

爲了解決這個問題,網關路由器須要藉助傳輸層端口,一般狀況下是TCP或UDP端口,由此來生成一張端口轉換表。

讓咱們經過一個實例來講明端口轉換表如何運做:

假設局域網主機A192.168.1.100須要與公網上的目標主機B210.199.38.2:80進行一次TCP通訊。其中A所在局域網的網關C的公網IP地址爲210.177.63.2。

步驟以下:

1)局域網主機A192.168.1.100發出TCP鏈接請求,A上的TCP端口爲系統分配的53600。該TCP握手包中,包含源地址和端口192.168.1.100:53600,目的地址和端口210.199.38.2:80。

2)網關C將該包的原地址和端口修改成210.177.63.2:63000,其中63000是網關分配的臨時端口。

3)網關C在端口轉換表中增長一條記錄:

4)網關C將修改後的TCP包發送至目的主機B。

5)目的主機B收到後,發送響應TCP包。該響應TCP包含有如下信息:源地址和端口210.199.38.2:80,目的地址和端口210.177.63.2:63000。

6)網關C收到這個來自B的響應包後,隨即在端口轉換表中查找記錄。該記錄須符合如下條件:目的主機IP==210.199.38.2,目的主機端口==80,網關端口==63000。

7)網關C搜索到這條記錄,記錄顯示內網主機IP爲192.168.1.100,內網主機端口爲53600。

8)網關C將該包的目的地址和端口修改成192.168.1.100:53600。

9)網關C隨即將該修改後的TCP包轉發至192.168.1.100:53600,即局域網主機A。此時運輸層數據的一次交換已完成。

4.4 問題來了

在網關C上,因爲端口數量有限(0~65535),端口轉換表的維護佔用系統資源,所以不能無休止地向端口轉換表中增長記錄。對於過時的記錄,網關須要將其刪除。

如何判斷哪些是過時記錄?

網關認爲:一段時間內無活動的鏈接是過時的,應定時檢測轉換表中的非活動鏈接,並將之丟棄。而這個丟棄的過程,網關不會以任何的方式通告該鏈接的任何一端。

經過下圖能夠更直觀的理解這個過程: 

▲ 上圖引用自《TCP保活(TCP keepalive)

那麼問題就來了:若是一個客戶端應用程序因爲業務須要,須要與服務端維持長鏈接(例如基於TCP的IM聊天應用),而若是在特別長的時間內這個鏈接沒有任何的數據交換,網關會認爲這個鏈接過時並將這個鏈接從端口轉換表中丟棄。該鏈接被丟棄時,客戶端和服務端對此是徹底無感知的。在鏈接被丟棄後,客戶端將收不到服務端的數據推送,客戶端發送的數據包也不能到達服務端。

一個具體的例子來感覺一下這個問題的嚴重性:

某財務應用,在客戶端須要填寫大量的表單數據,在客戶端與服務器端創建TCP鏈接後,客戶端終端使用者將花費幾分鐘甚至幾十分鐘填寫表單相關信息,終端使用者終於填好表單所需信息後,點擊「提交」按鈕。

結果,這個時候因爲中間設備早已經將這個TCP鏈接從鏈接表中刪除了,其將直接丟棄這個報文或者給客戶端發送RST報文,應用故障產生,這將致使客戶端終端使用者全部的工做將須要從新來過,給使用者帶來極大的不便和損失。

4.5 解決方法

針對上述問題,TCP協議這一層的解決方法就是利用KeepAlive機制維持長鏈接,讓網關認爲咱們的TCP鏈接是活動的,從而避免網關「幹掉」咱們的長鏈接。

經過NAT這個具體的例子,相信你已經能更具體地理解TCP協議中KeepAlive保活機制的必要性了。

五、TCP Keepalive工做原理

5.1 技術原理

當一個 TCP 鏈接創建以後,啓用 TCP Keepalive 的一端便會啓動一個計時器,當這個計時器數值到達 0 以後(也就是通過tcp_keep-alive_time時間後,這個參數以後會講到),一個 TCP 探測包便會被髮出。這個 TCP 探測包是一個純 ACK 包(RFC1122#TCP Keep-Alives規範建議:不該該包含任何數據,但也能夠包含1個無心義的字節,好比0x0),其 Seq號 與上一個包是重複的,因此其實探測保活報文不在窗口控制範圍內。

若是一個給定的鏈接在兩小時內(默認時長)沒有任何的動做,則服務器就向客戶發一個探測報文段,客戶主機必須處於下表中的4個狀態之一。 

詳細解釋一下就是:

1)客戶主機依然正常運行,並從服務器可達。客戶的TCP響應正常,而服務器也知道對方是正常的,服務器在兩小時後將保活定時器復位。

2)客戶主機已經崩潰,而且關閉或者正在從新啓動。在任何一種狀況下,客戶的TCP都沒有響應。服務端將不能收到對探測的響應,並在75秒後超時。服務器總共發送10個這樣的探測 ,每一個間隔75秒。若是服務器沒有收到一個響應,它就認爲客戶主機已經關閉並終止鏈接。

3)客戶主機崩潰並已經從新啓動。服務器將收到一個對其保活探測的響應,這個響應是一個復位,使得服務器終止這個鏈接。

4)客戶機正常運行,可是服務器不可達,這種狀況與2相似,TCP能發現的就是沒有收到探測的響應。

直觀來講,TCP KeepAlive的交互過程大體以下圖所示: 

▲ 上圖引用自《TCP保活(TCP keepalive)

5.2 具體使用舉例

以linux內核爲例,應用程序若想使用TCP Keepalive,須要設置SO_KEEPALIVE套接字選項才能生效。

對應的,有三個重要的參數:

  • 1)tcp_keepalive_time,在TCP保活打開的狀況下,最後一次數據交換到TCP發送第一個保活探測包的間隔,即容許的持續空閒時長,或者說每次正常發送心跳的週期,默認值爲7200s(2h);
  • 2)tcp_keepalive_probes 在tcp_keepalive_time以後,沒有接收到對方確認,繼續發送保活探測包次數,默認值爲9(次);
  • 3)tcp_keepalive_intvl,在tcp_keepalive_time以後,沒有接收到對方確認,繼續發送保活探測包的發送頻率,默認值爲75s。

上面談的是linux內核參數的配置,實際上其餘編程語言有相應的設置方法。

例如,Java的Netty服務器框架中也提供了相關接口:

ServerBootstrap b = new ServerBootstrap();

            b.group(bossGroup, workerGroup)

             .channel(NioServerSocketChannel.class)

             .option(ChannelOption.SO_BACKLOG, 100)

             // 心跳監測

             .childOption(ChannelOption.SO_KEEPALIVE, true)

             .handler(new LoggingHandler(LogLevel.INFO))

             .childHandler(new ChannelInitializer<SocketChannel>() {

                 @Override

                 public void initChannel(SocketChannel ch) throwsException {

                     ch.pipeline().addLast(

                             new EchoServerHandler());

                 }

             });

            // Start the server.

            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.

            f.channel().closeFuture().sync();

PS:Java程序只能作到設置SO_KEEPALIVE選項,至於TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數配置,應用層面是無法設置的。

六、TCP KeepAlive可能致使的問題

Keepalive 技術只是TCP協議中的一個可選項。由於不當的配置可能會引發一些問題,因此默認是關閉的。

具體來講,可能致使下列問題:

  • 1)在短暫的故障期間,Keepalive設置不合理時可能會由於短暫的網絡波動而斷開健康的TCP鏈接;
  • 2)須要消耗額外的寬帶和流量(對於如今這個時代來講,這貌似已經不是問題了);
  • 3)在以流量計費的互聯網環境中增長了費用開銷。

七、TCP KeepAlive在移動網絡時代的侷限性

不能否認,TCP協議做爲TCP/IP協議族中最重要部分,對互聯的發展確實功不可沒(見:《技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點))。

但現在移動網絡時代,無線通訊愈來愈普及,做爲上個世紀中期發明的TCP協議來講,客觀的講,在某些場景下確實有先天不足(見:《5G時代已經到來,TCP/IP老矣,尚能飯否?)。

那麼,又回到了本文開頭的問題——「既然TCP協議自己有KeepAlive,爲何還要自已在應用層實現網絡保活/心跳機制?」。

以移動端IM應用爲例:

  • 1)一方面,運營商ISP的網絡資源更爲稀缺,TCP協議默認2小時的KeepAlive基本不可能實現IM長鏈接「保活」(爲了提高無線網絡資源的利用率,運營商長則幾分鐘,短則數十秒就有可能回收空閒的網絡鏈接)。
  • 2)另外一面,無線網絡自己存在弱網問題,即便TCP鏈接是「好的」,但實際上處於「假死」狀態,也沒法起到長鏈接該有的做用。

因此說,IM應用層自已作網絡保活(心跳機制)是不可避免的。

有關這方面的更多資料,有興趣,能夠深刻閱讀下面這幾篇:

爲什麼基於TCP協議的移動端IM仍然須要心跳保活機制?

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

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

IM開發者的零基礎通訊技術入門(十三):爲何手機信號差?一文即懂!

IM開發者的零基礎通訊技術入門(十四):高鐵上無線上網有多難?一文即懂!

八、知識拓展:TCP Keepalive和HTTP Keep-Alive有什麼區別?

不少人會把TCP Keepalive 和 HTTP Keep-Alive 這兩個概念搞混淆。

這裏簡單介紹下HTTP Keep-Alive 。

在HTTP/1.0中,默認使用的是短鏈接。也就是說,瀏覽器和服務器每進行一次HTTP操做,就創建一次鏈接,但任務結束就中斷鏈接。若是客戶端瀏覽器訪問的某個HTML或其餘類型的 Web頁中包含有其餘的Web資源,如JavaScript文件、圖像文件、CSS文件等;當瀏覽器每遇到這樣一個Web資源,就會創建一個HTTP會話。

但從 HTTP/1.1起,默認使用長鏈接,用以保持鏈接特性。使用長鏈接的HTTP協議,會在響應頭加上Connection、Keep-Alive字段。

以下圖所示:

HTTP 1.0 和 1.1 在 TCP鏈接使用方面的差別以下圖所示: 

通俗地總結一下:

  • 1)HTTP的Keep-Alive是爲了讓TCP鏈接活得更久一點,在發起多個http請求時能複用同一個鏈接,提升通訊效率;
  • 2)TCP的KeepAlive機制意圖在於探測鏈接的對端是否存活,是一種檢測TCP鏈接情況的保鮮機制。

九、參考資料

[1] TCP保活(TCP keepalive)

[2] TCP協議的KeepAlive機制與HeartBeat心跳包

[3] HTTP keep-alive和TCP keepalive的區別,你瞭解嗎?

[4] TCP KeepAlive 與 HTTP Keep-Alive 區別

[5] tcp鏈接探測Keepalive和心跳包

[6] TCP keepalive的探究 (1) : NAT和保活機制

[7] 理解TCP長鏈接(Keepalive)

[8] 爲什麼基於TCP協議的移動端IM仍然須要心跳保活機制?

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

[10] IM開發者的零基礎通訊技術入門(十三):爲何手機信號差?一文即懂!

本文已同步發佈於「即時通信技術圈」公衆號。

▲ 本文在公衆號上的連接是:點此進入。同步發佈連接是:http://www.52im.net/thread-3506-1-1.html

相關文章
相關標籤/搜索