close() 和 shutdown() 在线上到底有什么区别?为什么有时对方读到的是“连接被重置”而不是正常的结束?半关闭是怎么回事?这篇用抓包逐个验证:普通挥手、半关闭、带未读数据的 close()、SHUT_RD、同时关闭。
客户端:ACK → 服务端:ACK → 客户端:ACK+PSH(2字节) → 服务端:ACK → 客户端:ACK+FIN → 服务端:ACK → 服务端:ACK+FIN → 客户端:ACK
共 8 个包(握手之外)。四次挥手:客户端 FIN → 服务端 ACK;服务端 FIN → 客户端 ACK;这里服务端的 ACK 和 FIN 分开发了
重复 10 次,包数分布(握手之外):{8: 10}每个方向各一次 FIN + ACK。服务端的 ACK 与 FIN 常常分开发,因为 FIN 要等应用调用 close()。
客户端:ACK → 服务端:ACK → 客户端:ACK+PSH(7字节) → 客户端:ACK+FIN → 服务端:ACK → 服务端:ACK → 服务端:ACK+PSH(26字节) → 服务端:ACK+FIN → 客户端:ACK → 客户端:ACK 客户端在 FIN 之后仍然收到了服务端的 26 字节数据:b'RESPONSE-after-EOF:REQUEST';这就是“半关闭”——TCP 的两个方向是独立关闭的
shutdown(SHUT_WR) 只发 FIN,连接的另一个方向继续可用:客户端发完请求后仍然收到了服务端的 26 字节应答。
服务端(accepted 套接字)未读完数据就 close():客户端:ACK → 服务端:ACK → 客户端:ACK+PSH(11字节) → 服务端:ACK → 服务端:ACK+RST 客户端随后 recv:ConnectionResetError: Connection reset by peer 对照:服务端先把数据读完再 close():客户端:ACK → 服务端:ACK → 客户端:ACK+PSH(11字节) → 服务端:ACK → 服务端:ACK+FIN → 客户端:ACK → 客户端:ACK+FIN → 服务端:ACK;客户端 recv 得到 b''(正常 EOF) SO_LINGER(开,0 秒)后 close():客户端:ACK → 服务端:ACK → 服务端:ACK+RST;客户端 recv:ConnectionResetError: Connection reset by peer 所以 close 不一定发 FIN:缓冲区里有没读的数据(或 SO_LINGER 0)会直接发 RST,对方看到的是“连接被重置”而不是正常的 EOF——数据可能因此丢失
close() 时接收缓冲区里有没读的数据,发的是 RST;SO_LINGER(开,0)也是 RST。对方看到的是连接被重置,数据可能丢失。
客户端:ACK → 服务端:ACK → 客户端:ACK+PSH(17字节) → 服务端:RST 服务端对 SHUT_RD 之后到达的数据 recv 得到 b'';客户端:第二次 send 报错 Broken pipe。SHUT_RD 只是本地的行为,线上不发任何包(没有 FIN),对方不知道
SHUT_RD 本身不发包,但之后对方发来的数据会被 RST。
10 次里两端都发出了 FIN 的 10 次(同一个包会被 dummynet 在 lo0 上多次抓到,已按序号去重),其中两个 FIN 相隔不到 40 ms(来不及等对方的 ACK,也就是交叉)的 10 次
一次交叉的序列:客户端:ACK → 服务端:ACK → 服务端:ACK+FIN → 客户端:ACK+FIN → 服务端:ACK → 客户端:ACK
close 之后 30 ms 时 netstat 看到的这些连接的状态(两端各一行):{'FIN_WAIT_1': 14, 'LISTEN': 10, 'CLOSING': 6}
同时关闭时两端都经过 FIN_WAIT_1 → CLOSING → TIME_WAIT,和一方先关一方后关的路径(FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT / CLOSE_WAIT → LAST_ACK)不同(下一篇状态机用 netstat 逐个看)两端同时 close() 时 FIN 交叉,状态走 FIN_WAIT_1 → CLOSING → TIME_WAIT。