TCP保活:心跳包/乒乓包/SO_KEEPALIVE
引言:
- 長連線斷開後一直佔用系統資源,可以通過心跳包判斷連線是否斷開;使用心跳包檢測到連線已經死了,就斷開連線。
- 總的來說,心跳包主要也就是用於長連線的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。
TCP保活機制
1.心跳包
由應用程式自己傳送心跳包來檢測連線是否正常,大致的方法是:伺服器在一個 Timer事件中定時向客戶端傳送一個短小精悍的資料包,然後啟動一個低級別的執行緒,在該執行緒中不斷檢測客戶端的迴應, 如果在一定時間內沒有收到客戶端的迴應,即認為客戶端已經掉線;同樣,如果客戶端在一定時間內沒有收到伺服器的心跳包,則認為連線不可用。
注意
2.乒乓包
舉例:微信朋友圈有人評論,客戶端怎麼知道有人評論?伺服器怎麼將評論發給客戶端的?
- 微信客戶端每隔一段時間就向伺服器詢問,是否有人評論?
- 當伺服器檢查到有人給評論時,伺服器傳送一個乒乓包給客戶端,該乒乓包中攜帶的資料是[此時有人評論的標誌位]
注:步驟1和2,伺服器和客戶端不需要建立連線,只是傳送簡單的乒乓包。 - 當客戶端接收到伺服器回覆的帶有評論標誌位的乒乓包後,才真正的去和伺服器通過三次握手建立連線;建立連線後,伺服器將評論的資料傳送給客戶端。
注意:乒乓包是攜帶很簡單的資料的包
3.設定TCP屬性: SO_KEEPALIVE
1.因為要考慮到一個伺服器通常會連線多個客戶端,因此由使用者在應用層自己實現心跳包,程式碼較多 且稍顯複雜,而利用TCP/IP協議層為內建的KeepAlive功能來實現心跳功能則簡單得多。
2.不論是服務端還是客戶端,一方開啟KeepAlive功能後,就會自動在規定時間內向對方傳送心跳包, 而另一方在收到心跳包後就會自動回覆,以告訴對方我仍然線上。
3.因為開啟KeepAlive功能需要消耗額外的寬頻和流量,所以TCP協議層預設並不開啟KeepAlive功 能,儘管這微不足道,但在按流量計費的環境下增加了費用,另一方面,KeepAlive設定不合理時可能會 因為短暫的網路波動而斷開健康的TCP連線。並且,預設的KeepAlive超時需要7,200,000 MilliSeconds, 即2小時,探測次數為5次。對於很多服務端應用程式來說,2小時的空閒時間太長。
4.因此,我們需要手工開啟KeepAlive功能並設定合理的KeepAlive引數。
在《UNIX網路程式設計第1卷》中也有詳細的闡述:
SO_KEEPALIVE:保持連線,檢測對方主機是否崩潰,避免(伺服器)永遠阻塞於TCP連線的輸入。設定該選項後,如果2小時內在此套介面的任一方向都沒有資料交換,TCP就自動給對方 發一個保持存活探測分節(keepalive
probe)。這是一個對方必須響應的TCP分節.它會導致以下三種情況:
- 對方接收一切正常:以期望的ACK響應。2小時後,TCP將發出另一個探測分節。
- 對方已崩潰且已重新啟動:以RST響應。套介面的待處理錯誤被置為ECONNRESET,套介面本身則被關閉。
- 對方無任何響應:源自berkeley的TCP傳送另外8個探測分節,相隔75秒一個,試圖得到一個響應。在發出第一個探測分節11分鐘15秒後若仍無響應就放棄。套介面的待處理錯誤被置為ETIMEOUT,套介面本身則被關閉。如ICMP錯誤是“host unreachable(主機不可達)”,說明對方主機並沒有崩潰,但是不可達,這種情況下待處理錯誤被置為EHOSTUNREACH。
根據上面的介紹可以知道對端以一種非優雅的方式斷開連線的時候,可以設定SO_KEEPALIVE屬性使得在2小時以後發現對方的TCP連線是否依然存在。如果不能接受如此之長的等待時間,從TCP-Keepalive-HOWTO上可以知道一共有兩種方式可以設定:
- 修改核心關於網路方面的配置引數
- SOL_TCP欄位的TCP_KEEPIDLE,TCP_KEEPINTVL,TCP_KEEPCNT三個選項
int keepIdle = 6; /*開始首次KeepAlive探測前的TCP空閉時間 */
int keepInterval = 5; /* 兩次KeepAlive探測間的時間間隔 */
int keepCount = 3; /* 判定斷開前的KeepAlive探測次數 */
Setsockopt(listenfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepIdle, sizeof(keepIdle));
Setsockopt(listenfd, SOL_TCP,TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
Setsockopt(listenfd,SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));