主题
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 只出现在主动关闭方,作用有两个:
- 最后那个 ACK 丢了,对方重传 FIN 时还能再回一次 ACK。
- 让本次连接的重复报文在网络中消亡,避免相同四元组的新连接收到旧数据。
相关参数(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()。