简介

本文基础框架为Springboot,使用Netty构建网络连接。主要描述了使用Netty的心跳处理,保证长连接的通讯正常。

实现逻辑

上代码之前简单说明一下实现逻辑,有利于理解代码。

Netty为我们提供了一个handler。当channel空闲达到这个handler的条件时,会触发一个状态的变化,我们拿到这个状态,就可以进行心跳处理。

IdleStateHandler(0,0,0)。

  • 第一个参数readerIdleTimeSeconds。如果在channel中长时间读不到数据,达到了这个参数设置的时间(单位秒)。那么就会触发状态READER_IDLE
  • 第二个参数writeIdleTimeSeconds。如果长时间没有向channel中写数据,达到了这个参数设置的时间(单位秒)。那么就会触发状态WRITER_IDLE。
  • 第三个参数allIdleTimeSeconds。可以理解为前两个参数的合并。不管是长时间没有读还是写,达到了这个时间(单位秒)。那么就会触发状态ALL_IDLE。

那么怎么拿到这个状态呢。在心跳handler中使用userEventTriggered方法。从ChannelHandlerContext中读取。下面代码中会有。

代码

Netty服务类。运行bind方法将启动netty服务。使用Springboot的话,可以使用@Component交给容器管理。需要使用的地方,自动注入然后调用bind方法。如果没有使用Spring容器,那么直接new就可以。

public class EchoServer {
    public void bind(int port) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //心跳
                            ch.pipeline().addLast(new IdleStateHandler(5, 0, 0))
                                    //解码-粘包拆包处理
                                    .addLast(new LengthFieldBasedFrameDecoder(1024*20, 0, 2, 0, 2))
                                    //编码
                                    .addLast(new LengthFieldPrepender(2))
                                    //自定义解码
                                    .addLast(new JsonDecoder())
                                    //自定义编码
                                    .addLast(new JsonEncoder())
                                    //心跳
                                    .addLast(new HeartBeatHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

简单说明一下和心跳实现有关的:

  • ch.pipeline().addLast(new IdleStateHandler(5, 0, 0)) 在"流水线"上绑定了IdleStateHandler。只设置了第一个参数5。表明,如果在channel中,5秒未读取到数据,将会触发READER_IDLE状态。
  • .addLast(new LengthFieldBasedFrameDecoder(1024*20, 0, 2, 0, 2)) 在"流水线"上绑定长度解码器。这个解码器也是Netty提供的。它能解决粘包拆包的问题。我认为实现比较简单,而且也比较适合复杂业务。
  • .addLast(new HeartBeatHandler())这个就是具体的心跳业务实现了。里面有心跳包的处理、状态的捕获等。那么我们看这个handler的代码。
public class HeartBeatHandler extends SimpleChannelInboundHandler<CustomProtocol> {

    private final static Logger logger = LoggerFactory.getLogger(HeartBeatHandler.class);

    private final static String HEART_TYPE="heartbeat";

    private int readIdleTimes=0;

    /**
     * 取消绑定
     * @param ctx
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

    }
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
            if (idleStateEvent.state() == IdleState.READER_IDLE) {
                readIdleTimes++;
                logger.info("已经5秒没有收到信息!");
                //向客户端发送心跳包 探究客户端业务处理状态
                ctx.writeAndFlush(new CustomProtocol(HEART_TYPE, null,"ping"));
            }
            if (readIdleTimes==12){
                logger.info("关闭链接");
                ctx.channel().close();
            }
        }
        super.userEventTriggered(ctx, evt);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, CustomProtocol customProtocol) throws Exception {
        readIdleTimes=0;
        //不是心跳包 透传
        if (!HEART_TYPE.equals(customProtocol.getType())){
            ctx.fireChannelRead(customProtocol);
            return;
        }
        if ("ping".equals(customProtocol.getMsgBody())){
            ctx.writeAndFlush(new CustomProtocol(HEART_TYPE, null,"pong"));
        }
    }
}
  • channelInactive 链接关闭时触发。可以放一些业务逻辑
  • userEventTriggered 这个方法中写的就是捕获channel状态了。因为我在IdleStateHandler中配的是第一个参数,所以这里使用的判断是idleStateEvent.state() == IdleState.READER_IDLE。如果配第二个就用WRITER_IDLE。第三个就用ALL_IDLE。
  • 心跳逻辑是,5秒没接到数据,发送一个心跳包给客户端。如果连续12次都没有收到任何回复。也就是1分钟客户端未响应。会断开channel
  • channelRead0就是正常收到数据走的方法了。我根据包中的数据类型判断,如果不是心跳包,触发下一个handler的channelRead0方法
ctx.fireChannelRead(customProtocol);

心跳的处理到这就结束了。以上代码实现了,服务端5s在channel读取不到数据,向客户端发送心跳包等待回应,如果连续一分钟都没有回应,断开连接的业务逻辑。希望对大家能有所帮助。如果有不足之处还请指正。
最后把业务包对象CustomProtocol的代码粘出来,毕竟代码中用到了。

/**
 * @description:协议实体类
 */
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomProtocol implements Serializable {
    //消息类型
    private String type;

    //设备编号
    private Integer deviceId;

    //消息体
    private String msgBody;

    @Override
    public String toString(){
        return JSONObject.toJSONString(this);
    }

}