Netty空闲检测之读空闲

前提: 需要读者对Netty有一定的了解

空闲检测的重要性不言而喻, 如果你的服务器作为服务的提供者, 其他客户端连接到你的服务器之后, 很长时间一直不发送数据, 那么这些无效的连接占用着你的服务器资源, 需要在指定的时间之后, 把这些无效连接关闭掉.

Netty为我们提供了一个拿来即用的空闲检测处理器

io.netty.handler.timeout.IdleStateHandler

它同时是一个入站和出站处理器,有channelRead()和write()方法 .
本篇文章讲解这个类是如何进行读空闲检测 .

写空闲的检测在另一篇文章

在这里插入图片描述
上面这张图概述了空闲检测在Pipeline中的位置, 下面这段代码, 是RocketMQ中的代码

// 源码位置: org.apache.rocketmq.remoting.netty.NettyRemotingServer#start

this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
    .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
    .option(ChannelOption.SO_BACKLOG, 1024)
    .option(ChannelOption.SO_REUSEADDR, true)
    .option(ChannelOption.SO_KEEPALIVE, false)
    .childOption(ChannelOption.TCP_NODELAY, true)
    .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())
    .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())
    .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
                .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                .addLast(defaultEventExecutorGroup,
                    // 编码器
                    encoder,
                    // 解码器
                    new NettyDecoder(),
                    // 空闲检测
                    new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                    // 连接管理
                    connectionManageHandler,
                    // 业务处理
                    serverHandler
                );
        }
    });

空闲检测大多数与连接管理一起结合使用

下面代码是Dubbo(v2.6.9)中的代码

// 源码位置: com.alibaba.dubbo.remoting.transport.netty4.NettyServer#doOpen

bootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
    .childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
    .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) throws Exception {
            NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
            ch.pipeline()
            		// 解码器
                    .addLast("decoder", adapter.getDecoder())
                    // 编码器
                    .addLast("encoder", adapter.getEncoder())
                    // 业务处理
                    .addLast("handler", nettyServerHandler);
        }
    });

在Dubbo中并没有看到在RocketMQ中的空闲检测和连接管理的Handler, 但是并不表示Dubbo没有这个功能 . Dubbo并没有使用Netty自带的空闲检测IdleStateHandler,而是自己实现的, 但其算法逻辑还是有相似的地方. Dubbo会有专门的线程池处理连接检查的工作(com.alibaba.dubbo.remoting.exchange.support.header.HeartBeatTask#run) . 言外之意, 不管代码如何写, 如何实现, 该有的功能必须要有, 毕竟它们都是作为服务端, 设计上必须要包括空闲检测这块逻辑.

接下来,我们就分析下Netty的空闲检测IdleStateHandler类.

看一下它的构造器

public IdleStateHandler(boolean observeOutput,
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
            
    // 判空
    if (unit == null) {
        throw new NullPointerException("unit");
    }
    // 是否观察输出缓冲区的变化, 默认值=false .这个在写空闲的文章中再说明这个属性作用.
    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);
    }
}

本篇文章讲解读空闲,我们假设在创建IdleStateHandler对象的时候,传入的readerIdleTime=2s, 则readerIdleTimeNanos =2000000000.

在IdleStateHandler添加到Pipeline的时候会调用initialize方法.

// 源码位置: io.netty.handler.timeout.IdleStateHandler#initialize

private void initialize(ChannelHandlerContext ctx) {
    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);
    }
}

IdleStateHandler的initialize方法会向scheduledTaskQueue中放入ScheduledFutureTask 任务, 而ScheduledFutureTask内部包含ReaderIdleTimeoutTask .

NioEventLoop会有两个任务队列, 一个是taskQueue, 另一个是scheduledTaskQueue.
scheduledTaskQueue是一个延时任务队列.

等延时时间到了readerIdleTimeNanos之后, 会执行ScheduledFutureTask , 进而执行ReaderIdleTimeoutTask .

IdleStateHandler源码注释

Netty在读取数据的时候,会调用每个Handler的channelRead方法, 数据读取完成之后会调用每个Handler的channelReadComplete方法. 自然也会调用到IdleStateHandler对应的channelRead和channelReadComplete方法.

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) { // 如果设置了读空闲 或者 读写空闲
        // 标记正在读取数据
        reading = true;
        firstReaderIdleEvent = firstAllIdleEvent = true;
    }
    ctx.fireChannelRead(msg);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
        // 更新最后读取数据的时间
        lastReadTime = ticksInNanos();
        // 读取数据已完成, 标记成没有正在读取数据, 与263行相互呼应
        reading = false;
    }
    ctx.fireChannelReadComplete();
}

在读取数据的过程中会设置reading=true,当数据读取完成之后,再将这个开关设置成reading=false.

还有一点需要说明, Netty一定是按照我们设置的readerIdleTimeNanos时长到达的时候,空闲任务就会被触发.

【第一种场景】 正在读数据的过程,触发了读空闲任务
在这里插入图片描述

如上图所示, 在Z时刻的时候对端发送过来了数据, Netty开始读取数据, 在Q时刻(即当前时刻currentTime)读空闲任务被触发. 它发现正在读取数据,说明读并不空闲,于是重新将一个读空闲的任务再次放入到scheduledTaskQueue中. 延时时间不变,依然是readerIdleTimeNanos.

【第二种场景】读取已经完成,但还没有超过一个readerIdleTimeNanos时长.

在这里插入图片描述

如上图, 在Z时刻开始读取的数据, 在A时刻数据读取完成, 由于在第一种场景的时候,在Q时刻将空闲任务放到了scheduledTaskQueue中. 到了B点的时候,readerIdleTimeNanos的时长到达了, 于是读空闲任务又被调用. 这个时候,Netty发现A->B的时长小于readerIdleTimeNanos,说明还没有到达readerIdleTimeNanos的时长. 于是依然需要把空闲任务再次放入到scheduledTaskQueue中, 但是这个时候放入的readerIdleTimeNanos值不在是原值了,它的值等于图中B,C两点的时长,即nextDelay的值, 因为A到C的时长才是一个完整的readerIdleTimeNanos, 它需要补偿这个差值. 因为在上面部分也说明了, Netty一定是经过readerIdleTimeNanos的时长触发读空闲任务.

如果在B,C之间又发生了读操作,那么当C点的时刻读空闲任务被执行的时候,它的场景与场景一一样.
如果在B,C之间没有发生读操作,那么当C点的时刻读空闲任务被执行的时候,它的场景是第三种场景,如下

【第三种场景】 读已经空闲了

在这里插入图片描述
如上图所示, 在Q时刻发现已经超过了readerIdleTimeNanos的时长,但是没有发生读操作,于是就要触发事件,通知其他Handler,比如连接管理器,在文章一开始,RocketMQ的源码中,就在Pipeline中添加了一个连接管理,就是用来处理触发了读空闲事件的, RocketMQ的源码如下

// 源码位置: org.apache.rocketmq.remoting.netty.NettyRemotingServer.NettyConnectManageHandler

@ChannelHandler.Sharable
class NettyConnectManageHandler extends ChannelDuplexHandler {
    

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
		    // 空闲事件
            if (event.state().equals(IdleState.ALL_IDLE)) {
                final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
                // 关闭连接
                RemotingUtil.closeChannel(ctx.channel());
                if (NettyRemotingServer.this.channelEventListener != null) {
                    NettyRemotingServer.this
                        .putNettyEvent(new NettyEvent(NettyEventType.IDLE, remoteAddress, ctx.channel()));
                }
            }
        }

        ctx.fireUserEventTriggered(evt);
    }

}

RocketMQ发现已经出现了读空闲,于是就把连接关闭了.

关于IdleStateHandler的源码注释请移步 IdleStateHandler源码注释

以上就是简单介绍了下IdleStateHandler触发读空闲的三种情况.


个人站点
语雀

公众号

微信公众号

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值