netty源码深入研究(从客户端入手)第四篇(读写超时详解)

怎么设置读写超时的监听函数呢,首先从文档开始,或者看看官方有没有例子,一般任何平台的官方都会或多或少的提供例子。

官方文档有这样一个类new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds),我这里就用中式英语翻译,大体的意思就是空闲状态的处理者,第一个参数设置读超时时间,第二个参数设置写超时时间,第三个设置总的超时时间。

怎么用呢,咱们先看看它的父类是什么public class IdleStateHandler extends ChannelDuplexHandler,那么ChannelDuplexHandler又是什么鬼,public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter,继承与ChannelInboundHandlerAdapter,前几篇介绍过ChannelInboundHandlerAdapter这个类标记了这个管道是用来连接读管道的,那么疑问来了,写超时它是怎么计算的。通过以下入口点,进入空闲状态的处理者内部。

ch.pipeline().addLast("IdleState",
new IdleStateHandler(5, 5, 10));

  public IdleStateHandler(boolean observeOutput,
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        if (unit == null) {
            throw new NullPointerException("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,设置时间为0,总时间取默认值和设置值的最大值。直接点类进入后我们只能跟踪到构造方法,其他方法基本上都是被回调的方法,看这个

    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
            // channelActive() event has been fired already, which means this.channelActive() will
            // not be invoked. We have to initialize here instead.
            initialize(ctx);
        } else {
            // channelActive() event has not been fired yet.  this.channelActive() will be invoked
            // and initialization will occur there.
        }
    }

handlerAdded()方法在什么时候被执行呢,第一篇讲过连接之前,要进行注册,那么跟进去看看是不是那个地方执行的,该方法在DefaultChannelPipeline的callHandlerAdded0方法里调用
 public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
        	//检查handler是否合法
            checkMultiplicity(handler);

            newCtx = newContext(group, filterName(name, handler), handler);

            addLast0(newCtx);

            // If the registered is false it means that the channel was not registered on an eventloop yet.
            // In this case we add the context to the pipeline and add a task that will call
            // ChannelHandler.handlerAdded(...) once the channel is registered.
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                    	//被调用
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        //被调用
        callHandlerAdded0(newCtx);
        return this;
    }
看代码中文注释部分,此方法原来是在添加的时候被调用,和注册没关系

如果此时连接已经建立,那么就初始化这个管道,0k,开始执行initialize方法,

 private void initialize(ChannelHandlerContext ctx) {
    	//第一次进入的时候state为默认值,肯定不是1和2,跳过
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        //设置是否监听写的信息,默认不监听
        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);
        }
    }

启动任务最后还是加入到NioEventLoop的消息队列中,并延时执行,任务执行每次都是在读写以后。接下来看一下这几个任务都干了什么

读任务的代码:

 protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            //是否正在读,每次调用管道读的时候reading会变成true,而读完后reading变成false
            if (!reading) {
            	//没有读则计算时间
                nextDelay -= ticksInNanos() - lastReadTime;
            }
             //如果计算值为负数,则超时
            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 {
                	//通知超时
                    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);
            }
        }
    }

通过上面代码可以发现,每次执行到任务时获取当前的时间减去上一次读取的时间,如果大于最大读取时间的话,那么回调超时接口。那么回调的是什么呢?跟进看一下

 protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

最后又通过管道包装类把回调分发出去

    @Override
    public ChannelHandlerContext fireUserEventTriggered(final Object event) {
        invokeUserEventTriggered(findContextInbound(), event);
        return this;
    }

最后又执行到循环调用队列方法的地方

 private void invokeUserEventTriggered(Object event) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).userEventTriggered(this, event);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireUserEventTriggered(event);
        }
    }

因为将管道添加进去的时候,invokeHander已然返回true,所以必然执行 ((ChannelInboundHandler) handler()).userEventTriggered(this, event);


也就是每一个读管道的

public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
			throws Exception {
		// TODO Auto-generated method stub
		if (evt instanceof IdleStateEvent) {
			IdleStateEvent event = (IdleStateEvent) evt;
			if (event.state().equals(IdleState.ALL_IDLE)) {
				// 未进行写操作发送心跳包
				String heratBeat = "{\"event\":\"HEARTBEAT\"}";
				ctx.write(Unpooled.copiedBuffer(heratBeat.getBytes()));
				ctx.flush();
			}
		}
		super.userEventTriggered(ctx, evt);
	}
这个方法,我们可以在这个里面做一些事情,比如一定时间没有和服务器发信息了,那么发送心跳,看 super.userEventTriggered(ctx, evt),它的父类方法都做了什么

  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

奥,这不是又把事件分发给队列的下一个管道处理类了吗,默认的解析器我们不会重写 userEventTriggered方法,所以他会直接分发给下一个管道类处理。


那么继续,从上面的读超时任务,知道有一个最后读取数据的时间变量lastReadTime,那么它是怎么计算的呢?


 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
            lastReadTime = ticksInNanos();
            reading = false;
        }
        ctx.fireChannelReadComplete();
    }

这个值是读数据完成的回调方法里执行的,第二篇有详细介绍
 public final void read() {
            final ChannelConfig config = config();
            final ChannelPipeline pipeline = pipeline();
            final ByteBufAllocator allocator = config.getAllocator();
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            allocHandle.reset(config);

            ByteBuf byteBuf = null;
            boolean close = false;
            try {
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    if (allocHandle.lastBytesRead() <= 0) {
                        // nothing was read. release the buffer.
                        byteBuf.release();
                        byteBuf = null;
                        close = allocHandle.lastBytesRead() < 0;
                        break;
                    }

                    allocHandle.incMessagesRead(1);
                    readPending = false;
                    //读管道处理操作回调
                    pipeline.fireChannelRead(byteBuf);
                    byteBuf = null;
                } while (allocHandle.continueReading());
                //读取完成的操作回调
                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();

                if (close) {
                    closeOnRead(pipeline);
                }
            } catch (Throwable t) {
                handleReadException(pipeline, byteBuf, t, close, allocHandle);
            } finally {
                // Check if there is a readPending which was not processed yet.
                // This could be for two reasons:
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
                //
                // See https://github.com/netty/netty/issues/2254
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
    }
看代码中文注释,现在读超时的思路已经清晰。接着看写任务做了哪些

 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)) {
                        return;
                    }

                    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);
            }
        }
    }
执行逻辑和读差不多,其他跳过直接看 lastWriteTime这个参数哪来的

 private final ChannelFutureListener writeListener = new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            lastWriteTime = ticksInNanos();
            firstWriterIdleEvent = firstAllIdleEvent = true;
        }
    };

哎要,它是在回调函数中设置的值


 public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        // Allow writing with void promise if handler is only configured for read timeout events.
        if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
            ChannelPromise unvoid = promise.unvoid();
            unvoid.addListener(writeListener);
            ctx.write(msg, unvoid);
        } else {
            ctx.write(msg, promise);
        }
    }

是在write方法中加入的回调值,额什么时候执行write呢
   private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }
只要实现了此接口就能监听的管道的写消息,所以 IdleStateHandler实现了 implements ChannelOutboundHandler接口,写方法当然会执行,就和第二篇写消息流程一样,不再具体分析。

第五篇的时候,我会来一个netty的总结,把类关系图弄一下,那样感觉更直观。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值