跳到正文

TCP 连接管理:三次握手与四次挥手 ​

一句话:TCP 用三次握手同步初始序列号并确认双方收发能力,用四次挥手各自关闭发送方向;TIME_WAIT 不是抽象地「等 2MSL」,在 Linux 上它是一个固定 60 秒的内核常量,用来保证最后的 ACK 能重传、旧报文不会串进新连接。

适用:Linux 4.x–6.x,ss 来自 iproute2,tcpdump 4.x+。参数默认值以 sysctl 实机输出为准,不同发行版和内核版本会改。

为什么需要 ​

IP 只提供尽力而为的投递:可能丢、可能重复、可能乱序。TCP 要在其上提供可靠、有序、双向的字节流,连接建立阶段必须完成两件事:

  • 同步初始序列号(ISN):后续所有重传、排序、去重都靠序号。
  • 确认双方都能发、都能收:避免网络里滞留的旧 SYN 建立一个「客户端早已不要」的连接。

关闭阶段则要处理全双工:一方没有数据要发,不代表另一方也没有。两个方向必须分别关闭,于是有了四次挥手。

核心机制 ​

三次握手 ​

C -> S   SYN      seq=x                客户端进入 SYN_SENT
S -> C   SYN+ACK  seq=y, ack=x+1       服务器进入 SYN_RCVD
C -> S   ACK      seq=x+1, ack=y+1     双方进入 ESTABLISHED

为什么不是两次:

  • 只有两次时,服务器收到 SYN 就认为连接建立并分配资源。网络里延迟的旧 SYN 到达后,服务器会留下一个半开连接(half-open connection),客户端根本不认,资源被白占,这也是 SYN Flood 的土壤。
  • 第三次 ACK 让服务器确认「客户端确实收到了我的 SYN,并且现在还想连」。旧 SYN 触发的 SYN+ACK 会被客户端用 RST 拒绝,服务器随即释放。

两个队列要分清:服务器收到 SYN 后把连接放进 SYN 队列(request_sock),收到第三次 ACK 才移到 accept 队列,等应用 accept() 取走。两个队列都可能溢出,但原因和对策不同。

ISN 不能固定:可预测的序列号会被用于注入和伪造连接。Linux 用时间加上四元组哈希生成,尽量随机。

四次挥手 ​

C -> S   FIN  seq=u                 主动关闭方进入 FIN_WAIT_1
S -> C   ACK  ack=u+1               被动方进入 CLOSE_WAIT
S -> C   FIN  seq=v, ack=u+1        被动方进入 LAST_ACK
C -> S   ACK  ack=v+1               主动方进入 TIME_WAIT

为什么是四次:FIN 只关闭「我这边不再发数据」这一个方向。被动方收到 FIN 后可能还有数据要发,所以先 ACK,等自己的数据发完再发 FIN。如果确实没有数据,ACK 和 FIN 可以合在一个包里,抓包常看到 3 个包,但逻辑仍是四步。FIN 本身占一个序列号。

TIME_WAIT:Linux 上是 60 秒,不是 2×MSL ​

RFC 793 的模型是「等待 2 倍 MSL」,MSL(Maximum Segment Lifetime)是报文在网络中的最大生存时间,RFC 给的建议值是 2 分钟。但 Linux 不按 MSL 计时:

  • include/net/tcp.h 定义 TCP_TIMEWAIT_LEN = 60 * HZ,即 60 秒,没有对应的 sysctl 可以直接改。
  • net.ipv4.tcp_fin_timeout=60 管的是 孤儿 FIN_WAIT_2 连接的超时,不是 TIME_WAIT,调它不会缩短 TIME_WAIT。
  • 所以「Linux 默认 MSL=30s,等 2MSL=60s」这个说法是错的:结论凑巧接近,但机制不是这样。

TIME_WAIT 只出现在主动关闭方,作用有两个:

  1. 最后那个 ACK 丢了,对方重传 FIN 时还能再回一次 ACK。
  2. 让本次连接的重复报文在网络中消亡,避免相同四元组的新连接收到旧数据。

相关参数(Linux 4.x–6.x,默认值以实机为准):

  • net.ipv4.tcp_tw_reuse(默认 0):设为 1 后允许主动发起连接时复用 TIME_WAIT 套接字,依赖时间戳;它不减少 TIME_WAIT 数量,只是让客户端端口更快复用。
  • net.ipv4.tcp_tw_recycle:已在 Linux 4.12 移除。它在 NAT 环境下按源 IP 判断时间戳,会导致连接随机失败,不要再找这个开关。
  • net.ipv4.tcp_max_tw_buckets:TIME_WAIT 套接字上限,超限会直接回收并打日志,默认值随内存和版本变化。

最小上手 ​

终端 1 起一个服务:

bash
python3 -m http.server 8000

终端 2 观察队列和状态:

bash
ss -lnt 'sport = :8000'      # 监听套接字:Recv-Q=当前 accept 队列长度,Send-Q=backlog 上限
curl -s http://127.0.0.1:8000/ >/dev/null
ss -tan 'sport = :8000 or dport = :8000'
ss -tan state time-wait | wc -l

终端 3(需要 root)抓握手和挥手:

bash
tcpdump -i any -nn -c 20 \
  'tcp port 8000 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)'

期望看到 SYN、SYN+ACK、ACK;关闭时看到 FIN/ACK、FIN/ACK、ACK。ss 对监听套接字的显示约定是:Send-Q 为 backlog 上限,Recv-Q 为当前已排队、尚未被 accept() 取走的连接数(iproute2 的 ss 语义,旧 netstat 显示不同)。

生产动作 ​

按「现象 → 命令 → 判断」排:

  • accept 队列溢出:ss -lnt 里 Recv-Q 持续接近 Send-Q;nstat -az TcpExtListenOverflows TcpExtListenDrops 持续增长 → 应用 accept() 太慢或 backlog 太小。net.core.somaxconn=4096(Linux 5.4+,旧版 128),应用传入的 backlog 不能超过它;tcp_abort_on_overflow=0 时表现为丢 ACK 后重传,客户端看到的是延迟而不是报错。
  • SYN 队列溢出:ss -tan state syn-recv | wc -l 偏高;nstat -az TcpExtTCPReqQFullDrop 增长 → 可能是 SYN Flood 或握手太慢。net.ipv4.tcp_max_syn_backlog 默认值随版本和内存变化,以 sysctl 为准;tcp_syncookies 多数发行版默认开启,开启后服务器不保存半开状态,但会限制部分 TCP 选项。
  • TIME_WAIT 堆积:ss -tan state time-wait | wc -l 高 → 先判断谁是主动关闭方。客户端侧可以开 tcp_tw_reuse=1;服务端优先用长连接、让客户端先关,减少短连接。不要动已经移除的 tcp_tw_recycle。只有端口耗尽或 tcp_max_tw_buckets 溢出才需要处理,单纯数量多不是故障。
  • CLOSE_WAIT 堆积:ss -tan state close-wait 高 → 对方已发 FIN,本端应用没有 close()。用 lsof -p <pid> 看 fd,常见于连接池泄漏、异常路径没关流、response body 没读完。这是代码问题,调内核参数无效。
  • 重传和超时:ss -ti 看 retrans、rtt;nstat -az TcpRetransSegs;配合 tcpdump 判断是单边重传还是中间设备丢包。
  • 长连接被静默断开:检查 LB/NAT/防火墙的空闲超时是否短于 TCP keepalive。Linux 默认 tcp_keepalive_time=7200、tcp_keepalive_intvl=75、tcp_keepalive_probes=9(秒),应用层心跳通常要设得更短。

边界 ​

  • 三次握手成功不代表应用可用;连接建立后对端可能立刻 RST 或不响应,健康检查必须发应用层请求。
  • 四次挥手是逻辑模型;实际包数受延迟 ACK、捎带确认和 FIN 合并影响,不能拿「一定四个包」判断对错。
  • TIME_WAIT 不是 bug;它保护的是连接复用的正确性,只有在端口/bucket 层面造成实际故障时才需要干预。
  • tcp_tw_reuse 只对主动连接生效,且依赖时间戳;它不是服务端 TIME_WAIT 的万能药。
  • 调大 backlog 不能解决 accept() 慢,只会把溢出点往后推;应用不取,队列迟早还是满。
  • 中间设备可能改写 MSS、丢弃带选项的包或提前回收连接;抓包位置不同,看到的握手可能不一样。

讲清楚 ​

TCP 在不可靠的 IP 上做可靠字节流。三次握手的本质是双方各自确认「我发的你能收到、你发的我也能收到」,同时同步初始序列号;少一次就会让网络里滞留的旧 SYN 留下半开连接。挥手四次是因为 TCP 全双工,两个方向要分别关闭,被动方先 ACK,等自己的数据发完再 FIN;没有数据时 ACK 和 FIN 会合并,所以抓包可能只有三个包。TIME_WAIT 只出现在主动关闭方,Linux 固定 60 秒,由 TCP_TIMEWAIT_LEN 决定,不是 2×MSL,也不能用 tcp_fin_timeout 调;它的作用是让最后的 ACK 可重传、让旧报文消亡。生产上先分清是 SYN 队列、accept 队列、TIME_WAIT 还是 CLOSE_WAIT:前两个看 ss 队列和 nstat 溢出计数,TIME_WAIT 看主动关闭方和端口耗尽,CLOSE_WAIT 基本都是应用忘了 close()。

把一门技术讲透到既懂也会用