优质博文:IT-BLOG-CN
Netty
支持心跳机制,可以检测远程服务端是否存活或者活跃。心跳是在TCP
长连接中,客户端和服务端定时向对方发送数据包通知对方自己还在线,保证连接的有效性的一种机制。在服务器和客户端之间一定时间内没有数据交互时, 即处于idle
状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 即一个PING-PONG
交互。当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保TCP
连接的有效性。
Netty
提供了IdleStateHandler
可以对三种类型心跳进行检测,是用来监测连接的空闲情况。然后我们就可以根据心跳情况,来实现具体的处理逻辑,比如说断开连接、重新连接等等。同时,还提供了ReadTimeoutHandler
,WriteTimeoutHandler
检测连接的有效性。
名称 | 作用 |
---|---|
IdleStateHandler |
当连接的空闲时间(读或者写)太长时,将会触发一个IdleStateEvent 事件。然后,你可以通过你的ChannelInboundHandler 中重写userEventTrigged 方法来处理该事件。 |
ReadTimeoutHandler |
如果在指定的事件没有发生读事件,就会抛出这个异常,并自动关闭这个连接。你可以在exceptionCaught 方法中处理这个异常。 |
WriteTimeoutHandler |
当一个写操作不能在一定的时间内完成时,抛出此异常,并关闭连接。你同样可以在exceptionCaught 方法中处理这个异常。 |
IdleStateHandler
也是一个ChannelHandler
,也需要被载入到ChannelPipeline
中,加入我们在服务器端的ChannelInitializer
中。我们在channel
链中加入了IdleSateHandler
,第一个参数是5
,单位是秒,那么这样做的意思就是:在服务器端会每隔5
秒来检查一下channelRead
方法被调用的情况,如果在5
秒内该链上的channelRead
方法都没有被触发,就会调用userEventTriggered
方法:
//创建服务类
ServerBootstrap serverBootstrap = new ServerBootstrap();
//boss和worker
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
//设置线程池
serverBootstrap.group(boss,worker);
//设置socket工厂,Channel 是对 Java 底层 Socket 连接的抽象
serverBootstrap.channel(NioServerSocketChannel.class);
//设置管道工厂
serverBootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
//设置后台转换器(二进制转换字符串)
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ServerSocketHandler());
}
});
并且还是个ChannelInboundHandler
,是用来处理入站事件的。看下它的构造器:
public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}
名称 | 作用 |
---|---|
readerIdleTimeSeconds |
读超时。即当在指定的时间间隔内没有从Channel 读取到数据时,会触发一个READER_IDLE 的IdleStateEvent 事件 |
writerIdleTimeSeconds |
写超时。即当在指定的时间间隔内没有数据写入到Channel 时,会触发一个WRITER_IDLE 的IdleStateEvent 事件 |
allIdleTimeSeconds |
读/写超时。即当在指定的时间间隔内没有读或写操作时,会触发一个ALL_IDLE 的IdleStateEvent 事件 |
【1】handlerAdded
和handlerRemoved
:IdleStateHandler
是在创建IdleStateHandler
实例并添加到ChannelPipeline
时添加定时任务来进行定时检测的,具体在initialize(ctx)
方法实现;同时在从ChannelPipeline
移除或Channel
关闭时,移除这个定时检测,具体在destroy()
实现。IdleStateHandler
的channelActive()
方法在socket
通道建立时被触发。
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
this.initialize(ctx);
}
}
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
this.destroy();
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
this.destroy();
super.channelInactive(ctx);
}
【2】initialize
:根据配置的readerIdleTime
,WriteIdleTIme
等超时事件参数往任务队列taskQueue
中添加定时任务task
。我先先查看ReaderIdleTimeoutTask
的作用。
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) {
// 这里的 schedule 方法会调用 eventLoop 的 schedule 方法,将定时任务添加进队列中
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);
}
}
}
定时任务添加到对应线程EventLoopExecutor
对应的任务队列taskQueue
中,在对应线程的run()
方法中循环执行。只要给定的参数大于0
,就创建一个定时任务,每个事件都创建。同时,将state
状态设置为1
,防止重复初始化。调用initOutputChanged
方法,初始化监控出站数据属性
private void initOutputChanged(ChannelHandlerContext ctx) {
if (this.observeOutput) {
Channel channel = ctx.channel();
Unsafe unsafe = channel.unsafe();
ChannelOutboundBuffer buf = unsafe.outboundBuffer();
// 记录了出站缓冲区相关的数据,buf 对象的 hash 码,和 buf 的剩余缓冲字节数
if (buf != null) {
this.lastMessageHashCode = System.identityHashCode(buf.current());
this.lastPendingWriteBytes = buf.totalPendingWriteBytes();
this.lastFlushProgress = buf.currentProgress();
}
}
}
这边会触发一个ReaderIdleTimeoutTask
:nextDelay
的初始化值为超时秒数readerIdleTimeNanos
。如果检测的时候没有正在读,且计算多久没读了,nextDelay
-= 当前时间 - 上次读取时间,假如这个结果是6s
,说明最后一次调用channelRead
已经是6s
之前的事情了,你设置的是5s
,那么nextDelay
则为-1
,说明超时了。则创建IdleStateEvent
事件,IdleState
枚举值为READER_IDLE
,然后调用channelIdle
方法分发给下一个ChannelInboundHandler
,通常由用户自定义一个ChannelInboundHandler
来捕获并处理。
private final class ReaderIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
protected void run(ChannelHandlerContext ctx) {
long nextDelay = IdleStateHandler.this.readerIdleTimeNanos;
if (!IdleStateHandler.this.reading) {
nextDelay -= IdleStateHandler.this.ticksInNanos() - IdleStateHandler.this.lastReadTime;
}
if (nextDelay <= 0L) {
IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = IdleStateHandler.this.firstReaderIdleEvent;
IdleStateHandler.this.firstReaderIdleEvent = false;
try {
IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
IdleStateHandler.this.channelIdle(ctx, event);
} catch (Throwable var6) {
ctx.fireExceptionCaught(var6);
}
} else {
IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
firstxxxxIdleEvent
作用:假设当你的客户端应用每次接收数据是30
秒,而你的写空闲时间是25
秒,那么,当你数据还没有写出的时候,写空闲时间触发了。实际上是不合乎逻辑的。因为你的应用根本不空闲。
Netty
的解决方案是:记录最后一次输出消息的相关信息,并使用一个值firstXXXXIdleEvent
表示是否再次活动过,每次读写活动都会将对应的first
值更新为true
,如果是false
,说明这段时间没有发生过读写事件。同时如果第一次记录出站的相关数据和第二次得到的出站相关数据不同,则说明数据在缓慢的出站,就不用触发空闲事件。
总的来说,这个字段就是用来对付 “客户端接收数据奇慢无比,慢到比空闲时间还多” 的极端情况。所以,Netty
默认是关闭这个字段的。
总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发·UserEventTriggered`方法:
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
【3】写任务的run
方法逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对 出站较慢数据的判断。
if (hasOutputChanged(ctx, first)) {
return;
}
如果这个方法返回true
,就不执行触发事件操作了,即使时间到了。看看该方法实现:
private boolean hasOutputChanged(ChannelHandlerContext ctx, boolean first) {
if (observeOutput) {
// 如果最后一次写的时间和上一次记录的时间不一样,说明写操作进行过了,则更新此值
if (lastChangeCheckTimeStamp != lastWriteTime) {
lastChangeCheckTimeStamp = lastWriteTime;
// 但如果,在这个方法的调用间隙修改的,就仍然不触发事件
if (!first) { // #firstWriterIdleEvent or #firstAllIdleEvent
return true;
}
}
Channel channel = ctx.channel();
Unsafe unsafe = channel.unsafe();
ChannelOutboundBuffer buf = unsafe.outboundBuffer();
// 如果出站区有数据
if (buf != null) {
// 拿到出站缓冲区的 对象 hashcode
int messageHashCode = System.identityHashCode(buf.current());
// 拿到这个 缓冲区的 所有字节
long pendingWriteBytes = buf.totalPendingWriteBytes();
// 如果和之前的不相等,或者字节数不同,说明,输出有变化,将 "最后一个缓冲区引用" 和 “剩余字节数” 刷新
if (messageHashCode != lastMessageHashCode || pendingWriteBytes != lastPendingWriteBytes) {
lastMessageHashCode = messageHashCode;
lastPendingWriteBytes = pendingWriteBytes;
// 如果写操作没有进行过,则任务写的慢,不触发空闲事件
if (!first) {
return true;
}
}
}
}
return false;
}
如果用户没有设置需要观察出站情况。就返回false
,继续执行事件。如果设置了观察出站的情况,且最后一次写的时间和上一次记录的时间不一样,说明写操作刚刚做过了,则更新此值,但仍然需要判断这个first
的值,如果这个值还是false
,说明在这个写事件是在两个方法调用间隙完成的,或者是第一次访问这个方法,就仍然不触发事件。如果不满足上面的条件,就取出缓冲区对象,如果缓冲区没对象了,说明没有发生写的很慢的事件,就触发空闲事件。反之,记录当前缓冲区对象的hashcode
和剩余字节数,再和之前的比较,如果任意一个不相等,说明数据在变化,或者说数据在慢慢的写出去。那么就更新这两个值,留在下一次判断。继续判断first
,如果是fasle
,说明这是第二次调用,就不用触发空闲事件了。
【4】所有事件的run
方法:这个类叫做AllIdleTimeoutTask
,表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致,除了这里:
long nextDelay = allIdleTimeNanos;
if (!reading) {
// 当前时间减去 最后一次写或读 的时间 ,若大于0,说明超时了
nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
}
这里的时间计算是取读写事件中的最大值来的。然后像写事件一样,判断是否发生了写的慢的情况。最后调用ctx.fireUserEventTriggered(evt)
方法。
通常这个使用的是最多的。构造方法一般是:
pipeline.addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));
读写都是0
表示禁用,30
表示30
秒内没有任务读写事件发生,就触发事件。注意,当不是0
的时候,这三个任务会重叠。
【1】IdleStateHandler
心跳检测主要是通过向线程任务队列中添加定时任务,判断channelRead()
方法或write()
方法是否调用空闲超时,如果超时则触发超时事件执行自定义userEventTrigger()
方法;
【2】Netty
通过IdleStateHandler
实现最常见的心跳机制不是一种双向心跳的PING-PONG
模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源;如果服务端一段时间内没有收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费;在这种心跳模式下服务端可以感知客户端的存活情况,无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线;
【3】要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty
中的channelInactive()
方法是通过Socket
连接关闭时挥手数据包触发的,因此可以通过channelInactive()
方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知;