面试里被问烂,运维中却天天踩坑:明明服务在监听,客户端却连不上;抓包只看到 SYN 没有回包;服务器上堆了几万个 TIME_WAIT;程序里连接池泄漏,CLOSE_WAIT 越积越多。这些问题的答案,都藏在 TCP 的"三次握手"和"四次挥手"里。
这篇文章不背八股,而是把握手和挥手的每一个报文、每一次状态变化讲透,配上 ss、tcpdump 的真实命令,让你能看懂抓包、能判断连接卡在哪一步、能解释 TIME_WAIT 和 CLOSE_WAIT 带来的实际影响。读完你应该能做到:看到连接异常,知道该查哪个状态、哪个队列、哪个内核参数。
一、为什么连接需要"握手"
TCP 是面向连接的协议,双方在传数据前必须先确认三件事:彼此都愿意通信、彼此的序号从哪里开始、连接确实可达。因为网络不可靠,你不能假设"我发了对方一定收到",必须靠一来一回的确认来同步状态。
握手的核心价值是同步初始序号(ISN)并防止历史连接干扰。如果只握两次,服务端无法确认客户端的接收能力,也可能被一个早已失效的旧 SYN 建出一个假连接。三次是目前最少的、能同时完成双向确认的次数。
二、三次握手完整流程
假设客户端端口 50000,服务端 80 端口,三个报文依次是:
- 第一次:客户端发 SYN。客户端选一个初始序号(比如 Seq=x),置 SYN=1,状态从 CLOSED 进入 SYN_SENT。意思是"我想连你,我的序号从 x 开始"。
- 第二次:服务端回 SYN+ACK。服务端选自己的初始序号 Seq=y,置 SYN=1、Ack=x+1,状态从 LISTEN 进入 SYN_RCVD。意思是"我同意,我的序号从 y 开始,我期待你下一个字节是 x+1"。
- 第三次:客户端回 ACK。客户端置 Ack=y+1,Seq=x+1,进入 ESTABLISHED;服务端收到后也进入 ESTABLISHED。连接建立完成。
用抓包看最直观(抓包里 S 是 SYN,. 是 ACK,S. 是 SYN+ACK):
# 抓一次握手过程,-nn 不解析名字,-S 显示绝对序号
sudo tcpdump -nn -S -i eth0 'tcp port 80 and (tcp[tcpflags] & (tcp-syn|tcp-ack) != 0)'
风险提示:抓包需要 root 或 CAP_NET_RAW 权限,且在高流量网卡上长时间全量抓包会占满磁盘和 CPU,务必加过滤条件和
-c限制条数。
为什么不能是两次
如果只有两次(客户端 SYN、服务端 SYN+ACK),客户端其实还没告诉服务端"我收到你的回包了"。这会导致:服务端回完 SYN+ACK 就单方面认为连接建好了,一旦这个回包在网络上延迟,客户端早已放弃,服务端却白白维护了一个半开连接,浪费资源。第三次 ACK 的作用就是让服务端确认"客户端确实收到了、连接确实双向可达"。
三、四次挥手完整流程
关闭连接比建立更啰嗦,因为 TCP 是全双工,两个方向的数据流要分别关闭。整个流程:
- 第一次:主动关闭方发 FIN。比如客户端发 FIN(Seq=u),表示"我没数据要发了",进入 FIN_WAIT_1。
- 第二次:被动方回 ACK。服务端回 Ack=u+1,进入 CLOSE_WAIT。此时客户端进入 FIN_WAIT_2。这就是半关闭状态:客户端不再发数据,但还能收服务端剩下的数据。
- (服务端可能还要继续把没发完的数据发完,客户端一直能收到。)
- 第三次:被动方发 FIN。服务端数据发完,发 FIN(Seq=w),进入 LAST_ACK。
- 第四次:主动方回 ACK。客户端回 Ack=w+1,进入 TIME_WAIT,等待 2MSL 后进入 CLOSED;服务端收到 ACK 后进入 CLOSED,连接彻底释放。
# 抓挥手过程:FIN 和 RST 标志
sudo tcpdump -nn -i eth0 'tcp port 80 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'
# 看本机连接状态分布
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
为什么是四次而不是三次
因为"关闭"这件事,服务端 ACK 和 FIN 通常不能合并。客户端发 FIN 时,只是说"我不发了",但服务端可能还有数据要发。所以服务端先回一个 ACK(第二次),把数据发完后再发 FIN(第三次)。这两个动作之间有间隔,就拆成了四次。如果服务端恰好也没数据要发,ACK 和 FIN 可能一起发送,看起来像三次,但协议逻辑上仍是四个步骤。
四、状态转换全表
TCP 连接的生命周期由有限状态机描述,运维最需要认识这几个状态:
| 状态 | 出现在哪一方 | 含义 | 关注点 |
|---|---|---|---|
| LISTEN | 服务端 | 监听端口,等待连接 | 服务没起来时该状态缺失 |
| SYN_SENT | 客户端 | 已发 SYN,等 SYN+ACK | 大量堆积说明对端不回或丢包 |
| SYN_RCVD | 服务端 | 收到 SYN,已回 SYN+ACK | 大量堆积可能是 SYN Flood |
| ESTABLISHED | 双方 | 连接已建立,可传数据 | 数据是正常连接数 |
| FIN_WAIT_1 | 主动关闭方 | 已发 FIN,等 ACK | |
| FIN_WAIT_2 | 主动关闭方 | 收到 ACK,等对方 FIN | 大量堆积说明对端一直不发 FIN |
| TIME_WAIT | 主动关闭方 | 等 2MSL,防止旧包干扰 | 短连接业务高发,属正常但要控制 |
| CLOSE_WAIT | 被动关闭方 | 收到 FIN,本端还没 close | 大量堆积几乎一定是应用 bug |
| LAST_ACK | 被动关闭方 | 已发 FIN,等最后 ACK | |
| CLOSED | 双方 | 连接关闭 |
用一条命令看全貌:
# 按状态统计当前连接数
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 找出处于 CLOSE_WAIT 的连接及其进程(关键排查命令)
sudo ss -antp | grep CLOSE_WAIT
# 统计每个进程的 CLOSE_WAIT 数量
sudo ss -antp | grep CLOSE_WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn
五、握手相关的两个队列:SYN 队列和 Accept 队列
服务端收到 SYN 后,并不是立刻交给应用,而是先进内核的两个队列:
- SYN 队列(半连接队列):存放已收到 SYN、已回 SYN+ACK、但还没收到最终 ACK 的连接(状态 SYN_RCVD)。长度由
tcp_max_syn_backlog影响。 - Accept 队列(全连接队列):存放已完成三次握手、等待应用调用
accept()取走的连接(状态 ESTABLISHED)。长度由listen()的 backlog 参数和somaxconn共同决定,取较小值。
如果应用处理 accept 太慢,Accept 队列满了,新的握手会受影响,表现就是客户端偶发连接超时。查看队列溢出:
# 查看全连接队列溢出次数(n 表示 listen backlog)
ss -lnt | grep ':80'
# 查看内核统计:全连接队列溢出
netstat -s | grep -i 'listen'
# 或
nstat -az | grep -i -E 'ListenOverflows|ListenDrops|TCPBacklogDrop'
如果 ListenOverflows 持续增长,说明 Accept 队列被打满,需要调大 somaxconn 和应用 backlog,或者提升应用 accept 速度。
六、TIME_WAIT 的实际影响
TIME_WAIT 只出现在主动关闭的一方,持续 2MSL(通常 60 秒)。它的存在有意义:一是保证最后那个 ACK 能重传(对端没收到 FIN 重发时可以正确响应),二是让本次连接的旧数据包在网络中消散,避免污染新连接。
但高频短连接(比如爬虫、API 网关做健康检查、没配连接池的 HTTP 客户端)会产生海量 TIME_WAIT,占满本地端口,导致"无法建立新连接"。查看:
# 统计 TIME_WAIT 数量
ss -ant | grep -c TIME_WAIT
# 查看本地端口范围
cat /proc/sys/net/ipv4/ip_local_port_range
处理方向后面 027 篇会专门展开,核心是:优先用长连接/连接池,其次是扩大本地端口范围、开启 tcp_tw_reuse,不要轻易开启已废弃的 tcp_tw_recycle。
常见问题
Q:抓包只看到 SYN 没有 SYN+ACK,是什么问题?
A:三种可能:服务端没监听该端口(通常会回 RST)、服务端 SYN 队列满(丢弃 SYN)、中间防火墙丢包。用 ss -lnt 确认服务端是否在听,用 netstat -s 看是否有 SYN 丢弃计数。
Q:TIME_WAIT 很多,是不是服务器有问题?
A:不一定是故障。主动关闭方出现 TIME_WAIT 是协议正常行为,短连接多就会多。只有占了大量端口、影响新连接时才需要优化。
Q:CLOSE_WAIT 堆积说明什么?
A:说明本端收到了对端的 FIN,但应用没有调用 close() 关闭套接字。几乎百分之百是应用代码问题:连接没释放、异常分支没关连接、连接池配置不当。
Q:三次握手可以带数据吗?
A:可以。TCP Fast Open 允许在 SYN 或 SYN+ACK 中携带数据,减少一次往返。但需要内核和应用同时支持,且有安全权衡。
Q:为什么挥手要等 2MSL?
A:MSL 是报文最大生存时间,2MSL 保证"本端最后一个 ACK 丢失后,对端重发的 FIN 能在本端还在 TIME_WAIT 时被处理",同时让旧报文彻底消亡。
总结
- 三次握手:SYN → SYN+ACK → ACK,核心是双向确认可达并同步初始序号;两次不足以确认客户端接收能力,四次是浪费。
- 四次挥手:FIN → ACK → FIN → ACK,因为全双工两个方向要分别关闭,半关闭期间被动方还能发数据。
- 记住状态机里三个高频状态:TIME_WAIT(主动关闭方,正常但需控制)、CLOSE_WAIT(被动关闭方未 close,多为应用 bug)、SYN_RCVD(可能与 SYN Flood 有关)。
- 服务端连接队列分 SYN 队列和 Accept 队列,前者由
tcp_max_syn_backlog影响,后者受somaxconn与 listen backlog 限制,溢出用nstat观察。 - 排查连接问题用
ss -antp看状态和进程,用tcpdump抓握手挥手报文,先定位卡在哪一步,再对症下药。
企业QQ咨询




