深入理解IdleStateHandler ,ReadTimeoutHandler,WriteTimeoutHandler

1 概述

Netty 提供了 IdleStateHandlerReadTimeoutHandlerWriteTimeoutHandler 三个 Handler 检测连接的有效性。
1654653385(1).png

2 区别

IdleStateHandler 可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会触发用户 handler 的 userEventTriggered 方法。
ReadTimeoutHandler事件则是当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会直接抛出异常并且断开通道的连接。
WriteTimeoutHandler 事件当执行chanel的wite方法后会执行延时任务,延时的时间就是设置的超时时间,然后isDone判断write是否执行完毕,如果write没有执行完毕就抛出异常并断开channel的连接。

3 IdleStateHandler解析

3.1 4 个属性

private final boolean observeOutput; //是否考虑出站时较慢的情况。默认值是 false
private final long readerIdleTimeNanos;//读事件空闲时间,0 则禁用事件
private final long writerIdleTimeNanos;//写事件空闲时间,0 则禁用事件
private final long allIdleTimeNanos;//读或写空闲时间,0 则禁用事件

3.2 handlerAdded 方法

当该 handler 被添加到 pipeline 中时,则调用 initialize 方法

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) {
        //这里的 schedule 方法会调用 eventLoop 的 schedule 方法,将定时任务添加进队列中
        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, newAllIdleTimeoutTask(ctx),
                                  allIdleTimeNanos, TimeUnit.NANOSECONDS);
    }
}

只要给定的参数大于 0,就创建一个定时任务,每个事件都创建。同时,将 state 状态设置为 1,防止重复初始化。调用 initOutputChanged 方法,初始化 “监控出站数据属性”。

3.3 3 个定时任务类

image.png
这 3 个定时任务分别对应 读,写,读或者写 事件。
共有一个父类(AbstractIdleTask)。这个父类提供了一个模板方法
当通道关闭了,就不执行任务了。反之,执行子类的 run 方法

private abstract static classAbstractIdleTask implements Runnable {
    private final ChannelHandlerContext ctx;
    AbstractIdleTask(ChannelHandlerContext ctx) {
        this.ctx = ctx;
    }
    @Override
    public void run() {
        if (!ctx.channel().isOpen()) {//判断通道是否关闭
            return;
        }
        run(ctx);
    }
    protected abstract void run(ChannelHandlerContext ctx);
}

3.4 读事件的 run 方法即 (即 ReaderIdleTimeoutTask 的run 方法)分析

  • 代码及其说明
@Override
protected void run(ChannelHandlerContext ctx) {
    long nextDelay = readerIdleTimeNanos;
    if (!reading) {
        nextDelay -= ticksInNanos() - lastReadTime;
    }
    if (nextDelay <= 0) {
        // Reader is idle - set a new timeout and notify the callback.
        // 用于取消任务 promise
        readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        boolean first = firstReaderIdleEvent;
        firstReaderIdleEvent = false;
        try {
            //再次提交任务
            IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
            //触发用户 handler use
            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);
    }
}

说明:

  1. 得到用户设置的超时时间。
  2. 如果读取操作结束了(执行了 channelReadComplete 方法设置) ,就用当前时间减去给定时间和最后一次读(执操作的时间行了 channelReadComplete 方法设置),如果小于 0,就触发事件。反之,继续放入队列。间隔时间是新的计算时间。
  3. 触发的逻辑是:首先将任务再次放到队列,时间是刚开始设置的时间,返回一个 promise 对象,用于做取消操作。然后,设置 first 属性为 false ,表示,下一次读取不再是第一次了,这个属性在channelRead 方法会被改成 true。
  4. 创建一个 IdleStateEvent 类型的写事件对象,将此对象传递给用户的 UserEventTriggered 方法。完成触发事件的操作。
  5. 总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发 UserEventTriggered 方法。

3.5 写事件的 run 方法(即 WriterIdleTimeoutTask的 run 方法)

run 代码和分析

@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)) {//多了这个针对出站较慢数据的判断hasOutputChanged
                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);
    }
}

说明:
写任务的 run 代码逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对出站较慢数据的判断hasOutputChanged

3.6 所有事件的 run 方法(即 AllIdleTimeoutTask 的 run 方法) 分析

@Override
protected void run(ChannelHandlerContext ctx) {
    long nextDelay = allIdleTimeNanos;
    if (!reading) {
        nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
    }
    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);
    }
}

说明:
表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致:
需要大家注意的地方是

long nextDelay = allIdleTimeNanos;
if (!reading) {
    // 当前时间减去 最后一次写或读 的时间 ,若大于 0,说明超时了
    nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
}

这里的时间计算是取读写事件中的最大值来的。然后像写事件一样,判断是否发生了写的慢的情况。

3.7 小结 Netty 的心跳机制

  1. IdleStateHandler 可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会

触发用户 handler 的 userEventTriggered 方法。

  1. 用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关闭连接。
  2. IdleStateHandler 的实现基于 EventLoop 的定时任务,每次读写都会记录一个值,在定时任务运行的时候,

通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是否空闲。

  1. 内部有 3 个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了。
  2. 同时,IdleStateHandler 内部也考虑了一些极端情况:客户端接收缓慢,一次接收数据的速度超过了设置的

空闲时间。Netty 通过构造方法中的 observeOutput 属性来决定是否对出站缓冲区的情况进行判断。

  1. 如果出站缓慢,Netty 不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。
  2. 因为第一次无法判断是出站缓慢还是空闲。当然,出站缓慢的话,可能造成 OOM , OOM 比空闲的问题更大。所以,当你的应用出现了内存溢出,OOM 之类,并且写空闲极少发生(使用了 observeOutput 为 true),那么就需要注意是不是数据出站速度过慢。

4 ReadTimeoutHandler解析

/**
 * Raises a {@link ReadTimeoutException} when no data was read within a certain
 * period of time.
 */
public class ReadTimeoutHandler extends IdleStateHandler {
    
}

看注释就很明确了,如果在指定的时间内没有数据被读取,则抛出一个ReadTimeoutException。
我们来直接分析下源码

public class ReadTimeoutHandler extends IdleStateHandler {
    private boolean closed;
    // 默认以秒为单位
    public ReadTimeoutHandler(int timeoutSeconds) {
        this(timeoutSeconds, TimeUnit.SECONDS);
    }
 
    public ReadTimeoutHandler(long timeout, TimeUnit unit) {
        super(timeout, 0, 0, unit);
    }
// 这个重写了IdleStateHandler.channelIdle()方法
 @Override
    protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        assert evt.state() == IdleState.READER_IDLE;
        readTimedOut(ctx);
    }
 
    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        // 如果当前Handler没有关闭,则直接抛出一个ReadTimeoutException并关闭当前channel
        if (!closed) {
            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }
}

代码比较少,重要的就是重写了IdleStateHandler.channelIdle()方法。
ReadTimeoutHandler处理更直接,不需要下一个ChannelHandler处理了,直接抛出异常,关闭channel。

4.1 与IdleStateHandler区别

都是利用定时器调度任务完成。 IdleStateHandler超时调用handler的userEventTriggered方法。ReadTimeOutHandler超时抛出异常,调用handler的exceptionCaught方法,并且会关闭channel。

5 WriteTimeoutHandler解析

// Raises a {@link WriteTimeoutException} when a write operation cannot finish in a certain period of time.
public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter {
}

从注释中我们了解到WriteTimeoutHandler与ReadTimeoutHandler比较类似,如果在指定时间内写操作没有完成的话,则直接抛出WriteTimeoutException异常。
这个与IdleStateHandler中的write事件监测有所不同(IdleStateHandler监测的是在指定时间内没有发生写事件,则发送WRITE_IDLE事件),需要注意

5.1 WriteTimeoutHandler

public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter {
    private static final long MIN_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(1);
 
	// 指定超时时间
    private final long timeoutNanos;
 
	// 一个双向链表的task,后续会分析
    private WriteTimeoutTask lastTask;
    private boolean closed;
 
	public WriteTimeoutHandler(int timeoutSeconds) {
        this(timeoutSeconds, TimeUnit.SECONDS);
    }
 
	// 对写超时时间进行设置
    public WriteTimeoutHandler(long timeout, TimeUnit unit) {
        ObjectUtil.checkNotNull(unit, "unit");
 
        if (timeout <= 0) {
            timeoutNanos = 0;
        } else {
            timeoutNanos = Math.max(unit.toNanos(timeout), MIN_TIMEOUT_NANOS);
        }
    }
}

5.2 WriteTimeoutHandler.write() 重写写方法

public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter {
	@Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (timeoutNanos > 0) {
            promise = promise.unvoid();
            // 创建一个定时任务,具体见下面
            scheduleTimeout(ctx, promise);
        }
        ctx.write(msg, promise);
    }
 
	private void scheduleTimeout(final ChannelHandlerContext ctx, final ChannelPromise promise) {
        // 创建一个WriteTimeoutTask,具体见2.3
        final WriteTimeoutTask task = new WriteTimeoutTask(ctx, promise);
        // 在延迟timeoutNanos后执行task
        task.scheduledFuture = ctx.executor().schedule(task, timeoutNanos, TimeUnit.NANOSECONDS);
 
        // 如果task没有执行结束,则将当前task添加到lastTask的后一个节点,并添加监听
        if (!task.scheduledFuture.isDone()) {
            addWriteTimeoutTask(task);
            promise.addListener(task);
        }
    }
}

5.3 WriteTimeoutTask 超时任务

private final class WriteTimeoutTask implements Runnable, ChannelFutureListener {
 
    private final ChannelHandlerContext ctx;
    private final ChannelPromise promise;
 
    // WriteTimeoutTask is also a node of a doubly-linked list
    WriteTimeoutTask prev;
    WriteTimeoutTask next;
 
    ScheduledFuture<?> scheduledFuture;
 
    WriteTimeoutTask(ChannelHandlerContext ctx, ChannelPromise promise) {
        this.ctx = ctx;
        this.promise = promise;
    }
 
    @Override
    public void run() {
        // 当前任务执行时机就是在经过timeoutNanos延时后执行的,如果这时write任务还没有完成,说明已经超时了
        if (!promise.isDone()) {
            try {
                // 超时则抛出异常,具体见2.3.1
                writeTimedOut(ctx);
            } catch (Throwable t) {
                ctx.fireExceptionCaught(t);
            }
        }
        removeWriteTimeoutTask(this);
    }
 
	// 用于监听write事件的完成,完成后,直接取消scheduledFuture,并将当前task从链表中删除
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        // scheduledFuture has already be set when reaching here
        scheduledFuture.cancel(false);
        removeWriteTimeoutTask(this);
    }
}

5.4 WriteTimeoutHandler.writeTimedOut()超时触发异常

public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter {
	protected void writeTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            // 直接抛出异常,并关闭连接
            ctx.fireExceptionCaught(WriteTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
IdleStateHandler是Netty提供的一个用于检测空闲连接的处理器,它可以检测连接的读、写或读写空闲时间,当连接在指定时间内没有进行读写操作时,就会触发IdleStateEvent事件。你可以通过添加ChannelDuplexHandler继承类的方式来使用IdleStateHandler。 下面是一个配置IdleStateHandler的例子: ```java public class MyIdleStateHandler extends ChannelDuplexHandler { private static final int READER_IDLE_TIME_SECONDS = 60; // 读空闲时间 private static final int WRITER_IDLE_TIME_SECONDS = 0; // 写空闲时间,0表示不检测写空闲时间 private static final int ALL_IDLE_TIME_SECONDS = 0; // 读写空闲时间,0表示不检测读写空闲时间 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent) evt; switch (event.state()) { case READER_IDLE: System.out.println("读空闲"); break; case WRITER_IDLE: System.out.println("写空闲"); break; case ALL_IDLE: System.out.println("读写空闲"); break; default: break; } } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("连接成功"); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("连接断开"); super.channelInactive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); super.exceptionCaught(ctx, cause); } } ``` 在上面的例子中,我们设置了读空闲时间为60秒,写空闲时间为0秒(表示不检测写空闲时间),读写空闲时间为0秒(表示不检测读写空闲时间)。当连接进入空闲状态时,userEventTriggered方法会被调用,并根据空闲状态类型打印相应的信息。 在使用IdleStateHandler时,需要注意以下几点: - 只有当channel的读或写事件在一段时间内没有被触发时,才会触发IdleStateEvent事件。 - IdleStateHandler只会检测连接空闲时间,不会对连接进行任何处理,需要我们在userEventTriggered方法中进行相应的处理操作。 - IdleStateHandler只能添加到ChannelPipeline的头部或尾部。 - 如果配置了IdleStateHandler,就不需要再自己实现定时器来检测连接空闲时间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值