一、问题
在使用 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 记录了未确认数据的数量,可以使用ioctl
的SIOCOUTQ
选项查询,如果这个数字达到 0,我们至少可以确认所有的发送数据到达了远程操作系统,只是只能在 Linux 平台下实现。