如何正确关闭 TCP 连接

一、问题

在使用 TCP 网络编程时,比如 send/write 调用成功后,调用 close 关闭 socket。这种场景中有可能出现,发送的数据,传输到对端不完整。

二、原因

send() 成功返回只意味着内核接收了数据,并准备在某个特定的时间发送他们。内核在接收到数据后,还要把数据包发送到网卡等等一系列过程之后,最终数据才能到达对端主机。对端主机的内核收到数据后,然后拥有对应 socket 的进程从中读取数据。此时,数据才算是真正的完成了传输。

当调用 close() 关闭 socket 的时候,整个 TCP 连接也随之关闭。但此时可能还有数据在内核的发送缓冲区中,或者已经发送但未被确认。发送方如果 send() 后立即进行 close(),就可能出现数据其实还未发送的情况。

设置 socket 选项 SO_LINGER 会尝试将残留在内核发送缓冲区的数据发送给对方,这种看似解决了问题,但有时依然会出现数据发送不全的问题。

原因在于,发送方执行 close() 的时候,如果他的接收缓冲区中仍有数据没有读取,或者调用 close() 后有新的数据到达,这时他会发送一个 RST 报文告知对方数据丢失,没有正常使用 FIN 四次挥手断开连接,因此设置 SO_LINGER 没有效果。

三、解决

那么如果发送方先读取了自己接收缓冲区的数据,再进行 close(),此时问题也不会得到解决。

需要借助 shutdown()shutdown() 会发送一个 FIN 给对方,告知对方我即将关闭 socket。此时可以通过 recv() 返回 0(收到 EOF)检测到接收端的关闭。

因此,正确的关闭逻辑应该是:

  • 发送方进行 send() 完成之后,然后调用 shutdown() 关闭 socket 的写。然后通过检测 recv() 返回 0(也即由于接收方 close 导致)。最后再去 close
  • 接收方,通过判断 recv() 返回值为 0(由于发送方 shutdown 导致),然后写完自己还需要写的数据,最后 close 掉

注意,如果遇到恶意的或错误的 client,永远不 close(),则服务器 recv() 将不会返回 0(阻塞且 errno == EAGAIN),因此需要加一个超时控制,若 shutdown(WR) 若干秒后 recv() 未返回 0,则直接 close() 强制关闭此连接。

即使如此,shutdown() 也不能保证接收方接受到所有数据,这只是发送方能做到的最大努力。最好的办法还是像 HTTP 协议那样,附有消息的长度信息,这就需要有能力自己设计协议。

还有一种方法,Linux 记录了未确认数据的数量,可以使用ioctlSIOCOUTQ选项查询,如果这个数字达到 0,我们至少可以确认所有的发送数据到达了远程操作系统,只是只能在 Linux 平台下实现。