Netty之IdleStateHandler工作原理

858 阅读6分钟

官方说明:

当通道有一段时间没有执行读取、写入或同时执行这两种操作时,触发IdleStateEvent。

例如:

一个在30秒内没有出站流量时发送ping消息的示例。当60秒内没有入站流量时,连接将关闭。

IdleStateHandler 既是出站处理器也是入站处理器,继承了 ChannelDuplexHandler 。通常在 initChannel 方法中将 IdleStateHandler 添加到 pipeline 中。然后在自己的 handler 中重写 userEventTriggered 方法,当发生空闲事件(读或者写),就会触发这个方法,并传入具体事件。可以通过 Context 对象尝试向目标 Socekt 写入数据,并设置一个 监听器,如果发送失败就关闭 Socket (Netty 准备了ChannelFutureListener.CLOSE_ON_FAILURE 监听器用来实现关闭 Socket 逻辑)。 这样,就实现了一个简单的心跳服务。

参考学习文章:

简介

Netty 通过 IdleStateHandler 实现最常见的心跳机制不是一种双向心跳的 PING-PONG 模式,而是客户端发送心跳数据包,服务端接收心跳但不回复,因为如果服务端同时有上千个连接,心跳的回复需要消耗大量网络资源。

如果服务端一段时间内一直收到客户端的心跳数据包则认为客户端已经下线,将通道关闭避免资源的浪费。在这种心跳模式下服务端可以感知客户端的存活情况,无论是宕机的正常下线还是网络问题的非正常下线,服务端都能感知到,而客户端不能感知到服务端的非正常下线。

要想实现客户端感知服务端的存活情况,需要进行双向的心跳;Netty 中的 channelInactive() 方法是通过 Socket 连接关闭时挥手数据包触发的,因此可以通过 channelInactive() 方法感知正常的下线情况,但是因为网络异常等非正常下线则无法感知。

构造器

构造器,也就是程序的入口,我们在创建pipeline时,放在对应的handler前面,这个handler中填写了usertriggered方法,当有超时时间发生时,调用这个方法,处理业务逻辑,四个属性分别是读、写、读写时间以及设置的时间单位。在构造器中会调用另一个重载的构造器,检测输入的时间的合法性。

初始化

在IdleStateHandler加入到pipeline之后就会触发initialize方法。

private void initialize(ChannelHandlerContext ctx) {
    // ---------------------省略---------------------
    initOutputChanged(ctx);

    lastReadTime = lastWriteTime = ticksInNanos();
    if (readerIdleTimeNanos > 0) {
        readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                                     readerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    if (writerIdleTimeNanos > 0) {
        writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                                     writerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    if (allIdleTimeNanos > 0) {
        allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                                  allIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
}

首先会进入initOutputChanged(ctx);,去初始化 “监控出站数据属性” ,记录最后一次记录时的缓冲区数据

假设:当你的客户端应用每次接收数据是30秒,而你的写空闲时间是 25 秒,那么,当你数据还没有写出的时候,写空闲时间触发了。实际上是不合乎逻辑的。因为你的应用根本不空闲。

Netty 的解决方案是:记录最后一次输出消息的相关信息,并使用一个值 firstXXXXIdleEvent 表示是否再次活动过,每次读写活动都会将对应的 first 值更新为 true,如果是 false,**说明这段时间没有发生过读写事件。**同时如果第一次记录出站的相关数据和第二次得到的出站相关数据不同,则说明数据在缓慢的出站,就不用触发空闲事件。总的来说,这个字段就是用来对付 “客户端接收数据奇慢无比,慢到比空闲时间还多” 的极端情况。所以,Netty 默认是关闭这个字段的。

随后,不管是处理读、写还是读和写,都会进入io.netty.handler.timeout.IdleStateHandler#schedule方法,点进去其实就是创建定时任务的方法。

对应:内部类ReaderIdleTimeoutTask、WriterIdleTimeoutTask和AllIdleTimeoutTask,均继承了AbstractIdleTask

image.png

private abstract static class AbstractIdleTask implements Runnable {
// ---------------------省略---------------------
    @Override
    public void run() {
        if (!ctx.channel().isOpen()) {
            return;
        }
        run(ctx);
    }

    protected abstract void run(ChannelHandlerContext ctx);//子类去实现
}

  • 读事件:ReaderIdleTimeoutTask

     @Override
     protected void run(ChannelHandlerContext ctx) {
         long nextDelay = readerIdleTimeNanos;
         if (!reading) {
             nextDelay -= ticksInNanos() - lastReadTime;
             //没有超时,计算最后一次读取是多久以前的,假设为x,
             //更新nextDelay = nextDelay-x,如果小于0说明超时了
         }
     ​
         if (nextDelay <= 0) {
             //超时的情况,重置定时任务
             // Reader is idle - set a new timeout and notify the callback.
             readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
     ​
             boolean first = firstReaderIdleEvent;
             firstReaderIdleEvent = false;
     ​
             try {
                 //创建一个IdleStateHandler触发的读空闲事件。
                 IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                 //触发下一个 handler 的 userEventTriggered 方法
                 channelIdle(ctx, event);
             } catch (Throwable t) {
                 ctx.fireExceptionCaught(t);
             }
         } else {
             //没有超时,重新设置新的超时时间为nextDelay
             // Read occurred before the timeout - set a new timeout with shorter delay.
             readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
         }
     }
    
  • 写事件:WriterIdleTimeoutTask,逻辑和读事件差不多

      @Override
     protected void run(ChannelHandlerContext ctx) {
     ​
         long lastWriteTime = IdleStateHandler.this.lastWriteTime;
         long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);//和读一样
         if (nextDelay <= 0) {
             //超时
             // Writer is idle - set a new timeout and notify the callback.
             writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);
     ​
             boolean first = firstWriterIdleEvent;
             firstWriterIdleEvent = false;
     ​
             try {
                 if (hasOutputChanged(ctx, first)) {//因为是写超时
                     //当且仅当IdleStateHandler是在启用observeOutput的情况下构造的,
                     //并且在该方法的两个连续调用之间观察到ChannelOutboundBuffer发生了变化时,返回true。简单来说,如果是true,说明数据只是写的很慢,为了避免OOM,不算超时。
                     return;
                 }
                     //创建一个IdleStateHandler触发的写空闲事件,并传给下一个handler
                 IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
                 channelIdle(ctx, event);
             } catch (Throwable t) {
                 ctx.fireExceptionCaught(t);
             }
         } else {
             //未超时,同读
             // Write occurred before the timeout - set a new timeout with shorter delay.
             writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
         }
     }
    
  • 读写:AllIdleTimeoutTask

     @Override
     protected void run(ChannelHandlerContext ctx) {
     ​
         long nextDelay = allIdleTimeNanos;
         if (!reading) {
             nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
             //通过读写事件中的最大值更新nextDelay
         }
         if (nextDelay <= 0) {//和写一样
             // Both reader and writer are idle - set a new timeout and
             // notify the callback.
             allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS);
     ​
             boolean first = firstAllIdleEvent;
             firstAllIdleEvent = false;
     ​
             try {
                 if (hasOutputChanged(ctx, first)) {//和写一样
                     return;
                 }
     ​
                 IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first);
                 channelIdle(ctx, event);
             } catch (Throwable t) {
                 ctx.fireExceptionCaught(t);
             }
         } else {
             // Either read or write occurred before the timeout - set a new
             // timeout with shorter delay.
             allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
         }
     }
    
  • 关于io.netty.handler.timeout.IdleStateHandler#hasOutputChanged

     private boolean hasOutputChanged(ChannelHandlerContext ctx, boolean first) {
         if (observeOutput) {
     ​
             //如果传入write()的ChannelPromises似乎已完成,我们可以使用此快捷方式。它指示消息级别上的“更改”,我们只是假设字节级别上发生了更改。如果用户没有观察到通道可写性事件,那么他们最终会OOME,并且显然存在不同的问题,空闲是他们最不关心的问题。
             if (lastChangeCheckTimeStamp != lastWriteTime) {
                 lastChangeCheckTimeStamp = lastWriteTime;
     ​
                 // But this applies only if it's the non-first call.
                 if (!first) {
                     return true;
                 }
             }
     ​
             Channel channel = ctx.channel();
             Unsafe unsafe = channel.unsafe();
             ChannelOutboundBuffer buf = unsafe.outboundBuffer();
     ​
             if (buf != null) {
                 int messageHashCode = System.identityHashCode(buf.current());
                 long pendingWriteBytes = buf.totalPendingWriteBytes();
     ​
                 if (messageHashCode != lastMessageHashCode || pendingWriteBytes != lastPendingWriteBytes) {
                     lastMessageHashCode = messageHashCode;
                     lastPendingWriteBytes = pendingWriteBytes;
     ​
                     if (!first) {
                         return true;
                     }
                 }
     ​
                 long flushProgress = buf.currentProgress();
                 if (flushProgress != lastFlushProgress) {
                     lastFlushProgress = flushProgress;
     ​
                     if (!first) {
                         return true;
                     }
                 }
             }
         }
     ​
         return false;
     }
    

有一个问题:为什么在没有超时的情况下,减小超时时间,这样下一次进入这个handler的时候不就会容易触发IdleStateEvent了吗?