Netty如何高效接收网络数据?ByteBuffer动态自适应扩缩容机制二
2. Netty接收网络数据流程总览
我们直接按照老规矩,先从整体上把整个OP_READ事件的逻辑处理框架提取出来,让大家先总体俯视下流程全貌,然后在针对每个核心点位进行各个击破。
Netty接收网络数据流程.png
流程中相关置灰的步骤为Netty处理连接关闭时的逻辑,和本文主旨无关,我们这里暂时忽略,等后续笔者介绍连接关闭时,会单独开一篇文章详细为大家介绍。
从上面这张Netty接收网络数据总体流程图可以看出NioSocketChannel在接收网络数据的整个流程和我们在上篇文章?《Netty如何高效接收网络连接》中介绍的
NioServerSocketChannel在接收客户端连接时的流程在总体框架上是一样的。
NioSocketChannel在接收网络数据的过程处理中,也是通过在一个do{....}while(...)循环read loop中不断的循环读取连接NioSocketChannel上的数据。
同样在NioSocketChannel读取连接数据的read loop中也是受最大读取次数的限制。默认配置最多只能读取16次,超过16次无论此时NioSocketChannel中是否还有数据可读都不能在进行读取了。
这里read loop循环最大读取次数可在启动配置类ServerBootstrap中通过ChannelOption.MAX_MESSAGES_PER_READ选项设置,默认为16。
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.MAX_MESSAGES_PER_READ, 自定义次数)
**Netty这里为什么非得限制read loop的最大读取次数呢?**为什么不在read loop中一次性把数据读取完呢?
这时候就是考验我们大局观的时候了,在前边的文章介绍中我们提到Netty的IO模型为主从Reactor线程组模型,在Sub Reactor Group中包含了多个Sub Reactor专门用于监听处理客户端连接上的IO事件。
为了能够高效有序的处理全量客户端连接上的读写事件,Netty将服务端承载的全量客户端连接分摊到多个Sub Reactor中处理,同时也能保证Channel上IO处理的线程安全性。
其中一个Channel只能分配给一个固定的Reactor。一个Reactor负责处理多个Channel上的IO就绪事件,Reactor与Channel之间的对应关系如下图所示:
image.png
而一个Sub Reactor上注册了多个NioSocketChannel,Netty不可能在一个NioSocketChannel上无限制的处理下去,要将读取数据的机会均匀分摊给其他NioSocketChannel,所以需要限定每个NioSocketChannel上的最大读取次数。
此外,Sub Reactor除了需要监听处理所有注册在它上边的NioSocketChannel中的IO就绪事件之外,还需要腾出事件来处理有用户线程提交过来的异步任务。从这一点看,Netty也不会一直停留在NioSocketChannel的IO处理上。所以限制read loop的最大读取次数是非常必要的。
关于Reactor的整体运转架构,对细节部分感兴趣的同学可以回看下笔者的?《一文聊透Netty核心引擎Reactor的运转架构》这篇文章。
所以基于这个原因,我们需要在read loop循环中,每当通过doReadBytes方法从NioSocketChannel中读取到数据时(方法返回值会大于0,并记录在allocHandle.lastBytesRead中),都需要通过allocHandle.incMessagesRead(1)方法统计已经读取的次数。当达到16次时不管NioSocketChannel是否还有数据可读,都需要在read loop末尾退出循环。转去执行Sub Reactor上的异步任务。以及其他NioSocketChannel上的IO就绪事件。平均分配,雨露均沾!!
public abstract class MaxMessageHandle implements ExtendedHandle {
//read loop总共读取了多少次
private int totalMessages;
@Override
public final void incMessagesRead(int amt) {
totalMessages += amt;
}
}
本次read loop读取到的数据大小会记录在allocHandle.lastBytesRead中
public abstract class MaxMessageHandle implements ExtendedHandle {
//本次read loop读取到的字节数
private int lastBytesRead;
//整个read loop循环总共读取的字节数
private int totalBytesRead;
@Override
public void lastBytesRead(int bytes) {
lastBytesRead = bytes;
if (bytes > 0) {
totalBytesRead += bytes;
}
}
}
• lastBytesRead < 0:表示客户端主动发起了连接关闭流程,Netty开始连接关闭处理流程。这个和本文的主旨无关,我们先不用管。后面笔者会专门用一篇文章来详解关闭流程。
• lastBytesRead = 0:表示当前NioSocketChannel上的数据已经全部读取完毕,没有数据可读了。本次OP_READ事件圆满处理完毕,可以开开心心的退出read loop。
• 当lastBytesRead > 0:表示在本次read loop中从NioSocketChannel中读取到了数据,会在NioSocketChannel的pipeline中触发ChannelRead事件。进而在pipeline中负责IO处理的ChannelHandelr中响应,处理网络请求。
fireChannelRread.png
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
.......处理网络请求,比如解码,反序列化等操作.......
}
}
最后会在read loop循环的末尾调用allocHandle.continueReading()判断是否结束本次read loop循环。这里的结束循环条件的判断会比我们在介绍NioServerSocketChannel接收连接时的判断条件复杂很多,笔者会将这个判断条件的详细解析放在文章后面细节部分为大家解读,这里大家只需要把握总体核心流程,不需要关注太多细节。
总体上在NioSocketChannel中读取网络数据的read loop循环结束条件需要满足以下几点:
• 当前NioSocketChannel中的数据已经全部读取完毕,则退出循环。
• 本轮read loop如果没有读到任何数据,则退出循环。
• read loop的读取次数达到16次,退出循环。
当满足这里的read loop退出条件之后,Sub Reactor线程就会退出循环,随后会调用allocHandle.readComplete()方法根据本轮read loop总共读取到的字节数totalBytesRead来决定是否对用于接收下一轮OP_READ事件数据的ByteBuffer进行扩容或者缩容。
最后在NioSocketChannel的pipeline中触发ChannelReadComplete事件,通知ChannelHandler本次OP_READ事件已经处理完毕。
fireChannelReadComplete.png
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
.......处理网络请求,比如解码,反序列化等操作.......
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
......本次OP_READ事件处理完毕.......
......决定是否向客户端响应处理结果......
}
}