Netty——心跳监测机制

文章目录

  • [1. 简介](#1. 简介)
  • [2. 实现](#2. 实现)
    • [2.1 心跳包的定义](#2.1 心跳包的定义)
    • [2.2 空闲事件检测](#2.2 空闲事件检测)
      • [2.2.1 空闲事件类型](#2.2.1 空闲事件类型)
      • [2.2.2 空闲检测机制](#2.2.2 空闲检测机制)
        • [1. 时间戳记录](#1. 时间戳记录)
        • [2. 定时任务驱动](#2. 定时任务驱动)
        • [3. 定时任务循环触发](#3. 定时任务循环触发)
        • [4. 示意图](#4. 示意图)
    • [2.3 使用 IdleStateHandler](#2.3 使用 IdleStateHandler)
      • [2.3.1 空闲事件的处理器 HeartbeatHandler](#2.3.1 空闲事件的处理器 HeartbeatHandler)
      • [2.3.2 服务器类 HeartbeatServer](#2.3.2 服务器类 HeartbeatServer)
      • [2.3.3 客户端类 HeartbeatClient](#2.3.3 客户端类 HeartbeatClient)
  • [3. 总结](#3. 总结)

1. 简介

心跳监测机制 是一种在网络通信、分布式系统等领域广泛应用的技术手段,用于检测 连接的有效性 和 系统的健康状态 。它的基本原理是 在通信双方或系统组件之间 定期 发送一种特定的消息 (即 心跳包),这种消息就像人体的心跳一样,有规律地进行传输,以此来表明双方之间的连接正常且对方处于活跃状态。

2. 实现

2.1 心跳包的定义

客户端与服务器通信时需要定义消息的协议,比如 自定义 Netty 编解码器 中提到的具有四部分内容(magic, code, len, data)的消息协议,可以按照 code 来区分消息的类型。

为了避免心跳包占用太多的资源,需要 尽量减少心跳包中包含的数据实际上心跳包中也不需要包含数据,只需要将心跳包发送到对端,让其知道自己存活即可。可以像如下代码定义消息类型和生成心跳包:

java 复制代码
/**
 * 消息
 */
public class Msg {

    /**
     * 默认的魔数
     */
    public static final short DEFAULT_MAGIC = 9000;

    /**
     * 业务码的枚举类
     */
    public enum CodeEnum {
    
        /**
         * 心跳包的业务码
         */
        HEARTBEAT(1000);

        private final int code;

        CodeEnum(int code) {
            this.code = code;
        }

        public int getCode() {
            return code;
        }
    }

    /**
     * 魔数
     */
    private short magic;

    /**
     * 业务码
     */
    private int code;

    /**
     * 消息内容的长度
     */
    private int len;

    /**
     * 消息的内容
     */
    private byte[] data;

    public Msg(int code, byte[] data) {
        this.magic = DEFAULT_MAGIC;
        this.code = code;
        this.len = data.length;
        this.data = data;
    }

    /**
     * 创建一个心跳包
     */
    public static Msg heartbeatMsg() {
        return new Msg(CodeEnum.HEARTBEAT.code, new byte[0]);
    }

    public short getMagic() {
        return magic;
    }

    public void setMagic(short magic) {
        this.magic = magic;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
}

2.2 空闲事件检测

2.2.1 空闲事件类型

  • 读空闲事件
    • 触发条件:在指定时间内,未接收到任何数据
    • 用途:检测对端是否断开连接或长时间无响应。
  • 写空闲事件
    • 触发条件:在指定时间内,未发送任何数据
    • 用途:用于发送心跳包或维持连接活性。
  • 全空闲事件
    • 触发条件:在指定时间内,既未接收到数据也未发送数据
    • 用途:检测连接完全无活动的场景(如双向通信中断)。

2.2.2 空闲检测机制

1. 时间戳记录

每次执行读 / 写操作时,更新对应的最后活动时间戳 (lastReadTime/lastWriteTime):

  • 读操作:在 初始化时读操作完成时 (在 readComplete 事件的处理方法中),更新 lastReadTime
  • 写操作:在 初始化时写操作完成时 (在 write 方法的回调监听器中),更新 lastWriteTime
2. 定时任务驱动

基于 Netty 的 ScheduledExecutorService,在初始化时启动三个独立的定时任务:读空闲检测 ReaderIdleTimeoutTask、写空闲检测 WriterIdleTimeoutTask、全空闲检测 AllIdleTimeoutTask

3. 定时任务循环触发

以上提到的三个定时任务并没有设置固定的时间间隔,而是根据实际情况设置不同延迟时间的定时任务,具体的思想如下所示:

注:

  • 以下会称 用户在 IdleStateHandler 构造器中设置的时间间隔标准时间间隔
  • 读操作执行了两个重要操作:
    • 处理 read 事件时将 reading 设置为 true
    • 处理 readComplete 事件时将 reading 设置为 false
  • 只要理解了 读空闲检测 和 写空闲检测,全空闲检测 就很容易理解,这里就不做赘述。
  • ReaderIdleTimeoutTask
    • 如果正在读取,即 reading == true,则设置一个延迟时间为标准时间间隔的定时任务,否则执行下列操作
    • 计算 nextDelaynextDelay = 当前时间 - lastReadTime + 标准时间间隔
    • 如果 nextDelay > 0,则说明以当前时间戳为基准,往前推一个标准时间间隔,这个时间段中执行了读操作,所以应该 缩短 下一个定时任务的时间间隔,即将 nextDelay 作为其延迟时间,从而保证 读空闲检测 覆盖了 最后一个读操作结束后的一个标准时间间隔
    • 否则这个时间段中没有执行读操作,应保持标准时间间隔不变,即将 标准时间间隔 作为下一个定时任务的延迟时间。
  • WriterIdleTimeoutTask
    • 计算 nextDelaynextDelay = 当前时间 - lastWriteTime + 标准时间间隔
    • 如果 nextDelay > 0,则说明以当前时间戳为基准,往前推一个标准时间间隔,这个时间段中执行了写操作,所以应该 缩短 下一个定时任务的时间间隔,即将 nextDelay 作为其延迟时间,从而保证 写空闲检测 覆盖了 最后一个写操作结束后的一个标准时间间隔
    • 否则这个时间段中没有执行写操作,应保持标准时间间隔不变,即将 标准时间间隔 作为下一个定时任务的延迟时间。

问:为什么读操作有 reading 标志来判断是否正在读,而写操作没有 writing 标志来判断是否正在写?

答:其实很简单,原因在于读操作和写操作的特性不同:

  • 读操作特性:读操作可能会持续较长时间 。在这个过程中,如果没有 reading 标志,IdleStateHandler 可能会误判为读空闲,从而触发不必要的空闲事件。
  • 写操作特性:写操作通常具有原子性和即时性 。当调用 ChannelHandlerContextwritewriteAndFlush 方法时,Netty 会将写请求封装成消息对象放入发送队列,底层的 I/O 线程会尽快将其发送出去。即使在高并发场景下,写操作也会被高效处理,一般不会出现长时间处于 "正在写" 的状态。因此,不需要额外的标志来跟踪写操作的进行状态。
4. 示意图

注意:read 时的定时任务处理没有发出读空闲事件(依赖 reading 标志),write 时的定时任务处理发出了写空闲事件。实际上 write 占用的时间很短,很难遇到写空闲处理示意图中 write 时的定时任务处理发出写空闲事件

注:图中提到的"某种原因导致定时任务延迟执行"中,"某种原因"可能是 任务队列中其它任务都正在执行,定时任务需要排队等待

2.3 使用 IdleStateHandler

2.3.1 空闲事件的处理器 HeartbeatHandler

java 复制代码
/**
 * 空闲事件的处理器
 */
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof Msg message && message.getCode() == Msg.CodeEnum.HEARTBEAT.getCode()) {
            System.out.println("接收到心跳包");
        } else {
            super.channelRead(ctx, msg);
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent event) {
            if (event.state() == IdleState.READER_IDLE) {
                System.out.println("读空闲,断开连接");
                ctx.close();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                System.out.println("发送心跳包");
                ctx.writeAndFlush(Msg.heartbeatMsg());
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("断开连接");
        super.channelInactive(ctx);
    }
}

2.3.2 服务器类 HeartbeatServer

java 复制代码
/**
 * 拥有心跳机制的服务器
 */
public class HeartbeatServer {

    public static void main(String[] args) {
        final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        final EventLoopGroup workerGroup = new NioEventLoopGroup();

        final ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast(new MsgDecoder())
                                .addLast(new MsgEncoder())
                                // 如果 3s 内没有读操作,则触发读空闲事件
                                .addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS))
                                .addLast(new HeartbeatHandler());
                    }
                });

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }));

        bootstrap.bind(8888);
    }
}

2.3.3 客户端类 HeartbeatClient

java 复制代码
/**
 * 拥有心跳机制的客户端
 */
public class HeartbeatClient {

    public static void main(String[] args) {
        final EventLoopGroup group = new NioEventLoopGroup();

        final Bootstrap bootstrap = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                .addLast(new MsgDecoder())
                                .addLast(new MsgEncoder())
                                // 如果 2s 内没有写操作,则触发写空闲事件
                                .addLast(new IdleStateHandler(0, 2, 0, TimeUnit.SECONDS))
                                .addLast(new HeartbeatHandler());
                    }
                });

        Runtime.getRuntime().addShutdownHook(new Thread(group::shutdownGracefully));

        bootstrap.connect("127.0.0.1", 8888);
    }
}

注意:如果想测试服务器在触发读空闲事件后断开与客户端的连接,则可以注释掉客户端中添加 IdleStateHandler 的操作。

3. 总结

心跳监测机制对于高性能的服务器来说必不可少 ,它广泛用于 需要检测连接是否有效 的场景,如果连接无效,则断开连接,从而节省服务器/客户端的资源;而且可以在写空闲的时候给对端发送心跳包,从而让其知道连接有效,自己处于存活状态。

在 Netty 中,使用 IdleStateHandler 可以轻松地实现心跳监测机制,它的核心原理在于 记录最后一次操作的时间戳使用有延迟时间的定时任务检测并触发

相关推荐
RainbowSea1 分钟前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea9 分钟前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄2 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝2 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖2 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s9123601012 小时前
rust 同时处理多个异步任务
java·数据库·rust
9号达人2 小时前
java9新特性详解与实践
java·后端·面试
cg50172 小时前
Spring Boot 的配置文件
java·linux·spring boot
啊喜拔牙2 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
anlogic3 小时前
Java基础 4.3
java·开发语言