說道「心跳」這個詞你們都不陌生,固然不是指男女之間的心跳,而是和長鏈接相關的。java
顧名思義就是證實是否還活着的依據。git
什麼場景下須要心跳呢?github
目前咱們接觸到的大可能是一些基於長鏈接的應用須要心跳來「保活」。shell
因爲在長鏈接的場景下,客戶端和服務端並非一直處於通訊狀態,若是雙方長期沒有溝通則雙方都不清楚對方目前的狀態;因此須要發送一段很小的報文告訴對方「我還活着」
。網絡
同時還有另外幾個目的:app
正好藉着在 cim有這樣兩個需求來聊一聊。ide
心跳其實有兩種實現方式:spa
TCP
協議實現(keepalive
機制)。因爲 TCP
協議過於底層,對於開發者來講維護性、靈活度都比較差同時還依賴於操做系統。操作系統
因此咱們這裏所討論的都是應用層的實現。線程
如上圖所示,在應用層一般是由客戶端發送一個心跳包 ping
到服務端,服務端收到後響應一個 pong
代表雙方都活得好好的。
一旦其中一端延遲 N 個時間窗口沒有收到消息則進行不一樣的處理。
先拿客戶端來講吧,每隔一段時間客戶端向服務端發送一個心跳包,同時收到服務端的響應。
常規的實現應當是:
「本地時間」
是否超過閾值。這樣確實也能實現心跳,但並不友好。
在正常的客戶端和服務端通訊的狀況下,定時任務依然會發送心跳包;這樣就顯得沒有意義,有些多餘。
因此理想的狀況應當是客戶端收到的寫消息空閒時才發送這個心跳包去確認服務端是否健在。
好消息是 Netty
已經爲咱們考慮到了這點,自帶了一個開箱即用的 IdleStateHandler
專門用於心跳處理。
來看看 cim
中的實現:
在 pipeline
中加入了一個 10秒沒有收到寫消息的 IdleStateHandler
,到時他會回調 ChannelInboundHandler
中的 userEventTriggered
方法。
因此一旦寫超時就立馬向服務端發送一個心跳(作的更完善應當在心跳發送失敗後有必定的重試次數);
這樣也就只有在空閒時候纔會發送心跳包。
但一旦間隔許久沒有收到服務端響應進行重連的邏輯應當寫在哪裏呢?
先來看這個示例:
當收到服務端響應的 pong 消息時,就在當前 Channel 上記錄一個時間,也就是說後續能夠在定時任務中取出這個時間和當前時間的差額來判斷是否超過閾值。
超過則重連。
同時在每次心跳時候都用當前時間和以前服務端響應綁定到 Channel
上的時間相減判斷是否須要重連便可。
也就是 heartBeatHandler.process(ctx);
的執行邏輯。
僞代碼以下:
@Override public void process(ChannelHandlerContext ctx) throws Exception { long heartBeatTime = appConfiguration.getHeartBeatTime() * 1000; Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel()); long now = System.currentTimeMillis(); if (lastReadTime != null && now - lastReadTime > heartBeatTime){ reconnect(); } }
一切看起來也沒毛病,但實際上卻沒有這樣實現重連邏輯。
最主要的問題仍是對 IdleStateHandler
理解有誤。
咱們假設下面的場景:
userEventTriggered
執行定時任務。當前時間 - 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
同時感謝 95老徐以及閃電俠的一塊兒排查。
因此咱們不能依據此來關閉客戶端的鏈接,而是要像上文同樣判斷 Channel
上綁定的時間與當前時間只差是否超過了閾值。
以上則是 cim
服務端的實現,邏輯和開頭說的一致,也和 Dubbo
的心跳機制有些相似。
因而來作個試驗:正常通訊的客戶端和服務端,當我把客戶端直接斷網時,服務端會自動剔除客戶端。
這樣就實現了文初的兩個要求。
同時也踩了兩個誤區,坑一我的踩就能夠了,但願看過本文的都有所收穫避免踩坑。
本文全部相關代碼都在此處,感興趣的能夠自行查看:
https://github.com/crossoverJie/cim
若是本文對你有所幫助還請不吝轉發。