Netty源码分析——AUTOREAD

3,841 阅读4分钟

Netty源码分析——AUTOREAD

前言

这个是设置在Channel上的一个属性,主要控制管道的自动读。可能对于很多初学者来说,都对这个Channel的自动读表示困惑,这篇文章主要来看下这个自动读如何工作以及什么时候关闭自动读(自动读默认开启)。

上代码

这个要追溯到我们的读操作了,我们以服务端为例,服务端先是有个叫Boss的Reacotr线程,不断的轮训ACCEPT事件,如果轮训到,就创建一个Channel并且注册READ事件,把这个Channel分配给一个叫WorkerReactor线程。这个逻辑不说了,详细的内容看之前的:Boss和Worker

分配给WorkerChannel,在NIO模式下是NioSocketChannel,我们看下它的读取操作:

// sth...
try {
do {
byteBuf = allocHandle.allocate(allocator);
//真正的把数据读取到ByteBuf里
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}

allocHandle.incMessagesRead(1);
readPending = false;
// 触发管道的读操作,处理数据
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
// 读操作是否结束
} while (allocHandle.continueReading());

allocHandle.readComplete();
// 这里触发读完成操作
pipeline.fireChannelReadComplete();

if (close) {
closeOnRead(pipeline);
}
}
// sth...

我们讲过之前的大部分步骤,这里看这个pipeline.fireChannelReadComplete。这里会从HeadContext节点开始向后传递,看下HeadContext#channelReadComplete

ctx.fireChannelReadComplete();
readIfIsAutoRead();

private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}

这里看这个readIfIsAutoRead,这里出现了自动读类似的字眼。具体的方法内容也很简单,如果Channel设置了自动读,就执行channel.read()。这个方法我们之前也讲过了,最终传递给tail,再从tail传递给head,然后执行unsafe.beginRead(),最终是给这个NioSocketChannel注册一个READ事件。

注册以后,这个Channel就可以继续在一个Worker Reactor线程里,继续做读操作。

注意,这个管道能够在一次读操作之后继续读的要求,就算我们的主题——AUTOREAD,关于如何设置自动读,就不细说了。那我们到此就可以推断出这个自动读的作用:在第一次读操作结束之后,是否还进行读操作。这里需要注意,如果这个NioSocketChannel第一次被注册到Worker上,就算这个管道被设置为非自动读,也是会进行一次读操作的,换句话说,每个管道都会至少进行一次读(除非客户端就根本没东西写给服务端,那么当然就不会进行读操作)。

作用和使用场景

说完了原理,我们说下什么时候把这个自动读关掉,已经什么时候重新开启。

这个其实是做流控用的。举个例子,我们服务端有个线程池,固定大小500个线程。这时候我们可能有很多的客户端链接,一下子把线程池撑满了,这时候我们可以关掉一部分管道的自动读。

以上场景可能不是非常恰当,我可以在管道中设置一个流控Handler。这个Handler每次读到数据的时候,就看看线程池的size,如果超过某个值,我们就关闭这个管道的自动读:channel.config().setAutoRead(false)。然后继续传递。等到这个管道读结束了,就不会再出发下次读了,这样当然也不会占用我们的线程池了。

这里我们注意一个问题,我们不从Channel里读数据,并不代表这个Channel关闭了。我们可以用一个定时任务检测我们的线程池,如果低于某个值,我们调用Channelchannel.config().setAutoRead(true)即可开启自动读。

这里可能各位有个问题,之前我们也说了,调用readIfIsAutoRead(或者说fireChannelReadComplete)是在一次读结束,但是之前我们设置了禁止自动读,那么自然也没人来执行ctx.fireChannelReadComplete了,这时候我把Channel的自动读打开也没用啊,因为没人能触发readIfIsAutoRead给这个Channel注册自动读了。

这里还是要追一下代码,看下DefaultChannelConfig#setAutoRead

boolean oldAutoRead = AUTOREAD_UPDATER.getAndSet(this, autoRead ? 1 : 0) == 1;
if (autoRead && !oldAutoRead) {
channel.read();
} else if (!autoRead && oldAutoRead) {
autoReadCleared();
}
return this;

这里我们看到,如果把管道的自动读设置为true的时候,是会主动调用一次channel.read()来进行READ事件的注册的。

注意

但是使用自动读也要注意一件事情。

自动读如果关闭后,对端发送FIN的时候,接收端应用层也是感知不到的。这样带来一个后果就是对端发送了FIN,然后内核将这个socket的状态变成CLOSE_WAIT。但是因为应用层感知不到,所以应用层一直没有调用close。这样的socket就会长期处于CLOSE_WAIT状态。特别是一些使用连接池的应用,如果将连接归还给连接池后,一定要记着自动读一定是打开的。不然就会有大量的连接处于CLOSE_WAIT状态。

说白了就是,如果服务端关闭自动读,但是关闭之后内核中的socket收到了客户端传来的关闭命令,这时候应用层没有从Channel中读取数据,自然也就不知道客户端已经要求关闭了。这里要特别注意。