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


1、引言

說道“心跳”這個詞大家都不陌生,當然不是指男女之間的心跳,而是和長連接相關的。顧名思義就是證明是否還活着的依據。

什么場景下需要心跳呢?目前我們接觸到的大多是一些基於長連接的應用需要心跳來“保活”。

由於在長連接的場景下,客戶端和服務端並不是一直處於通信狀態,如果雙方長期沒有溝通則雙方都不清楚對方目前的狀態,所以需要發送一段很小的報文告訴對方“我還活着”。

同時還有另外幾個目的:

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

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

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

本文配套的CIM源碼地址:

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

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

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

有關網絡編程基礎知識,請閱讀以下資料:

TCP/IP詳解 - 第11章·UDP:用戶數據報協議

TCP/IP詳解 - 第17章·TCP:傳輸控制協議

TCP/IP詳解 - 第18章·TCP連接的建立與終止

TCP/IP詳解 - 第21章·TCP的超時與重傳

通俗易懂-深入理解TCP協議(上):理論基礎》(推薦)

網絡編程懶人入門(一):快速理解網絡通信協議(上篇)

網絡編程懶人入門(二):快速理解網絡通信協議(下篇)

有關Netty框架方面的知識,請閱讀以下資料:

Netty源碼在線閱讀版》(推薦)

Netty API文檔在線版》(推薦)

新手入門:目前為止最透徹的的Netty高性能原理和框架架構解析

寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略

少啰嗦!一分鍾帶你讀懂Java的NIO和經典IO的區別

史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

學習交流:

- 即時通訊/推送技術開發交流5群:215477170[推薦]

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

(本文同步發布於:http://www.52im.net/thread-2799-1-1.html

2、關於作者

 

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

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

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

本文作者的其它文章:

拿起鍵盤就是干:跟我一起徒手開發一套分布式IM系統

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

3、相關文章

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

為何基於TCP協議的移動端IM仍然需要心跳保活機制?

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

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

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

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

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

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

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

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

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

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

拿起鍵盤就是干:跟我一起徒手開發一套分布式IM系統

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

4、心跳實現方式

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

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

2)應用層自己實現。

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

所以我們這里所討論的都是應用層的實現:

 

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

5、客戶端自動重連

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

常規的實現應當是:

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();

    }

}

6、IdleStateHandler 誤區

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

我們假設下面的場景:

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

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

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

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

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

IdleStateHandler 作為一個 ChannelInbound 也重寫了 channelInactive() 方法。

 
\
 

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

7、靠譜實現

因此我們得有一個單獨的線程來判斷是否需要重連,不依賴於 IdleStateHandler。

於是 cim 在客戶端感知到網絡斷開時就會開啟一個定時任務:

 

之所以不在客戶端啟動就開啟,是為了節省一點線程消耗。網絡問題雖然不可避免,但在需要的時候開啟更能節省資源。

 
 

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

同時來驗證一下效果:

啟動兩個服務端,再啟動客戶端連接上一台並保持長連接。這時突然手動關閉一台服務,客戶端可以自動重連到可用的那台服務節點。

 
 

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

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

 

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

這時我關掉連接上的這台節點:

1kill-9 2142

 
 

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

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

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

有關消息發送誤區:

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

其中是 isSuccess 並不能作為消息發送成功與否的標准:

 

也就是說即便是客戶端直接斷網,服務端這里發送消息后拿到的 success 依舊是 true。這是因為這里的 success 只是告知我們消息寫入了 TCP 緩沖區成功了而已。

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

 

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

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

 
 
 

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

於是來做個試驗:正常通信的客戶端和服務端,當我把客戶端直接斷網時,服務端會自動剔除客戶端。

 
 

9、本文小結

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

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

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

同時也踩了兩個誤區,坑一個人踩就可以了,希望看過本文的都有所收獲避免踩坑。

本文所有相關代碼都在此處,感興趣的可以自行查看:

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

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

附錄:更多參考資料匯總

[1] IM代碼實踐(適合新手):

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

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

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

詳解Netty的安全性:原理介紹、代碼演示(上篇)

詳解Netty的安全性:原理介紹、代碼演示(下篇)

微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]

Java NIO基礎視頻教程、MINA視頻教程、Netty快速入門視頻 [有源碼]

輕量級即時通訊框架MobileIMSDK的iOS源碼(開源版)[附件下載]

開源IM工程“蘑菇街TeamTalk”2015年5月前未刪減版完整代碼 [附件下載]

微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]

NIO框架入門(一):服務端基於Netty4的UDP雙向通信Demo演示 [附件下載]

NIO框架入門(二):服務端基於MINA2的UDP雙向通信Demo演示 [附件下載]

NIO框架入門(三):iOS與MINA2、Netty4的跨平台UDP雙向通信實戰 [附件下載]

NIO框架入門(四):Android與MINA2、Netty4的跨平台UDP雙向通信實戰 [附件下載]

用於IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]

高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]

一個WebSocket實時聊天室Demo:基於node.js+socket.io [附件下載]

Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]

高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]

開源libco庫:單機千萬連接、支撐微信8億用戶的后台框架基石 [源碼下載]

分享java AMR音頻文件合並源碼,全網最全

微信團隊原創Android資源混淆工具:AndResGuard [有源碼]

一個基於MQTT通信協議的完整Android推送Demo [附件下載]

Android版高仿微信聊天界面源碼 [附件下載]

高仿手機QQ的Android版鎖屏聊天消息提醒功能 [附件下載]

高仿iOS版手機QQ錄音及振幅動畫完整實現 [源碼下載]

Android端社交應用中的評論和回復功能實戰分享[圖文+源碼]

Android端IM應用中的@人功能實現:仿微博、QQ、微信,零入侵、高可擴展[圖文+源碼]

仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]

Android版仿微信朋友圈圖片拖拽返回效果 [源碼下載]

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

拿起鍵盤就是干:跟我一起徒手開發一套分布式IM系統

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

>> 更多同類文章 …… 

[2] 網絡編程基礎資料:

TCP/IP詳解 - 第11章·UDP:用戶數據報協議

TCP/IP詳解 - 第17章·TCP:傳輸控制協議

TCP/IP詳解 - 第18章·TCP連接的建立與終止

TCP/IP詳解 - 第21章·TCP的超時與重傳

技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)

通俗易懂-深入理解TCP協議(上):理論基礎

通俗易懂-深入理解TCP協議(下):RTT、滑動窗口、擁塞處理

理論經典:TCP協議的3次握手與4次揮手過程詳解

理論聯系實際:Wireshark抓包分析TCP 3次握手、4次揮手過程

計算機網絡通訊協議關系圖(中文珍藏版)

UDP中一個包的大小最大能多大?

P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介

P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解

P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解

通俗易懂:快速理解P2P技術中的NAT穿透原理

高性能網絡編程(一):單台服務器並發TCP連接數到底可以有多少

高性能網絡編程(二):上一個10年,著名的C10K並發連接問題

高性能網絡編程(三):下一個10年,是時候考慮C10M並發問題了

高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索

高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型

高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型

不為人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)

不為人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)

不為人知的網絡編程(三):關閉TCP連接時為什么會TIME_WAIT、CLOSE_WAIT

不為人知的網絡編程(四):深入研究分析TCP的異常關閉

不為人知的網絡編程(五):UDP的連接性和負載均衡

不為人知的網絡編程(六):深入地理解UDP協議並用好它

不為人知的網絡編程(七):如何讓不可靠的UDP變的可靠?

不為人知的網絡編程(八):從數據傳輸層深度解密HTTP

不為人知的網絡編程(九):理論聯系實際,全方位深入理解DNS

網絡編程懶人入門(一):快速理解網絡通信協議(上篇)

網絡編程懶人入門(二):快速理解網絡通信協議(下篇)

網絡編程懶人入門(三):快速理解TCP協議一篇就夠

網絡編程懶人入門(四):快速理解TCP和UDP的差異

網絡編程懶人入門(五):快速理解為什么說UDP有時比TCP更有優勢

網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門

網絡編程懶人入門(七):深入淺出,全面理解HTTP協議

網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長連接

網絡編程懶人入門(九):通俗講解,有了IP地址,為何還要用MAC地址?

技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解

讓互聯網更快:新一代QUIC協議在騰訊的技術實踐分享

現代移動端網絡短連接的優化手段總結:請求速度、弱網適應、安全保障

聊聊iOS中網絡編程長連接的那些事

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

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

IPv6技術詳解:基本概念、應用現狀、技術實踐(上篇)

IPv6技術詳解:基本概念、應用現狀、技術實踐(下篇)

從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路

腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手

腦殘式網絡編程入門(二):我們在讀寫Socket時,究竟在讀寫什么?

腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識

腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)

腦殘式網絡編程入門(五):每天都在用的Ping命令,它到底是什么?

腦殘式網絡編程入門(六):什么是公網IP和內網IP?NAT轉換又是什么鬼?

以網游服務端的網絡接入層設計為例,理解實時通信的技術挑戰

邁向高階:優秀Android程序員必知必會的網絡基礎

全面了解移動端DNS域名劫持等雜症:技術原理、問題根源、解決方案等

美圖App的移動端DNS優化實踐:HTTPS請求耗時減小近半

Android程序員必知必會的網絡通信傳輸層協議——UDP和TCP

IM開發者的零基礎通信技術入門(一):通信交換技術的百年發展史(上)

IM開發者的零基礎通信技術入門(二):通信交換技術的百年發展史(下)

IM開發者的零基礎通信技術入門(三):國人通信方式的百年變遷

IM開發者的零基礎通信技術入門(四):手機的演進,史上最全移動終端發展史

IM開發者的零基礎通信技術入門(五):1G到5G,30年移動通信技術演進史

IM開發者的零基礎通信技術入門(六):移動終端的接頭人——“基站”技術

IM開發者的零基礎通信技術入門(七):移動終端的千里馬——“電磁波”

IM開發者的零基礎通信技術入門(八):零基礎,史上最強“天線”原理掃盲

IM開發者的零基礎通信技術入門(九):無線通信網絡的中樞——“核心網”

IM開發者的零基礎通信技術入門(十):零基礎,史上最強5G技術掃盲

IM開發者的零基礎通信技術入門(十一):為什么WiFi信號差?一文即懂!

IM開發者的零基礎通信技術入門(十二):上網卡頓?網絡掉線?一文即懂!

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

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

IM開發者的零基礎通信技術入門(十五):理解定位技術,一篇就夠

百度APP移動端網絡深度優化實踐分享(一):DNS優化篇

百度APP移動端網絡深度優化實踐分享(二):網絡連接優化篇

百度APP移動端網絡深度優化實踐分享(三):移動端弱網優化篇

技術大牛陳碩的分享:由淺入深,網絡編程學習經驗干貨總結

可能會搞砸你的面試:你知道一個TCP連接上能發起多少個HTTP請求嗎?

知乎技術分享:知乎千萬級並發的高性能長連接網關技術實踐

>> 更多同類文章 ……

[3] NIO異步網絡編程資料:

Java新一代網絡編程模型AIO原理及Linux系統AIO介紹

有關“為何選擇Netty”的11個疑問及解答

開源NIO框架八卦——到底是先有MINA還是先有Netty?

選Netty還是Mina:深入研究與對比(一)

選Netty還是Mina:深入研究與對比(二)

NIO框架入門(一):服務端基於Netty4的UDP雙向通信Demo演示

NIO框架入門(二):服務端基於MINA2的UDP雙向通信Demo演示

NIO框架入門(三):iOS與MINA2、Netty4的跨平台UDP雙向通信實戰

NIO框架入門(四):Android與MINA2、Netty4的跨平台UDP雙向通信實戰

Netty 4.x學習(一):ByteBuf詳解

Netty 4.x學習(二):Channel和Pipeline詳解

Netty 4.x學習(三):線程模型詳解

Apache Mina框架高級篇(一):IoFilter詳解

Apache Mina框架高級篇(二):IoHandler詳解

MINA2 線程原理總結(含簡單測試實例)

Apache MINA2.0 開發指南(中文版)[附件下載]

MINA、Netty的源代碼(在線閱讀版)已整理發布

解決MINA數據傳輸中TCP的粘包、缺包問題(有源碼)

解決Mina中多個同類型Filter實例共存的問題

實踐總結:Netty3.x升級Netty4.x遇到的那些坑(線程篇)

實踐總結:Netty3.x VS Netty4.x的線程模型

詳解Netty的安全性:原理介紹、代碼演示(上篇)

詳解Netty的安全性:原理介紹、代碼演示(下篇)

詳解Netty的優雅退出機制和原理

NIO框架詳解:Netty的高性能之道

Twitter:如何使用Netty 4來減少JVM的GC開銷(譯文)

絕對干貨:基於Netty實現海量接入的推送服務技術要點

Netty干貨分享:京東京麥的生產級TCP網關技術實踐總結

新手入門:目前為止最透徹的的Netty高性能原理和框架架構解析

寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略

少啰嗦!一分鍾帶你讀懂Java的NIO和經典IO的區別

史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

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

>> 更多同類文章 ……

(本文同步發布於:http://www.52im.net/thread-2799-1-1.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM