Netty源码剖析之IdleStateHandler空闲检测机制

版本信息:

JDK1.8

Netty-all:4.1.38.Final

 空闲检测机制的介绍:

在各种论坛上,笔者看到大部分的博客都是把netty的IdleStateHandler说成心跳机制,而笔者认为这并不是心跳机制,仅仅是跟心跳机制比较相似,并且IdleStateHandler能做的并不仅仅只有"心跳机制",笔者认为叫做空闲检测机制比较适合。

为什么说叫做空闲检测机制比较合适呢?因为netty是封装了NIO,而提及IO无非就是输入输出,换种方式说无非就是读和写。那么在读和写的过程中能不能注册一个监听器,监听是否一直有读写请求,如果达到一定的时间没有读写请求那么就判定为空闲状态,达到空闲状态后,是不是可以对其做一些操作,比如释放连接资源、缓冲区的刷出等等操作。

IdleStateHandler应用:

读空闲:释放连接资源(但是这里并不是心跳机制,因为心跳机制的定义是给客户端发送心跳包,需要客户端回应ACK才是心跳机制的定义,这里仅仅是判断有没有读到客户端的数据)

写空间:写缓冲区的刷出(可以设置一个阈值,当多少秒没有写数据了,就一次性把写缓冲区数据刷出,提升性能,可以理解为批处理~)

使用IdleStateHandler一定要注意,定时器的频率不能太高,不然太部分时间在做判断会一定影响到业务代码的执行。而且这里的定时器的精准度不会特别精准,使用时需要考虑精准度问题~

IdleStateHandler源码分析:

netty中提供了空闲检测机制,既然是对读写操作的监听,那么肯定是在Pipeline的回调过程中实现,所以肯定需要实现ChannelHandler,并且同时能监听读和写,肯定是实现ChannelInboundHandler和ChannelOutboundHandler。

public class IdleStateHandler extends ChannelDuplexHandler {}

public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implements ChannelOutboundHandler {}

先从构造方法入手~

/**
 * @param observeOutput 是否观察输出数据
 * @param readerIdleTime 对读空闲的阈值时间
 * @param writerIdleTime 对写空闲的阈值时间
 * @param allIdleTime 对读写空闲的阈值时间
 * @param unit 时间单位
 * 
 * */
public IdleStateHandler(boolean observeOutput,
    long readerIdleTime, long writerIdleTime, long allIdleTime,
    TimeUnit unit) {

    this.observeOutput = observeOutput;

    if (readerIdleTime <= 0) {
        readerIdleTimeNanos = 0;
    } else {
        // 默认一毫秒。当然用户可通过构造方法传入阈值时间
        readerIdleTimeNanos = Math.max(unit.toNanos(readerIdleTime), MIN_TIMEOUT_NANOS);
    }
    if (writerIdleTime <= 0) {
        writerIdleTimeNanos = 0;
    } else {
        // 默认一毫秒。当然用户可通过构造方法传入阈值时间
        writerIdleTimeNanos = Math.max(unit.toNanos(writerIdleTime), MIN_TIMEOUT_NANOS);
    }
    if (allIdleTime <= 0) {
        allIdleTimeNanos = 0;
    } else {
        // 默认一毫秒。当然用户可通过构造方法传入阈值时间
        allIdleTimeNanos = Math.max(unit.toNanos(allIdleTime), MIN_TIMEOUT_NANOS);
    }
}

构造方法就非常的简单,用户可以传入读写空闲的阈值时间,这里对空闲的阈值时间做初始化工作,如果只对读做空闲监听,那么除了读其他的传入0即可,反之写是一样的。

既然是监听, 肯定需要使用到定时任务,那么需要有一个点触发开启定时任务。而这里是对读写做监听,所以在通道注册完毕的回调方法中开启定时任务就是非常的恰当。

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    // 通道是否已经激活
    if (ctx.channel().isActive()) {
        // 对读写空闲的定时任务做初始化
        initialize(ctx);
    }
    // 继续调用下一个ChannelHandlerContext的channelRegistered
    super.channelRegistered(ctx);
}

这里继续看到 initialize初始化方法。

private void initialize(ChannelHandlerContext ctx) {

    // 用状态控制是否已经初始化完毕。
    switch (state) {
    case 1:
    case 2:
        return;
    }

    state = 1;
    // 在构造方法中boolean observeOutput参数控制,默认为false
    // 初始化对输出数据改变的监听
    initOutputChanged(ctx);

    // 获取到当前系统时间。
    lastReadTime = lastWriteTime = ticksInNanos();

    // readerIdleTimeNanos在构造方法中设置,由用户传入,默认最小值为1毫秒。
    if (readerIdleTimeNanos > 0) {
        // 往EventLoop中注册定时任务
        readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
            readerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    // writerIdleTimeNanos在构造方法中设置,由用户传入,默认最小值为1毫秒。
    if (writerIdleTimeNanos > 0) {
        // 往EventLoop中注册定时任务
        writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
            writerIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
    // allIdleTimeNanos在构造方法中设置,由用户传入,默认最小值为1毫秒。
    if (allIdleTimeNanos > 0) {
        // 往EventLoop中注册定时任务
        allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
            allIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
}

这里也非常的简单,往EventLoop中注册定时任务,定时任务的参数都是从构造方法中用户传入的

那么接下来看到定时任务触发时回调的方法。而我们这里只关心读空闲检测。也即读的定时任务。

// 任务的回调点。
// 执行线程为EventLoop,也即netty工作线程。
@Override
protected void run(ChannelHandlerContext ctx) {
    // 用户设置的读空闲阈值
    long nextDelay = readerIdleTimeNanos;

    // 如果正在读,那就直接跳过计算,直接注册下一次的定时任务即可。
    if (!reading) {
        nextDelay -= ticksInNanos() - lastReadTime;
    }

    // 达到设置的空闲读阈值
    if (nextDelay <= 0) {           
        // 虽然已经达到读空闲阈值,但是用户可能会设置几次的阈值,所以还需要注册定时任务。
        readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

        // 默认为true,以后为false。
        // 用于判断是否是第一次。
        boolean first = firstReaderIdleEvent;
        firstReaderIdleEvent = false;

        try {
            // 触发IdleStateEvent事件
            // 后续ChannelHandler实现userEventTriggered方法,可以判断IdleStateEvent事件,做出特殊处理
            IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
            channelIdle(ctx, event);
        } catch (Throwable t) {
            ctx.fireExceptionCaught(t);
        }
    } else {
        // 都在读了,所以不存在空闲。
        // 但是还是需要设置下一次定时任务。
        readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
    }
}

这里就是定时任务的回调方法,对这里做一个总结:

  1. 获取到用户设置的读空闲阈值
  2. 如果正在读,那么直接设置下一次定时任务即可。
  3. 如果达到了用户设置的读空闲阈值,那么还是需要设置下一次定时任务,因为用户可能会设置一个次数阈值,所以还需要定时任务在做后续处理
  4. 发送IdleStateEvent事件
  5. 触发后续的ChannelHandler实现userEventTriggered方法,userEventTriggered方法中可以判断IdleStateEvent事件,做出特殊处理

用户只需要在Pipeline中IdleStateHandler的后面写一个ChannelHandler实现userEventTriggered方法,对事件做判断即可,如果是IdleStateEvent事件,就做出对应的特殊处理即可(比如释放连接资源等等操作)

你可能感兴趣的:(Netty源码,源码解读,java,netty源码)