bigywater.tech
网络协议 · 39

四次挥手:FIN、半关闭与 shutdown / close 的区别

close() 和 shutdown() 在线上到底有什么区别?为什么有时对方读到的是“连接被重置”而不是正常的结束?半关闭是怎么回事?这篇用抓包逐个验证:普通挥手、半关闭、带未读数据的 close()、SHUT_RD、同时关闭。

8 个包
一次普通的 close 在握手之外的包数(10 次都一样)
RST 而不是 FIN
接收缓冲区里有没读的数据时 close() 发出的包;对方 recv 得到 ConnectionResetError
10 / 10
加了 50 ms 延迟后同时关闭时 FIN 交叉的次数

四次挥手

实测 · 本机执行输出
  客户端: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 字节应答。

close 与 RST

实测 · 本机执行输出
  服务端(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。对方看到的是连接被重置,数据可能丢失。

SHUT_RD

实测 · 本机执行输出
  客户端: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。

分步演示

四次挥手
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上做;第五节的延迟由 netlab 在 lo0 的 9400~9499 端口注入,dummynet 会让同一个包被 tcpdump 抓到多次,已按 (源端口, 序号, 标志) 去重;netstat 的状态是在 close 之后 30 ms 取的快照
    • 没有验证:真实网络(en0)上的挥手;TIME_WAIT 的持续时间与影响(见下一篇之后);FIN 丢失后的重传;半关闭对负载均衡 / 代理的影响;不同系统对 SHUT_RD 之后到达的数据的处理
    京ICP备2026067502号-1京公网安备11010502063487号