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被自动中断
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值