
一文搞懂Netty发送数据全流程 | 你想知道的细节全在这里(九)
6. 处理Socket可写但已经写满16次还没写完的情况
当 write loop 结束后,这时 writeSpinCount 的值会有两种情况:
•writeSpinCount < 0:这种情况有点不好理解,我们在介绍 Netty 通过零拷贝的方式传输网络文件也就是这里的 case 0 分支逻辑时,详细介绍了 doWrite0 方法的几种返回值,当 Netty 在传输文件的过程中发现 Socket 缓冲区已满无法在继续写入数据时,会返回 WRITE_STATUS_SNDBUF_FULL = Integer.MAX_VALUE,这就使得 writeSpinCount的值 < 0。随后 break 掉 write loop 来到 incompleteWrite(writeSpinCount < 0) 方法中,最后会在 incompleteWrite 方法中向 reactor 注册 OP_WRITE 事件。当 Socket 缓冲区变得可写时,epoll 会通知 reactor 线程继续发送文件。
•writeSpinCount == 0:这种情况很好理解,就是已经写满了 16 次,但是还没写完,同时 Socket 的写缓冲区未满,还可以继续写入。这种情况下即使 Socket 还可以继续写入,Netty 也不会再去写了,因为执行 flush 操作的是 reactor 线程,而 reactor 线程负责执行注册在它上边的所有 channel 的 IO 操作,Netty 不会允许 reactor 线程一直在一个 channel 上执行 IO 操作,reactor 线程的执行时间需要均匀的分配到每个 channel 上。所以这里 Netty 会停下,转而去处理其他 channel 上的 IO 事件。
那么还没写完的数据,Netty会如何处理呢?
这个方法的 if 分支逻辑,我们在介绍do {.....}while()循环体 write loop 中发送逻辑时已经提过,在 write loop 循环发送数据的过程中,如果发现 Socket 缓冲区已满,无法写入数据时( localWrittenBytes <= 0),则需要向 reactor 注册 OP_WRITE 事件,等到 Socket 缓冲区变为可写状态时,epoll 会通知 reactor 线程继续写入剩下的数据。
注意 if 分支处理的情况是还没写满 16 次,但是 Socket 缓冲区已满,无法写入的情况。
而 else 分支正是处理我们这里正在讨论的情况即 Socket 缓冲区是可写的,但是已经写满 16 次,在本轮 write loop 中不能再继续写入的情况。
这时 Netty 会将 channel 中剩下的待写数据的 flush 操作封装程 flushTask,丢进 reactor 的普通任务队列中,等待 reactor 执行完其他 channel 上的 io 操作后在回过头来执行未写完的 flush 任务。
忘记 Reactor 整体运行逻辑的同学,可以在回看下笔者的这篇文章?《一文聊透Netty核心引擎Reactor的运转架构》
这里我们看到 flushTask 中的任务是直接再次调用 flush0 继续回到发送数据的逻辑流程中。
细心的同学可能会有疑问,为什么这里不在继续注册 OP_WRITE 事件而是通过向 reactor 提交一个 flushTask 来完成 channel 中剩下数据的写入呢?
原因是这里我们讲的 else 分支是用来处理 Socket 缓冲区未满还是可写的,但是由于用户本次要发送的数据太多,导致写了 16 次还没写完的情形。
既然当前 Socket 缓冲区是可写的,我们就不能注册 OP_WRITE 事件,否则这里一直会不停地收到 epoll 的通知。因为 JDK NIO Selector 默认的是 epoll 的水平触发。
忘记水平触发和边缘触发这两种 epoll 工作模式的同学,可以在回看下笔者的这篇文章?《聊聊Netty那些事儿之从内核角度看IO模型》
所以这里只能向 reactor 提交 flushTask 来继续完成剩下数据的写入,而不能注册 OP_WRITE 事件。
注意:只有当 Socket 缓冲区已满导致无法写入时,Netty 才会去注册 OP_WRITE 事件。这和我们之前介绍的 OP_ACCEPT 事件和 OP_READ 事件的注册时机是不同的。
这里大家可能还会有另一个疑问,就是为什么在向 reactor 提交 flushTask 之前需要清理 OP_WRITE 事件呢? 我们并没有注册 OP_WRITE 事件呀~~
在为大家解答这个疑问之前,笔者先为大家介绍下 Netty 是如何处理 OP_WRITE 事件的,当大家明白了 OP_WRITE 事件的处理逻辑后,这个疑问就自然解开了。
7. OP_WRITE事件的处理
在?《一文聊透Netty核心引擎Reactor的运转架构》一文中,我们介绍过,当 Reactor 监听到 channel 上有 IO 事件发生后,最终会在 processSelectedKey 方法中处理 channel 上的 IO 事件,其中 OP_ACCEPT 事件和 OP_READ 事件的处理过程,笔者已经在之前的系列文章中介绍过了,这里我们聚焦于 OP_WRITE 事件的处理。
这里我们看到当 OP_WRITE 事件发生后,Netty 直接调用 channel 的 forceFlush 方法。
其实 forceFlush 方法中并没有什么特殊的逻辑,直接调用 flush0 方法再次发起 flush 操作继续 channel 中剩下数据的写入。
注意这里的 clearOpWrite() 方法,由于 channel 上的 OP_WRITE 事件就绪,表明此时 Socket 缓冲区变为可写状态,从而 Reactor 线程再次来到了 flush 流程中。
当 ChannelOutboundBuffer 中的数据全部写完后 in.isEmpty() ,就需要清理 OP_WRITE 事件,因为此时 Socket 缓冲区是可写的,这种情况下当数据全部写完后,就需要取消对 OP_WRITE 事件的监听,否则 epoll 会不断的通知 Reactor。
同理在 incompleteWrite 方法的 else 分支也需要执行 clearOpWrite() 方法取消对 OP_WRITE 事件的监听。
