心跳检测机制
目的:就是用于检测 检测通信对方是否还”在线”,如果已经断开,就要释放资源 或者 尝试重连。
大概的实现原理就是:在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle
状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG
交互。双方都可以知道连接的有效性。
我们可以通过两种方式实现心跳机制:
-
使用 TCP 协议层面的
keepalive
机制。 -
在应用层上实现自定义的心跳机制(就是,自己编写程序,规定双方心跳交互的规则)。
但是,大部分都是在应用层自己实现这个机制。
应用层实现心跳机制好处:
- 一是灵活, 应用层心跳规则可以自由定义,可实现各时间间隔(秒级、毫秒级)的检测,包里还可以携带额外的信息。
- 二是通用, 应用层的心跳规则不依赖于底层协议。如果后期想把TCP改为UDP,协议层不提供心跳机制了,但应用层的心跳依旧是通用的。
TCP协议的 KeepAlive 机制
TCP 长连接
在 TCP 协议层面上, 提供了 keepalive
机制, 用于检测 TCP 连接的死活。
tcp-keepalive
机制,由操作系统内核实现;
默认不开启, 需要自行开启。
开启之后有三个参数会生效,来决定一个 keepalive 的行为:
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
tcp_keepalive_time
: 在 TCP 保活打开的情况下,最后一次数据交换到 TCP 发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2 小时);
tcp_keepalive_probes
: 在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包次数,默认值为 9(次);
tcp_keepalive_intvl
:在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为 75s。
修改系统 tcp-keepalive 参数配置
Linux环境下,修改/etc/sysctl.conf
文件
# 表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是7200秒(2小时),改为5秒钟。
net.ipv4.tcp_keepalive_time = 5
# 如果对方不予应答,探测包的发送次数
net.ipv4.tcp_keepalive_probes = 5
# 探测消息发送的频率
net.ipv4.tcp_keepalive_intvl = 1
使用场景
比如 HTTP/1.0
通过在 header
头中添加 Connection:Keep-Alive
参数,如果当前请求需要保活则添加该参数作为标识,否则服务端就不会保持该连接的状态,发送完数据之后就关闭连接。
HTTP/1.1
以后 Keep-Alive
是默认打开的。
比如 监控系统,IM系统,即时报价系统,推送服务等等。像这些场景都是比较注重实时性,如果每次发送数据都要进行一次DNS解析,建立连接的过程肯定是极其影响体验。
TCP keepalive 的缺点
-
首先,它不是 TCP 的标准协议, 并且是默认关闭的。
-
TCP keepalive
机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 2个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够。 -
TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了。
-
TCP KeepAlive 仅限于 检测连接的死活。
例如:某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态,对客户端而言,这时的最好选择就是断线后重新连接其他服务器,而不是一直认为当前服务器是可用状态一直向当前服务器发送些必然会失败的请求。
Netty 提供了 tcp-keepalive 的设置
// 主从线程池
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
// server 启动引导
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 配置 server 启动参数
serverBootstrap.group(bossGroup,workerGroup)
.childOption(ChannelOption.SO_KEEPALIVE,true)
childOption(ChannelOption.SO_KEEPALIVE,true)
就表示打开 TCP 的 keepAlive 设置。
Netty 心跳检测机制
在 Netty 中, 实现心跳机制的关键是 IdleStateHandler
, 它可以对一个 Channel
的 读/写 设置 定时器, 当 Channel
在一定事件间隔内没有数据交互时(即处于 idle 状态), 就会回调 ChannelHandler
的userEventTriggered
方法。
UML 图
实现步骤
Pipeline
中 添加IdleStateHandler
Netty提供了心跳检测类IdleStateHandler,它有三个参数,分别是读超时时间、写超时时间和读写超时时间。
- readerIdleTime:读超时时间;
- writerIdleTime:写超时时间;
- allIdleTime:读/写超时. 即当在指定的事件间隔内既没有读又没有写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
注意:如果 参数是 0,代表不检测
// 当 IdleStateEvent 触发后 , 就 可以 调用 super.userEventTriggered(ctx, evt); 传递给管道 的下一个handler去处理
// 通过调用(触发)下一个handler 的 userEventTiggered , 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
public IdleStateHandler(
long readerIdleTime, long writerIdleTime, long allIdleTime,
TimeUnit unit) {
this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}
这里最重要是的 readerIdleTime ,当设置了 readerIdleTime 以后,Netty 会每隔 readerIdleTime 时间去检查一次channelRead
方法被调用的情况,如果在readerIdleTime
时间内该channel上的channelRead
方法没有被触发,就会调用userEventTriggered
方法。
// netty 的心跳检测机制的 Handler
pipeline.addLast("heartbeatHandler",new IdleStateHandler(5,0,0, TimeUnit.SECONDS));
// 字符串解码 和 编码
pipeline.addLast("stringDecoder",new StringDecoder());
pipeline.addLast("stringEncoder",new StringEncoder());
// 添加自定义的业务Handler
pipeline.addLast("myHandler",new MyHandler());
- 重写
ChannelInboundHandlerAdapter
的userEventTriggered
方法,实现事件触发后的业务逻辑
IdleStateHandler
是实现心跳的关键, 它会根据不同的 I/O idle
类型来产生不同的 IdleStateEvent
事件, 而这个事件的捕获处理, 其实就是在 userEventTriggered
方法中实现的.
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 判断 事件是否是 IdleStateEvent
if (evt instanceof IdleStateEvent) {
//
IdleStateEvent event = (IdleStateEvent) evt;
//
switch (event.state()) {
case READER_IDLE: {
System.out.println("===服务端===(READER_IDLE, 读超时)");
// 失败计数器次数大于等于3次的时候,关闭链接,等待client重连
if (unRecPingTimes > MAX_UN_REC_PING_TIMES) {
System.out.println("===服务端===(读超时,关闭chanel)");
// 连续超过N次未收到client的ping消息,那么关闭该通道,等待client重连
unRecPingTimes = 0;
ctx.channel().writeAndFlush("close").addListener(ChannelFutureListener.CLOSE);
} else {
// 失败计数器加 1
unRecPingTimes++;
// 回显 心跳数据,如果客户端已经关闭,就关闭 channel
ctx.writeAndFlush("PONG");
}
break;
}
case WRITER_IDLE: {
System.out.println("===服务端===(WRITER_IDLE, 写超时)");
break;
}
case ALL_IDLE: {
System.out.println("===服务端===(ALL_IDLE, 读写超时)");
break;
}
default: {
}
}
} else {
super.userEventTriggered(ctx, evt);
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/69734.html