简介
本文基础框架为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);
}
}