netty heartbeat機制

前提:

本文會直接使用netty的LineBasedFrameDecoder和StringDecoder作爲例子,介紹heatbeat機制,因此讀者需要具備一定netty編程基礎。

問題由來

所謂心跳, 就在在 TCP長連接中, 客戶端和服務器之間定期發送的一種特殊數據包, 通知對方自己還在線, 以確保 TCP 連接的有效性。

因爲網絡的不可靠性, 有可能在 TCP 保持長連接的過程中, 由於某些突發情況, 例如網線被拔出, 突然掉電等, 會造成服務器和客戶端的連接中斷. 在這些突發情況下, 如果恰好服務器和客戶端之間沒有交互的話, 那麼它們是不能在短時間內發現對方已經掉線的. 爲了解決這個問題, 我們就需要引入心跳機制.

心跳機制的工作原理是: 在服務器和客戶端之間一定時間內沒有數據交互時, 即處於 idle 狀態時, 客戶端或服務器會發送一個特殊的數據包給對方, 當接收方收到這個數據報文後, 也立即發送一個特殊的數據報文, 迴應發送方, 此即一個 PING-PONG 交互. 自然地, 當某一端收到心跳消息後, 就知道了對方仍然在線, 這就確保 TCP 連接的有效性.
如何實現心跳

兩種方式實現心跳機制:
使用 TCP 協議層面的 keepalive 機制.
在應用層上實現自定義的心跳機制.

雖然在 TCP 協議層面上, 提供了 keepalive 保活機制, 但是使用它有幾個缺點:
它不是 TCP 的標準協議, 並且是默認關閉的.
TCP keepalive 機制依賴於操作系統的實現, 默認的 keepalive 心跳時間是 兩個小時, 並且對 keepalive 的修改需要系統調用(或者修改系統配置), 靈活性不夠.

TCP keepalive 與 TCP 協議綁定, 因此如果需要更換爲 UDP 協議時, keepalive 機制就失效了.

基於以上缺點, 一般實踐中, 人們大多數都是選擇在應用層上實現自定義的心跳.

解決辦法

Netty已經爲我們提供瞭如何實現心跳功能的辦法, 這就是IdleStateHandler。 在 Netty 中, 實現心跳機制的關鍵是 IdleStateHandler, 它可以對一個 Channel 的 讀/寫設置定時器, 當 Channel 在一定事件間隔內沒有數據交互時(即處於 idle 狀態), 就會觸發指定的事件.

最終實現

我的例子比較簡單,就是服務器端如果發現某個客戶端連續3次(idle時間第5秒)都沒有發送數據,就斷開連接。 也就是如果某個連接已經超過15秒都不想服務器上傳消息,服務器就認爲該客戶端異常

通常空閒時間以及異常是根據業務定義的。 我們的業務很簡單,服務器給讓客戶端執行job, 客戶端邊執行邊發送結果。如果客戶端沒有job執行,也有需要不斷向服務器發送ping,表示自己還在active狀態,這樣服務器端就修改該客戶端的狀態爲ready,一旦條件符合就選擇該客戶端執行任務。(現在的示例中,客戶端沒有檢測服務器端是否正常,只是保證自己(client端),如果沒有job日誌需要發送時,定期(也就是寫空閒時)向服務器端發送心跳)。 示例比較簡單,讀者需要根據自己的業務進行判斷和修改,但基本用法是一致。 本文示例直接使用行分隔符作爲消息解碼器,實際當中我們使用的protobuf相關的decoder和encoder。爲了示例簡單易懂,就使用最簡單的LineBasedFrameDecoder

完整代碼在這裏, 歡迎fork, 加星。 謝謝!


import com.yq.uitl.SocketUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@NoArgsConstructor
public class ServerSideHandler extends SimpleChannelInboundHandler<String> {
    private int idleCounter = 0;

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        log.info("---Connection Created from {}", ctx.channel().remoteAddress());
        SocketUtils.sendHello(ctx, "server", false);
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // Send the received message to all channels but the current one.
        log.info("ip:{}--- msg:{}", ctx.channel().remoteAddress(), msg);
        idleCounter = 0;;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.warn("Unexpected exception from downstream.", cause);
        ctx.close();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
            throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)) {
                log.warn("第" + idleCounter +"次沒收到客戶端信息了。ip={}", ctx.channel().remoteAddress());
                if (idleCounter > 3) {
                    // 超時關閉channel
                    log.warn("已經連續三次沒收到客戶端信息了, 關閉不活躍的連接={}", ctx);
                    ctx.close();
                } else {
                    idleCounter++;
                }
            } else if (event.state().equals(IdleState.WRITER_IDLE)) {
                log.info("寫空閒");
            } else if (event.state().equals(IdleState.ALL_IDLE)) {
                log.info("ALL_IDLE");
                // 發送心跳
                ctx.channel().write("ping\n");
            }
        }
        super.userEventTriggered(ctx, evt);
    }
}

客戶端檢測寫空閒,發現自己已經8秒沒有寫消息給服務器,就發送一個ping消息到服務器。

package com.yq.client;


import com.yq.uitl.SocketUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@NoArgsConstructor
public class ClientSideHandler extends SimpleChannelInboundHandler<String> {
    private int idleCounter = 0;

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        System.out.println("connected");
        log.info("---Connection Created from {}", ctx.channel().remoteAddress());
        //SocketUtils.sendHello(ctx,"Client", false);

        String str20 = "012345678901234567890123456789";
        SocketUtils.sendLineBaseText(ctx, str20);
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // Send the received message to all channels but the current one.
        log.info("ip={}--- msg={}", ctx.channel().remoteAddress(), msg);

        idleCounter = 0;
        String str20 = "01234567890123456789";
        SocketUtils.sendLineBaseText(ctx, str20);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.warn("Unexpected exception from downstream.", cause);
        ctx.close();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
            throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state().equals(IdleState.READER_IDLE)) {
                log.warn("5秒沒收到服務器端信息了.");
            } else if (event.state().equals(IdleState.WRITER_IDLE)) {
                log.warn("第" + idleCounter +"次沒向服務器端發送信息了。ip={}", ctx.channel().remoteAddress());
                if (idleCounter > 1) {
                    log.warn("向服務器端發送一次心跳");
                    // 發送心跳
                    SocketUtils.sendLineBaseText(ctx, "ping");
                    idleCounter = 0;
                } else {
                    idleCounter++;
                }
            } else if (event.state().equals(IdleState.ALL_IDLE)) {
                log.info("ALL_IDLE");
                // 發送心跳
                 SocketUtils.sendLineBaseText(ctx, "ping");
            }
        }
        super.userEventTriggered(ctx, evt);
    }

}

執行結果
服務器端日誌

[INFO ] 2019-09-01 12:51:30,732 [ nioEventLoopGroup-3-1:4299 ] method:com.yq.server.ServerSideHandler.channelRead0(ServerSideHandler.java:27)
ip:/192.168.1.104:60581--- msg:012345678901234567890123456789
[INFO ] 2019-09-01 12:51:30,735 [ nioEventLoopGroup-3-1:4302 ] method:com.yq.server.ServerSideHandler.channelRead0(ServerSideHandler.java:27)
ip:/192.168.1.104:60581--- msg:01234567890123456789
[WARN ] 2019-09-01 12:51:35,738 [ nioEventLoopGroup-3-1:9305 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第0次沒收到客戶端信息了。ip=/192.168.1.104:60581
[WARN ] 2019-09-01 12:51:40,737 [ nioEventLoopGroup-3-1:14304 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第1次沒收到客戶端信息了。ip=/192.168.1.104:60581
[WARN ] 2019-09-01 12:51:45,740 [ nioEventLoopGroup-3-1:19307 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第2次沒收到客戶端信息了。ip=/192.168.1.104:60581
[INFO ] 2019-09-01 12:51:45,741 [ nioEventLoopGroup-3-1:19308 ] method:com.yq.server.ServerSideHandler.channelRead0(ServerSideHandler.java:27)
ip:/192.168.1.104:60581--- msg:ping
[WARN ] 2019-09-01 12:51:50,742 [ nioEventLoopGroup-3-1:24309 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第0次沒收到客戶端信息了。ip=/192.168.1.104:60581
[WARN ] 2019-09-01 12:51:55,742 [ nioEventLoopGroup-3-1:29309 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第1次沒收到客戶端信息了。ip=/192.168.1.104:60581
[WARN ] 2019-09-01 12:52:00,743 [ nioEventLoopGroup-3-1:34310 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第2次沒收到客戶端信息了。ip=/192.168.1.104:60581
[INFO ] 2019-09-01 12:52:00,744 [ nioEventLoopGroup-3-1:34311 ] method:com.yq.server.ServerSideHandler.channelRead0(ServerSideHandler.java:27)
ip:/192.168.1.104:60581--- msg:ping
[WARN ] 2019-09-01 12:52:05,745 [ nioEventLoopGroup-3-1:39312 ] method:com.yq.server.ServerSideHandler.userEventTriggered(ServerSideHandler.java:43)
第0次沒收到客戶端信息了。ip=/192.168.1.104:60581

客戶端日誌

[INFO ] 2019-09-01 12:51:30,734 [ nioEventLoopGroup-2-1:1335 ] method:com.yq.client.ClientSideHandler.channelRead0(ClientSideHandler.java:31)
ip=/192.168.1.104:5566--- msg=HELLO from HeatBeatDemo server
[INFO ] 2019-09-01 12:51:30,734 [ nioEventLoopGroup-2-1:1335 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] WRITE: 22B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 |0123456789012345|
|00000010| 36 37 38 39 0d 0a                               |6789..          |
+--------+-------------------------------------------------+----------------+
[INFO ] 2019-09-01 12:51:30,735 [ nioEventLoopGroup-2-1:1336 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] FLUSH
[INFO ] 2019-09-01 12:51:30,735 [ nioEventLoopGroup-2-1:1336 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] READ COMPLETE
[WARN ] 2019-09-01 12:51:35,737 [ nioEventLoopGroup-2-1:6338 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第0次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:51:40,736 [ nioEventLoopGroup-2-1:11337 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第1次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:51:45,736 [ nioEventLoopGroup-2-1:16337 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第2次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:51:45,737 [ nioEventLoopGroup-2-1:16338 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:54)
向服務器端發送一次心跳
[INFO ] 2019-09-01 12:51:45,737 [ nioEventLoopGroup-2-1:16338 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] WRITE: 6B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 70 69 6e 67 0d 0a                               |ping..          |
+--------+-------------------------------------------------+----------------+
[INFO ] 2019-09-01 12:51:45,738 [ nioEventLoopGroup-2-1:16339 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] FLUSH
[WARN ] 2019-09-01 12:51:50,739 [ nioEventLoopGroup-2-1:21340 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第0次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:51:55,739 [ nioEventLoopGroup-2-1:26340 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第1次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:52:00,740 [ nioEventLoopGroup-2-1:31341 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第2次沒向服務器端發送信息了。ip=/192.168.1.104:5566
[WARN ] 2019-09-01 12:52:00,741 [ nioEventLoopGroup-2-1:31342 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:54)
向服務器端發送一次心跳
[INFO ] 2019-09-01 12:52:00,742 [ nioEventLoopGroup-2-1:31343 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] WRITE: 6B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 70 69 6e 67 0d 0a                               |ping..          |
+--------+-------------------------------------------------+----------------+
[INFO ] 2019-09-01 12:52:00,742 [ nioEventLoopGroup-2-1:31343 ] method:io.netty.util.internal.logging.Slf4JLogger.info(Slf4JLogger.java:101)
[id: 0xe8f2e2a1, L:/192.168.1.104:60581 - R:/192.168.1.104:5566] FLUSH
[WARN ] 2019-09-01 12:52:05,743 [ nioEventLoopGroup-2-1:36344 ] method:com.yq.client.ClientSideHandler.userEventTriggered(ClientSideHandler.java:52)
第0次沒向服務器端發送信息了。ip=/192.168.1.104:5566

如果我們使用telnet連接服務器,會發現因爲我們在telent中有超過15秒不發消息,telent被自動中斷
在這裏插入圖片描述

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