
我为Netty贡献源码|且看Netty如何应对TCP连接的正常关闭(六)
6. 主动关闭方发起 TCP 半关闭流程
在 TCP 半关闭的场景下,主动关闭方需要调用 shutdownOutput 方法向被动关闭方发送 FIN 开始 TCP 半关闭流程。
在本小节的示例中,客户端可以在自己的 ChannelHandler 中调用 Channel 的 shutdownOutput 方法来发起 TCP 半关闭流程。
下面我们就来分析下在 netty 中对于 shutdownOutput 的实现。
从如上代码中,我们可以看出对于 shutdownOutput 的操作也是必须在 Reactor 线程中完成。
这里大家可以发现 shutdownOutput 半关闭的流程其实和 close 的流程非常的相似。
一开始都需要通过 ChannelOutboundBuffer 是否为 null 来判断当前 Channel 是否已经关闭了,如果已经关闭,则停止执行后续半关闭流程。
当 shutdownOutput 方法调用之后,主动关闭方连接的写通道就被关闭了,所以在这个状态下是不允许用户继续向 Channel 写入数据的, 所以这里会将 Channel 对应的写入缓冲队列 ChannelOutboundBuffer 设置为 null 。
和前边我们介绍调用 close 方法发起 TCP 连接的正常关闭流程一样,这里也会调用 prepareToClose() 方法来处理设置 SO_LINGER 选项的情况。
如果 Socket 设置了 SO_LINGER 选项则需要首先将 Channel 注销,后续的半关闭流程需要在 GlobalEventExecutor 线程中执行。否则继续在 Reactor 线程中执行。
关于 prepareToClose() 方法的详细介绍,大家可以回看本文中的《 2.1.3 针对 SO_LINGER 选项的处理》小节
接下来就会调用 doShutdownOutput() 方法关闭底层 JDK NIO SocketChannel 的写通道。此时内核协议栈会向对端发送 FIN 发起 TCP 半关闭流程。
TCP连接半关闭.png
当半关闭流程发起之后,ShutdownOutput 的核心任务就算结束了,此时就需要设置用户持有的 shutdownOutputPromise 成功,随后用户就会得到通知。
最后在 Reactor 线程中清理 ChannelOutboundBuffer 中的待发送数据,并在 pipeline 中传播 ChannelOutputShutdownEvent 事件。相关的清理细节笔者已经在本文前边相关的章节中详细介绍过了,这里不在重复。
ChannelOutputShutdownEvent 是一种 UserEventTriggered 事件,它是 netty 提供的一种事件扩展机制可以允许用户自定义异步事件,这样可以使得用户能够灵活的定义各种复杂场景的处理机制。
UserEventTriggered 也是一种 Inbound 类事件,在 pipeline 中的传播反向也是从前向后传播。
image.png
我们可以在 ChannelHandler 中这样捕获 ChannelOutputShutdownEvent 写通道关闭事件:
此时主动关闭方已经关闭了写通道,进入 FIN_WAIT2 状态。因为现在读通道还没有关闭,所以在 FIN_WAIT2 状态下还是可以继续接受并处理对端发来的数据的。
理想很美好,现实却很骨感,在本小节中主动关闭方在 FIN_WAIT2 状态下真的可以接收来自对端的数据吗??
大家先可以结合笔者在 《 2.1.3 针对 SO_LINGER 选项的处理》小节中介绍的内容以及本小节介绍的 TCP 写通道关闭流程,对照下面这副图认真思考下这个问题。
TCP连接半关闭.png
7. 啊哈!!Bug !!
在为大家解释这个 Bug 之前,笔者先再次带大家回顾下本文《 2.1.3 针对 SO_LINGER 选项的处理》小节中 prepareToClose 方法的逻辑,它有两个关键点:
•当使用了 SO_LINGER 选项后,调用 Socket 的 close 方法会阻塞关闭流程,所以需要将 Socket 的关闭动作放在 GlobalEventExecutor 中执行。
•当使用了 SO_LINGER 选项后,为了防止在延迟关闭期间继续处理读写事件,产生不必要的 CPU 资源浪费,所以需要调用 doDeregister() 方法将 Channel 从 Reactor 中注销掉。
这些逻辑在 close 的关闭场景中是合理的,但是在 shutdownOutput 半关闭场景就出问题了。
假设用户在开启了 SO_LINGER 选项的情况下,调用 shutdownOutput 半关闭 TCP 连接,那么用户的本意是只关闭写通道,但是仍然希望在 FIN_WAIT2 状态下接收来自服务端发送过来的数据,实现优雅关闭。
但实际上 netty 在 shutdownOutput 方法中调用了 prepareToClose() 方法从而间接导致了 doDeregister() 方法的调用,Channel 从 Reactor 中注销掉,也就是说从此以后无法在产生 OP_READ 活跃事件无法接收并且处理服务端发送过来的数据。
由于以上原因,如下如图所示,主动关闭方在 FIN_WAIT2 状态下是无法接收到数据的,因为此时 Channel 已经从 Reactor 上注销了。
SO_LINGERHalfClosureBug.png
另外还有一点就是,无论 SO_LINGER 选项是否设置,shutdown 系统调用函数均不会阻塞,这里和 close 系统调用不同。所以这里也并不需要用一个 GlobalEventExecutor 去执行 shutdownOutput 任务,直接在 Reactor 线程中执行即可。
所以综合以上两点原因,在 shutdownOutput 中是不需要调用 prepareToClose() 方法的。
现在我们知道了 Bug 产生的原因,那么修复过程就变的非常简单了~~~
