心跳:即在 TCP 长连接中,客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线,以确保 TCP 连接的有效性。
在 Netty 中,实现心跳机制的关键是 IdleStateHandler
,看下它的构造器:
其中:
注意:
三个参数的构造器,默认的时间单位是秒。若需要指定其他时间单位,可以使用四个参数的构造方法。
要实现 Netty服务端心跳检测机制,需要在服务器端的 ChannelInitializer中加入如下的代码:
pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
我们指定 readerIdleTime参数指定超过 3秒还没收到客户端的连接,会触发 IdleStateEvent事件并且交给下一个handler处理,下一个handler必须实现 userEventTriggered方法
处理对应事件。
public class HeartBeatServer {
public static void main(String[] args) throws Exception {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class).
childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// IdleStateHandler的readerIdleTime参数指定超过3秒还没收到客户端的连接,
// 会触发IdleStateEvent事件并且交给下一个handler处理,下一个handler必须实现userEventTriggered方法处理对应事件
pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartBeatServerHandler());
}
});
System.out.println("服务端已经准备就绪...");
ChannelFuture future = bootstrap.bind(19000).sync();
System.out.println("服务器启动完成,等待客户端的连接和数据...");
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
Handler处理:
public class HeartBeatServerHandler extends SimpleChannelInboundHandler<String> {
private AtomicInteger readIdleTimes = new AtomicInteger(0);
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
System.out.println(" ====== > [server] message received : " + s);
if ("Heartbeat Packet".equals(s)) {
ctx.channel().writeAndFlush("ok");
} else {
System.out.println(" 其他信息处理 ... ");
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
readIdleTimes.incrementAndGet(); // 读空闲的计数加1
break;
case WRITER_IDLE:
eventType = "写空闲";
// 不处理
break;
case ALL_IDLE:
eventType = "读写空闲";
// 不处理
break;
}
System.out.println(ctx.channel().remoteAddress() + " 超时事件:" + eventType);
if (readIdleTimes.get() > 3) {
System.out.println("[server]读空闲超过3次,关闭连接,释放更多资源");
ctx.channel().writeAndFlush("idle close");
ctx.channel().close();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
}
}
public class HeartBeatClient {
public static void main(String[] args) throws Exception {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new HeartBeatClientHandler());
}
});
System.out.println("客户端准备就绪,随时可以连接服务端");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 19000).sync();
System.out.println("客户端已连接到服务器...");
//获取通道,模拟发送心跳包
Channel channel = channelFuture.channel();
String text = "Heartbeat Packet";
Random random = new Random();
while (channel.isActive()) {
int num = random.nextInt(8); //随机睡眠
Thread.sleep(num * 1000);
channel.writeAndFlush(text);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
Handler处理:
public class HeartBeatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(" client received :" + msg);
if (msg != null && msg.equals("idle close")) {
System.out.println("服务端关闭了该连接,客户端也关闭");
ctx.channel().closeFuture();
}
}
}
查看 IdleStateHandler中的 channelRead方法:
在 Read 网络数据时,如果我们可以确保每个 InboundHandler 都把数据往后传递了,就调用了相关的 fireChannelRead 方法。
该方法只是进行了 handler火炬传递,不做任何业务逻辑处理,让 channelPipe中的下一个 handler处理 channelRead方法。
查看 IdleStateHandler中的 channelActive方法:
initialize()方法是 IdleStateHandler的精髓
,源码如下:
private void initialize(ChannelHandlerContext ctx) {
switch(this.state) {
case 1:
case 2:
return;
default:
this.state = 1;
this.initOutputChanged(ctx);
this.lastReadTime = this.lastWriteTime = this.ticksInNanos();
if (this.readerIdleTimeNanos > 0L) {
this.readerIdleTimeout = this.schedule(ctx, new IdleStateHandler.ReaderIdleTimeoutTask(ctx), this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (this.writerIdleTimeNanos > 0L) {
this.writerIdleTimeout = this.schedule(ctx, new IdleStateHandler.WriterIdleTimeoutTask(ctx), this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (this.allIdleTimeNanos > 0L) {
this.allIdleTimeout = this.schedule(ctx, new IdleStateHandler.AllIdleTimeoutTask(ctx), this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
}
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
return ctx.executor().schedule(task, delay, unit);
}
这里会触发一个Task任务(ReaderIdleTimeoutTask)。
查看这个 ReaderIdleTimeoutTask类的 run方法,源码如下:
nextDelay:是用当前时间减去最后一次channelRead方法调用的时间。
– 求知若饥,虚心若愚。