TCP 三次握手与四次挥手
一、概述
TCP 是面向连接的协议,通信之前需要先建立连接,通信结束后需要释放连接。建立连接使用三次握手(Three-Way Handshake),释放连接使用四次挥手(Four-Way Handshake)。
为什么需要握手和挥手
TCP 提供可靠传输,双方需要在传输数据之前先「商量好」一些参数,比如各自的初始序列号。握手过程就是双方互相确认对方的收发能力正常,并同步这些参数。
挥手则是为了优雅地关闭连接。因为 TCP 是全双工的(双方可以同时发送和接收),所以每个方向的关闭需要单独进行。
二、三次握手
过程描述
三次握手发生在客户端调用 connect() 时,由操作系统内核自动完成。
客户端 服务端
(CLOSED) (LISTEN)
| |
| -------- SYN, seq=x ----------------> | 第一次握手
| |
(SYN_SENT) (SYN_RCVD)
| |
| <------ SYN+ACK, seq=y, ack=x+1 ----- | 第二次握手
| |
| -------- ACK, ack=y+1 --------------> | 第三次握手
| |
(ESTABLISHED) (ESTABLISHED)详细步骤
第一次握手:客户端向服务端发送 SYN 报文,其中包含客户端的初始序列号 seq=x。发送后客户端进入 SYN_SENT 状态,表示等待服务端确认。
第二次握手:服务端收到 SYN 后,向客户端回复 SYN+ACK 报文。这个报文包含服务端的初始序列号 seq=y,以及对客户端 SYN 的确认 ack=x+1。发送后服务端进入 SYN_RCVD 状态。
第三次握手:客户端收到 SYN+ACK 后,向服务端发送 ACK 报文,确认号为 ack=y+1。发送后客户端进入 ESTABLISHED 状态。服务端收到这个 ACK 后也进入 ESTABLISHED 状态。至此连接建立完成。
状态变化总结
| 步骤 | 发送方 | 报文 | 发送方状态变化 | 接收方状态变化 |
|---|---|---|---|---|
| 1 | 客户端 | SYN | CLOSED → SYN_SENT | LISTEN(不变) |
| 2 | 服务端 | SYN+ACK | LISTEN → SYN_RCVD | SYN_SENT(不变) |
| 3 | 客户端 | ACK | SYN_SENT → ESTABLISHED | SYN_RCVD → ESTABLISHED |
为什么是三次,不是两次
如果只有两次握手,存在一个严重问题:无法防止历史连接的建立。
假设客户端发送的第一个 SYN 包因为网络延迟,在连接释放后才到达服务端。服务端收到后会以为是新的连接请求,回复 SYN+ACK,然后直接进入 ESTABLISHED 状态。但客户端知道这是过期的请求,不会理会。结果就是服务端白白浪费资源维护一个不存在的连接。
三次握手让客户端有机会通过第三次 ACK 来确认这个连接是否有效。如果收到过期的 SYN+ACK,客户端可以发送 RST 拒绝。
为什么不是四次
第二次握手时,服务端的 SYN 和 ACK 可以合并在一个报文里发送,没必要分成两个报文。所以三次就够了。
第三次握手可以携带数据吗
可以。第三次握手的 ACK 报文可以携带应用层数据,这样可以减少一个 RTT 的延迟。前两次握手不能携带数据,因为此时连接还未建立。
半连接队列和全连接队列
服务端在处理三次握手时,通常会把连接分成两个阶段管理:
半连接队列
半连接队列里存放的是:
已经收到客户端 SYN,也已经回了 SYN+ACK,但还没有收到最后 ACK 的连接。
此时服务端通常处于 SYN_RCVD 状态。也就是说,这些连接还没有真正建立完成,只是在等待第三次握手的 ACK。
全连接队列
当服务端收到第三次握手的 ACK 后,连接才算真正建立成功。此时连接会从半连接队列移入全连接队列。
全连接队列里存放的是:
三次握手已经完成、处于
ESTABLISHED状态,但还没有被应用程序通过accept()取走的连接。
所以可以把它简单记成:
- 半连接队列等最后一个 ACK
- 全连接队列等应用来 accept
accept() 在这里做了什么
应用程序调用 accept() 时,本质上就是:
从全连接队列里取出一个已经建立好的连接,生成对应的已连接 socket,交给应用层继续读写。
这也是为什么说:
- 三次握手完成是内核把连接建好了
accept()是应用程序正式接手这个连接
SYN Flood 为什么能打到半连接队列
SYN Flood 是一种典型的洪泛攻击。攻击者会:
- 疯狂发送大量 SYN
- 服务端为这些请求创建半连接状态并回 SYN+ACK
- 攻击者故意不回最后 ACK,或者伪造源 IP 让 ACK 根本回不来
结果就是:
- 半连接队列被大量占满
- 服务端资源被耗尽
- 正常用户的新连接可能建不起来
常见防御手段包括:
- 开启 SYN Cookie
- 增大半连接队列
- 缩短半连接保留时间 / 调整重传次数
- 借助防火墙、高防或流量清洗过滤异常 SYN
三、四次挥手
四次挥手的过程描述
四次挥手发生在任意一方调用 close() 时。以客户端主动关闭为例:
客户端 服务端
(ESTABLISHED) (ESTABLISHED)
| |
| -------- FIN, seq=u ----------------> | 第一次挥手
| |
(FIN_WAIT_1) (CLOSE_WAIT)
| |
| <-------- ACK, ack=u+1 -------------- | 第二次挥手
| |
(FIN_WAIT_2) |
| [服务端可能还有数据要发送] |
| |
| <-------- FIN, seq=v ---------------- | 第三次挥手
| |
(TIME_WAIT) (LAST_ACK)
| |
| -------- ACK, ack=v+1 --------------> | 第四次挥手
| |
| (CLOSED)
|
| [等待 2MSL]
|
(CLOSED)四次挥手的详细步骤
第二次挥手:服务端收到 FIN 后,发送 ACK 确认。发送后进入 CLOSE_WAIT 状态。客户端收到 ACK 后进入 FIN_WAIT_2 状态。
此时客户端到服务端方向的连接已关闭,但服务端可能还有数据需要发送给客户端。服务端会继续发送剩余数据。
第三次挥手:服务端发送完所有数据后,发送 FIN 报文,表示「我也没有数据了」。发送后进入 LAST_ACK 状态。
第四次挥手:客户端收到 FIN 后,发送 ACK 确认,然后进入 TIME_WAIT 状态。服务端收到 ACK 后进入 CLOSED 状态。客户端等待 2MSL 后也进入 CLOSED 状态。
为什么是四次,不是三次
TCP 是全双工协议,数据可以在两个方向同时传输。关闭连接时需要分别关闭两个方向:
- 客户端发 FIN:关闭「客户端→服务端」方向
- 服务端发 FIN:关闭「服务端→客户端」方向
服务端收到客户端的 FIN 后,可能还有数据没发完,所以只能先回 ACK,等数据发完再发 FIN。因此不能像握手那样把 SYN 和 ACK 合并。
可以变成三次吗
可以。如果服务端收到 FIN 时恰好没有数据要发,可以把 ACK 和 FIN 合并成一个报文发送。这种情况下就变成了「三次挥手」。
四、TIME_WAIT 状态
基本概念
TIME_WAIT 是主动关闭方在发送最后一个 ACK 后进入的状态。这个状态需要持续 2MSL(Maximum Segment Lifetime,报文最大生存时间),Linux 默认 MSL 为 30 秒,所以 TIME_WAIT 持续 60 秒。
为什么需要 TIME_WAIT
原因一:确保最后的 ACK 能够到达
如果客户端发送的最后一个 ACK 丢失了,服务端会重传 FIN。客户端需要保持 TIME_WAIT 状态才能接收到这个重传的 FIN,然后再次发送 ACK。
如果客户端直接进入 CLOSED 状态,收到重传的 FIN 后会回复 RST,导致服务端误以为发生了错误。
正常情况:
客户端(TIME_WAIT) ----ACK----> 服务端 -----> 正常关闭
ACK 丢失时:
客户端(TIME_WAIT) <----FIN---- 服务端(重传)
客户端(TIME_WAIT) ----ACK----> 服务端 -----> 正常关闭原因二:让旧连接的报文从网络中消失
假设客户端和服务端之间的连接使用了四元组 (192.168.1.1:5000, 220.181.38.148:80)。关闭连接后,如果立即复用这个四元组建立新连接,旧连接中延迟到达的数据包可能会被错认为新连接的数据,造成数据错乱。
等待 2MSL 可以确保旧连接的所有报文都已经从网络中消失。
TIME_WAIT 过多的问题
在高并发短连接场景下,如果服务端主动关闭连接,会积累大量 TIME_WAIT 状态的连接,占用内存和端口资源。
从工程角度看,出现大量 TIME_WAIT 往往说明:
- 系统里短连接很多
- 连接复用做得不够
- 本机经常是主动关闭方
也就是说,它不一定意味着程序报错,但通常意味着:
连接建得太频繁、关得太快。
常见场景包括:
- HTTP Keep-Alive 没生效
- 连接池没有正确复用
- 代理层和上游之间频繁新建连接
- 服务端 keep-alive 超时过短,主动关闭大量连接
- 本机大量作为客户端访问上游服务,并在请求结束后主动断开
排查命令:
# 查看 TIME_WAIT 连接数量
netstat -an | grep TIME_WAIT | wc -l
# 或使用 ss 命令(更快)
ss -s常见优化方案:
| 方案 | 说明 |
|---|---|
| 使用长连接(Keep-Alive) | 复用连接,减少创建销毁次数 |
| 让客户端主动关闭 | TIME_WAIT 转移到客户端 |
| 开启 tcp_tw_reuse | 允许复用 TIME_WAIT 连接(仅对发起方有效) |
注意:tcp_tw_recycle 已在 Linux 4.12 中移除,不要使用。它在 NAT 环境下会导致连接失败。
五、CLOSE_WAIT 状态
CLOSE_WAIT 是被动关闭方在收到 FIN 并发送 ACK 后进入的状态。正常情况下,应用程序应该很快调用 close() 发送 FIN,然后进入 LAST_ACK 状态。
CLOSE_WAIT 堆积的问题
CLOSE_WAIT 表示:
对方已经发来 FIN,本端内核也回了 ACK,但应用程序还没有把连接真正关闭。
所以如果服务端出现大量 CLOSE_WAIT 连接,通常意味着程序有 bug:收到对方的关闭请求后,没有及时调用 close() 关闭连接。
常见原因:
- 代码中忘记调用
close() - 程序阻塞在某个操作上,无法执行到
close() - 连接资源泄漏
- 异常分支里提前返回,没有走到 finally / defer / try-with-resources
- 响应体、socket、数据库连接、下游 client 对象没有正确释放
这也是为什么说:
大量 CLOSE_WAIT 往往更像代码层面的问题,而不是网络层面的问题。
排查方法:
# 查找 CLOSE_WAIT 连接对应的进程
lsof -i -n | grep CLOSE_WAIT六、状态转换完整图
+---------+
| CLOSED |
+---------+
|
主动打开 | 被动打开
v
+---------+ +---------+
|SYN_SENT | | LISTEN |
+---------+ +---------+
| |
收到 SYN+ACK | | 收到 SYN
v v
+----------------------+
| ESTABLISHED |
+----------------------+
|
主动关闭 | 被动关闭
v
+-----------+ +------------+
|FIN_WAIT_1 | | CLOSE_WAIT |
+-----------+ +------------+
| |
收到 ACK | | 发送 FIN
v v
+-----------+ +------------+
|FIN_WAIT_2 | | LAST_ACK |
+-----------+ +------------+
| |
收到 FIN | | 收到 ACK
v v
+-----------+ +-----------+
| TIME_WAIT | | CLOSED |
+-----------+ +-----------+
|
等待 2MSL
v
+-----------+
| CLOSED |
+-----------+七、常见面试题
Q1: 为什么 TCP 连接需要三次握手?
三次握手的目的是:同步双方的初始序列号、确认双方的收发能力正常、防止历史连接的初始化。两次握手无法防止失效的连接请求报文到达服务端造成错误。
Q2: 第三次握手失败会发生什么?
服务端会重传 SYN+ACK,默认重传次数由 tcp_synack_retries 参数控制。如果一直收不到 ACK,最终服务端会超时关闭这个半连接。
Q3: 什么是 SYN Flood 攻击?
攻击者伪造大量不存在的源 IP 发送 SYN 请求,服务端发出 SYN+ACK 后收不到 ACK 回复,大量半连接(SYN_RCVD 状态)堆积耗尽资源。
防御手段包括:SYN Cookie(不保存半连接状态)、增大半连接队列、减少 SYN+ACK 重传次数。
Q4: 为什么需要四次挥手?
TCP 是全双工的,每个方向需要单独关闭。一方发 FIN 表示「我不发了」,但对方可能还有数据要发,所以要分别关闭两个方向。
Q5: TIME_WAIT 为什么等待 2MSL?
一是确保最后的 ACK 到达对方(如果丢失,对方会重传 FIN,一来一回最多 2MSL);二是让本连接的所有报文从网络中消失,防止干扰复用相同四元组的新连接。
Q6: 服务端出现大量 CLOSE_WAIT 怎么办?
这通常是程序 bug,收到客户端的 FIN 后没有调用 close()。需要检查代码中是否有连接泄漏、异常分支未释放资源、线程阻塞导致关闭逻辑没执行到。
Q7: 为什么线上服务会出现大量 TIME_WAIT?
通常说明系统里存在大量短连接,并且本机经常作为主动关闭方。常见原因包括连接复用不足、Keep-Alive 配置不合理、连接池未生效,或者本机频繁作为客户端访问上游服务后主动断开。
八、实战命令
抓包观察三次握手
# 开启抓包(监听 80 端口)
sudo tcpdump -i any port 80 -nn -S
# 另一个终端发起请求
curl http://example.com
# 在抓包输出中观察:
# Flags [S] - SYN(第一次握手)
# Flags [S.] - SYN+ACK(第二次握手)
# Flags [.] - ACK(第三次握手)查看连接状态
# 查看所有连接状态统计
ss -s
# 查看 TIME_WAIT 数量
ss -ant | grep TIME-WAIT | wc -l
# 查看 CLOSE_WAIT 数量
ss -ant | grep CLOSE-WAIT | wc -l
# 查看 TCP 相关参数
sysctl -a | grep tcp_tw九、小结
| 知识点 | 核心要点 |
|---|---|
| 三次握手 | 同步序列号 + 确认收发能力 + 防止历史连接 |
| 四次挥手 | 全双工需要分别关闭两个方向 |
| TIME_WAIT | 主动关闭方进入,等 2MSL,确保 ACK 到达 + 旧报文消失 |
| CLOSE_WAIT | 被动关闭方进入,堆积说明代码未调用 close() |
