TCP 三次握手与四次挥手
校招考察频度:★ 高频(核心必背·高频生死线) 一句话速记:三次握手确认双方“收发双向通畅”;四次挥手因全双工需双向独立断开;TIME_WAIT 等 2MSL 防丢 ACK 灭旧包,CLOSE_WAIT 堆积必定是业务代码漏调 close。
TCP 连接的建立与断开是校招面试中考查频率最高的问题,没有之一。考官不仅看你能不能默写时序图,更看重你对“为什么必须是三次/四次”的逻辑自洽性,以及面对线上连接堆积故障时的排障思路。
一、三次握手:双方确认收发能力(打电话通俗比喻)
TCP 是面向连接的、可靠的、基于字节流的传输层协议。三次握手的核心本质是:在不可靠的信道上,用最少的成本建立起互信,并同步双方的初始序列号(ISN, Initial Sequence Number)。
TCP 三次握手:同步双方初始序列号
打电话通俗演练法(面试极简脱稿表达)
第 1 次握手(客户端发 SYN):
“喂,你能听到我说话吗?”
-> 服务端收到后确认:客户端的发送正常,服务端的接收正常。
第 2 次握手(服务端发 SYN+ACK):
“听到了!我也在说话,你能听到我说话吗?”
-> 客户端收到后确认:服务端的发送和接收都正常,客户端自己的发送和接收也都正常!(客户端心里完全有底了)
第 3 次握手(客户端发 ACK):
“听到了!那咱们开始聊正事吧!”
-> 服务端收到后确认:客户端的接收也正常!(服务端心里也完全有底了,连接建立)为什么不能是“两次握手”?
- 原因 1:防止失效的历史连接请求突然传到服务端产生错误(RFC 规范核心理由)。
假设客户端发了一个 SYN 报文在某个网络节点滞留了,客户端超时放弃并重开连接;等旧的 SYN 慢悠悠到达服务端时,若只有两次握手,服务端一发 ACK 就单方面认为连接建立,一直在干等客户端发数据,导致服务端资源白白浪费。 - 原因 2:确认双方的双向收发能力。两次握手只能证明客户端能发、服务端能收,但服务端根本不知道自己的发信能不能被客户端收到。
二、四次挥手:全双工独立断开
TCP 是全双工通信(Full-Duplex):发送端和接收端都可以同时发送和接收数据。两个方向的数据流是完全独立的,因此每个方向都需要单独关闭(主动方说我不发了,被动方说好;被动方处理完说我也不发了,主动方说好)。
四次挥手:两个方向独立关闭
为什么挥手需要四次,而握手只有三次?
- 建立连接时:服务端收到客户端的 SYN,此时服务端的响应和连接建立意图是同步的,所以可以把
SYN + ACK合并成一个数据包一起发过去(四次变三次)。 - 关闭连接时:当主动方发送
FIN时,仅仅代表主动方不会再发送新的应用数据了;而被动方收到FIN时,可能还有数据正缓冲在发送队列里没有传完,所以被动方必须先立即回一个 ACK 告诉对方我收到你的关闭意向,等自己把所有剩下的数据都发完了,才单独再发一个FIN。因此第二步和第三步不能合并,必须走四次。
三、灵魂考点:TIME_WAIT 状态与 2MSL
主动关闭方发出最后一个 ACK 后,必须在 TIME_WAIT 状态停留 2MSL(Maximum Segment Lifetime,报文最大生存时间):
为什么必须是 2MSL?(两大核心原因,大厂必答)
- 保证第四次挥手的 ACK 能可靠到达被动方:
如果主动方发完最后一个 ACK 就直接 CLOSED,万一这个 ACK 在网络中丢了,被动方就会超时重传第三次的FIN。如果此时主动方已经没了,就会直接回一个RST报错,导致被动方无法优雅地正常关闭(报错 Connection reset by peer)。主动方等 2MSL(来回各一个 MSL),一旦被动方重传 FIN,主动方还能再次补发 ACK。 - 防止“已失效的历史报文”混淆新连接:
经过 2MSL 的时间,本连接产生的所有网络报文都一定已经在网络中老化消逝(TTL 耗尽丢弃),确保后面复用相同四元组(源IP、源端口、目的IP、目的端口)的新连接不会收到上一个旧连接的残留脏数据。
四、生产事故与高频排查手册
| 故障现象 | 发生在哪一侧 | 根本原因 | 生产排查与排障应对方案 |
|---|---|---|---|
| 大量 TIME_WAIT 堆积 | 主动关闭方(通常是高并发短连接客户端,或作为反向代理的 Nginx) | 系统高并发下频繁主动断开短连接,每个连接都要等 2MSL(Linux 默认 60s),耗尽本地可用端口(Local Port Exhaustion)。 | 1. 开启连接复用:HTTP 开启 keep-alive 长连接,数据库用连接池;2. 系统内核调优:打开 net.ipv4.tcp_tw_reuse = 1(复用处于 TIME_WAIT 的套接字);3. 扩大可用端口范围 ip_local_port_range。 |
| 大量 CLOSE_WAIT 堆积 | 被动关闭方(通常是业务应用服务端) | 对端已经发了 FIN 想要关闭,但业务代码收到 EOF 后一直没有调用 socket.close(),导致连接一直挂在被动等待态。纯粹是代码 Bug! | 1. 排查代码:如 HTTP Client 忘记关闭 response.Body、数据库连接未归还连接池、线程池被打满导致无法处理关闭事件;2. 依靠 netstat -antp | grep CLOSE_WAIT 定位具体的进程 PID 和代码调用堆栈。 |
| SYN Flood 洪水攻击 | 服务端 | 攻击者伪造海量虚假 IP 发送 SYN,服务端分配资源放入**半连接队列(SYN 队列)**并回 SYN+ACK,由于 IP 不存在收不到第三次 ACK,导致半连接队列被瞬间撑爆。 | 1. 开启 tcp_syncookies = 1:半连接队列满时不分配上下文,把信息编码在序列号中,收到第三次 ACK 再解码验证;2. 调大 net.ipv4.tcp_max_syn_backlog;3. 防火墙配置防御清洗。 |
五、面试追问
- 三次握手中,第三次 ACK 能带数据吗?
能!第一次 SYN 和第二次 SYN+ACK 绝对不能带数据(否则容易遭到泛洪攻击),但第三次握手时客户端已经处于 ESTABLISHED 状态,完全可以附带要发送的 HTTP 请求数据。 - 如果已经建立了连接,客户端突然宕机了怎么办?
TCP 自带 保活机制(Keepalive Timer)。服务端若长时间(默认通常 2 小时)没有收到客户端数据,就会启动保活定时器,每隔 75 秒发送一个探测报文,连续 9 次无响应则判定客户端死亡并强制关闭连接。生产中应用层通常会自行设计心跳包(如每 30 秒 ping-pong)提早发现断连。