從keep-alive原理 分析TCP遊戲服務端心跳包的實用功能

QQ截圖20150918140635.jpg

http://www.cocoachina.com/game/20150918/13501.html

整理自知乎,文/郭無心

遊戲服務器常常有心跳包的設計。

我們的心跳包就是爲了防止Socket斷開連接,或是TCP的連接斷開嗎?

答案是否定的,TCP連接的通道是個虛擬的,連接的維持靠的是兩端TCP軟件對連接狀態的維護。

TCP 連接自身有維護連接的機制,說白了就是自身有長時間沒有數據包情況下的判斷連接是否還存在的檢測,清除死連接,即使在沒有數據來往的時候,TCP也就可以(在啓動TCP這個功能的前提下)自動發包檢測是否連接正常,這個不需要我們處理。

服務端設計心跳包的目的:

探知對端應用是否存活,服務端客戶端都可以發心跳包,一般都是客戶端發送心跳包,服務端用於判斷客戶端是否在線,從而對服務端內存緩存數據進行清理(玩家下線等);問題在於,通過TCP四次握手斷開的設定,我們也是可以通過Socket的read方法來判斷TCP連接是否斷開,從而做出相應的清理內存動作,那麼爲什麼我們還需要使用客戶端發送心跳包來判斷呢?

第一種判斷客戶端是否在線策略:

直接監控TCP傳輸協議的返回值,通過返回值處理應用層的存活判斷

比如在C++當中

使用poll的IO複用方法時:

1
2
if(fds[i].revents & POLLERR)
if(fds[i].events & POLLDHU

P)

通過上述判斷可以探知TCP連接的正確性從而在服務器也關閉對應的連接,此時調用close()函數纔會釋放相關的資源。

比如說進行如下處理:

/*如果客戶端關閉連接,則服務器也關閉對應的連接,並將用戶總數減1*/

1
2
3
4
5
6
users[fds[i].fd] = users[fds[user_counter].fd];
close(fds[i].fd);
fds[i] = fds[user_counter];
i--;
user_counter--;
printf("a client left\n");

又比如在Java中:

在Java的阻塞編程中:通過

1
2
3
4
5
6
7
8
9
10
ServerSocket ss = new ServerSocket(10021);
Socket so = ss.accept();
// 獲取相關流對象
InputStream in = so.getInputStream();
byte[] bytes = new byte[1024];
int num = in.read(bytes);
if(num == -1)//表明讀到了流的末尾,事實也就是client端斷開了連接,比如調用close()
{
    so.close();
}

在Java的非阻塞編程當中:通過

1
2
3
4
5
6
7
8
SelectionKey key = selector.register(socketChannel,ops,handle);
SocketChannel socketChanel = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = socketChannel.read(buffer);
if(num == -1)
{
    key.channel().close();
}

上述連接處理方式,返回-1也好,收到POLLERR POLLDHUP也好,都是收到了客戶端的fin或者rst之後的反應,所以根據四次分手原則,我們調用close方法,發送fin給客戶端。

上面這種策略通過TCP協議的返回值來得知客戶端TCP斷開,從而得知客戶端掉線。

當前提是如果提前根據ip或者mac做了記錄,所以可以在服務器端收到TCP連接中斷的消息後,調用close,並且通過socket得到玩家socket數據(具體如IP地址),從而獲得user信息從而清除數據。

那麼這種方式有什麼不完美呢?或者說有什麼缺陷呢?

主要原因就是TCP的斷開可能有時候是不能瞬時探知的,甚至是不能探知的,也可能有很長時間的延遲,如果前端沒有正常的斷開TCP連接,四次握手沒有發起,服務端無從得知客戶端的掉線,那麼就要依靠開啓TCP的keep alive機制,but TCP協議的keep alive機制是不建議開啓的,即使開啓了默認的時間間隔是2h,淚奔!如果要服務端維持一個2h的死鏈接,那是可怕的,如果我們調整了時間間隔,也是有problem的,因爲TCP本身就不建議TCP層的心跳檢測,因爲這可能導致一個完好的TCP連接在中間網絡中斷的情況下重啓⊙▂⊙。

下面有必要先介紹下TCP的keep-alive機制。

關於TCP自己的keep-alive機制,參看TCP/IP詳解當中有如下介紹:

使用keeplive機制,可以檢測到網絡異常。

一、什麼是keepalive定時器?

在一個空閒的(idle)TCP連接上,沒有任何的數據流,許多TCP/IP的初學者都對此感到驚奇。也就是說,如果TCP連接兩端沒有任何一個進程在向對方發送數據,那麼在這兩個TCP模塊之間沒有任何的數據交換。你可能在其它的網絡協議中發現有輪詢(polling),但在TCP中它不存在。言外之意就是我們只要啓動一個客戶端進程,同服務器建立了TCP連接,不管你離開幾小時,幾天,幾星期或是幾個月,連接依舊存在。中間的路由器可能崩潰或者重啓,電話線可能go down或者back up,只要連接兩端的主機沒有重啓,連接依舊保持建立。

這就可以認爲不管是客戶端的還是服務器端的應用程序都沒有應用程序級(application-level)的定時器來探測連接的不活動狀態(inactivity),從而引起任何一個應用程序的終止。然而有的時候,服務器需要知道客戶端主機是否已崩潰並且關閉,或者崩潰但重啓。許多實現提供了存活定時器來完成這個任務。

存活定時器是一個包含爭議的特徵。許多人認爲,即使需要這個特徵,這種對對方的輪詢也應該由應用程序來完成,而不是由TCP中實現。此外,如果兩個終端系統之間的某個中間網絡上有連接的暫時中斷,那麼存活選項(option)就能夠引起兩個進程間一個良好連接的終止。例如,如果正好在某個中間路由器崩潰、重啓的時候發送存活探測,TCP就將會認爲客戶端主機已經崩潰,但事實並非如此。

存活(keepalive)並不是TCP規範的一部分。在Host Requirements RFC羅列有不使用它的三個理由:
(1)在短暫的故障期間,它們可能引起一個良好連接(good connection)被釋放(dropped);
(2)它們消費了不必要的寬帶;
(3)在以數據包計費的互聯網上它們(額外)花費金錢。然而,在許多的實現中提供了存活定時器;

一些服務器應用程序可能代表客戶端佔用資源,它們需要知道客戶端主機是否崩潰。存活定時器可以爲這些應用程序提供探測服務。Telnet服務器和Rlogin服務器的許多版本都默認提供存活選項。

個人計算機用戶使用TCP/IP協議通過Telnet登錄一臺主機,這是能夠說明需要使用存活定時器的一個常用例子。如果某個用戶在使用結束時只是關掉了電源,而沒有註銷(log off),那麼他就留下了一個半打開(half-open)的連接。在圖18.16,我們看到如何在一個半打開連接上通過發送數據,得到一個復位(reset)返回,但那是在客戶端,是由客戶端發送的數據。如果客戶端消失,留給了服務器端半打開的連接,並且服務器又在等待客戶端的數據,那麼等待將永遠持續下去。存活特徵的目的就是在服務器端檢測這種半打開連接。

二、keepalive如何工作?

在此描述中,我們稱使用存活選項的那一段爲服務器,另一端爲客戶端。也可以在客戶端設置該選項,且沒有不允許這樣做的理由,但通常設置在服務器。如果連接兩端都需要探測對方是否消失,那麼就可以在兩端同時設置(比如NFS)。

若在一個給定連接上,兩小時之內無任何活動,服務器便向客戶端發送一個探測段。(我們將在下面的例子中看到探測段的樣子。)客戶端主機必須是下列四種狀態之一:

1) 客戶端主機依舊活躍(up)運行,並且從服務器可到達。從客戶端TCP的正常響應,服務器知道對方仍然活躍。服務器的TCP爲接下來的兩小時復位存活定時器,如果在這兩個小時到期之前,連接上發生應用程序的通信,則定時器重新爲往下的兩小時復位,並且接着交換數據。

2) 客戶端已經崩潰,或者已經關閉(down),或者正在重啓過程中。在這兩種情況下,它的TCP都不會響應。服務器沒有收到對其發出探測的響應,並且在75秒之後超時。服務器將總共發送10個這樣的探測,每個探測75秒。如果沒有收到一個響應,它就認爲客戶端主機已經關閉並終止連接。

3) 客戶端曾經崩潰,但已經重啓。這種情況下,服務器將會收到對其存活探測的響應,但該響應是一個復位,從而引起服務器對連接的終止。

4) 客戶端主機活躍運行,但從服務器不可到達。這與狀態2類似,因爲TCP無法區別它們兩個。它所能表明的僅是未收到對其探測的回覆。

服務器不必擔心客戶端主機被關閉然後重啓的情況(這裏指的是操作員執行的正常關閉,而不是主機的崩潰)。當系統被操作員關閉時,所有的應用程序進程(也就是客戶端進程)都將被終止,客戶端TCP會在連接上發送一個FIN。收到這個FIN後,服務器TCP向服務器進程報告一個文件結束,以允許服務器檢測這種狀態。

在第一種狀態下,服務器應用程序不知道存活探測是否發生。凡事都是由TCP層處理的,存活探測對應用程序透明,直到後面2,3,4三種狀態發生。在這三種狀態下,通過服務器的TCP,返回給服務器應用程序錯誤信息。(通常服務器向網絡發出一個讀請求,等待客戶端的數據。如果存活特徵返回一個錯誤信息,則將該信息作爲讀操作的返回值返回給服務器。)在狀態2,錯誤信息類似於“連接超時”。狀態3則爲“連接被對方復位”。第四種狀態看起來像連接超時,或者根據是否收到與該連接相關的ICMP錯誤信息,而可能返回其它的錯誤信息。

具體來說:

在TCP協議的機制裏面,本身的心跳包機制,也就是TCP協議中的SO_KEEPALIVE,系統默認是設置2小時的心跳頻率。需要用要用setsockopt將SOL_SOCKET.SO_KEEPALIVE設置爲1纔是打開,並且可以設置三個參數tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl,

分別表示連接閒置多久開始發keepalive的ACK包、發幾個ACK包不回覆才當對方死了、兩個ACK包之間間隔多長。

TCP協議會向對方發一個帶有ACK標誌的空數據包(KeepAlive探針),對方在收到ACK包以後,如果連接一切正常,應該回復一個ACK;如果連接出現錯誤了(例如對方重啓了,連接狀態丟失),則應當回覆一個RST;如果對方沒有回覆,服務器每隔多少時間再發ACK,如果連續多個包都被無視了,說明連接被斷開了。

“心跳檢測包”是屬於TCP協議底層的檢測機制,上位機軟件只是解析顯示網口的有用數據包,收到心跳包報文屬於TCP協議層的數據,一般軟件不會將它直接在應用層顯示出來,所以看不到。以太網中的“心跳包”可以通過“以太網抓包軟件”分析TCP/IP協議層的數據流看到。報文名稱”TCP Keep-Alive”。

一些比較可靠的以太網轉串口模塊,都有心跳包的檢測,比如致遠電子的ZNE-100TL模塊,配置“心跳包檢測”間隔時間設爲“10”秒,使用一款”wireshark”的抓包軟件來實際查看下TCP/IP協議層“心跳包”數據。

看了上面的內容,使用TCP自己的keep-alive機制,也是可以實現連接維持,通過TCP傳輸層的心跳包探知兩端TCP連接的正確性,從而得知應用層的情況(TCP在,應用一定在,TCP不在了,應用一定不在了),但是這不是最優選擇!

那麼既然有TCP的心跳機制,我們爲什麼還要在應用層實現自己的心跳檢測機制呢?

評論中@Raynor所說:

tcpip詳解卷1有網絡異常中斷的3種情況,比如os回收端口的時候發送的rst包,如果os掛了就不會發這個消息了。 另外如果網絡異常,有可能收到路由器返回的icmp主機不可達消息從而關閉連接。 keepalive之所以不靠譜,是因爲需要從socket error獲知連接斷開,而且是被動斷開。 而應用層自己實現的heartbeat可以自主檢測超時,更方便修改超時時間和斷開前處理。

以及@李樂所說:

keepalive設計初衷清除和回收死亡時間長的連接,不適合實時性高的場合,而且它會先要求連接一定時間內沒有活動,週期長,這樣其實已經斷開很長一段時間,沒有及時性。而且keepalive好像還不能主動通知應用層,需要主動調用api去檢測異常。

三、使用自己應用層的心跳包,上述方法用於正常情況下的TCP連接維護

場景舉例如下:在遊戲服務器當中,內存中維護着衆多玩家的在線數據,以方便調用,比如玩家的英雄隊伍信息,玩家的世界位置信息,在玩家下線的時候,服務器必須知道並且清除掉數據(不然就會出現一個已經下線的玩家出現在世界上),在上述舉例中,在服務器端收到TCP連接中斷的消息後,調用close,期間可以通過socket得到玩家socket數據,從而獲得user信息(如果提前根據ip或者mac做了記錄)從而清除數據。

但是如果不是正常的玩家下線,TCP的四次握手沒有成功,比如網絡直接中斷,client端的TCP協議的fin包沒有發出去,服務端就不能及時探知玩家下線,如果依賴上面講的TCP自己的keep alive探測機制,時間較長,做不到及時處理下線玩家,並且性能不佳,因爲TCP/IP的設計者本身就不支持TCP的心跳,因爲這可能因爲中間網絡的短暫中斷導致兩端良好的TCP連接斷開。所以一般不啓用TCP的心跳機制,那我們怎麼處理這些異常下線呢?如果不處理,服務端將一直維護着TCP死連接,導致網絡資源(一臺服務器可以支持的TCP連接有限)和內存資源(內存中可能維護着該玩家的數據)的佔用,所以就要用到應用層的心跳包了

下面解釋下應用層的心跳包:

心跳包,通常是客戶端每隔一小段時間向服務器發送的一個數據包,通知服務器自己仍然在線,服務器與客戶端之間每隔一段時間 進行一次交互,來判斷這個鏈接是否有效,並傳輸一些可能有必要的數據。通常是在建立了一個TCP的socket連接後無法保證這個連接是否持續有效,這時候兩邊應用會通過定時發送心跳包來保證連接是有效的。因按照一定的時間間隔發送,類似於心跳,所以叫做心跳包。事實上爲了保持長連接(長連接指的是建立一次TCP連接之後,就認爲連接有效,利用這個連接去不斷傳輸數據,不斷開TCP連接),至於包的內容,是沒有特別規定的,不過一般都是很小的包,或者只是包含包頭的一個空包。

1442556223679840.jpg

那麼心跳包的意義就在於方便的在服務端管理客戶端的在線情況,並且可以防止TCP的死連接問題,避免出現長時間不在線的死鏈接仍然出現在服務端的管理任務中。

再舉下面一個例子說明下爲什麼TCP自身的四次握手斷開機制不能完全保證應用程序探知連接斷開從而避免死連接。

(1)做一個遊戲客戶端和服務器端的測試,手動強制關閉客戶端進程,然後查看服務器的情況,結果往往是,服務器收到了客戶端關閉的事件。其實, 這樣會忽略一個問題,沒有觸發異常中斷事件,比如網絡中斷。

(2)手動關閉客戶端進程,事實上並不能測試出想要的結果,因爲進程是在應用層的,所以,這種測試方法不能保證網絡驅動層也不發送數據報文給服務器。經過測試會發現,當應用層強制結束進程時,對於TCP連接,驅動層會發送reset數據包!而服務器收到這個數據包就可以正常關閉了!

(3)那麼,如果網絡異常甚至直接拔掉網線呢,服務器收不到這個數據包,就會導致死連接存在!

(4)所以,心跳包是必要的,或者使用TCP協議本身的Keep-alive來設置(但是keep-alive不夠好)

我們不能誤解TCP連接如同一條繩子,一方斷開了,另外一方必然會知道的。事實上TCP連接,這個“面向連接”的物理連接並不存在,它只是抽象出來的概念,是一個虛擬的連接,對於物理層,對於網線、光纖而言,不存在連接不連接的概念,因爲,對它們而言,無非就是一些電流脈衝而已,具體就是一個一個的IP報文。

TCP的連接,不過是通過ACK、SEQ這些機制來模擬實現的,具體比如差錯重傳,擁塞控制

那麼心跳包的檢測發送處理對服務器資源的耗費怎麼判斷?

這個要看心跳包發送的頻率,我們可以自行設置

另外這裏有個例程,模擬了socket心跳包的C語言實現:《Socket心跳包異常檢測的C語言實現,服務器與客戶端代碼案例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章