文中引用了參考資料中的部分內容,本文參考資料詳見文末“參考資料”一節,感謝資料分享者。
1、引言
對于IM開發者而言,網絡保活這件事再熟悉不過了,比如這是我最近一篇有關網絡保活話題文章《一文讀懂即時通訊應用中的網絡心跳包機制:作用、原理、實現思路等》,以及我分享的大量代碼實戰編碼中也都必須要考慮這個問題的實現,比如最近的這篇《跟著源碼學IM(五):正確理解IM長連接、心跳及重連機制,并動手實現》。
對于IM這種應用而言,應用層的網絡保活的最直接辦法就是心跳機制,比如主流的IM里有微信、QQ、釘釘、易信等等,可能代碼實現細節有所差異,但理論上無一例外都是這樣實現。(PS:沒錯,當初微信跟運營商間的“信令危機”就是跟這個有關)
所謂的網絡心跳,通常是客戶端每隔一小段時間向服務器發送一個數據包(即心跳包),通知服務器自己仍然在線(心跳包中同時可能傳輸一些必要的數據)。發送心跳包,從通信層面來說就是為了保持長連接,至于這個包的內容,是沒有什么特別規定的,但在移動端IM中為了省流量,一般都是很小的包(比如某些第3方的IM云為了說明心跳不費流量,號稱1字節的心跳包)。
但經常有人會問到,既然TCP協議本身有KeepAlive保活這個東西(見:《TCP/IP詳解 卷1 - 第23章·TCP的保活定時器》),為什么還要自已在應用層去實現網絡保活/心跳機制呢?
沒錯,通常面視即時通訊/IM方面的程序員時,這幾乎是必提問題!
要解答這個問題,我通常建議看看《為什么說基于TCP的移動端IM仍然需要心跳保活?》這篇。但限于篇幅,該篇并沒有深入探討TCP協議本身的KeepAlive機制,所以這次借本文想把TCP協議的KeepAlive保活機制給詳細的整理出來,以便大家能深入其中一窺究竟。
學習交流:
- 即時通訊/推送技術開發交流5群:215477170 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文已同步發布于:http://www.52im.net/thread-3506-1-1.html)
2、系列文章
本文是系列文章中的第12篇,本系列文章的大綱如下:
3、TCP KeepAlive的初衷
采用TCP連接的C/S模式應用中,當連接的雙方在連接空閑狀態時,如果任意一方意外崩潰、當機、網線斷開或路由器故障,另一方無法得知TCP連接已經失效。
那么,連接的另一方并不知道對端的情況,它會一直維護這個連接。而作為“服務端”來說,長時間的積累會導致非常多的半打開連接,造成端系統資源的消耗和浪費,且有可能導致在一個無效的數據鏈路層面發送業務數據,結果就是發送失敗。
所以各端要做到快速感知失敗,減少無效鏈接操作,這就有了TCP的KeepAlive保活探測機制。
PS:這樣寬泛的說TCP的KeepAlive機制的必要性,貌似還不是很有說服力,下節將帶著具體的例子深入分析。
4、從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保活機制的必要性了。
5、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等參數配置,應用層面是沒法設置的。
6、TCP KeepAlive可能導致的問題
Keepalive 技術只是TCP協議中的一個可選項。因為不當的配置可能會引起一些問題,所以默認是關閉的。
具體來說,可能導致下列問題:
- 1)在短暫的故障期間,Keepalive設置不合理時可能會因為短暫的網絡波動而斷開健康的TCP連接;
- 2)需要消耗額外的寬帶和流量(對于現在這個時代來說,這貌似已經不是問題了);
- 3)在以流量計費的互聯網環境中增加了費用開銷。
7、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開發者的零基礎通信技術入門(十四):高鐵上無線上網有多難?一文即懂!》
8、知識拓展: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連接狀況的保鮮機制。
9、參考資料
[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開發者的零基礎通信技術入門(十三):為什么手機信號差?一文即懂!
附錄:更多網絡編程精華文章
[1] 網絡編程(基礎)資料:
《網絡編程懶人入門(一):快速理解網絡通信協議(上篇)》
《網絡編程懶人入門(二):快速理解網絡通信協議(下篇)》
《網絡編程懶人入門(三):快速理解TCP協議一篇就夠》
《網絡編程懶人入門(四):快速理解TCP和UDP的差異》
《網絡編程懶人入門(五):快速理解為什么說UDP有時比TCP更有優勢》
《網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門》
《網絡編程懶人入門(七):深入淺出,全面理解HTTP協議》
《網絡編程懶人入門(八):手把手教你寫基于TCP的Socket長連接》
《網絡編程懶人入門(九):通俗講解,有了IP地址,為何還要用MAC地址?》
《網絡編程懶人入門(十):一泡尿的時間,快速讀懂QUIC協議》
《網絡編程懶人入門(十一):一文讀懂什么是IPv6》
《網絡編程懶人入門(十二):快速讀懂Http/3協議,一篇就夠!》
《腦殘式網絡編程入門(一):跟著動畫來學TCP三次握手和四次揮手》
《腦殘式網絡編程入門(二):我們在讀寫Socket時,究竟在讀寫什么?》
《腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識》
《腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)》
《腦殘式網絡編程入門(五):每天都在用的Ping命令,它到底是什么?》
《腦殘式網絡編程入門(六):什么是公網IP和內網IP?NAT轉換又是什么鬼?》
《腦殘式網絡編程入門(七):面視必備,史上最通俗計算機網絡分層詳解》
《腦殘式網絡編程入門(八):你真的了解127.0.0.1和0.0.0.0的區別?》
《腦殘式網絡編程入門(九):面試必考,史上最通俗大小端字節序詳解》
《網絡編程入門從未如此簡單(一):假如你來設計網絡,會怎么做?》
《網絡編程入門從未如此簡單(二):假如你來設計TCP協議,會怎么做?》
>> 更多同類文章 ……
[2] 網絡編程(高階)資料:
《高性能網絡編程(一):單臺服務器并發TCP連接數到底可以有多少》
《高性能網絡編程(二):上一個10年,著名的C10K并發連接問題》
《高性能網絡編程(三):下一個10年,是時候考慮C10M并發問題了》
《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》
《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》
《高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型》
《高性能網絡編程(七):到底什么是高并發?一文即懂!》
《不為人知的網絡編程(一):淺析TCP協議中的疑難雜癥(上篇)》
《不為人知的網絡編程(二):淺析TCP協議中的疑難雜癥(下篇)》
《不為人知的網絡編程(三):關閉TCP連接時為什么會TIME_WAIT、CLOSE_WAIT》
《不為人知的網絡編程(四):深入研究分析TCP的異常關閉》
《不為人知的網絡編程(五):UDP的連接性和負載均衡》
《不為人知的網絡編程(六):深入地理解UDP協議并用好它》
《不為人知的網絡編程(七):如何讓不可靠的UDP變的可靠?》
《不為人知的網絡編程(八):從數據傳輸層深度解密HTTP》
《不為人知的網絡編程(九):理論聯系實際,全方位深入理解DNS》
《不為人知的網絡編程(十):深入操作系統,從內核理解網絡包的接收過程(Linux篇)》
《不為人知的網絡編程(十一):從底層入手,深度分析TCP連接耗時的秘密》
《不為人知的網絡編程(十二):徹底搞懂TCP協議層的KeepAlive保活機制》
《IM開發者的零基礎通信技術入門(十一):為什么WiFi信號差?一文即懂!》
《IM開發者的零基礎通信技術入門(十二):上網卡頓?網絡掉線?一文即懂!》
《IM開發者的零基礎通信技術入門(十三):為什么手機信號差?一文即懂!》
《IM開發者的零基礎通信技術入門(十四):高鐵上無線上網有多難?一文即懂!》
《IM開發者的零基礎通信技術入門(十五):理解定位技術,一篇就夠》
>> 更多同類文章 ……
本文已同步發布于“即時通訊技術圈”公眾號。

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