文章目录
- [1. 简介](#1. 简介)
- [2. 实现](#2. 实现)
-
- [2.1 心跳包的定义](#2.1 心跳包的定义)
- [2.2 空闲事件检测](#2.2 空闲事件检测)
-
- [2.2.1 空闲事件类型](#2.2.1 空闲事件类型)
- [2.2.2 空闲检测机制](#2.2.2 空闲检测机制)
-
- [1. 时间戳记录](#1. 时间戳记录)
- [2. 定时任务驱动](#2. 定时任务驱动)
- [3. 定时任务循环触发](#3. 定时任务循环触发)
- [4. 示意图](#4. 示意图)
- [2.3 使用 IdleStateHandler](#2.3 使用 IdleStateHandler)
-
- [2.3.1 空闲事件的处理器 HeartbeatHandler](#2.3.1 空闲事件的处理器 HeartbeatHandler)
- [2.3.2 服务器类 HeartbeatServer](#2.3.2 服务器类 HeartbeatServer)
- [2.3.3 客户端类 HeartbeatClient](#2.3.3 客户端类 HeartbeatClient)
- [3. 总结](#3. 总结)
1. 简介
心跳监测机制 是一种在网络通信、分布式系统等领域广泛应用的技术手段,用于检测 连接的有效性 和 系统的健康状态 。它的基本原理是 在通信双方或系统组件之间 定期 发送一种特定的消息 (即 心跳包),这种消息就像人体的心跳一样,有规律地进行传输,以此来表明双方之间的连接正常且对方处于活跃状态。
2. 实现
2.1 心跳包的定义
客户端与服务器通信时需要定义消息的协议,比如 自定义 Netty 编解码器 中提到的具有四部分内容(magic, code, len, data
)的消息协议,可以按照 code
来区分消息的类型。
为了避免心跳包占用太多的资源,需要 尽量减少心跳包中包含的数据 。实际上心跳包中也不需要包含数据,只需要将心跳包发送到对端,让其知道自己存活即可。可以像如下代码定义消息类型和生成心跳包:
java
/**
* 消息
*/
public class Msg {
/**
* 默认的魔数
*/
public static final short DEFAULT_MAGIC = 9000;
/**
* 业务码的枚举类
*/
public enum CodeEnum {
/**
* 心跳包的业务码
*/
HEARTBEAT(1000);
private final int code;
CodeEnum(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
/**
* 魔数
*/
private short magic;
/**
* 业务码
*/
private int code;
/**
* 消息内容的长度
*/
private int len;
/**
* 消息的内容
*/
private byte[] data;
public Msg(int code, byte[] data) {
this.magic = DEFAULT_MAGIC;
this.code = code;
this.len = data.length;
this.data = data;
}
/**
* 创建一个心跳包
*/
public static Msg heartbeatMsg() {
return new Msg(CodeEnum.HEARTBEAT.code, new byte[0]);
}
public short getMagic() {
return magic;
}
public void setMagic(short magic) {
this.magic = magic;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
2.2 空闲事件检测
2.2.1 空闲事件类型
- 读空闲事件 :
- 触发条件:在指定时间内,未接收到任何数据。
- 用途:检测对端是否断开连接或长时间无响应。
- 写空闲事件 :
- 触发条件:在指定时间内,未发送任何数据。
- 用途:用于发送心跳包或维持连接活性。
- 全空闲事件 :
- 触发条件:在指定时间内,既未接收到数据也未发送数据。
- 用途:检测连接完全无活动的场景(如双向通信中断)。
2.2.2 空闲检测机制
1. 时间戳记录
每次执行读 / 写操作时,更新对应的最后活动时间戳 (lastReadTime
/lastWriteTime
):
- 读操作:在 初始化时 或 读操作完成时 (在
readComplete
事件的处理方法中),更新lastReadTime
。 - 写操作:在 初始化时 或 写操作完成时 (在
write
方法的回调监听器中),更新lastWriteTime
。
2. 定时任务驱动
基于 Netty 的 ScheduledExecutorService
,在初始化时启动三个独立的定时任务:读空闲检测 ReaderIdleTimeoutTask
、写空闲检测 WriterIdleTimeoutTask
、全空闲检测 AllIdleTimeoutTask
。
3. 定时任务循环触发
以上提到的三个定时任务并没有设置固定的时间间隔,而是根据实际情况设置不同延迟时间的定时任务,具体的思想如下所示:
注:
- 以下会称 用户在
IdleStateHandler
构造器中设置的时间间隔 为 标准时间间隔。- 读操作执行了两个重要操作:
- 处理
read
事件时将reading
设置为true
。- 处理
readComplete
事件时将reading
设置为false
。- 只要理解了 读空闲检测 和 写空闲检测,全空闲检测 就很容易理解,这里就不做赘述。
ReaderIdleTimeoutTask
:- 如果正在读取,即
reading == true
,则设置一个延迟时间为标准时间间隔的定时任务,否则执行下列操作。 - 计算
nextDelay
:nextDelay = 当前时间 - lastReadTime + 标准时间间隔
。 - 如果
nextDelay > 0
,则说明以当前时间戳为基准,往前推一个标准时间间隔,这个时间段中执行了读操作,所以应该 缩短 下一个定时任务的时间间隔,即将nextDelay
作为其延迟时间,从而保证 读空闲检测 覆盖了 最后一个读操作结束后的一个标准时间间隔。 - 否则这个时间段中没有执行读操作,应保持标准时间间隔不变,即将
标准时间间隔
作为下一个定时任务的延迟时间。
- 如果正在读取,即
WriterIdleTimeoutTask
:- 计算
nextDelay
:nextDelay = 当前时间 - lastWriteTime + 标准时间间隔
。 - 如果
nextDelay > 0
,则说明以当前时间戳为基准,往前推一个标准时间间隔,这个时间段中执行了写操作,所以应该 缩短 下一个定时任务的时间间隔,即将nextDelay
作为其延迟时间,从而保证 写空闲检测 覆盖了 最后一个写操作结束后的一个标准时间间隔。 - 否则这个时间段中没有执行写操作,应保持标准时间间隔不变,即将
标准时间间隔
作为下一个定时任务的延迟时间。
- 计算
问:为什么读操作有
reading
标志来判断是否正在读,而写操作没有writing
标志来判断是否正在写?答:其实很简单,原因在于读操作和写操作的特性不同:
- 读操作特性:读操作可能会持续较长时间 。在这个过程中,如果没有
reading
标志,IdleStateHandler
可能会误判为读空闲,从而触发不必要的空闲事件。- 写操作特性:写操作通常具有原子性和即时性 。当调用
ChannelHandlerContext
的write
或writeAndFlush
方法时,Netty 会将写请求封装成消息对象放入发送队列,底层的 I/O 线程会尽快将其发送出去。即使在高并发场景下,写操作也会被高效处理,一般不会出现长时间处于 "正在写" 的状态。因此,不需要额外的标志来跟踪写操作的进行状态。
4. 示意图
注意:read
时的定时任务处理没有发出读空闲事件(依赖 reading
标志),write
时的定时任务处理发出了写空闲事件。实际上 write
占用的时间很短,很难遇到写空闲处理示意图中 write
时的定时任务处理发出写空闲事件 。
注:图中提到的"某种原因导致定时任务延迟执行"中,"某种原因"可能是 任务队列中其它任务都正在执行,定时任务需要排队等待。
2.3 使用 IdleStateHandler
2.3.1 空闲事件的处理器 HeartbeatHandler
java
/**
* 空闲事件的处理器
*/
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Msg message && message.getCode() == Msg.CodeEnum.HEARTBEAT.getCode()) {
System.out.println("接收到心跳包");
} else {
super.channelRead(ctx, msg);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent event) {
if (event.state() == IdleState.READER_IDLE) {
System.out.println("读空闲,断开连接");
ctx.close();
} else if (event.state() == IdleState.WRITER_IDLE) {
System.out.println("发送心跳包");
ctx.writeAndFlush(Msg.heartbeatMsg());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("断开连接");
super.channelInactive(ctx);
}
}
2.3.2 服务器类 HeartbeatServer
java
/**
* 拥有心跳机制的服务器
*/
public class HeartbeatServer {
public static void main(String[] args) {
final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
final EventLoopGroup workerGroup = new NioEventLoopGroup();
final ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new MsgDecoder())
.addLast(new MsgEncoder())
// 如果 3s 内没有读操作,则触发读空闲事件
.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS))
.addLast(new HeartbeatHandler());
}
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}));
bootstrap.bind(8888);
}
}
2.3.3 客户端类 HeartbeatClient
java
/**
* 拥有心跳机制的客户端
*/
public class HeartbeatClient {
public static void main(String[] args) {
final EventLoopGroup group = new NioEventLoopGroup();
final Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new MsgDecoder())
.addLast(new MsgEncoder())
// 如果 2s 内没有写操作,则触发写空闲事件
.addLast(new IdleStateHandler(0, 2, 0, TimeUnit.SECONDS))
.addLast(new HeartbeatHandler());
}
});
Runtime.getRuntime().addShutdownHook(new Thread(group::shutdownGracefully));
bootstrap.connect("127.0.0.1", 8888);
}
}
注意:如果想测试服务器在触发读空闲事件后断开与客户端的连接,则可以注释掉客户端中添加 IdleStateHandler
的操作。
3. 总结
心跳监测机制对于高性能的服务器来说必不可少 ,它广泛用于 需要检测连接是否有效 的场景,如果连接无效,则断开连接,从而节省服务器/客户端的资源;而且可以在写空闲的时候给对端发送心跳包,从而让其知道连接有效,自己处于存活状态。
在 Netty 中,使用 IdleStateHandler
可以轻松地实现心跳监测机制,它的核心原理在于 记录最后一次操作的时间戳 和 使用有延迟时间的定时任务检测并触发。