TCP 两次挥手?记录一次抓包异常
问题简述
我们都知道,正常的 TCP 关闭连接时是需要发生四次挥手的。但是在 wireshark 的一次抓包过程中,我却偶然发现了有一个 tcp 连接只发生了两次挥手,即客户端发送了 FIN 报文,并且收到了服务端的 ACK 报文,但是服务端却没有继续发送 FIN 报文,本次异常断开的抓包结果如下,图中的 192.168.10.1 是某经过系统刷机的路由器:
这其实是由于服务端在收到客户端的关闭请求后,并没有使用系统调用 shutdown 或者 close 来对 tcp 通道进行关闭,导致这时的 tcp 连接变成了一个半双工的状态,即服务端仍然保持写入状态(虽然并没有数据需要写入)。这里可能是路由器的 http 服务中忘记对客户端连接进行关闭所导致的一次 tcp 异常断开。
孤儿 socket 在 tcp 挥手的作用
在本次的 tcp 连接断开异常后,如果服务端还有数据需要写入,但是此时客户端中的浏览器已经关闭,这种情况下操作系统是如何处理的呢?
这里需要提到一个概念:孤儿 socket。这是 socket 的一个特殊的状态,该状态下用户进程不再继续对 socket 持有,不进行写入和读取操作,但是 socket 仍未完成 tcp 的四次挥手,这时系统栈中仍然会保留该 socket 的资源来进行 tcp 挥手操作。孤儿 socket 的引入就是为了解决 tcp 连接不能够正常断开的情况的。在孤儿 socket 的引入后,一次 tcp 挥手的流程可以分为不同的情况:
不优雅的 tcp 关闭(一):客户端直接调用 close
- 客户端需要关闭连接时,直接使用 close 系统调用,发送一个 FIN 报文,此后 socket 资源栈全部由系统负责(孤儿 socket)。
- 服务端接收到 FIN 报文,并回复 ACK 报文,若此时服务端无数据需要写入,则再发送一个 FIN,接收到 ACK 后 socket 正常释放。
- 若服务端接收到 FIN 报文后,仍然需要写入数据,由于对端已经全双工关闭,无法接受到数据,此时对端的操作系统协议栈在收到数据后会给服务端发送 RST 报文,提醒对端协议栈进行关闭。此时进程继续写入数据会收到一个 EOF 提醒,进程必须放弃写入,否则继续写入将会收到一个 SIGPIPE 信号。
不优雅的 tcp 关闭(二):客户端进程崩溃
- 客户端进程崩溃会导致服务端进程不会收到任何 FIN 报文,此时的客户端 socket 同样是一个孤儿 socket。
- 客户端进程崩溃的情况下,系统的协议栈会自动给对方发送 RST 报文来重置连接。
优雅的 tcp 关闭:使用 shutdown 进行关闭
- 客户端需要关闭连接时,使用 shutdown 关闭写入端,使 tcp 成为半双工状态,向对方发送 FIN 报文。
- 服务端接收到 FIN 报文,回复 ACK 报文,在完成写入后使用 close 或 shutdown 系统调用关闭连接,发送 FIN 报文。
- 客户端接收到 FIN 报文并且回复一个 ACK 报文。
HTTP 重定向
如果仔细看上述的抓包结果,可以注意到当客户端收到一个 HTTP 302 报文后,客户端会主动关闭 tcp 连接,并且重启一个新的 tcp 连接来访问重定向后的 url。这是因为重定向后的结果可能在一个新的 host 下,所以旧连接不一定可用,重新启动一个 tcp 连接更加安全。