"说说 TCP 三次握手。"
这句话每年出现在无数次面试中。大多数人背过答案:SYN → SYN-ACK → ACK。
但真正能回答下面这些追问的人,很少:
- 为什么是三次?两次不行吗?
- 四次挥手为什么要等 2MSL ?
TIME_WAIT 状态为什么让服务器"卡住"?
- 客户端和服务端各自经历了哪些状态?
这篇文章不背答案——我们把每一步都画出来,搞清楚背后的逻辑。如果你想系统性补全网络知识体系,一份条理清晰的知识图谱能帮你把零散的概念串联起来,在面试和工作里游刃有余。
一、先搞清楚:TCP 连接是什么?
TCP 连接的本质,是通信双方在内核里各自维护了一组状态和缓冲区。
连接的建立,就是双方就"我能收到你、你能收到我"这件事,达成共识的过程。
每一个 TCP 数据包,头部都带着几个关键标志位:
SYN —— 我想建立连接
ACK —— 我确认收到了你的包
FIN —— 我要关闭连接了
RST —— 直接重置,出问题了
带着这个背景,我们来看握手。
二、三次握手:不是仪式,是必要条件
完整流程图解

第一次握手:客户端发 SYN("我想建连接,我的初始序号是 x"),自己进入 SYN_SENT 状态。
第二次握手:服务端收到 SYN,回一个 SYN-ACK("收到了,我的序号是 y,我确认你的序号 x"),进入 SYN_RCVD 状态。
第三次握手:客户端收到 SYN-ACK,发 ACK("收到,咱俩都确认了"),双方进入 ESTABLISHED,连接建立!
三、灵魂拷问:为什么不能两次握手?
这是面试最常被追问的问题,很多人背了答案但说不出逻辑。
我们用一个场景来理解:
设想网络很差,客户端发了一个连接请求,超时没收到回复,于是重发了一次。第一次的请求包"失踪"了——其实没丢,只是在网络里转悠,很久之后才到达服务端。
如果只有两次握手,服务端收到这个"迟到的"SYN,直接认为连接建立了,开始分配资源等待数据。但客户端早就不认这条连接了。结果是:服务端白白消耗资源,浪费连接。
三次握手的关键:第三次 ACK 让服务端知道客户端确实还在、确实想建连接。没有第三次,服务端无法区分"真实的新连接"和"迟来的旧请求"。
四、序列号 seq 是什么?

为什么不从 0 开始?序列号(seq)有两个关键作用:保证数据有序、检测重复包。
初始序列号(ISN)之所以随机,是为了防止上一条连接的"迷路包"被新连接误认。如果每次都从 0 开始,旧连接残留的包序号和新连接重叠,就会出现数据错乱。
五、四次挥手:断开连接为什么比建立还麻烦?

四次挥手比三次握手多一步,原因很简单:TCP 是全双工的。
建立连接时,双方同时开始通信,所以 SYN 和 SYN-ACK 可以合并。
关闭连接时,"我不发了"(FIN)和"我也不发了"(服务端的 FIN)是两个独立的事件——服务端收到客户端的 FIN 后,可能还有数据没发完,必须先 ACK 确认,等数据发完,再发自己的 FIN。所以拆成四步。
六、TIME_WAIT 是什么?为什么等 2MSL?
这是面试加分项,也是生产服务器最常见的"坑"之一。
主动关闭连接的一方(通常是客户端)在发完最后一个 ACK 后,不会立刻 CLOSED,而是等 2MSL 时间(MSL = Maximum Segment Lifetime,报文最大存活时间,Linux 默认 60s,2MSL = 120s)。

TIME_WAIT 在生产中的影响:服务器主动关闭大量短连接时,会堆积大量 TIME_WAIT 状态,导致端口耗尽,新连接 bind 失败。解决办法:
// 服务端启动时设置 SO_REUSEADDR,允许复用 TIME_WAIT 状态的端口
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
或者在系统层面开启:
# 允许 TIME_WAIT 状态的 socket 复用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
七、完整状态机:TCP 连接一生经历的所有状态

这张图是面试中最值得背下来的——蓝色是客户端走的路,绿色是服务端走的路,两条路在 ESTABLISHED 汇合,又在不同的步骤分开。
八、用代码验证:亲眼看到状态变化
# 终端1:启动一个服务端
nc -l 8080
# 终端2:查看连接状态
ss -tn state established
# 或者
netstat -an | grep 8080
# 终端3:模拟客户端
nc 127.0.0.1 8080
# 终端2 再看,能看到 ESTABLISHED 状态
# 按 Ctrl+C 断开后再看,能短暂看到 TIME_WAIT
// 用 setsockopt 查看/设置 TCP 相关选项
struct tcp_info info;
socklen_t len = sizeof(info);
getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &len);
// info.tcpi_state 就是当前连接状态(数字对应 TCP_ESTABLISHED=1 等)
printf("TCP state: %d\n", info.tcpi_state);
九、高频面试题精析
Q:三次握手中,第三次 ACK 丢失会怎样?
服务端处于 SYN_RCVD 状态,会重传 SYN-ACK(默认最多 5 次,间隔指数退避)。如果客户端已经进入 ESTABLISHED 并发了数据,服务端收到数据后也会进入 ESTABLISHED。如果一直没收到,服务端超时后关闭连接。
Q:为什么四次挥手不能合并成三次?
因为服务端收到 FIN 时,可能还有数据没发完,必须先 ACK 确认 FIN,等数据发完后,再单独发 FIN。如果服务端没有"剩余数据要发"这种情况,理论上可以把 ACK 和 FIN 合并(变成三次)——这在某些场景确实会发生(延迟 ACK 机制下)。
Q:SYN Flood 攻击是怎么利用握手过程的?
攻击者发大量 SYN 包,但不完成握手,服务端的 SYN_RCVD 半连接队列被打满,正常连接进不来。对策:开启 SYN Cookie(echo 1 > /proc/sys/net/ipv4/tcp_syncookies),服务端不分配资源,而是在 SYN-ACK 里带一个特殊 cookie,收到 ACK 后验证 cookie 再建连接。想深入了解这类防御机制的底层原理,查阅技术文档中关于内核参数调优的部分是极好的途径。
Q:close() 和 shutdown() 触发挥手有什么区别?
close() 引用计数为 0 才真正关闭,如果 fd 被 dup() 过,另一个副本还在,不会发 FIN。shutdown(fd, SHUT_WR) 立即发 FIN,不管引用计数。要可靠地触发四次挥手,用 shutdown()。
十、一图总结:三次握手 vs 四次挥手

结语
把三次握手和四次挥手搞清楚,你会发现 TCP 的每一步都有严密的逻辑:
- 三次,是为了最少次数内确认双向通信正常
- 四次,是因为关闭是两个独立的半关闭
- TIME_WAIT,是为了最后一个 ACK 能送达 + 旧包彻底消散
下次面试被问到,不用再背答案,把这几张图在脑子里过一遍,逻辑自然就出来了。当你在网络编程或后端架构的实践中需要处理更复杂的连接问题时,这些基础状态机知识会自动为你导航。