深色模式
02 · TCP 连接管理:三次握手、四次挥手、TIME_WAIT
面试第一高频。但这题真正的分水岭不在"背出三次",而在**"为什么不是两次"和"TIME_WAIT 为什么必须等 2MSL"**。
一句话本质
TCP 是双向的可靠通道。建立时要双方各自确认"我能发、你能收",断开时要双方各自确认"我没数据了"——所以各要一轮半。
生活类比:打电话
| TCP | 打电话 |
|---|---|
| 三次握手 | "喂,你能听见吗?" → "能听见,你能听见我吗?" → "能听见" |
| 四次挥手 | "我说完了" → "好,我知道了" → (对方也说完)"我也说完了" → "好,挂吧" |
| TIME_WAIT | 挂电话后等一会再走,万一对方还有话没说完 |
| 半连接队列 | 电话响了但还没接起来的那些 |
| SYN Flood | 有人一直打骚扰电话,让你接线员忙不过来 |
为什么不能只问一句"能听见吗"? 因为你说"能听见吗"只证明你的话筒好使,不能证明你的听筒好使。必须对方回一句、你再确认一句,双方才都确认了"我发得出、我收得到"。
核心机制
先看全景:建立要 3 个报文段,断开要 4 个。
一、三次握手(建立连接)
三次各自确认了什么(这是回答"为什么三次"的核心):
| 次数 | 谁确认了什么 |
|---|---|
| 第 1 次后 | 服务器确认:客户端发送能力正常、自己接收能力正常 |
| 第 2 次后 | 客户端确认:自己发送正常、自己接收正常、服务器收发都正常 |
| 第 3 次后 | 服务器确认:自己发送正常、客户端接收正常 |
只有第 3 次完成后,双方才都确认了"四个方向全通"。
二、为什么不是两次(最高频追问)
假设只有两次握手(客户端发 SYN,服务器回 SYN+ACK 就认为连接建立):
场景:一个"迷路的旧 SYN"
① 客户端很久以前发了一个 SYN(seq=100),在网络里堵了很久
② 客户端等不到,超时重发了一个新的 SYN(seq=200),完成握手,传完数据,正常关闭
③ 此时,那个迷路的旧 SYN(seq=100)终于到达服务器
两次握手的情况:
服务器收到旧 SYN > 立刻回 SYN+ACK > 并认为"连接已建立",分配资源、进入 ESTABLISHED
但客户端根本没想连(它认为这是垃圾包,回一个 RST 或者直接忽略)
> 服务器白建了一个连接,占着资源干等,直到超时
三次握手的情况:
服务器回 SYN+ACK 后进入 SYN_RCVD(半开状态),不分配完整资源
客户端收到这个莫名其妙的 SYN+ACK > 回 RST
服务器收到 RST > 释放,什么损失都没有一句话总结:三次握手的第三次,是让服务器"确认客户端真的收到了我的 SYN",从而避免被历史连接骗走资源。
(另一种说法:两次握手无法防止"已失效的连接请求突然又传到服务端",本质是同一件事。)
三、四次挥手(断开连接)
TCP 是全双工的,两个方向要各自关闭:
客户端发完 FIN 后进入 FIN_WAIT_1,收到服务器的 ACK 就转 FIN_WAIT_2——此时「客户端 → 服务器」这个方向已经关了,但反方向还开着。服务器那边进入 CLOSE_WAIT,把剩余数据发完,再补一个 FIN;客户端回最后一个 ACK,进入 TIME_WAIT。
注意 FIN_WAIT_2 和 CLOSE_WAIT 是同一时刻、分处两端的两个状态:一个在等对方的 FIN,一个在等自己的程序调 close()。
为什么是四次不是三次:因为服务器收到 FIN 后,可能还有数据没发完。所以 ACK 和 FIN 不能合并,必须分开发。
例外:如果服务器收到 FIN 时确实没数据要发了,ACK 和 FIN 可以合并成一次(延迟确认机制),变成"三次挥手"。这是常见的追问点。
四、TIME_WAIT:为什么必须等 2MSL
主动关闭的一方(这里是客户端)在发完最后一个 ACK 后,进入 TIME_WAIT,等 2MSL(MSL = 报文最大生存时间,Linux 上通常 60 秒,所以等 120 秒)才真正关闭。
两个理由(必须都说):
理由 1:保证最后一个 ACK 能到达对方
如果最后那个 ACK 丢了:
服务器会重发 FIN(因为它没收到 ACK)
此时客户端如果已经关闭 > 收到 FIN 会回 RST
服务器收到 RST 会认为"异常终止",而不是"正常关闭"
如果客户端还在 TIME_WAIT:
收到重发的 FIN > 再回一次 ACK > 服务器正常关闭理由 2:让旧连接的残留报文在网络中消亡
如果没有 TIME_WAIT,客户端立刻用相同的四元组建新连接:
旧连接的延迟报文(比如一个迟到的数据段)到达服务器
服务器以为是新连接的数据 > 数据错乱
等 2MSL,足以让这个方向上所有报文都过期消失
(一来一回的最长时间 = 2 × MSL)为什么是 2MSL 而不是 1MSL:因为要覆盖"我的 ACK 到对方"+"对方的 FIN 到我"这一个完整往返。
五、CLOSE_WAIT 过多 = 代码 bug
CLOSE_WAIT 状态:被动关闭方收到了 FIN,回了 ACK,但自己的程序还没调 close()大量 CLOSE_WAIT 意味着:你的程序收到了对方的关闭请求,但没有正确关闭 socket(忘了 close()、或者连接池没释放、或者异常路径没走到 finally)。
这不是网络问题,是代码问题。而大量 TIME_WAIT 通常是正常的(主动关闭方多),可以通过 SO_REUSEADDR、调整内核参数缓解。
| 状态 | 谁的问题 | 怎么办 |
|---|---|---|
| 大量 TIME_WAIT | 主动关闭方,通常正常 | 调 tcp_tw_reuse、用长连接 |
| 大量 CLOSE_WAIT | 被动关闭方,代码 bug | 检查 close 调用、连接池泄漏 |
六、SYN 队列与 SYN Flood
服务器维护两个队列:
半连接队列(SYN queue):收到 SYN,回了 SYN+ACK,等对方 ACK
全连接队列(accept queue):握手完成,等应用调 accept()
SYN Flood 攻击:
攻击者发大量 SYN,但**不回最后一个 ACK**
> 半连接队列被塞满
> 正常用户的 SYN 被丢弃
> 服务不可用
防御:
① SYN Cookie:不回 SYN+ACK 时不分配资源,把状态编码进 seq 号
② 增大队列、缩短超时
③ 限流 / 防火墙为什么这么设计
反事实 1:如果两次握手
服务器会被历史 SYN 骗走资源(上面详述)。更严重的是:服务器无法区分"客户端想连"和"客户端早就不要了",在 SYN Flood 场景下更容易被打垮。
反事实 2:如果没有 TIME_WAIT
- 最后一个 ACK 丢了 > 服务器一直重发 FIN,客户端已关闭回 RST > 服务器报异常关闭,日志里全是错误。
- 旧连接的延迟报文污染新连接 > 数据错乱,且极难排查(偶发、无法复现)。
反事实 3:如果 TCP 是半双工(只能一方说)
那断开只要两次挥手就够了。但 TCP 是全双工,两个方向独立关闭,所以需要四次。
面试怎么问
Q:三次握手的过程?为什么不是两次?
先讲过程(SYN / SYN+ACK / ACK,带序号),再讲"四个方向"的确认逻辑,最后用"历史 SYN 骗资源"说明为什么两次不行。 加分:提到"第三次握手可以携带数据"(前两次不行,因为还没建立,带数据容易被利用放大攻击)。
Q:四次挥手的过程?为什么不是三次?
因为全双工,服务器收到 FIN 时可能还有数据要发,所以 ACK 和 FIN 分开。 加分:如果服务器确实没数据了,ACK+FIN 可以合并,实际就是三次。
Q:TIME_WAIT 为什么要等 2MSL?
两个理由:① 保证最后的 ACK 能到达(丢了还能重发);② 让旧连接的残留报文消亡,避免污染新连接。2MSL 覆盖一个完整往返。
Q:服务器出现大量 CLOSE_WAIT 怎么办?
这是代码没关闭连接的信号(不是网络问题)。检查:连接池是否泄漏、异常分支是否漏了 close、是否用了 try-with-resources / defer。
Q:SYN Flood 怎么防?
SYN Cookie(不预分配资源,把状态编进 seq)、增大半连接队列、缩短 SYN 超时、上游限流。
追问链:
三次握手过程?
> 为什么是三次不是两次?(历史 SYN 骗资源)
> 第三次能带数据吗?(能,前两次不能)
> 序号为什么是随机的?(防止历史报文被误认为当前连接 / 安全)
> 那 ISN 怎么生成的?(时钟 + 四元组哈希,Linux 做法)
> 断开为什么是四次?(全双工,两个方向独立关)
> TIME_WAIT 为什么 2MSL?(ACK 可达 + 残留报文消亡)
> 大量 TIME_WAIT 怎么办?(SO_REUSEADDR / 长连接 / 调参)和其他知识的连接
- < 来自 分层模型与一次请求的一生:TCP 头里的端口和序号,是握手的载体
- > 影响 TCP 可靠传输与拥塞控制:握手协商的 MSS 和窗口,决定后续传输效率
- > 影响 HTTP / HTTPS 与缓存:HTTP/1.1 的
Connection: keep-alive就是为了避免反复握手;HTTPS 在 TCP 之上再加一轮 TLS 握手 - > 影响 IO 模型与零拷贝:等设备的时候,CPU 在干什么:
accept()从全连接队列取连接,队列满了新连接会被丢弃或拒绝 - > 影响 工程实践:连接池、长连接、
keep-alive的存在理由就是"握手太贵"
自测 3 问
Q1:第三次握手可以携带数据吗?前两次呢?
点开看答案
第三次可以,前两次不行。
- 第一次(SYN)不能带数据:此时服务器还没分配资源,如果允许带数据,攻击者可以用一个 SYN 塞满数据,放大攻击(SYN Flood 的变种)。
- 第二次(SYN+ACK)也不能:客户端此时还没确认连接。
- 第三次(ACK)可以:客户端已经确认了服务器的收发能力,服务器收到这个 ACK 后连接就建立了,数据可以直接跟上。
实际上很多实现会把第一个数据段和第三次握手合并(TCP Fast Open 更进一步,在 SYN 里就带数据,用 Cookie 验证)。
Q2:为什么序号(ISN)要随机,不能从 0 或 1 开始?
点开看答案
两个原因:
- 防止历史报文被误接收。如果每次都从 1 开始,一个上一次连接的延迟报文(seq=500)到达时,可能正好落在新连接的接收窗口内,被当成有效数据。
- 安全。可预测的 ISN 让攻击者能伪造 TCP 包(TCP 序列号预测攻击),注入数据或劫持连接。
Linux 的 ISN 生成方式大致是:ISN = 时钟(每 4 微秒加 1)+ 四元组哈希。既难预测,又保证同一四元组短时间内不会重复。
Q3:大量 TIME_WAIT 是坏事吗?
点开看答案
不一定是坏事。TIME_WAIT 出现在主动关闭方,说明你的服务在主动关连接——这是正常的(比如 HTTP 短连接、服务端主动踢掉空闲连接)。
问题在于:
- 每个 TIME_WAIT 占用一个四元组,端口有限(约 6 万个可用端口),大量短连接会导致端口耗尽,无法建新连接。
- 内存占用(每个 TIME_WAIT 一个小结构)。
处理方式(按推荐顺序):
- 用长连接(HTTP keep-alive、连接池)——根治。
net.ipv4.tcp_tw_reuse = 1——允许复用 TIME_WAIT 的端口建新连接(安全,因为序号已经随机化)。- 不要用
tcp_tw_recycle——它在 NAT 环境下会导致连接失败,已被移除。
对比:CLOSE_WAIT 才是真问题,那是代码没关连接。
类比失效处 / 易错点
- "打电话"类比会让人以为握手是"聊天"。实际每个报文都有严格格式和序号,且可能丢失重传。
- "三次握手建立连接"不严谨。TCP 没有"连接"这个实体,所谓连接只是双方各自维护的一份状态(四元组 + 序号 + 窗口)。断网时双方状态还在,所以会"假连接"。
- TIME_WAIT 出现在主动关闭方,不是固定出现在客户端。谁先
close()谁进 TIME_WAIT。HTTP 里通常是服务端主动关(如果设了Connection: close),所以服务端 TIME_WAIT 多。 - 半关闭(half-close):调用
shutdown(SHUT_WR)只关写方向,还能继续读。这就是四次挥手能存在的基础。 - RST 不是"正常关闭"。收到 RST 意味着对方不认这个连接(端口没监听、进程崩了、或者被防火墙拦),会立刻释放,不走四次挥手。
✅ 验收清单
- 能画出三次握手的时序图,标出 seq/ack 的具体变化
- 能说清"三次各自确认了哪几个方向",并用此推出"为什么两次不行"
- 能用"历史 SYN 骗资源"完整讲一个两次握手会出事的场景
- 能画出四次挥手时序图,并解释"为什么 ACK 和 FIN 不能合并"
- 能说出 TIME_WAIT 等 2MSL 的两个理由,并解释为什么是 2 而不是 1
- 能区分 TIME_WAIT 和 CLOSE_WAIT 的成因,并说出各自的处理方式
- 能解释 SYN Flood 的原理和 SYN Cookie 的防御思路
- 合上书,用自己的话讲 3 分钟,中途不卡壳