Netty网络框架学习笔记-7((心跳)检测空闲连接以及超时)

1.0 前言:

为了能够及时的将资源释放出来,我们会检测空闲连接和超时。常见的方法是通过发送信息来测试一个不活跃的链接,通常被称为“心跳”,然后在远端确认它是否还活着。(还有一个方法是比较激进的,简单地断开那些指定的时间间隔的不活跃的链接)。

处理空闲连接是一项常见的任务,Netty 提供了几个 ChannelHandler 实现此目的。

名称

描述

IdleStateHandler

如果连接闲置时间过长,则会触发 IdleStateEvent 事件。在 ChannelInboundHandler 中可以覆盖 userEventTriggered(...) 方法来处理 IdleStateEvent。

ReadTimeoutHandler

在指定的时间间隔内没有接收到入站数据则会抛出 ReadTimeoutException 并关闭 Channel。ReadTimeoutException 可以通过覆盖 ChannelHandler 的 exceptionCaught(…) 方法检测到。

WriteTimeoutHandler

WriteTimeoutException 可以通过覆盖 ChannelHandler 的 exceptionCaught(…) 方法检测到。

2.0 简单例子

用来检测连接是否在空闲状态下!

2.1 带心跳检测的服务端

/**
 * @Author: ZhiHao
 * @Date: 2022/4/24 17:08
 * @Description: 心跳服务器
 * @Versions 1.0
 **/
@Slf4j
public class HeartbeatServer {

    public static void main(String[] args) {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(2);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boosGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childOption(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.INFO)) // boosGroup服务器处理程序, 日志
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        // 添加框架提供的字符串编解码处理器
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new StringDecoder());
                        //1. IdleStateHandler 是 netty 提供的处理空闲状态的处理器
                        //2. long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
                        //3. long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
                        //4. long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
                        pipeline.addLast(new IdleStateHandler(5, 5, 10, TimeUnit.SECONDS));
                        //5. 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个 handler 去处理, 通过调用(触发)下一个 handler 的 userEventTiggered ,
                        // 加入自己的自定义处理器(在处理器该userEventTriggered方法中去处理 IdleStateEvent(读 空闲,写空闲,读写空闲))
                        pipeline.addLast(new HeartbeatServerHandle());
                    }
                });

        try {
            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        log.info("HeartbeatServer==, 服务器已经启动成功, 等待连接中!");
                    }
                }
            });
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("HeartbeatServer===, 发生异常:{}", e);
        } finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

2.1.1 服务端自定义处理器(包含处理空闲事件)

@Slf4j
public class HeartbeatServerHandle extends SimpleChannelInboundHandler<String> {

    private static ConcurrentHashMap<String,Long> concurrentHashMap = new ConcurrentHashMap<>();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        log.info("HeartbeatServerHandle-读取到信息:{}", msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 判断事件是否是IdleStateEvent
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateHandler = (IdleStateEvent) evt;
            IdleState state = idleStateHandler.state();
            String evtState = null;
            String key = ctx.channel().id().asLongText();
            Long count = concurrentHashMap.getOrDefault(key, 0L);
            switch(state) {
                case READER_IDLE:
                    evtState = "读空闲";
                    break;
                case WRITER_IDLE:
                    evtState = "写空闲";
                    break;
                case ALL_IDLE:
                    evtState = "读写空闲";
                    count++;
                    break;
                default:
                    break;
            }
            log.info("userEventTriggered-evtState:{}", evtState);
            // 空闲计数达5次, 进行测试连接是否正常
            if (count > 2L){
                ctx.writeAndFlush("测试客户端是否能接收信息")
                    // 发送失败时关闭通道, 在或者可以在达到空闲多少次后, 进行关闭通道
                        .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);  
                concurrentHashMap.remove(key);
                return;
            }
            concurrentHashMap.put(key,count);
        } else {
            // 事件不是一个 IdleStateEvent 的话,就将它传递给下一个处理程序
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
    }
}

2.2 客户端

@Slf4j
public class HeartbeatClient {

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

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        // 添加框架提供的字符串编解码处理器
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new HeartbeatClientHandle());
                    }
                });

        try {
            ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8888)).sync();
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        log.info("HeartbeatClient==, 客户端已经启动成功, 连接成功!");
                    }
                }
            });
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("HeartbeatClient===, 发生异常:{}", e);
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

2.2.1 客户端处理器

@Slf4j
public class HeartbeatClientHandle extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        log.info("HeartbeatClientHandle-读取到信息:{}", msg);
        //ctx.writeAndFlush("服务端你好!");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
    }
}

结果:

19:35:38.182 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲
19:35:38.182 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:写空闲
19:35:43.186 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读写空闲
19:35:43.186 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲
19:35:43.186 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:写空闲
19:35:48.189 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲
19:35:48.189 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:写空闲
19:35:53.191 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读写空闲
19:35:53.191 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲
19:35:53.191 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:写空闲
19:35:58.205 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲
19:35:58.205 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:写空闲
19:36:03.197 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读写空闲
19:36:03.210 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@40d2cc5c
19:36:03.214 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.HeartbeatServerHandle - userEventTriggered-evtState:读空闲

// -------------------------------------------------------------------------------------------------------
19:36:03.225 [nioEventLoopGroup-2-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@3a94a792
19:36:03.231 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.heartbeat.HeartbeatClientHandle - HeartbeatClientHandle-读取到信息:测试客户端是否能接收信息

一旦连接断开, 就不会在检测心跳!

3.0 另一种场景上报数据:

在物联网或者其他需要在连接空闲时候, 上报自身状态 (例如: 上报设备状态, 或上报服务器状态)

3.1 客户端改造

// 省略其他代码
Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        // 添加框架提供的字符串编解码处理器
                        pipeline.addLast(new StringEncoder());
                        pipeline.addLast(new StringDecoder());
                        //  在写空闲30秒的时候进行上报数据
                        pipeline.addLast(new IdleStateHandler(0,30,0, TimeUnit.SECONDS));
                        pipeline.addLast(new HeartbeatClientReportHandle());
                    }
                });
// 省略其他代码

3.1.1 客户端处理器

@Slf4j
public class HeartbeatClientReportHandle extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        log.info("HeartbeatClientReportHandle-读取到信息:{}", msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 判断事件是否是IdleStateEvent
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateHandler = (IdleStateEvent) evt;
            if (idleStateHandler.state().equals(IdleState.WRITER_IDLE)){
                ctx.writeAndFlush(String.format("当前设备号: %s, 状态:%s", RandomUtil.randomInt(),RandomUtil.randomInt()));
            }
        } else {
            // 事件不是一个 IdleStateEvent 的话,就将它传递给下一个处理程序
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
    }
}

结果:

// 服务端日志
15:58:45.697 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: -1691569318, 状态:-1274325485
15:59:15.680 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: 733278004, 状态:1290704661
15:59:45.683 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: 167749378, 状态:-1514067376
16:00:15.696 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: -156490593, 状态:-50335223
16:00:45.711 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: 1998916514, 状态:-1099924846
16:01:15.725 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: 1809189822, 状态:1953605308
16:01:45.731 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: -668619106, 状态:-1750520516
16:02:15.744 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.heartbeat.report.HeartbeatReportServerHandle - HeartbeatReportServerHandle-读取到信息:当前设备号: 2142985320, 状态:880620132

从结果可以看出, 客户端每隔39秒会上报一次数据。

PS: 这个是要在通道空闲时候才会进行催发, 我们可能模拟IdleStateHandler 写一个定时上报的处理器, 里面创建发送定时上报的事件传递给下一个处理器。

1