Netty 心跳机制详解

1、什么是心跳机制

在 TCP 保持长连接的过程中,客户端与服务端之间如果长时间没有交互的话,无法发现对方是否已经掉线。为了确保客户端与服务端是否都还正常工作,客户端和服务端需要定期发送心跳包来维护连接。其核心实现依赖的是 IdleStateHandler 类, IdleStateHandler 类的构造函数中的参数如下:

  public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }
  • readerIdleTime:读超时时限,当在指定时间间隔内没有从 Channel 读取到数据时,会触发一个 READER_IDLE 的 IdleStateEvent 事件。
  • writerIdleTime:写超时时限,当在指定时间间隔内没有数据写入到 Channel 时,会触发一个 WRITER_IDLE 的 IdleStateEvent 事件。
  • allIdleTime:读/写超时时限,当在指定时间间隔内没有读或写操作时,会触发一个 ALL_IDLE 的 IdleStateEvent 事件。
  • unit:时间单位。

2、心跳机制实现原理

  1. 在initChannel()中,往ChannelPipeline中添加IdleStateHandler实例,通过IdleStateHandler实例的构造函数来设置各种事件触发的时间,如下:
## 这里设置5秒内没有从 Channel 读取到数据时会触发一个 READER_IDLE 事件。
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
  1. 再往ChannelPipeline中添加一个 IdleStateEvent 事件的自定义事件处理类,这个处理类需要继承ChannelInboundHandlerAdapter类,然后重写userEventTriggered()方法,在这个方法里根据不同的事件类型发送相应的心跳检测信息。
## 添加 IdleStateEvent 事件的处理类
pipeline.addLast(new ServerHeartbeatHandler());
  1. 接收方再通过channelRead()方法对接收到的心跳信息进行相应的处理,从而完成整个心跳检测过程。

3、心跳机制简单实例

服务端实现心跳检测
  1. 启动一个服务端实例
/**
     * 创建服务端实例并绑定端口
     * @throws InterruptedException
     */
    public static void bind() throws InterruptedException {

        // 创建boss线程组,用于接收连接
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 创建worker线程组,用于处理连接上的I/O操作,含有子线程NioEventGroup个数为CPU核数大小的2倍
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 创建ServerBootstrap实例,服务器启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();

            // 使用链式编程配置参数
            // 将boss线程组和worker线程组暂存到ServerBootstrap
            bootstrap.group(bossGroup, workerGroup);
            // 设置服务端Channel类型为NioServerSocketChannel作为通道实现
            bootstrap.channel(NioServerSocketChannel.class);

            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    // 添加服务端自定义心跳机制Channel初始化类
                    pipeline.addLast(new ServerHeartbeatInitializer());
                }
            });
            // 设置启动参数,初始化服务器连接队列大小。服务端处理客户端连接请求是顺序处理,一个时间内只能处理一个客户端请求
            // 当有多个客户端同时来请求时,未处理的请求先放入队列中
            bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
            // 绑定端口并启动服务器,bind方法是异步的,sync方法是等待异步操作执行完成,返回ChannelFuture异步对象
            ChannelFuture channelFuture = bootstrap.bind(8888).sync();
            // 等待服务器关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            // 优雅地关闭boss线程组
            bossGroup.shutdownGracefully();
            // 优雅地关闭worker线程组
            workerGroup.shutdownGracefully();
        }
    }
  1. 创建服务端自定义心跳机制Channel初始化类,并往 ChannelPipeline 中添加 IdleStateHandler 实例以及服务端处理 IdleStateEvent 事件的处理类。
public class ServerHeartbeatInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
		
		## 添加IdleStateHandler实例,5秒内未读取到Channel的数据时,触发READER_IDLE事件
        pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
        ## 添加服务端自定义 IdleStateEvent 事件处理类
        pipeline.addLast(new ServerHeartbeatHandler());
    }
}
  1. 创建服务端自定义 IdleStateEvent 事件处理类,继承ChannelInboundHandlerAdapter类,重写userEventTriggered()方法。
public class ServerHeartbeatHandler extends ChannelInboundHandlerAdapter {
	/**
     * 事件触发后会调用此方法
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            if (e.state() == IdleState.READER_IDLE) {
                String content = "服务端读事件触发,向客户端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                // 发送心跳包
                ctx.writeAndFlush(byteBuf);
            } else if (e.state() == IdleState.WRITER_IDLE) {
                String content = "服务端写事件触发,向客户端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                // 发送心跳包
                ctx.writeAndFlush(byteBuf);
            } else if (e.state() == IdleState.ALL_IDLE) {
                String content = "服务端的读/写事件触发,向客户端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                // 发送心跳包
                ctx.writeAndFlush(byteBuf);
            }
        }
    }

	/**
     * 用于读取客户端发送的消息
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("执行 channelRead");
        // 处理接收到的数据
        ByteBuf byteBuf = (ByteBuf) msg;
        try {
            // 将接收到的字节数据转换为字符串
            String message = byteBuf.toString(CharsetUtil.UTF_8);
            // 打印接收到的消息
            System.out.println("接收到客户端消息为: " + message);
            // 发送响应消息给客户端
            ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端,我收到你的消息啦~", CharsetUtil.UTF_8));
        } finally {
            // 释放ByteBuf资源
            ReferenceCountUtil.release(byteBuf);
        }
    }
}

通过以上步骤,如果服务端5秒内没有从 Channel 中读取到 I/O 数据的话,就会触发一个 READER_IDLE 读事件,然后通过ServerHeartbeatHandler 中重写的userEventTriggered()方法中,根据 READER_IDLE 去向客户端发送对应的心跳包信息。如果客户端正常,则会在客户端的channelRead()方法中读取到这条心跳包信息,客户端再做出相应的响应信息返回给服务端。

客户端实现心跳检测
  1. 启动一个客户端实例
	/**
     * 创建客户端实例并向服务端发送连接请求
     */
    public static void start() {

        // 创建EventLoopGroup,用于处理客户端的I/O操作
        EventLoopGroup groupThread = new NioEventLoopGroup();

        try {
            // 创建Bootstrap实例,客户端启动对象
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(groupThread);
            // 设置服务端Channel类型为NioSocketChannel作为通道实现
            bootstrap.channel(NioSocketChannel.class);
            // 设置客户端处理
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ## 添加客户端自定义心跳机制Channel初始化类
                    socketChannel.pipeline().addLast(new ClientHeartbeatInitializer());
                }
            });
            // 向服务端发送连接请求
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8888).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 优雅地关闭线程
            groupThread.shutdownGracefully();
        }
    }
  1. 创建客户端自定义心跳机制Channel初始化类,并往 ChannelPipeline 中添加 IdleStateHandler 实例以及服务端处理 IdleStateEvent 事件的处理类。
public class ClientHeartbeatInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
		
		## 添加IdleStateHandler实例,5秒内未写入到Channel的数据时,触发 WRITER_IDLE 事件
        pipeline.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
        ## 添加客户端自定义 IdleStateEvent 事件处理类
        pipeline.addLast(new ClientHeartbeatHandler());
    }
}
  1. 创建客户端自定义 IdleStateEvent 事件处理类,继承ChannelInboundHandlerAdapter类,重写userEventTriggered()方法。
public class ClientHeartbeatHandler extends ChannelInboundHandlerAdapter {
    private int lossConnectCount = 0;

    /**
     * 事件触发后会调用此方法
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            if (e.state() == IdleState.WRITER_IDLE) {
                String content = "客户端写事件触发,向服务端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                ctx.writeAndFlush(byteBuf);
            } else if (e.state() == IdleState.READER_IDLE) {
                String content = "客户端读事件触发,向服务端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                ctx.writeAndFlush(byteBuf);
            }else if (e.state() == IdleState.ALL_IDLE) {
                String content = "客户端的读/写事件触发,向服务端发送心跳包";
                ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
                ctx.writeAndFlush(byteBuf);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 处理接收到的数据
        ByteBuf byteBuf = (ByteBuf) msg;
        try {
            // 将接收到的字节数据转换为字符串
            String message = byteBuf.toString(CharsetUtil.UTF_8);
            // 打印接收到的消息
            System.out.println("收到服务端响应的消息为: " + message);
            // TODO: 对数据进行业务处理
        } finally {
            // 释放ByteBuf资源
            ReferenceCountUtil.release(byteBuf);
        }
    }
}

通过以上步骤,如果客户端5秒内没有往 Channel 中写入 I/O 数据的话,就会触发一个 WRITER_IDLE 写事件,然后通过ClientHeartbeatHandler 中重写的userEventTriggered()方法中,根据 WRITER_IDLE 去向服务端发送对应的心跳包信息。如果服务端正常,则会在服务端的channelRead()方法中读取到这条心跳包信息,服务端再做出相应的响应信息返回给客户端。

你可能感兴趣的:(netty,netty)