說道「心跳」這個詞你們都不陌生,固然不是指男女之間的心跳,而是和長鏈接相關的。顧名思義就是證實是否還活着的依據。php
什麼場景下須要心跳呢?目前咱們接觸到的大可能是一些基於長鏈接的應用須要心跳來「保活」。html
因爲在長鏈接的場景下,客戶端和服務端並非一直處於通訊狀態,若是雙方長期沒有溝通則雙方都不清楚對方目前的狀態,因此須要發送一段很小的報文告訴對方「我還活着」。java
同時還有另外幾個目的:node
1)服務端檢測到某個客戶端遲遲沒有心跳過來能夠主動關閉通道,讓它下線;git
2)客戶端檢測到某個服務端遲遲沒有響應心跳也能重連獲取一個新的鏈接。程序員
本文正好藉着在CIM系統中有這樣兩個需求(CIM是本文做者從零開發的一個學習性質的IM系統,詳見《拿起鍵盤就是幹:跟我一塊兒徒手開發一套分佈式IM系統》),正好來聊一聊我是如何理解IM長鏈接的心跳及重連機制,以及又是怎麼踩坑已及填坑的。github
本文配套的CIM源碼地址:面試
主要鏡像:https://github.com/crossoverJie/cim算法
備用鏡像:https://github.com/52im/cim數據庫
閱讀本文須要必定的網絡編程以及Netty方面的知識。
有關網絡編程基礎知識,請閱讀如下資料:
《通俗易懂-深刻理解TCP協議(上):理論基礎》(推薦)
有關Netty框架方面的知識,請閱讀如下資料:
《Netty源碼在線閱讀版》(推薦)
《Netty API文檔在線版》(推薦)
《新手入門:目前爲止最透徹的的Netty高性能原理和框架架構解析》
學習交流:
- 即時通信/推送技術開發交流5羣:215477170[推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
(本文同步發佈於:http://www.52im.net/thread-2799-1-1.html)
crossoverJie(陳杰): 90後,畢業於重慶信息工程學院,現供職於重慶豬八戒網絡有限公司。
做者的博客:https://crossoverjie.top
做者的Github:https://github.com/crossoverJie
本文做者的其它文章:
➊ 有關網絡心跳保活方面的理論文章:
《微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)》
《移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》
➋ 有關網絡心跳保活方面的實踐文章:
《MobileIMSDK——一套開源的原創移動端即時通信框架(有完整的心跳保活邏輯和代碼實現)》
《一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)》
《手把手教你用Netty實現網絡通訊程序的心跳機制、斷線重連機制》
心跳其實有兩種實現方式:
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 理解有誤。
咱們假設下面的場景:
1)客戶端經過登陸連上了服務端並保持長鏈接,一切正常的狀況下雙方各發心跳包保持鏈接;
2)這時服務端突入出現 down 機,那麼理想狀況下應當是客戶端遲遲沒有收到服務端的響應從而 userEventTriggered 執行定時任務;
3)判斷當前時間 - UpdateWriteTime > 閾值 時進行重連。
但卻事與願違,並不會執行 二、3兩步。
由於一旦服務端 down 機、或者是與客戶端的網絡斷開則會回調客戶端的 channelInactive 事件。
IdleStateHandler 做爲一個 ChannelInbound 也重寫了 channelInactive() 方法。
這裏的 destroy() 方法會把以前開啓的定時任務都給取消掉。因此就不會再有任何的定時任務執行了,也就不會有機會執行這個重連業務。
所以咱們得有一個單獨的線程來判斷是否須要重連,不依賴於 IdleStateHandler。
因而 cim 在客戶端感知到網絡斷開時就會開啓一個定時任務:
之因此不在客戶端啓動就開啓,是爲了節省一點線程消耗。網絡問題雖然不可避免,但在須要的時候開啓更能節省資源。
在這個任務重其實就是執行了重連,限於篇幅具體代碼就不貼了,感興趣的能夠自行查閱。
同時來驗證一下效果:
啓動兩個服務端,再啓動客戶端鏈接上一臺並保持長鏈接。這時忽然手動關閉一臺服務,客戶端能夠自動重連到可用的那臺服務節點。
啓動客戶端後服務端也能收到正常的 ping 消息:
利用 :info 命令查看當前客戶端的連接狀態發現連的是 9000端口。
:info 是一個新增命令,能夠查看一些客戶端信息。
這時我關掉鏈接上的這臺節點:
1kill-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
[1] IM代碼實踐(適合新手):
《自已開發IM有那麼難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)》
《一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)》
《手把手教你用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與MINA二、Netty4的跨平臺UDP雙向通訊實戰 [附件下載]》
《NIO框架入門(四):Android與MINA二、Netty4的跨平臺UDP雙向通訊實戰 [附件下載]》
《用於IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]》
《高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]》
《一個WebSocket實時聊天室Demo:基於node.js+socket.io [附件下載]》
《Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]》
《高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]》
《開源libco庫:單機千萬鏈接、支撐微信8億用戶的後臺框架基石 [源碼下載]》
《微信團隊原創Android資源混淆工具:AndResGuard [有源碼]》
《一個基於MQTT通訊協議的完整Android推送Demo [附件下載]》
《高仿手機QQ的Android版鎖屏聊天消息提醒功能 [附件下載]》
《高仿iOS版手機QQ錄音及振幅動畫完整實現 [源碼下載]》
《Android端社交應用中的評論和回覆功能實戰分享[圖文+源碼]》
《Android端IM應用中的@人功能實現:仿微博、QQ、微信,零入侵、高可擴展[圖文+源碼]》
《仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]》
《Android版仿微信朋友圈圖片拖拽返回效果 [源碼下載]》
《適合新手:從零開發一個IM服務端(基於Netty,有完整源碼)》
《正確理解IM長鏈接的心跳及重連機制,並動手實現(有完整IM源碼)》
>> 更多同類文章 ……
[2] 網絡編程基礎資料:
《技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)》
《通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理》
《理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》
《P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》
《P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解》
《P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解》
《高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少》
《高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題》
《高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了》
《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》
《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》
《高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型》
《鮮爲人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)》
《鮮爲人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)》
《鮮爲人知的網絡編程(三):關閉TCP鏈接時爲何會TIME_WAIT、CLOSE_WAIT》
《鮮爲人知的網絡編程(七):如何讓不可靠的UDP變的可靠?》
《鮮爲人知的網絡編程(九):理論聯繫實際,全方位深刻理解DNS》
《網絡編程懶人入門(五):快速理解爲何說UDP有時比TCP更有優點》
《網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門》
《網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接》
《網絡編程懶人入門(九):通俗講解,有了IP地址,爲什麼還要用MAC地址?》
《技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解》
《現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障》
《移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」》
《移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結》
《從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路》
《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》
《腦殘式網絡編程入門(二):咱們在讀寫Socket時,究竟在讀寫什麼?》
《腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識》
《腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)》
《腦殘式網絡編程入門(五):天天都在用的Ping命令,它究竟是什麼?》
《腦殘式網絡編程入門(六):什麼是公網IP和內網IP?NAT轉換又是什麼鬼?》
《以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰》
《全面瞭解移動端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介紹》
《開源NIO框架八卦——究竟是先有MINA仍是先有Netty?》
《NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示》
《NIO框架入門(二):服務端基於MINA2的UDP雙向通訊Demo演示》
《NIO框架入門(三):iOS與MINA二、Netty4的跨平臺UDP雙向通訊實戰》
《NIO框架入門(四):Android與MINA二、Netty4的跨平臺UDP雙向通訊實戰》
《Netty 4.x學習(二):Channel和Pipeline詳解》
《Apache Mina框架高級篇(一):IoFilter詳解》
《Apache Mina框架高級篇(二):IoHandler詳解》
《Apache MINA2.0 開發指南(中文版)[附件下載]》
《實踐總結:Netty3.x升級Netty4.x遇到的那些坑(線程篇)》
《實踐總結:Netty3.x VS Netty4.x的線程模型》
《Twitter:如何使用Netty 4來減小JVM的GC開銷(譯文)》
《Netty乾貨分享:京東京麥的生產級TCP網關技術實踐總結》
《新手入門:目前爲止最透徹的的Netty高性能原理和框架架構解析》
《寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略》
《史上最強Java NIO入門:擔憂從入門到放棄的,請讀這篇!》
《手把手教你用Netty實現網絡通訊程序的心跳機制、斷線重連機制》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-2799-1-1.html)