
我为Netty贡献源码|且看Netty如何应对 TCP 连接的正常关闭(七)
8. 提交 PR ,修复 Bug
笔者首先向 Netty 社区提交了一个 Issue,在 Issue 中详细为社区人员描述了这个 Bug 产生的原因。也就是上一小节中的内容。
image.png
SO_LINGERHalfClosureBug.png
随后笔者按照《7. 啊哈!!Bug !!》小节中介绍的修复思路为这个 Issue 提交了修复 PR ,
PR.png
笔者修复后的 ShutdownOutput 流程逻辑如下:
image.png
编写单元测试,然后信心满满地等待 PR 被 Merged。
还是那句话 “理想很丰满,现实很骨感”,Netty 作为一个世界知名的高性能开源框架,必定有着非常严格的代码规范。比如:
•代码书写规范:函数与函数之间的空行个数,单行代码的长度,函数命名的长度, .... 等。
•注释的规范:单行注释的长度,字符与字符之间的空格,...... 等。
•单元测试规范。
PR 提交过去也是出现了很多规范类的 CheckStyle 错误,也是经过了多轮 Review 和多轮修改最终通过了 Netty 的 CI 流程被 Merged 进主干分支。并在 Netty 的
4.1.73.Final 中发布。
image.png
image.png
在 4.1.73.Final 版本发布之后,笔者第一时间拉下来最新的代码,看到 Git 记录中出现了自己的名字,想象着自己的代码跑在了各大知名框架中,还是很有成就感的一件事。
文章封面.png
9. 被动关闭方处理TCP半关闭流程
TCP连接半关闭.png
当主动关闭方调用 shutdownOutput 后,内核会检查此时 Socket 发送缓冲区是否还有数据,如果有就将数据发送出去,并关闭 Socket 的写通道,随后发送 FIN 给对端。
接下来的流程和《1. 正常 TCP 连接关闭》小节中的流程一样,服务端 OP_READ 事件活跃,Reactor 线程开始处理 OP_READ 事件。
这里通过 doReadBytes 方法从 Channel 中读取数据依然返回 -1 。随后又会进入 closeOnRead 方法处理半关闭逻辑。
9.1 closeOnRead
首先会调用 isInputShutdown0 方法判断服务端 Channel 的读通道是否已经关闭,现在客户端 Channel 的写通道已经关闭,但此时服务端才刚开始处理半关闭,所以现在服务端 Channel 读写通道都还没有关闭。
随后判断服务端是否支持半关闭 isAllowHalfClosure。
可通过如下配置开启半关闭的支持:
如果服务端开启了半关闭的支持 isAllowHalfClosure == true ,下面就正式进入了半关闭的处理流程:
1.调用 shutdownInput 方法关闭服务端 Channel 的读通道,如果此时 Socket 接收缓冲区还有数据,则会将这些数据统统丢弃。注意关闭读通道并不会向对端发送 FIN ,此时服务端连接依然处于 CLOSE_WAIT 状态。
2.在 pipeline 中触发 ChannelInputShutdownEvent 事件,我们可以在 ChannelInputShutdownEvent 事件的回调方法中,向客户端发送遗留的数据,做到真正的优雅关闭。这里就是图中处于 CLOSE_WAIT 状态下的服务端在半关闭场景下可以继续向处于 FIN_WAIT2 状态下的客户端发送数据的地方。
在连接半关闭的情况下,JDK NIO Selector 会不停的通知 OP_READ 事件活跃,所以 read loop 会一直不停的执行,当 Reactor 处理完 ChannelInputShutdownEvent 之后,由于 Selector 又会通知 OP_READ 事件活跃,所以半关闭流程再一次来到了 closeOnRead 方法。
那么此时服务端的读通道已经关闭了 isInputShutdown0 == true 。所以流程来到 else 分支。
•设置 inputClosedSeenErrorOnRead = true 表示此时 Channel 的读通道已经关闭了,不能再继续响应 OP_READ 事件,因为半关闭状态下,Selector 会不停的通知 OP_READ 事件,如果不停无脑响应的话,会造成极大的 CPU 资源的浪费。
不过 JDK 这样处理也是合理的,毕竟半关闭状态连接并没有完全关闭,只要连接没有完全关闭,就不停的通知你,直到关闭连接为止。
•在 pipeline 中触发 ChannelInputShutdownReadComplete 事件,此事件的触发标志着服务端在 CLOSE_WAIT 状态下已经将所有遗留的数据发送给了客户端,服务端可以在该事件的回调中关闭 Channel ,结束 CLOSE_WAIT 进入 LAST_ACK 状态。
因为半关闭的状态下,在没有调用 close 方法关闭 Channel 之前,JDK NIO Selector 会一直不停的通知 OP_READ 事件,所以流程马上又会回到 OP_READ 事件的处理方法中。
那么这次我们就不能在响应 OP_READ 事件了,需要调用 clearReadPending 方法将读事件从 Reactor 中取消掉,停止对 OP_READ 事件的监听。否则 Reactor 线程就会在半关闭期间内一直在这里空转,导致 CPU 100%。
这里的 shouldBreakReadReady 方法就是用来判断在半关闭期间是否取消 OP_READ 事件的监听。这里的 inputClosedSeenErrorOnRead 已经设置为 true 了。
到这里为止,netty 关于连接关闭所要面对的所有处理场景,笔者就为大家一一介绍完了。
总结
本文我们介绍了 netty 在面对 TCP 连接关闭时的三种处理场景时的处理逻辑和过程。
这三种处理场景分别是:TCP 连接的正常关闭,TCP 连接的异常关闭,以及用于优雅关闭的 TCP 连接的半关闭。同时我们也发现了 netty 关于半关闭处理时的一个 BUG 。
BUG :https://github.com/netty/netty/issues/11981
这个 Bug 导致主动关闭方在 FIN_WAIT2 状态下无法接受到来自被动关闭方在 CLOSE_WAIT 状态下发送的数据。随后又详细分析了这个 Bug 的整个修复过程。
其中我们还穿插介绍了 SO_LINGER 选项对于 TCP 连接关闭行为的影响,以及 netty 针对 SO_LINGER 选项的处理过程。
同时笔者还为大家列举了关于导致 TCP 连接异常关闭的 7 种场景:
1.半连接队列 SYN-Queue 已满
2.全连接队列 ACCEPT-Queue 已满
3.连接未被监听的端口
4.服务端程序崩溃
5.开启 SO_LINGER 选项设置 l_linger = 0
6.主动关闭方在关闭时 Socket 接收缓冲区还有未处理数据
7.主动关闭方 close 关闭但在 FIN_WAIT2 状态接收数据
以及 Netty 对 RST 包的处理流程。最后笔者还介绍了用于连接半关闭的系统调用 shutdown 的使用方法,以及 netty 对连接半关闭的流程处理逻辑。
其中笔者还详细对比了 shutdown 系统调用和 close 系统调用的区别与联系。它们在调用之后都会向对端发送 FIN 包。但是在设置 SO_LINGER 选项的时候 close 系统调用会阻塞,shutdown 系统调用则不会阻塞。
TCP连接半关闭.png
最后笔者需要特别强调的是在我们使用 shutdown 进行 TCP 连接的半关闭时,作为连接的被动关闭方,在最后一定要记得调用 close 方法来彻底关闭连接,并释放连接相关资源。否则被动关闭方就会一直停留在 CLOSE_WAIT 状态。
而作为主动关闭方在 FIN_WAIT2 状态下接收到来自被动关闭方在 CLOSE_WAIT 状态下发送的 FIN 之后,记得要释放客户端的资源。
好了,本文的内容就到这里,感谢大家收看到这里,我们下篇文章见~~~
