Netty 连接存活检测——如何判断连接是否断开?
一.前言
最近碰到了一个需求,需要判断客户端与服务端之间的连接是否已经断开,查阅资料后发现Netty提供了如下几种方式,现总结如下,方便后续查看。
二.存活检测方式
2.1 SO_KEEPALIVE
选项
Netty
提供了SO_KEEPALIVE
选项,当连接保持空闲一段时间时,TCP 会自动向远程对等方发送保持活动探测。但由于该时间间隔依赖于操作系统(通常默认值为2小时),因此此种方式不太被推荐使用。
2.2 IdleStateHandler
当Channel
在一段时间内没有执行读或写操作时,会触发IdleStateEvent
。
IdleStateHandler
关于读写空闲多久触发IdleStateEvent
包含如下三个参数:
readerIdleTime
:读空闲时间, 超过设定的时间,会触发IdleState.READER_IDLE
,若设置为0
表示禁用。writerIdleTime
:写空闲时间, 超过设定的时间,会触发IdleState.WRITE_IDLE
,若设置为0
表示禁用。allIdleTime
:读写都空闲时间,超过设定时间,会触发IdleState.ALL_IDLE
,若设置为0
表示禁用。
上述IdleStateEvent
可以在方法public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
中捕获。
下面是一个客户端应用IdleStateHandler
的示例,在该示例中,若30秒没有读写事件时,客户端会连续向服务端发送 3次PING
,每次间隔10秒,若期间收到了服务端回复的PING_ACK
则表明连接还存在,否则直接关闭该连接。
public class NettyRWTimeoutClient {
private static final int PORT = 8080;
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
// 30s内读写都空闲, 触发 IdleStateEvent
pipeline.addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));
pipeline.addLast(new ClientHandler());
}
});
ChannelFuture future = bootstrap.connect("localhost", PORT).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
class ClientHandler extends SimpleChannelInboundHandler<String> {
private final int maxPingAttempts = 3; // 最多发送3次PING
private int pingCount = 0;
private int pingTimeInterval = 10; // 每隔10s发送一次PING
private ScheduledFuture<?> pingTask;
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) {
if ("PING_ACK".equals(msg)) {
System.out.println("Received PING_ACK from server");
pingCount = 0; // PING_ACK, 重置Ping计数
if (pingTask != null) {
pingTask.cancel(false); // 取消后续PING任务
pingTask = null;
}
} else {
System.out.println("Received: " + msg);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.ALL_IDLE) {
sendPingToServer(ctx);
}
}
}
private void sendPingToServer(ChannelHandlerContext ctx) {
if (pingTask != null) {
return; // 任务已在进行中
}
System.out.println("start send PINT To Server");
pingTask = ctx.executor().scheduleAtFixedRate(() -> {
if (pingCount < maxPingAttempts) {
System.out.println("Sending PING " + (pingCount + 1));
ctx.writeAndFlush("PING");
pingCount++;
} else {
System.out.println("No response after " + maxPingAttempts + " PINGs, closing connection.");
ctx.close();
pingTask.cancel(false);
}
}, 0, pingTimeInterval, TimeUnit.SECONDS); // 每10s发送一次 PING
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
三.结语
上述博文在完成的过程中参考了如下资料:
- Reactor Netty Reference Guide
- Using Netty, understand its connection idle handling
- Netty 官方文档——ReadTimeoutHandler
以上便是本文的全部内容,如果觉得不错可以支持一下博主,若有任何问题也敬请批评指正。