正確理解IM長鏈接的心跳及重連機制,並動手實現(有完整IM源碼)

一、引言

說道「心跳」這個詞你們都不陌生,固然不是指男女之間的心跳,而是和長鏈接相關的。顧名思義就是證實是否還活着的依據。html

什麼場景下須要心跳呢?目前咱們接觸到的大可能是一些基於長鏈接的應用須要心跳來「保活」。git

因爲在長鏈接的場景下,客戶端和服務端並非一直處於通訊狀態,若是雙方長期沒有溝通則雙方都不清楚對方目前的狀態,因此須要發送一段很小的報文告訴對方「我還活着」。github

同時還有另外幾個目的:算法

1)服務端檢測到某個客戶端遲遲沒有心跳過來能夠主動關閉通道,讓它下線;apache

2)客戶端檢測到某個服務端遲遲沒有響應心跳也能重連獲取一個新的鏈接。編程

本文正好藉着在CIM系統中有這樣兩個需求(CIM是本文做者從零開發的一個學習性質的IM系統,詳見《拿起鍵盤就是幹:跟我一塊兒徒手開發一套分佈式IM系統》),正好來聊一聊我是如何理解IM長鏈接的心跳及重連機制,以及又是怎麼踩坑已及填坑的。微信

本文配套的CIM源碼地址:網絡

主要鏡像:https://github.com/crossoverJie/cimapp

備用鏡像:https://github.com/52im/cim框架

閱讀本文須要必定的網絡編程以及Netty方面的知識。

二、關於做者

crossoverJie(陳杰): 90後,畢業於重慶信息工程學院,現供職於重慶豬八戒網絡有限公司。

做者的博客:https://crossoverjie.top

做者的Github:https://github.com/crossoverJie

本文做者的其它文章:

拿起鍵盤就是幹:跟我一塊兒徒手開發一套分佈式IM系統

技術乾貨:從零開始,教你設計一個百萬級的消息推送系統

三、參考資料

➊ 有關網絡心跳保活方面的理論文章:

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

微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)

移動端IM實踐:實現Android版微信的智能心跳機制

移動端IM實踐:WhatsApp、Line、微信的心跳策略分析

一文讀懂即時通信應用中的網絡心跳包機制:做用、原理、實現思路等

融雲技術分享:融雲安卓端IM產品的網絡鏈路保活技術實踐

➋ 有關網絡心跳保活方面的實踐文章:

MobileIMSDK——一套開源的原創移動端即時通信框架(有完整的心跳保活邏輯和代碼實現)》

一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)

手把手教你用Netty實現網絡通訊程序的心跳機制、斷線重連機制

適合新手:從零開發一個IM服務端(基於Netty,有完整源碼)

拿起鍵盤就是幹:跟我一塊兒徒手開發一套分佈式IM系統

自已開發IM有那麼難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)

四、心跳實現方式

心跳其實有兩種實現方式:

1)TCP 協議實現(keepalive 機制,詳見《TCP/IP詳解 卷1:協議-第23章 TCP的保活定時器》);

2)應用層本身實現。

因爲 TCP 協議過於底層,對於開發者來講維護性、靈活度都比較差同時還依賴於操做系統(詳見:《爲什麼基於TCP協議的移動端IM仍然須要心跳保活機制?》)。

因此咱們這裏所討論的都是應用層的實現:

 

如上圖所示,在應用層一般是由客戶端發送一個心跳包 ping 到服務端,服務端收到後響應一個 pong 代表雙方都活得好好的。一旦其中一端延遲 N 個時間窗口沒有收到消息則進行不一樣的處理。

五、客戶端自動重連

先拿客戶端來講吧,每隔一段時間客戶端向服務端發送一個心跳包,同時收到服務端的響應。

 

常規的實現應當是:

  • 1)開啓一個定時任務,按期發送心跳包;
  • 2)收到服務端響應後更新本地時間;
  • 3)再有一個定時任務按期檢測這個「本地時間」是否超過閾值;
  • 4)超事後則認爲服務端出現故障,須要重連。

這樣確實也能實現心跳,但並不友好。

在正常的客戶端和服務端通訊的狀況下,定時任務依然會發送心跳包;這樣就顯得沒有意義,有些多餘。因此理想的狀況應當是客戶端收到的寫消息空閒時才發送這個心跳包去確認服務端是否健在。

好消息是 Netty 已經爲咱們考慮到了這點,自帶了一個開箱即用的 IdleStateHandler 專門用於心跳處理。

來看看 cim 中的實現:

 

在 pipeline 中加入了一個 10秒沒有收到寫消息的 IdleStateHandler,到時他會回調 ChannelInboundHandler 中的 userEventTriggered 方法。

 

因此一旦寫超時就立馬向服務端發送一個心跳(作的更完善應當在心跳發送失敗後有必定的重試次數)。

這樣也就只有在空閒時候纔會發送心跳包。但一旦間隔許久沒有收到服務端響應進行重連的邏輯應當寫在哪裏呢?

 

先來看這個示例:

 

當收到服務端響應的 pong 消息時,就在當前 Channel 上記錄一個時間,也就是說後續能夠在定時任務中取出這個時間和當前時間的差額來判斷是否超過閾值。

超過則重連。

 
 

同時在每次心跳時候都用當前時間和以前服務端響應綁定到 Channel 上的時間相減判斷是否須要重連便可。

也就是  heartBeatHandler.process(ctx); 的執行邏輯。

僞代碼以下:

@Override

public void process(ChannelHandlerContext ctx) throws Exception {

 

    longheartBeatTime = appConfiguration.getHeartBeatTime() * 1000;

 

    Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel());

    longnow = System.currentTimeMillis();

    if(lastReadTime != null&& now - lastReadTime > heartBeatTime){

        reconnect();

    }

}

六、IdleStateHandler 誤區

一切看起來也沒毛病,但實際上卻沒有這樣實現重連邏輯。最主要的問題仍是對 IdleStateHandler 理解有誤。

咱們假設下面的場景:

1)客戶端經過登陸連上了服務端並保持長鏈接,一切正常的狀況下雙方各發心跳包保持鏈接;

2)這時服務端突入出現 down 機,那麼理想狀況下應當是客戶端遲遲沒有收到服務端的響應從而 userEventTriggered 執行定時任務;

3)判斷當前時間 - UpdateWriteTime > 閾值 時進行重連。

但卻事與願違,並不會執行 二、3兩步。

由於一旦服務端 down 機、或者是與客戶端的網絡斷開則會回調客戶端的 channelInactive 事件。

IdleStateHandler 做爲一個 ChannelInbound 也重寫了 channelInactive() 方法。

 
 

這裏的 destroy() 方法會把以前開啓的定時任務都給取消掉。因此就不會再有任何的定時任務執行了,也就不會有機會執行這個重連業務。

七、靠譜實現

所以咱們得有一個單獨的線程來判斷是否須要重連,不依賴於 IdleStateHandler。

因而 cim 在客戶端感知到網絡斷開時就會開啓一個定時任務:

 

之因此不在客戶端啓動就開啓,是爲了節省一點線程消耗。網絡問題雖然不可避免,但在須要的時候開啓更能節省資源。

 
 

在這個任務重其實就是執行了重連,限於篇幅具體代碼就不貼了,感興趣的能夠自行查閱。

同時來驗證一下效果:

啓動兩個服務端,再啓動客戶端鏈接上一臺並保持長鏈接。這時忽然手動關閉一臺服務,客戶端能夠自動重連到可用的那臺服務節點。

 
 

啓動客戶端後服務端也能收到正常的 ping 消息:

利用 :info 命令查看當前客戶端的連接狀態發現連的是 9000端口。

 

:info 是一個新增命令,能夠查看一些客戶端信息。

這時我關掉鏈接上的這臺節點:

kill-9 2142

 
 

這時客戶端會自動重連到可用的那臺節點。這個節點也收到了上線日誌以及心跳包。

八、服務端自動剔除離線客戶端

如今來看看服務端,它要實現的效果就是延遲 N 秒沒有收到客戶端的 ping 包則認爲客戶端下線了,在 cim 的場景下就須要把他踢掉置於離線狀態。

有關消息發送誤區:

這裏依然有一個誤區,在調用 ctx.writeAndFlush() 發送消息獲取回調時。

其中是 isSuccess 並不能做爲消息發送成功與否的標準:

 

也就是說即使是客戶端直接斷網,服務端這裏發送消息後拿到的 success 依舊是 true。這是由於這裏的 success 只是告知咱們消息寫入了 TCP 緩衝區成功了而已。

和我以前有着同樣錯誤理解的不在少數,這是 Netty 官方給的回覆:

 

相關 issue:https://github.com/netty/netty/issues/4915

因此咱們不能依據此來關閉客戶端的鏈接,而是要像上文同樣判斷 Channel 上綁定的時間與當前時間只差是否超過了閾值。

 
 
 

以上則是 cim 服務端的實現,邏輯和開頭說的一致,也和 Dubbo 的心跳機制有些相似。

因而來作個試驗:正常通訊的客戶端和服務端,當我把客戶端直接斷網時,服務端會自動剔除客戶端。

 
 

九、本文小結

這樣就實現了文初的兩個要求:

1)服務端檢測到某個客戶端遲遲沒有心跳過來能夠主動關閉通道,讓它下線;

2)客戶端檢測到某個服務端遲遲沒有響應心跳也能重連獲取一個新的鏈接。

同時也踩了兩個誤區,坑一我的踩就能夠了,但願看過本文的都有所收穫避免踩坑。

本文全部相關代碼都在此處,感興趣的能夠自行查看:

主要鏡像:https://github.com/crossoverJie/cim

備用鏡像:https://github.com/52im/cim

相關文章
相關標籤/搜索