TCP 三次握手和四次挥手的过程是怎样的?
一句话回答
三次握手:客户端发 SYN,服务端回 SYN + ACK,客户端再回 ACK,目的是确认双方都能正常收发,并同步各自的初始序列号。四次挥手:主动关闭方发 FIN,对方先回 ACK,等自己的数据发完再发 FIN,主动方回 ACK 后进入 TIME_WAIT,等待 2MSL 才彻底关闭。挥手比握手多一次,是因为 TCP 是全双工的,两个方向要分别关闭。
详细解析
三次握手
客户端 服务端
CLOSED LISTEN
| |
|--- SYN, seq = x ------------------------------->|
SYN_SENT SYN_RCVD
|<------------ SYN + ACK, seq = y, ack = x + 1 ---|
ESTABLISHED |
|--- ACK, seq = x + 1, ack = y + 1 -------------->|
| ESTABLISHED
- 客户端发送 SYN,带上自己的初始序列号 x,进入 SYN_SENT
- 服务端回复 SYN + ACK,带上自己的初始序列号 y,并用 ack = x + 1 确认收到了客户端的 SYN,进入 SYN_RCVD
- 客户端回复 ACK(ack = y + 1),双方进入 ESTABLISHED。这个报文可以携带数据;前两次握手一般不携带数据(TCP Fast Open 是例外)
初始序列号不是固定从 0 开始,而是每次建立连接时随机生成:一是避免和同一四元组上旧连接残留的报文混淆,二是防止攻击者猜出序列号来伪造报文。
为什么是三次,不是两次
- 确认双方的收发能力:第一次握手后,服务端知道"客户端能发、自己能收";第二次后,客户端知道双方都能正常收发;第三次后,服务端才知道"自己能发、客户端能收"
- 同步双方的初始序列号:每一方的初始序列号都要得到对方确认。服务端的 SYN 和对客户端的 ACK 合并在一个报文里发送,所以是三次而不是四次
- 防止旧的连接请求造成干扰(RFC 793 给出的主要原因):网络中延迟到达的旧 SYN 如果被服务端收到,两次握手下服务端会直接建立连接、白白占用资源。三次握手下,客户端发现这个 SYN + ACK 不是对自己当前请求的回应,会回复 RST,连接不会建立(从输入 URL 到页面显示 的追问里也简单讲过)
四次挥手
主动关闭方 被动关闭方
ESTABLISHED ESTABLISHED
| |
|--- FIN, seq = u ------------------------------->|
FIN_WAIT_1 CLOSE_WAIT
|<--------------------------- ACK, ack = u + 1 ---|
FIN_WAIT_2 |
|<--------------------------------------- data ---| 半关闭:被动方仍可继续发送数据
|<------------------------------- FIN, seq = w ---|
TIME_WAIT LAST_ACK
|--- ACK, ack = w + 1 --------------------------->|
| CLOSED
|
CLOSED 等待 2MSL 后关闭
- 主动关闭方发送 FIN,表示自己不再发送数据,进入 FIN_WAIT_1
- 被动方回复 ACK,进入 CLOSE_WAIT;主动方收到后进入 FIN_WAIT_2。此时连接处于半关闭状态:主动方不再发送,但还能接收
- 被动方把剩余数据发完、应用调用 close 后,发送 FIN,进入 LAST_ACK
- 主动方回复 ACK,进入 TIME_WAIT,等待 2MSL 后关闭;被动方收到这个 ACK 后直接关闭
为什么是四次:TCP 是全双工的,每个方向要单独关闭。被动方收到 FIN 时可能还有数据没发完,所以先回 ACK,等数据发完再发自己的 FIN,两者往往分开发送。如果被动方没有数据要发,ACK 和 FIN 也可以合并成一个报文,挥手就变成了三次。
TIME_WAIT 为什么要等 2MSL
MSL(Maximum Segment Lifetime)是报文在网络中的最长生存时间。等待 2MSL 有两个目的:
- 确保最后一个 ACK 能到达:如果它丢了,被动方会重发 FIN,主动方还处在 TIME_WAIT,可以再回一次 ACK;如果已经直接关闭,收到 FIN 只能回 RST,对方就无法正常关闭。ACK 到达对方最多要一个 MSL,对方重发的 FIN 回来最多再要一个 MSL,所以是 2MSL
- 让旧报文在网络中消失:等足够长的时间,这个连接残留在网络中的报文都会过期,不会被之后使用相同四元组(源 IP、源端口、目的 IP、目的端口)的新连接误收
Linux 上 TIME_WAIT 固定持续 60 秒(内核常量 TCP_TIMEWAIT_LEN)。常被误认为能调整它的 net.ipv4.tcp_fin_timeout,控制的其实是应用已关闭的连接在 FIN_WAIT_2 状态最多停留多久。
TIME_WAIT 过多怎么办
TIME_WAIT 出现在主动关闭的一方。频繁新建短连接、又主动关闭连接的程序(比如每次调用下游接口都新建连接)会积累大量 TIME_WAIT,占用端口和一定的内存。连接同一个目标 IP 和端口时,可用的本地端口只有几万个(Linux 默认范围是 32768 到 60999),耗尽后就无法建立新连接。
解决思路是用长连接、连接池复用连接(见 keep-alive)。Linux 上还可以开启 net.ipv4.tcp_tw_reuse(依赖 TCP 时间戳),让主动发起的新连接复用处于 TIME_WAIT 的端口。早年常见的 tcp_tw_recycle 在 NAT 环境下会误丢正常连接,Linux 4.12 已经把它删除。
SYN Flood 和 SYN Cookie
SYN Flood 攻击:攻击者伪造大量源 IP 发送 SYN,却从不回复第三次握手的 ACK。服务端为每个 SYN 在半连接队列里保存状态,还要重传 SYN + ACK,队列很快被占满,正常用户的连接请求就进不来了。
SYN Cookie:服务端收到 SYN 时不保存状态,而是把连接的关键信息(如时间计数、四元组的哈希、MSS)编码进 SYN + ACK 的初始序列号里。客户端回复 ACK 时,服务端用 ack - 1 还原并校验这些信息,通过了才建立连接,伪造的 SYN 就占用不了服务端资源。Linux 通过 net.ipv4.tcp_syncookies 控制:默认值 1 表示半连接队列溢出时才启用,2 表示始终启用,0 表示关闭。此外还可以增大半连接队列、减少 SYN + ACK 的重传次数,或者借助云厂商的 DDoS 防护。
面试官可能追问
第二次握手的包丢了会怎样?
客户端收不到 SYN + ACK,会以为自己的 SYN 丢了,超时后重传 SYN;服务端收不到第三次握手的 ACK,也会超时重传 SYN + ACK。重传间隔每次翻倍,达到最大次数后放弃,Linux 中分别由 tcp_syn_retries 和 tcp_synack_retries 控制。如果丢的是第三次握手的 ACK,服务端同样会重传 SYN + ACK;客户端此时已经是 ESTABLISHED,如果它直接发送数据,数据报文里也带着 ACK,服务端收到后同样能进入 ESTABLISHED。
CLOSE_WAIT 太多说明什么?
CLOSE_WAIT 出现在被动关闭的一方:已经收到对方的 FIN,但自己的应用还没调用 close。正常情况下它很快就会结束,大量堆积通常是代码问题,比如异常分支里忘了关闭连接、连接没有归还给连接池。可以用 ss -tanp 找到对应的进程和对端地址,再去检查这部分代码的关闭逻辑。
什么是半连接队列和全连接队列?
服务端收到 SYN 后,把连接放进半连接队列(SYN 队列),状态是 SYN_RCVD;收到第三次握手的 ACK 后,连接移到全连接队列(accept 队列),等应用调用 accept() 取走。在 Linux 中,全连接队列的长度取 listen() 的 backlog 参数和系统参数 somaxconn 中的较小值。应用处理太慢、来不及 accept() 时全连接队列会满,新的连接请求会被丢弃或拒绝;半连接队列被 SYN Flood 占满时,可以用 SYN Cookie 应对。
易错点
- TIME_WAIT 出现在主动关闭的一方,不一定是客户端;服务端主动断开连接时,TIME_WAIT 就留在服务端
- SYN 和 FIN 即使不携带数据,也各占一个序列号,所以对它们的确认号是对方序列号加 1;不带数据的纯 ACK 不占序列号
- 挥手不一定是四次:被动方没有数据要发时,ACK 和 FIN 可以合并成一个报文
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。