🚀 优质资源分享 🚀
学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
🧡 Python实战微信订餐小程序 🧡 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
💛Python量化交易实战💛 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
本系列Netty源码解析文章基于 4.1.56.Final版本,公众号:bin的技术小屋
前文回顾
在前边的系列文章中,我们从内核如何收发网络数据开始以一个C10K的问题作为主线详细从内核角度阐述了网络IO模型的演变,最终在此基础上引出了Netty的网络IO模型如下图所示:
详细内容可回看《从内核角度看IO模型的演变》
后续我们又围绕着Netty的主从Reactor网络IO线程模型,在《Reactor模型在Netty中的实现》一文中详细阐述了Netty的主从Reactor模型的创建,以及介绍了Reactor模型的关键组件。搭建了Netty的核心骨架如下图所示:
在核心骨架搭建完毕之后,我们随后又在《详细图解Reactor启动全流程》一文中阐述了Reactor启动的全流程,一个非常重要的核心组件NioServerSocketChannel开始在这里初次亮相,承担着一个网络框架最重要的任务–高效接收网络连接。我们介绍了NioServerSocketChannel的创建,初始化,向Main Reactor注册并监听OP_ACCEPT事件的整个流程。在此基础上,Netty得以整装待发,枕戈待旦开始迎接海量的客户端连接。
随后紧接着我们在《Netty如何高效接收网络连接》一文中详细介绍了Netty高效接收客户端网络连接的全流程,在这里Netty的核心重要组件NioServerSocketChannel开始正是登场,在NioServerSocketChannel中我们创建了客户端连接NioSocketChannel,并详细介绍了NioSocketChannel的初始化过程,随后通过在NioServerSocketChannel的pipeline中触发ChannelRead事件,并最终在ServerBootstrapAcceptor中将客户端连接NioSocketChannel注册到Sub Reactor中开始监听客户端连接上的OP_READ事件,准备接收客户端发送的网络数据也就是本文的主题内容。
自此Netty的核心组件全部就绪并启动完毕,开始起飞~~~
之前文章中的主角是Netty中主Reactor组中的Main Reactor以及注册在Main Reactor上边的NioServerSocketChannel,那么从本文开始,我们文章中的主角就切换为Sub Reactor以及注册在SubReactor上的NioSocketChannel了。
下面就让我们正式进入今天的主题,看一下Netty是如何处理OP_READ事件以及如何高效接收网络数据的。
1. Sub Reactor处理OP_READ事件流程总览
客户端发起系统IO调用向服务端发送数据之后,当网络数据到达服务端的网卡并经过内核协议栈的处理,最终数据到达Socket的接收缓冲区之后,Sub Reactor轮询到NioSocketChannel上的OP_READ事件
就绪,随后Sub Reactor线程就会从JDK Selector上的阻塞轮询APIselector.select(timeoutMillis)
调用中返回。转而去处理NioSocketChannel上的OP_READ事件
。
注意这里的Reactor为负责处理客户端连接的Sub Reactor。连接的类型为NioSocketChannel,处理的事件为OP_READ事件。
在之前的文章中笔者已经多次强调过了,Reactor在处理Channel上的IO事件入口函数为NioEventLoop#processSelectedKey
。
public final class NioEventLoop extends SingleThreadEventLoop {
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
..............省略.................
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
..............处理OP_CONNECT事件.................
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
..............处理OP_WRITE事件.................
}
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
//本文重点处理OP\_ACCEPT事件
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
}
这里需要重点强调的是,当前的执行线程现在已经变成了Sub Reactor,而Sub Reactor上注册的正是netty客户端NioSocketChannel负责处理连接上的读写事件。
所以这里入口函数的参数AbstractNioChannel ch
则是IO就绪的客户端连接NioSocketChannel
。
开头通过ch.unsafe()
获取到的NioUnsafe操作类正是NioSocketChannel中对底层JDK NIO SocketChannel的Unsafe底层操作类。实现类型为NioByteUnsafe
定义在下图继承结构中的AbstractNioByteChannel
父类中。
下面我们到NioByteUnsafe#read
方法中来看下Netty对OP_READ事件
的具体处理过程:
2. Netty接收网络数据流程总览
我们直接按照老规矩,先从整体上把整个OP_READ事件的逻辑处理框架提取出来,让大家先总体俯视下流程全貌,然后在针对每个核心点位进行各个击破。
流程中相关置灰的步骤为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之间的对应关系如下图所示:
而一个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中响应,处理网络请求。