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

相关推荐
战族狼魂3 分钟前
基于SpringBoot+PostgreSQL+ROS Java库机器人数据可视化管理系统
java·spring boot·postgresql
半个脑袋儿10 分钟前
Java线程控制: sleep、yield、join深度解析
java
小智疯狂敲代码14 分钟前
Spring MVC-DispatcherServlet 的源码解析
java·面试
int0x0315 分钟前
Java中的内存"瘦身术":揭秘String Deduplication
java
半个脑袋儿15 分钟前
Java日期格式化中的“YYYY”陷阱:为什么跨年周会让你的年份突然+1?
java·后端
CHQIUU29 分钟前
Java 设计模式心法之第25篇 - 中介者 (Mediator) - 用“中央协调”降低对象间耦合度
java·设计模式·中介者模式
申城异乡人1 小时前
聊聊@Autowired与@Resource的区别
java·spring
爱编程的小新☆1 小时前
【MySQL】数据类型和表的操作
java·数据库·mysql
喝养乐多长不高1 小时前
详细PostMan的安装和基本使用方法
java·服务器·前端·网络协议·测试工具·https·postman