连接管理
- 连接管理
-
- [TCP 标志位:控制报文类型的 6 个比特](#TCP 标志位:控制报文类型的 6 个比特)
- 三次握手:建立连接
- 四次挥手:断开连接
-
- 四次挥手流程
- 状态流转说明
- [为什么需要 TIME_WAIT?⭐](#为什么需要 TIME_WAIT?⭐)
-
- [原因一:确保最后的 ACK 能到达对方](#原因一:确保最后的 ACK 能到达对方)
- 原因二:防止旧连接的数据包干扰新连接
- 为什么四次挥手不能像三次握手那样合并?⭐
- [TCP 状态转换图](#TCP 状态转换图)
- [Socket 编程与 TCP 状态的对应关系⭐](#Socket 编程与 TCP 状态的对应关系⭐)
连接管理
TCP 标志位:控制报文类型的 6 个比特
TCP 报头中有一个字节的标志位字段,其中 6 个标志位最为常用:
| 标志位 | 含义 | 作用 |
|---|---|---|
| SYN | Synchronize | 同步序号,用于建立连接 |
| ACK | Acknowledgment | 确认序号有效,表示这是一个确认报文 |
| FIN | Finish | 结束连接,用于断开连接 |
| RST | Reset | 强制重置连接 |
| PSH | Push | 通知接收方立即将数据交给应用层 |
| URG | Urgent | 紧急指针有效(很少使用) |
关键 :SYN 和 ACK 本身不携带载荷(即没有应用层数据),它们只用于控制连接的建立与确认。
三次握手:建立连接
为什么需要三次握手?
三次握手主要达成三个目标:
- 投石问路:确认通信路径上是否畅通(双方都能发送和接收);
- 验证能力:确认双方的发送能力和接收能力都正常;
- 协商关键数据:比如初始序号(ISN)、最大报文段长度(MSS)等。
其中初始序号的协商尤其重要------双方各自随机生成一个起始序号,避免历史连接的数据包干扰新连接。
三次握手流程⭐
客户端主动发起连接,服务器被动监听。
服务器 客户端 服务器 客户端 CLOSED LISTEN 收到 SYN,进入 SYN_RCVD 收到 SYN+ACK,进入 ESTABLISHED 收到 ACK,进入 ESTABLISHED 第一次握手:SYN (seq=x) 客户端进入 SYN_SENT 第二次握手:SYN+ACK (seq=y, ack=x+1) 第三次握手:ACK (seq=x+1, ack=y+1)
- 第一次握手 :客户端发送 SYN 段,序号
seq=x(x 为客户端随机生成的初始序号)。客户端进入SYN_SENT状态。 - 第二次握手 :服务器收到 SYN 后,回复 SYN+ACK 段。其中服务器自己的序号
seq=y(y 为服务器随机生成的初始序号),确认序号ack=x+1(表示已收到客户端的序号 x,期望下次收到 x+1)。服务器进入SYN_RCVD状态。 - 第三次握手 :客户端收到 SYN+ACK 后,回复 ACK 段,
seq=x+1,ack=y+1。客户端进入ESTABLISHED状态。服务器收到 ACK 后也进入ESTABLISHED状态。
注意:SYN 和 SYN+ACK 都不携带数据,但会消耗一个序号;纯 ACK 如果不携带数据,则不消耗序号。
| 状态 | 出现位置 | 含义 |
|---|---|---|
CLOSED |
连接的最初 / 完全关闭后 | 没有活动连接,或连接已彻底释放 |
LISTEN |
服务器调用 listen() 后 |
服务器正在监听端口,等待客户端发起连接 |
SYN_SENT |
客户端发送第一次握手(SYN)后 | 客户端已发出连接请求,等待服务器回复 SYN+ACK |
SYN_RCVD |
服务器收到 SYN 并回复 SYN+ACK 后 | 半连接状态,不分配大资源。服务器已收到客户端的 SYN,等待客户端的最终 ACK |
ESTABLISHED |
第三次握手(ACK)到达对方后 | 连接已成功建立,双方可以传输数据 |
补充说明:
CLOSED既是初始状态,也是TIME_WAIT超时后的最终状态。- 客户端从
CLOSED→SYN_SENT→ESTABLISHED。- 服务器从
LISTEN→SYN_RCVD→ESTABLISHED。
两次握手与三次握手对比⭐
如果只有两次握手,服务器无法确认客户端是否收到了自己的 SYN+ACK。而且,网络中可能滞留有旧的 SYN 段,两次握手会导致服务器错误地建立连接,浪费资源。三次握手可以让客户端在第三次 ACK 中确认对方的能力,同时通过序号机制区分新旧连接。
服务器 客户端 服务器 客户端 两次握手模型(不可靠) 服务器立即进入 ESTABLISHED! 开始分配资源等数据 客户端收到后也进入 ESTABLISHED 但若客户端根本没收到 SYN+ACK 呢? 服务器已盲目消耗资源 更糟:旧 SYN 迟到 服务器以为是新请求, 立即回复 SYN+ACK 并进入 ESTABLISHED 白白浪费资源 资源已浪费 ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) 旧 SYN (seq=old) 延迟到达 收到不请自来的 SYN+ACK,发 RST 拒绝
服务器 客户端 服务器 客户端 三次握手模型(可靠) 只进入 SYN_RCVD(半连接) 不分配大资源 收到 ACK 才进入 ESTABLISHED 确认客户端有能力且需要连接 处理旧 SYN 收到 RST,关闭半连接,无大损失 ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ③ ACK (ack=y+1) 旧 SYN 延迟到达 回复 SYN+ACK,保持 SYN_RCVD 半连接 客户端发现不对,发 RST
第x次握手数据丢失的情况分析⭐
在 TCP 三次握手建立连接的过程中,无论哪一次握手报文丢失,TCP 协议栈都会通过超时重传机制来补救。但具体表现和处理细节因丢失的阶段不同而有所差异。
| 丢失阶段 | 触发重传的一端 | 关键机制 |
|---|---|---|
| 第一次握手(SYN) | 客户端 | 客户端 SYN 重传,直到超时或成功 |
服务器 客户端 服务器 客户端 主动打开,发送 SYN 服务器未收到,无感知 进入 SYN_SENT, 启动重传定时器 收到 SYN, 进入 SYN_RCVD loop [超时重传(默认重试 5~6 次)] 收到 SYN+ACK, 进入 ESTABLISHED 收到 ACK,进入 ESTABLISHED 连接建立成功 ① SYN (seq=x) ❌ 丢失 ② 重传 SYN (seq=x) ③ SYN+ACK (seq=y, ack=x+1) ④ ACK (ack=y+1)
| 丢失阶段 | 触发重传的一端 | 关键机制 |
|---|---|---|
| 第二次握手(SYN+ACK) | 客户端 + 服务器 | 客户端重传 SYN,服务器重传 SYN+ACK,两端独立超时 |
服务器 客户端 服务器 客户端 收到 SYN,进入 SYN_RCVD 启动 SYN+ACK 重传定时器 未收到 SYN+ACK 启动 SYN 重传定时器 服务器 SYN+ACK 重传定时器超时 客户端 SYN 重传定时器超时 par [两端独立超时重传] 收到重传 SYN,识别为重复 立即回复 SYN+ACK 收到 SYN+ACK,进入 ESTABLISHED 收到 ACK,进入 ESTABLISHED 连接建立成功 ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ❌ 丢失 ③ 重传 SYN+ACK ❌ 仍丢失 ④ 重传 SYN (seq=x) ⑤ SYN+ACK (seq=y, ack=x+1) ⑥ ACK (ack=y+1)
| 丢失阶段 | 触发重传的一端 | 关键机制 |
|---|---|---|
| 第三次握手(ACK) | 服务器 | 服务器重传 SYN+ACK,客户端重传 ACK;数据报文也可能直接完成握手 |
服务器 客户端 服务器 客户端 收到 SYN+ACK,进入 ESTABLISHED 未收到 ACK,仍为 SYN_RCVD 启动 SYN+ACK 重传定时器 收到重复 SYN+ACK, 识别为重复,立即重传 ACK 收到 ACK,进入 ESTABLISHED 丢失 ACK 后立即发送 HTTP 请求等数据 检查 ack=y+1,确认客户端 已收到自己的 SYN 直接从 SYN_RCVD 进入 ESTABLISHED 并处理数据 alt [情况一:无数据发送(纯 ACK 丢失)] [情况二:客户端紧接着发送数据(捎带应答)] 连接建立成功 ① SYN (seq=x) ② SYN+ACK (seq=y, ack=x+1) ③ ACK (ack=y+1) ❌ 丢失 ④ 超时后重传 SYN+ACK ⑤ ACK (ack=y+1) ⑥ 数据包 (PSH, ACK, seq=x+1, ack=y+1) 内容:GET /index.html
四次挥手:断开连接
四次挥手流程
TCP 连接是全双工的,每个方向的数据传输可以独立关闭。四次挥手就是关闭一个方向上传输的完整过程,需要四个报文段。
被动关闭方(服务器) 主动关闭方(客户端) 被动关闭方(服务器) 主动关闭方(客户端) 初始状态:ESTABLISHED 进入 FIN_WAIT_1 进入 CLOSE_WAIT 收到 ACK,进入 FIN_WAIT_2 应用层读取剩余数据后 调用 close(),发送 FIN 进入 LAST_ACK 进入 TIME_WAIT 收到 ACK,进入 CLOSED 等待 2MSL 后,进入 CLOSED ① FIN (seq=u) ② ACK (ack=u+1) ③ FIN (seq=v, ack=u+1) ④ ACK (ack=v+1)
状态流转说明
为了更好地理解双方在整个挥手过程中的状态变化,下面给出一个状态流转图(以主动关闭方和被动关闭方为两条线)。
主动关闭方
被动关闭方
发送 FIN
收到 ACK
收到 FIN
发送 ACK
等待 2MSL 超时
收到 FIN
发送 ACK
应用调用 close()
发送 FIN
收到 ACK
ESTABLISHED
FIN_WAIT_1
FIN_WAIT_2
TIME_WAIT
CLOSED
CLOSE_WAIT
LAST_ACK
注意:
CLOSE_WAIT状态会持续到应用程序主动调用close(),如果应用不关闭,连接会长期滞留在此状态,导致资源泄漏。
| 状态 | 出现方 | 含义 |
|---|---|---|
FIN_WAIT_1 |
主动方 | 已发送 FIN,等待对方的 ACK |
FIN_WAIT_2 |
主动方 | 已收到 ACK,等待对方发送 FIN |
CLOSE_WAIT |
被动方 | 已收到 FIN 并回复 ACK,等待本地应用关闭 |
LAST_ACK |
被动方 | 已发送 FIN,等待对方的最终 ACK |
TIME_WAIT |
主动方 | 已发送最后 ACK,等待 2MSL 确保对方收到 |
CLOSED |
双方 | 连接已彻底释放 |
为什么需要 TIME_WAIT?⭐
主动关闭方在发送最后一个 ACK 后,必须等待 2MSL (Maximum Segment Lifetime,报文最大生存时间,典型值 30 秒~2 分钟)才能进入 CLOSED。原因有两点:
原因一:确保最后的 ACK 能到达对方
网络不可靠,最后一个 ACK 可能丢失。如果主动方直接关闭,被动方会因未收到 ACK 而不断重传 FIN。此时主动方的端口可能已被新连接占用,导致新的连接收到一个意外的 FIN,发生错乱。
在 TIME_WAIT 状态下,主动方可以:
- 收到重传的 FIN 后,重新发送 ACK;
- 保证被动方能正常进入
CLOSED。
下图展示了最后一个 ACK 丢失时的处理流程:
被动关闭方 主动关闭方 被动关闭方 主动关闭方 进入 TIME_WAIT,启动 2MSL 定时器 未收到 ACK,超时重传 FIN 仍在 TIME_WAIT, 收到重传 FIN 后重新发送 ACK 收到 ACK,进入 CLOSED 重新启动 2MSL 定时器(部分实现) FIN ACK FIN ACK 丢失 ❌ 重传 FIN ACK (ack=v+1)
原因二:防止旧连接的数据包干扰新连接
等待 2MSL 的时间,足以让网络中所有属于本次连接的残留报文(包括重传的 FIN、延迟到达的数据段等)全部消失。这样当同一个端口对(IP + 端口)被新连接复用时,就不会收到上一个连接的幽灵报文,保证新连接的 TCP 序列号空间干净。
为什么四次挥手不能像三次握手那样合并?⭐
在三次握手中,服务器可以把 SYN 和 ACK 合并在一个报文里(SYN+ACK),因为它们都由内核在收到 SYN 时立刻产生,时机完全同步。
但在四次挥手中:
- 第二次挥手(ACK):由内核 在收到 FIN 时立刻自动回复;
- 第三次挥手(FIN):需要等到应用层调用
close()才会触发。
这两个动作之间存在用户态的时间延迟(例如应用还需读取剩余数据、清理资源等),因此 ACK 和 FIN 通常是分开发送的,形成四次交互。
主动方 被动方内核 被动方应用 主动方 被动方内核 被动方应用 内核立即回复 ACK 通知应用有 FIN 到达 内核发送 FIN FIN ACK 读取剩余数据(如有) 应用调用 close() FIN ACK
当然,如果应用层恰好在收到 FIN 的那一刻也正要关闭连接(或根本没有数据要处理),内核在收到 FIN 后可以将 ACK 与 FIN 合并 成一个报文段(即 FIN+ACK),这被称为同时关闭优化。但这并非常态,所以普遍情况下依然是四次挥手。
主动方 被动方 主动方 被动方 应用恰好在此时也准备关闭 内核合并 ACK 和 FIN 进入 FIN_WAIT_1 / FIN_WAIT_2 等后续状态 FIN FIN+ACK (seq=y, ack=x+1)
TCP 状态转换图
下面用 Mermaid 状态图展示客户端和服务器在连接建立与断开过程中的状态迁移。
服务器调用 listen
客户端调用 connect
收到 SYN
收到 SYN+ACK
收到 ACK
主动关闭,发送 FIN
被动关闭,收到 FIN
收到 ACK
收到 FIN(同时关闭)
收到 FIN
应用程序调用 close,发送 FIN
收到 ACK
等待 2MSL 超时
收到 ACK
CLOSED
LISTEN
SYN_SENT
SYN_RCVD
ESTABLISHED
FIN_WAIT_1
CLOSE_WAIT
FIN_WAIT_2
CLOSING
TIME_WAIT
LAST_ACK
Socket 编程与 TCP 状态的对应关系⭐
| 应用操作 | TCP 发生事件 | 状态变化(客户端) | 状态变化(服务器) |
|---|---|---|---|
服务器 new ServerSocket(port) |
socket(), bind(), listen() |
- | CLOSED → LISTEN |
客户端 socket.connect() |
发送 SYN | CLOSED → SYN_SENT |
收到 SYN,回复 SYN+ACK,连接进入半连接队列 |
服务器 accept() 返回 |
三次握手完成,连接移到全连接队列 | SYN_SENT → ESTABLISHED(内核自动完成) |
连接在 TCP 层已为 ESTABLISHED,现在应用获取到它 |
客户端 close() |
发送 FIN | ESTABLISHED → FIN_WAIT_1 收到 ACK → FIN_WAIT_2 |
收到 FIN,回复 ACK → CLOSE_WAIT |
服务器读操作返回 EOF (如 scanner.hasNext()==false) |
应用层读取到 FIN 指示 | (客户端已关闭发送方向) | 仍为 CLOSE_WAIT,等待应用关闭 |
服务器 close() |
发送 FIN | 收到 FIN,回复 ACK → TIME_WAIT |
CLOSE_WAIT → LAST_ACK → 收到 ACK → CLOSED |
服务器端的典型流程:
服务器应用 服务器TCP 客户端TCP 客户端应用 服务器应用 服务器TCP 客户端TCP 客户端应用 CLOSED 进入 LISTEN 进入 SYN_SENT 进入 ESTABLISHED 返回已建立的连接 ESTABLISHED 双方数据通信(ESTABLISHED) 进入 FIN_WAIT_1 进入 CLOSE_WAIT 收到 ACK → FIN_WAIT_2 进入 LAST_ACK 进入 TIME_WAIT 收到 ACK → CLOSED 等待 2MSL → CLOSED new ServerSocket(port) / listen() socket.connect() ① SYN ② SYN+ACK ③ ACK accept() close() ① FIN ② ACK 读取到 EOF(scanner.hasNext()==false) close() ③ FIN ④ ACK