在 TCP 保持长连接的过程中,客户端与服务端之间如果长时间没有交互的话,无法发现对方是否已经掉线。为了确保客户端与服务端是否都还正常工作,客户端和服务端需要定期发送心跳包来维护连接。其核心实现依赖的是 IdleStateHandler 类, IdleStateHandler 类的构造函数中的参数如下:
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) {
this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}
## 这里设置5秒内没有从 Channel 读取到数据时会触发一个 READER_IDLE 事件。
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
## 添加 IdleStateEvent 事件的处理类
pipeline.addLast(new ServerHeartbeatHandler());
/**
* 创建服务端实例并绑定端口
* @throws InterruptedException
*/
public static void bind() throws InterruptedException {
// 创建boss线程组,用于接收连接
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 创建worker线程组,用于处理连接上的I/O操作,含有子线程NioEventGroup个数为CPU核数大小的2倍
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap实例,服务器启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
// 使用链式编程配置参数
// 将boss线程组和worker线程组暂存到ServerBootstrap
bootstrap.group(bossGroup, workerGroup);
// 设置服务端Channel类型为NioServerSocketChannel作为通道实现
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 添加服务端自定义心跳机制Channel初始化类
pipeline.addLast(new ServerHeartbeatInitializer());
}
});
// 设置启动参数,初始化服务器连接队列大小。服务端处理客户端连接请求是顺序处理,一个时间内只能处理一个客户端请求
// 当有多个客户端同时来请求时,未处理的请求先放入队列中
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 绑定端口并启动服务器,bind方法是异步的,sync方法是等待异步操作执行完成,返回ChannelFuture异步对象
ChannelFuture channelFuture = bootstrap.bind(8888).sync();
// 等待服务器关闭
channelFuture.channel().closeFuture().sync();
} finally {
// 优雅地关闭boss线程组
bossGroup.shutdownGracefully();
// 优雅地关闭worker线程组
workerGroup.shutdownGracefully();
}
}
public class ServerHeartbeatInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
## 添加IdleStateHandler实例,5秒内未读取到Channel的数据时,触发READER_IDLE事件
pipeline.addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
## 添加服务端自定义 IdleStateEvent 事件处理类
pipeline.addLast(new ServerHeartbeatHandler());
}
}
public class ServerHeartbeatHandler extends ChannelInboundHandlerAdapter {
/**
* 事件触发后会调用此方法
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.READER_IDLE) {
String content = "服务端读事件触发,向客户端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
// 发送心跳包
ctx.writeAndFlush(byteBuf);
} else if (e.state() == IdleState.WRITER_IDLE) {
String content = "服务端写事件触发,向客户端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
// 发送心跳包
ctx.writeAndFlush(byteBuf);
} else if (e.state() == IdleState.ALL_IDLE) {
String content = "服务端的读/写事件触发,向客户端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
// 发送心跳包
ctx.writeAndFlush(byteBuf);
}
}
}
/**
* 用于读取客户端发送的消息
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("执行 channelRead");
// 处理接收到的数据
ByteBuf byteBuf = (ByteBuf) msg;
try {
// 将接收到的字节数据转换为字符串
String message = byteBuf.toString(CharsetUtil.UTF_8);
// 打印接收到的消息
System.out.println("接收到客户端消息为: " + message);
// 发送响应消息给客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端,我收到你的消息啦~", CharsetUtil.UTF_8));
} finally {
// 释放ByteBuf资源
ReferenceCountUtil.release(byteBuf);
}
}
}
通过以上步骤,如果服务端5秒内没有从 Channel 中读取到 I/O 数据的话,就会触发一个 READER_IDLE 读事件,然后通过ServerHeartbeatHandler 中重写的userEventTriggered()方法中,根据 READER_IDLE 去向客户端发送对应的心跳包信息。如果客户端正常,则会在客户端的channelRead()方法中读取到这条心跳包信息,客户端再做出相应的响应信息返回给服务端。
/**
* 创建客户端实例并向服务端发送连接请求
*/
public static void start() {
// 创建EventLoopGroup,用于处理客户端的I/O操作
EventLoopGroup groupThread = new NioEventLoopGroup();
try {
// 创建Bootstrap实例,客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(groupThread);
// 设置服务端Channel类型为NioSocketChannel作为通道实现
bootstrap.channel(NioSocketChannel.class);
// 设置客户端处理
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
## 添加客户端自定义心跳机制Channel初始化类
socketChannel.pipeline().addLast(new ClientHeartbeatInitializer());
}
});
// 向服务端发送连接请求
ChannelFuture channelFuture = bootstrap.connect("localhost", 8888).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅地关闭线程
groupThread.shutdownGracefully();
}
}
public class ClientHeartbeatInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
## 添加IdleStateHandler实例,5秒内未写入到Channel的数据时,触发 WRITER_IDLE 事件
pipeline.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
## 添加客户端自定义 IdleStateEvent 事件处理类
pipeline.addLast(new ClientHeartbeatHandler());
}
}
public class ClientHeartbeatHandler extends ChannelInboundHandlerAdapter {
private int lossConnectCount = 0;
/**
* 事件触发后会调用此方法
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.WRITER_IDLE) {
String content = "客户端写事件触发,向服务端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
} else if (e.state() == IdleState.READER_IDLE) {
String content = "客户端读事件触发,向服务端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
}else if (e.state() == IdleState.ALL_IDLE) {
String content = "客户端的读/写事件触发,向服务端发送心跳包";
ByteBuf byteBuf = Unpooled.copiedBuffer(content, CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 处理接收到的数据
ByteBuf byteBuf = (ByteBuf) msg;
try {
// 将接收到的字节数据转换为字符串
String message = byteBuf.toString(CharsetUtil.UTF_8);
// 打印接收到的消息
System.out.println("收到服务端响应的消息为: " + message);
// TODO: 对数据进行业务处理
} finally {
// 释放ByteBuf资源
ReferenceCountUtil.release(byteBuf);
}
}
}
通过以上步骤,如果客户端5秒内没有往 Channel 中写入 I/O 数据的话,就会触发一个 WRITER_IDLE 写事件,然后通过ClientHeartbeatHandler 中重写的userEventTriggered()方法中,根据 WRITER_IDLE 去向服务端发送对应的心跳包信息。如果服务端正常,则会在服务端的channelRead()方法中读取到这条心跳包信息,服务端再做出相应的响应信息返回给客户端。