使用Netty,当然也要了解它的连接闲置处理

时间:2021-02-19 01:15:35

连接闲置

网络连接的闲置指的是当前网络连接处于空闲状态,即没有正在进行的数据传输或通信活动。当我们的某个连接不再发送请求或者接收响应的时候,这个连接就开始处于闲置状态。

网络连接的闲置时间越长,说明该连接越不活跃。此时,可以根据不同的场景,采取不同行为,比如:

  • 关闭连接

  • 发送心跳,长连接场景下要求定时发送心跳保持连接状态或者是探活

Netty连接闲置handler

Netty默认提供了io.netty.handler.timeout.IdleStateHandler管理连接闲置事件,它可以检测连接空闲时间,当连接在指定时间内没有读或者写操作时,就会触发一个IdleStateEvent事件:

IdleStateHandler主要提供有三个参数,设置为0表示禁用:

  1. readerIdleTimeSeconds:读空闲时间,即当服务器指定时间没有数据读取,会触发一个读闲置事件。

  1. writerIdleTimeSeconds:写空闲时间,即当服务器指定时间没有数据发送(或写入动作,参数不同阶段不一样),会触发一个写闲置事件。

  1. allIdleTimeSeconds:读/写空闲时间,客户端连接在指定时间内没有读/写操作时,就会触发一个IdleStateEvent的All事件。

    public IdleStateHandler(
            int readerIdleTimeSeconds,
            int writerIdleTimeSeconds,
            int allIdleTimeSeconds) {

        this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
             TimeUnit.SECONDS);
    }

读闲置

Netty的ReadTimeoutHandler是一个针对读闲置处理的handler,用于处理读取超时事件。当客户端或服务器在指定时间内没有收到数据时,这个类会触发一个读取超时事件。

在Netty中,使用IdleStateHandler来管理读取超时和写入超时。ReadTimeoutHandler继承了IdleStateHandler,并在其基础上实现了定制化的读取超时功能。当读取超时发生时,ReadTimeoutHandler会调用其channelRead方法,其中实现了具体的读取超时逻辑。

ReadTimeoutHandler在达到闲置时间的时候会抛出一个读超时异常,并关闭连接:

    protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }

我一般把ReadTimeoutHandler用在服务端,当某个客户端的连接闲置太长时间就关闭这个客户端的连接,释放资源。

一般情况下IdleStateHandler都是作为ChannelPipeline中的第一个Handler,用于处理连接的心跳检测。当连接超时时会触发IdleStateEvent事件,然后交给下一个handler处理该事件,所以ReadTimeoutHandler也是这样,示例如下:

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new ReadTimeoutHandler(READ_TIMEOUT_SECONDS));
        pipeline.addLast(new ServerHandler());
    }

写闲置

写闲置我一般用在客户端,比如达到闲置时间关闭与服务端的连接或者发送心跳给服务端以保持长连接。

写闲置则关闭示例如下:

public class ClientIdleHandler extends IdleStateHandler {

    private static final long IDLE_TIMEOUT = 30000;

    public ClientIdleHandler() {
        super(true, 0, IDLE_TIMEOUT, 0, TimeUnit.MILLISECONDS);
    }

    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        if (evt == IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT) {
            // 连接闲置,关闭连接
            ctx.close();
        }
    }
}

或者也可以不关闭,发送一个心跳包,但是要注意,这种情况,客户端的闲置时间要少于服务端的。

我们也可以增加一个定时任务来发送心跳包:

    // 重写handlerAdded方法,添加一个定时任务
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        ctx.executor().scheduleAtFixedRate(() -> {
            // 判断连接是否处于活动状态
            if (ctx.channel().isActive()) { 
                // 发送心跳包
                ctx.writeAndFlush(new Heartbeat()); 
            }
        }, 0, idleTimeSeconds, TimeUnit.SECONDS);
    }

读写闲置

上面的读闲置时间和写闲置时间都达到了才触发,两者取最大值:

            long nextDelay = allIdleTimeNanos;
            if (!reading) {
                nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
            }

写意图

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

在评估写闲置的时候还有一个参数:observeOutput。

非写闲置判定的时候,是以数据发送出去为准,还是有写的动作为准,默认该值是false,数据发送判定为写闲置,而设置为true就是写动作发生时,比如写入缓存但是还没发送就是非闲置。

依据观测当前缓存的数据变化情况和进度来判断。如下,和上次进行比对:

            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;
                    }
                }
            }
        }

误区

Netty提供了读闲置处理的ReadTimeoutHandler,是不是提供的也有写闲置的WriteTimeoutHandler。

明确说明:没有写闲置的WriteTimeoutHandler,但确实存在WriteTimeoutHandler,该hander表示的是写数据动作的超时handler,却不是表示连接写闲置的handler,当在指定时间内数据没写完,会抛出一个写超时异常,可以看下源码:

        public void run() {
            // Was not written yet so issue a write timeout
            // The promise itself will be failed with a ClosedChannelException once the close() was issued
            // See https://github.com/netty/netty/issues/2159
            if (!promise.isDone()) {
                try {
                    writeTimedOut(ctx);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            }
            removeWriteTimeoutTask(this);
        }


    protected void writeTimedOut(ChannelHandlerContext ctx) throws Exception {
        if (!closed) {
            ctx.fireExceptionCaught(WriteTimeoutException.INSTANCE);
            ctx.close();
            closed = true;
        }
    }