记一次netty--IdleStateHandler踩到的坑

公司一个项目使用netty框架来搭建。有个场景需要用到IdleStateHandler来检查客户端如果一段时间没有发送数据则主动断开连接。
查看IdleStateHandler的构造器,三个参数分别表示

 public IdleStateHandler(boolean observeOutput,
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {

中间三个的参数解释如下:

1)readerIdleTime:为读超时时间(即多长时间没有接受到客户端发送数据)
2)writerIdleTime:为写超时时间(即多长时间没有向客户端发送数据)
3)allIdleTime:所有类型的超时时间

我代码这样写

@Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            pipeline.addLast(
                                    new LengthFieldBasedFrameDecoder(ByteOrder.BIG_ENDIAN, 1000, 2, 2, -4, 0, true));
                            pipeline.addLast(encryptHandler);
                            pipeline.addLast(decryptHandler);
                            pipeline.addLast(new IdleStateHandler(3, 3, 3, TimeUnit.MINUTES));
                            pipeline.addLast(heartbeatHandler);
                        }

结果不管客户端有没有超时,3分钟过后都会断开连接。
百思不得其解后,决定去看下源码。下面解释下IdleStateHandler的运行原理。
第一步、channelActive()方法是在将handler添加进ChannelPipeline时触发的事件。这里只执行 initialize(ctx);就下传处理下一个handler

@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // This method will be invoked only if this handler was added
        // before channelActive() event is fired.  If a user adds this handler
        // after the channelActive() event, initialize() will be called by beforeAdd().
        initialize(ctx);
        super.channelActive(ctx);
    }

第二步、执行initialize方法的过程。这里只介绍ReaderIdleTimeout。

 private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
        //创建了一个ReaderIdleTimeoutTask,该类继承了Runnable接口。也就是说,该类每隔timeout时间就轮询是否超时。其实就是比较上次更新的时间和当前的时间
            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);
        }
    }

具体看下ReaderIdleTimeoutTask的run方法

 @Override
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            //计算时间差
            if (!reading) {
                nextDelay -= ticksInNanos() - lastReadTime;
            }
    //如果netDelay<=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;
        //创建一个IdleStateEvent 时间,调用channelIdle方法。该方法会把时间下传到下一个handler的userEventTriggered方法。
                try {
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }

说了这么多,那如果每次有读事件,lastReadTime在哪里更新呢。看下面

 @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
        //当发现数据读完时,会更新lastReadTime时间为当前时间
            lastReadTime = ticksInNanos();
            reading = false;
        }
        ctx.fireChannelReadComplete();
    }

以上就是IdleStateHandler的运行过程。
那我写的代码有什么问题呢?
原因就是我把IdleStateHandler放在了decryptHandler的后面。decryptHandler处理完业务逻辑后没有继续下传可读事件。所以IdleStateHandler一直没有更新时间。以为客户端是一直没有发送数据。
总结:最好是将IdleStateHandler放在入站的开头,并且重写userEventTriggered这个方法的handler必须在其后面。否则无法触发这个事件。

猜你喜欢

转载自blog.csdn.net/lalalahaitang/article/details/81512844