Appearance
TCP 连接管理:三次握手与四次挥手
概念
连接管理(connection management)指 TCP 在传数据之前先"商定一条连接"、传完之后再"拆掉这条连接"的全过程——建立连接用三次握手(three-way handshake),释放连接用四次挥手(four-way handshake)或叫四次握手。
★ 关键认识:TCP 的"连接"不是一根线,而是两端各存的一份状态。 所谓"建立了连接",指的是双方的套接字从 CLOSED 走进了 ESTABLISHED,各自记下了对方的 IP+端口、自己的初始序号、接收窗口与一堆计时器。 中间的路由器对此一无所知——它们只转发一个个独立的 IP 数据报,不做任何"连接"的登记。 这条认识直接决定了后面所有结论:连接可以"半开"、可以"复位"、可以被一个迟到的旧 SYN 骗出来。
★ 本章要解决的四件事:
| 问题 | 对应内容 |
|---|---|
| 怎么建 | 三次握手:SYN → SYN+ACK → ACK,两个 SYN 各占 1 个序号 |
| 为什么是三次 | 让服务端确认"客户端收到了我的 SYN+ACK";两次握手挡不住失效的旧 SYN |
| 怎么拆 | 四次挥手:半关闭使 ACK 与 FIN 分开;主动关闭方进 TIME_WAIT 等 2 MSL |
| 出事了怎么办 | SYN 洪泛 → SYN Cookie;对端消失 → RST 复位 |
与上一章的衔接:上一章(net/41-tcp.md)已经定死两条规矩——"序号是字节编号"、"SYN 与 FIN 各占 1 个序号"——本章就是拿这两条规矩,把握手与挥手的每个报文的 seq / ack 一步步算出来。 上一章"只有第一个 SYN 报文段的 ACK 位是 0、之后一律为 1"这个指纹,也正是本章识别"这是第几次握手"的依据。
原理
一、三次握手:三个报文与两次"占格"
★ 完整时序(本章锚点:客户端 ISN = 1000、服务端 ISN = 2000):
text
客户端 服务端
│ │ LISTEN
│ ① SYN, seq = 1000 ACK=0 SYN=1 FIN=0 │
├────────────────────────────────────────────────►│ SYN_RCVD
│ ★ SYN 占掉序号 1000 │
│ │
│ ② SYN+ACK, seq = 2000, ack = 1001 │
│◄────────────────────────────────────────────────┤
│ ★ SYN 占掉序号 2000; ack = 1001 表示 │
│ "1000 及之前的字节我都收到了" │
│ │
│ ③ ACK, seq = 1001, ack = 2001 │
├────────────────────────────────────────────────►│ ESTABLISHED
│ ★ 这一次不带 SYN/FIN, 纯粹是"我收到了" │
│ (可以捎带第 1 个数据字节) │
│ │
│ ====== 双向数据传输, 客户端序号从 1001 起, │
│ 服务端序号从 2001 起 ====== │★ 三次握手的三个报文逐项拆解(必背):
| 次序 | 方向 | seq | ack | 标志位 | 作用 |
|---|---|---|---|---|---|
| 1st | 客户端 → 服务端 | 1000(ISN) | 0 | SYN=1, ACK=0 | 请求建连 + 同步客户端初始序号 |
| 2nd | 服务端 → 客户端 | 2000(ISN) | 1001 | SYN=1, ACK=1 | 同意建连 + 同步服务端初始序号 + 确认客户端 SYN |
| 3rd | 客户端 → 服务端 | 1001 | 2001 | SYN=0, ACK=1 | 确认服务端的 SYN |
★ 由"SYN 占 1 个序号"直接推出的两条结论:
| 结论 | 推导 |
|---|---|
| 第 2 个报文的 ack = 1001 | 客户端的 SYN 序号是 1000,SYN 占掉这一格 → 下一个期望字节是 |
| 双方数据起点不同 | 客户端数据从 1001 起、服务端数据从 2001 起——两个方向各有独立序号空间 |
⚠️ 为什么要两次"占格":SYN 本身不携带数据,但必须"被确认"——如果 SYN 不占序号,服务端就无法用确认号表达"你的 SYN 我收到了"(确认号只能表达"下一个期望的字节编号")。 FIN 同理:所以
net/41-tcp.md那条"SYN 与 FIN 各占 1 个序号"不是拍脑袋的规定,而是"让控制位也能被累积确认"的必然设计。
★ 建连的时延账:第 3 个报文的 ACK 可以捎带第 1 个数据字节——所以"从发出 SYN 到第一个数据字节上路"只花 1 个 RTT。 这也是"HTTP 每次请求多一个 RTT"的根源(见 net/51-http.md)。
二、为什么是三次而不是两次
★ 这个问题的标准答案只有一条:防"已失效的连接请求报文段"。
★ 两次握手会出的事(设只用"客户端 SYN + 服务端 SYN+ACK"两次就建连):
text
★ 场景: 客户端的一个旧 SYN 在网络里滞留了很久, 早已"失效"
t0 客户端发 SYN#1 (seq = 1000), 网络拥塞, 报文被卡住
t1 客户端等不到答复, 超时重发 SYN#2 (seq = 3000), 正常建连、传完、关闭
t2 卡住的 SYN#1 终于到了服务端
★ 两次握手: 服务端直接认为自己与该客户端建立了连接
-> 分配缓冲区、进入 ESTABLISHED, 一直等客户端发数据
★ 而客户端早已关闭, 根本不理它 -> 服务端这条连接是"半开"的
-> 白占资源; 这样的旧 SYN 一多, 服务端资源被耗光
t3 ★ 三次握手: 服务端发出 SYN+ACK 后停在 SYN_RCVD
-> 客户端(已关闭)不会回第 3 个 ACK
-> 服务端超时后把这个半成品连接丢掉, 资源不受损★ 换一个角度理解第 3 次握手的作用:
| 报文 | 让谁"安心"了 | 说明 |
|---|---|---|
| 1st(SYN) | 服务端:知道"有客户端想连我",也知道客户端的发送能力正常 | 服务端还不知道自己的报文能不能到对方 |
| 2nd(SYN+ACK) | 客户端:知道服务端的收发能力都正常,自己的发送能力也被验证 | 客户端已可确认"双向都通" |
| 3rd(ACK) | 服务端:知道客户端的接收能力正常、自己的发送能力被验证 | ★ 第三次的真正价值:服务端第一次确认"对方活着且收到了我的 SYN+ACK" |
⚠️ 注意两道题的答法不同:问"为什么不能两次"→ 答"防失效的旧 SYN 浪费服务端资源";问"第 3 次握手的作用是什么"→ 答"让服务端确认客户端的接收能力正常 / 确认对方收到了自己的 SYN+ACK"。 考试按问法给分,两条都要背上。
⚠️ 别把"三次"说成"一方发了三次":三次握手是"三个报文段"——第 1、3 个由客户端发,第 2 个由服务端发;第 2 个报文同时带 SYN 和 ACK(所以叫 SYN+ACK),它一个报文干两件事,这也是"最少三次"的原因。
三、四次挥手:半关闭把 ACK 与 FIN 拆开
★ 完整时序(承接上面的锚点:客户端数据到 1200、服务端数据到 3000):
text
客户端 服务端
│ ④ FIN, seq = 1201, ack = 3001 ACK=1 FIN=1 │
├────────────────────────────────────────────────►│ CLOSE_WAIT
│ ★ FIN 占掉序号 1201 -> 客户端下一个序号 1202│
│ │ (服务端处理剩余数据)
│ ⑤ ACK, seq = 3001, ack = 1202 │
│◄────────────────────────────────────────────────┤
│ ★ 客户端 -> FIN_WAIT_2, 不再发数据, │
│ 但"还能收数据" = 半关闭 (half-close) │
│ │
│ ⑥ FIN, seq = 3001, ack = 1202 │
│◄────────────────────────────────────────────────┤ LAST_ACK
│ ★ FIN 占掉序号 3001 -> 服务端下一个序号 3002│
│ │
│ ⑦ ACK, seq = 1202, ack = 3002 │
├────────────────────────────────────────────────►│ CLOSED
│ │
│ ★ 客户端进 TIME_WAIT, 等 2 MSL = 60 s 后 │
│ 才真正 CLOSED │★ 四个报文逐项拆解(必背):
| 次序 | 方向 | seq | ack | 标志位 | 含义 |
|---|---|---|---|---|---|
| 4th(第 1 次挥手) | 主动方 → 被动方 | 1201 | 3001 | FIN=1, ACK=1 | "我没数据要发了"(FIN 占 1201) |
| 5th(第 2 次) | 被动方 → 主动方 | 3001 | 1202 | ACK=1 | "知道了"(不等于"我也没数据了") |
| 6th(第 3 次) | 被动方 → 主动方 | 3001 | 1202 | FIN=1, ACK=1 | "我也发完了"(FIN 占 3001) |
| 7th(第 4 次) | 主动方 → 被动方 | 1202 | 3002 | ACK=1 | "知道了",此后主动方进 TIME_WAIT |
★ 为什么挥手要四次,握手却只要三次:
| 对比项 | 三次握手 | 四次挥手 |
|---|---|---|
| 能不能合并 | 服务端的 SYN 与 ACK 天然可以合并(同意建连的同时确认对方) | ACK 与 FIN 通常不能合并 |
| 原因 | 建连时服务端没有"未完成的事" | ★ 半关闭:收到 FIN 只说明对方不发数据了,服务端可能还有数据没发完——所以先回 ACK("知道了"),等自己发完再发 FIN("我也发完了") |
| 结果 | 3 个报文 | 4 个报文 |
⚠️ "四次挥手一定四个报文吗":不一定。 若服务端收到 FIN 时恰好也没有数据要发,它可以把 ACK 与 FIN 合并成一个报文——此时全过程只需 3 个报文。 但考试默认按 4 个报文的通用情形作答,并把"半关闭"作为"为什么要四次"的理由。
⚠️ TCP 没有"最后一次挥手"的确认确认:第 7 个 ACK 之后再没有报文了——所以主动方无法知道这个 ACK 有没有丢。 这正是 TIME_WAIT 存在的第一条理由(见下一节)。
四、状态迁移与 TIME_WAIT
★ 两台状态机(11 个状态,必背"两个独有状态"):
| 状态 | 属于 | 说明 |
|---|---|---|
| CLOSED | 双方 | 初始状态 / 终止状态 |
| LISTEN | 服务端独有 | 监听端口,等待 SYN |
| SYN_SENT | 客户端独有 | 已发 SYN,等待对方 SYN+ACK |
| SYN_RCVD | 服务端 | 收到 SYN 并回了 SYN+ACK,等待第 3 个 ACK |
| ESTABLISHED | 双方 | 连接已建立,可以双向传数据 |
| FIN_WAIT_1 | 主动关闭方 | 已发 FIN,等对方的 ACK |
| FIN_WAIT_2 | 主动关闭方 | 已收到对方的 ACK,等对方的 FIN(半关闭) |
| CLOSE_WAIT | 被动关闭方 | 已收到 FIN 并回了 ACK,等本进程"发完数据后关闭" |
| LAST_ACK | 被动关闭方 | 已发 FIN,等最后一个 ACK |
| TIME_WAIT | 主动关闭方独有 | 已发最后一个 ACK,等 2 MSL |
| CLOSING | 双方(罕见) | 双方同时发 FIN 时的中间态(几乎不考) |
★ 两条状态路径(对照着背):
text
客户端(主动关闭): CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1
-> FIN_WAIT_2 -> TIME_WAIT -> (2 MSL) -> CLOSED
服务端(被动关闭): LISTEN -> SYN_RCVD -> ESTABLISHED -> CLOSE_WAIT
-> LAST_ACK -> CLOSED★ TIME_WAIT 的两条理由(必背,两条都要写):
| 理由 | 说的是什么 |
|---|---|
| ① 保证最后一个 ACK 能到达 | 若第 7 个 ACK 丢了,服务端会重传 FIN;此时主动方还在 TIME_WAIT,可以重发 ACK 并重新计时——若直接进 CLOSED,收到重传的 FIN 就只能回 RST,服务端会得到一个错误 |
| ② 让本次连接的旧报文段"自然消亡" | 主动方等 2 MSL 后才允许同号端口建立新连接——若不等,旧连接的迟到报文可能被新连接收下,污染数据 |
★ 2 MSL = 60 s 的算法:MSL(Maximum Segment Lifetime,报文段最大生存时间)常取 30 s——2 MSL = 2 × 30 s = 60 s。
⚠️ TIME_WAIT 带来的现实问题:只有主动关闭方进 TIME_WAIT——所以"服务器主动关连接"会让服务器积累大量 TIME_WAIT,占用端口与内存。 Web 服务器通常让服务端主动关闭(服务端知道什么时候发完),代价就是 TIME_WAIT 堆积——工程上有一堆解法(缩短 2 MSL、
SO_REUSEADDR、改用长连接),但都不得改变考试口径。
⚠️ MSL 与 RTT 是两个不同的量:MSL 是"报文在网络里最长能活多久"(上限,秒级);RTT 是"一来一回实际花多久"(毫秒级)。 别把"2 MSL 等待"算成"2 个 RTT"。
五、异常与安全:RST 与 SYN 洪泛
★ RST(reset,复位)是 TCP 的"紧急刹车",三种常见场合:
| 场合 | 现象 |
|---|---|
| 连接已不存在,却收到数据 | 对端崩溃重启后收到旧连接的数据 → 回 RST |
| 向未监听的端口发起连接 | 服务端回 RST(而不是静静地不回),客户端立刻得到"连接被拒绝" |
| 半开连接检测 | 一方发现对方已消失 → 发 RST 强制释放本地资源 |
★ SYN 洪泛(SYN flood)的原理与防线:
text
★ 攻击: 攻击者伪造大量源 IP, 只发第 1 次握手 (SYN), 不回第 3 次 ACK
-> 服务端为每个半成品分配资源、进 SYN_RCVD
-> 半连接队列(如 256 个)被占满
-> 正常用户发来的 SYN 直接被丢弃 -> 拒绝服务
★ 账: 若队列 256 个、攻击速率 1000 个 SYN/s
256 / 1000 = 0.256 s 就被占满
而正常情况下 一个 SYN_RCVD 要等 1 s 左右才超时回收
-> 队列"进得比出得快" -> 稳态就是满
★ 防线 SYN Cookie: 服务端收到 SYN 时不分配任何资源,
而是把连接信息(源 IP/端口、序号、时间) 编码进"自己的初始序号"里
-> 真正的资源要等第 3 个 ACK 到达、Cookie 校验通过后才分配示例
例 1:C 实现——握手与挥手的序号推算、状态、计时
参数:客户端 ISN = 1000、服务端 ISN = 2000;客户端发 200 B、服务端发 1000 B;MSL = 30 s。
#include <stdio.h>
/* 打印一个报文段: 次序 / 方向 / 序号 / 确认号 / 三个标志位 */
static void seg(const char *no, const char *dir, unsigned seq, unsigned ack,
int af, int sf, int ff) {
printf(" %-6s %-6s seq=%-6u ack=%-6u ACK=%d SYN=%d FIN=%d\n",
no, dir, seq, ack, af, sf, ff);
}
int main(void) {
const unsigned C_ISN = 1000, S_ISN = 2000;
printf("(1) 三次握手 (客户端 ISN = %u, 服务端 ISN = %u)\n", C_ISN, S_ISN);
seg("1st", "C->S", C_ISN, 0, 0, 1, 0);
printf(" ★ SYN 占掉序号 %u -> 客户端数据从 %u 起\n", C_ISN, C_ISN + 1);
seg("2nd", "S->C", S_ISN, C_ISN + 1, 1, 1, 0);
printf(" ★ SYN 占掉序号 %u -> 服务端数据从 %u 起\n", S_ISN, S_ISN + 1);
seg("3rd", "C->S", C_ISN + 1, S_ISN + 1, 1, 0, 0);
printf(" 连接建立; 耗时 1 个 RTT (第 3 次的 ACK 可捎带数据)\n");
printf("\n");
printf("(2) 数据传输与四次挥手\n");
unsigned cseq = C_ISN + 1, sseq = S_ISN + 1; /* 两个方向的数据起点 */
printf(" 客户端发 200 B: 占用 [%u, %u], 下一序号 %u\n", cseq, cseq + 199, cseq + 200);
cseq += 200;
printf(" 服务端发 1000 B: 占用 [%u, %u], 下一序号 %u\n", sseq, sseq + 999, sseq + 1000);
sseq += 1000;
printf("\n");
seg("4th", "C->S", cseq, sseq, 1, 0, 1);
printf(" ★ FIN 占掉序号 %u -> 客户端下一序号 %u\n", cseq, cseq + 1);
cseq += 1;
seg("5th", "S->C", sseq, cseq, 1, 0, 0);
seg("6th", "S->C", sseq, cseq, 1, 0, 1);
printf(" ★ FIN 占掉序号 %u -> 服务端下一序号 %u\n", sseq, sseq + 1);
sseq += 1;
seg("7th", "C->S", cseq, sseq, 1, 0, 0);
printf(" ★ 服务端若无数据要发, 第 5/6 次可合并 (3 个报文完成关闭)\n");
printf("\n");
printf("(3) 状态迁移与计时\n");
printf(" 客户端: CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1\n");
printf(" -> FIN_WAIT_2 -> TIME_WAIT -> (2 MSL = %.0f s) -> CLOSED\n", 2 * 30.0);
printf(" 服务端: LISTEN -> SYN_RCVD -> ESTABLISHED -> CLOSE_WAIT\n");
printf(" -> LAST_ACK -> CLOSED\n");
printf(" ★ 只有主动关闭方进 TIME_WAIT; 2 x 30 s = 60 s\n");
printf("\n");
printf("(4) 为什么不能只握两次\n");
printf(" 失效的旧 SYN 迟到 -> 服务端误以为新连接 -> 单方面建立并占资源\n");
printf(" ★ 第 3 次握手的作用 = 让服务端确认客户端收到了自己的 SYN+ACK\n");
printf(" ★ SYN 洪泛: 只发第 1 次 -> 半连接队列占满 -> 正常用户连不上\n");
printf(" 防线: SYN Cookie (不分配资源, 把状态编码进序号)\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
(1) 三次握手 (客户端 ISN = 1000, 服务端 ISN = 2000)
1st C->S seq=1000 ack=0 ACK=0 SYN=1 FIN=0
★ SYN 占掉序号 1000 -> 客户端数据从 1001 起
2nd S->C seq=2000 ack=1001 ACK=1 SYN=1 FIN=0
★ SYN 占掉序号 2000 -> 服务端数据从 2001 起
3rd C->S seq=1001 ack=2001 ACK=1 SYN=0 FIN=0
连接建立; 耗时 1 个 RTT (第 3 次的 ACK 可捎带数据)
(2) 数据传输与四次挥手
客户端发 200 B: 占用 [1001, 1200], 下一序号 1201
服务端发 1000 B: 占用 [2001, 3000], 下一序号 3001
4th C->S seq=1201 ack=3001 ACK=1 SYN=0 FIN=1
★ FIN 占掉序号 1201 -> 客户端下一序号 1202
5th S->C seq=3001 ack=1202 ACK=1 SYN=0 FIN=0
6th S->C seq=3001 ack=1202 ACK=1 SYN=0 FIN=1
★ FIN 占掉序号 3001 -> 服务端下一序号 3002
7th C->S seq=1202 ack=3002 ACK=1 SYN=0 FIN=0
★ 服务端若无数据要发, 第 5/6 次可合并 (3 个报文完成关闭)
(3) 状态迁移与计时
客户端: CLOSED -> SYN_SENT -> ESTABLISHED -> FIN_WAIT_1
-> FIN_WAIT_2 -> TIME_WAIT -> (2 MSL = 60 s) -> CLOSED
服务端: LISTEN -> SYN_RCVD -> ESTABLISHED -> CLOSE_WAIT
-> LAST_ACK -> CLOSED
★ 只有主动关闭方进 TIME_WAIT; 2 x 30 s = 60 s
(4) 为什么不能只握两次
失效的旧 SYN 迟到 -> 服务端误以为新连接 -> 单方面建立并占资源
★ 第 3 次握手的作用 = 让服务端确认客户端收到了自己的 SYN+ACK
★ SYN 洪泛: 只发第 1 次 -> 半连接队列占满 -> 正常用户连不上
防线: SYN Cookie (不分配资源, 把状态编码进序号)⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
- 第 5 与第 6 个报文的 seq 都是 3001:因为服务端一个数据字节都没发——它的 FIN 就落在自己数据的起点上。 若服务端在两次挥手之间又发了 100 B,那么第 6 个报文的 seq 就是 3101。
ack与ACK是两个东西:ack是那个 32 位的数字;ACK是标志位,表示"确认号字段有效"。 三次握手之后每个报文段的 ACK 位都是 1——所以上面 4~7 号报文的 ACK 位全为 1。
例 2:Python——两台状态机、失效场景枚举与队列账
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
C_ISN, S_ISN = 1000, 2000
print('=== ① 三次握手的三个报文 ===')
rows = [('1st', 'C->S', C_ISN, 0, '0 1 0', '请求建连'),
('2nd', 'S->C', S_ISN, C_ISN + 1, '1 1 0', '同意 + 同步自己序号'),
('3rd', 'C->S', C_ISN + 1, S_ISN + 1, '1 0 0', '确认对方的 SYN')]
print(' ' + pad('次序', 6) + pad('方向', 8) + pad('seq', 10) + pad('ack', 10)
+ pad('A S F', 10) + '作用')
for no, d, sq, ak, fl, mean in rows:
print(' ' + pad(no, 6) + pad(d, 8) + pad(str(sq), 10) + pad(str(ak), 10)
+ pad(fl, 10) + mean)
print(' ★ A S F = ACK / SYN / FIN 三个标志位; 只有第 1 个报文 ACK = 0')
print(' ★ 两个 SYN 各占 1 格 -> 客户端数据从 %d 起, 服务端数据从 %d 起'
% (C_ISN + 1, S_ISN + 1))
print()
print('=== ② 四次挥手 (客户端数据到 1200, 服务端数据到 3000) ===')
cseq, sseq = 1201, 3001
wave = [('4th', 'C->S', cseq, sseq, '1 0 1', '我没数据发了 (FIN 占格)'),
('5th', 'S->C', sseq, cseq + 1, '1 0 0', '知道了, 但我可能还有数据'),
('6th', 'S->C', sseq, cseq + 1, '1 0 1', '我也发完了 (FIN 占格)'),
('7th', 'C->S', cseq + 1, sseq + 1, '1 0 0', '知道了 -> 主动方进 TIME_WAIT')]
print(' ' + pad('次序', 6) + pad('方向', 8) + pad('seq', 10) + pad('ack', 10)
+ pad('A S F', 10) + '含义')
for no, d, sq, ak, fl, mean in wave:
print(' ' + pad(no, 6) + pad(d, 8) + pad(str(sq), 10) + pad(str(ak), 10)
+ pad(fl, 10) + mean)
print(' ★ 第 5/6 次若可合并 -> 只要 3 个报文; 默认情形按 4 个答')
print(' ★ 第 5 次的 ack 已是 %d: FIN 占掉了序号 %d' % (cseq + 1, cseq))
print()
print('=== ③ 建连与拆连的时延账 (RTT = 100 ms) ===')
RTT = 100
print(' ' + pad('阶段', 16) + pad('报文数', 10) + pad('耗时', 18) + '说明')
print(' ' + pad('三次握手', 16) + pad('3', 10) + pad('1 RTT', 18)
+ '第 3 个 ACK 可捎带数据')
print(' ' + pad('四次挥手', 16) + pad('4', 10) + pad('1.5 RTT', 18)
+ '服务端立即发 FIN 时')
print(' ' + pad('TIME_WAIT 等待', 16) + pad('0', 10) + pad('2 MSL = 60 s', 18)
+ '★ 与 RTT 无关')
print(' ' + pad('建连 + 拆连', 16) + pad('7', 10) + pad('2.5 RTT = %.0f ms' % (2.5 * RTT), 18)
+ '不含 TIME_WAIT')
print()
print('=== ④ 两次握手挡不住的三种失效场景 ===')
bad = [('旧 SYN 迟到', '客户端早已关闭, 服务端单方面建连 -> 半开连接白占资源'),
('服务端乱发 SYN+ACK', '客户端无法判断这是新连接还是旧连接的重传'),
('一方收发能力未被验证', '两次只验证了客户端->服务端与服务端->客户端的通告, '
'服务端没得到"对方收到了"的确认')]
for i, (a, b) in enumerate(bad, 1):
print(' %d. %s: %s' % (i, a, b))
print(' ★ 第 3 次握手让服务端第一次确认"对方收到了我的 SYN+ACK"')
print()
print('=== ⑤ 半连接队列与 SYN 洪泛 ===')
q, rate, timeout = 256, 1000, 1.0
print(' 队列长度 = %d, 攻击速率 = %d SYN/s, 超时回收 = %.1f s' % (q, rate, timeout))
print(' ' + pad('情形', 18) + pad('队列占用', 12) + pad('占满耗时', 12) + '说明')
print(' ' + pad('正常(1 SYN/s)', 18) + pad('1', 12) + pad('不占满', 12) + '稳态很空')
print(' ' + pad('洪泛(1000 SYN/s)', 18) + pad('256', 12)
+ pad('%.3f s' % (q / rate), 12) + '进得比出得快 -> 稳态即满')
print(' ★ 防线 SYN Cookie: 不分配资源, 把状态编码进自己的 ISN')
print(' ★ 用了 Cookie, 队列占用与攻击速率无关')
print()
print('=== ⑥ 11 个状态归类 ===')
state = [('CLOSED', '双方', '初始 / 终止'), ('LISTEN', '服务端独有', '等待 SYN'),
('SYN_SENT', '客户端独有', '已发 SYN'), ('SYN_RCVD', '服务端', '等待第 3 个 ACK'),
('ESTABLISHED', '双方', '可双向传数据'), ('FIN_WAIT_1', '主动方', '等对方的 ACK'),
('FIN_WAIT_2', '主动方', '半关闭, 等对方的 FIN'),
('CLOSE_WAIT', '被动方', '等本进程关闭'), ('LAST_ACK', '被动方', '等最后一个 ACK'),
('TIME_WAIT', '主动方独有', '等 2 MSL'), ('CLOSING', '双方', '同时发 FIN, 罕见')]
for a, b, c in state:
print(' ' + pad(a, 14) + pad(b, 14) + c)
print(' ★ 独有状态: 服务端 LISTEN / 客户端 SYN_SENT / 主动关闭方 TIME_WAIT')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 三次握手的三个报文 ===
次序 方向 seq ack A S F 作用
1st C->S 1000 0 0 1 0 请求建连
2nd S->C 2000 1001 1 1 0 同意 + 同步自己序号
3rd C->S 1001 2001 1 0 0 确认对方的 SYN
★ A S F = ACK / SYN / FIN 三个标志位; 只有第 1 个报文 ACK = 0
★ 两个 SYN 各占 1 格 -> 客户端数据从 1001 起, 服务端数据从 2001 起
=== ② 四次挥手 (客户端数据到 1200, 服务端数据到 3000) ===
次序 方向 seq ack A S F 含义
4th C->S 1201 3001 1 0 1 我没数据发了 (FIN 占格)
5th S->C 3001 1202 1 0 0 知道了, 但我可能还有数据
6th S->C 3001 1202 1 0 1 我也发完了 (FIN 占格)
7th C->S 1202 3002 1 0 0 知道了 -> 主动方进 TIME_WAIT
★ 第 5/6 次若可合并 -> 只要 3 个报文; 默认情形按 4 个答
★ 第 5 次的 ack 已是 1202: FIN 占掉了序号 1201
=== ③ 建连与拆连的时延账 (RTT = 100 ms) ===
阶段 报文数 耗时 说明
三次握手 3 1 RTT 第 3 个 ACK 可捎带数据
四次挥手 4 1.5 RTT 服务端立即发 FIN 时
TIME_WAIT 等待 0 2 MSL = 60 s ★ 与 RTT 无关
建连 + 拆连 7 2.5 RTT = 250 ms 不含 TIME_WAIT
=== ④ 两次握手挡不住的三种失效场景 ===
1. 旧 SYN 迟到: 客户端早已关闭, 服务端单方面建连 -> 半开连接白占资源
2. 服务端乱发 SYN+ACK: 客户端无法判断这是新连接还是旧连接的重传
3. 一方收发能力未被验证: 两次只验证了客户端->服务端与服务端->客户端的通告, 服务端没得到"对方收到了"的确认
★ 第 3 次握手让服务端第一次确认"对方收到了我的 SYN+ACK"
=== ⑤ 半连接队列与 SYN 洪泛 ===
队列长度 = 256, 攻击速率 = 1000 SYN/s, 超时回收 = 1.0 s
情形 队列占用 占满耗时 说明
正常(1 SYN/s) 1 不占满 稳态很空
洪泛(1000 SYN/s) 256 0.256 s 进得比出得快 -> 稳态即满
★ 防线 SYN Cookie: 不分配资源, 把状态编码进自己的 ISN
★ 用了 Cookie, 队列占用与攻击速率无关
=== ⑥ 11 个状态归类 ===
CLOSED 双方 初始 / 终止
LISTEN 服务端独有 等待 SYN
SYN_SENT 客户端独有 已发 SYN
SYN_RCVD 服务端 等待第 3 个 ACK
ESTABLISHED 双方 可双向传数据
FIN_WAIT_1 主动方 等对方的 ACK
FIN_WAIT_2 主动方 半关闭, 等对方的 FIN
CLOSE_WAIT 被动方 等本进程关闭
LAST_ACK 被动方 等最后一个 ACK
TIME_WAIT 主动方独有 等 2 MSL
CLOSING 双方 同时发 FIN, 罕见
★ 独有状态: 服务端 LISTEN / 客户端 SYN_SENT / 主动关闭方 TIME_WAIT五条结论:
- ★ 两个 SYN 各占 1 个序号:客户端 ISN 1000 → 数据从 1001 起;服务端 ISN 2000 → 数据从 2001 起;第 2 个报文的 ack = 1001、第 3 个报文的 ack = 2001。
- ★ 挥手时 FIN 同样占格:客户端 FIN 落在 1201(数据已到 1200)、服务端 FIN 落在 3001;所以第 5、6 个报文的 ack 都是 1202。
- ★ 建连 1 个 RTT、拆连 1.5 个 RTT:加上 TIME_WAIT 的 2 MSL = 60 s,一次"建—传—拆"的完整生命周期远长于前两者——但 60 s 与 RTT 无关,是"报文最大生存时间"决定的。
- ★ 第 3 次握手不可省:它让服务端第一次确认"客户端收到了我的 SYN+ACK";两次握手时旧 SYN 会让服务端建出一条半开连接、白占资源。
- ★ SYN Cookie 让队列占用与攻击速率解耦:服务端在收到第 3 个 ACK 之前不分配任何资源,把连接信息编码进 ISN 里。
考点
考点
1. 必背结论
- ★ 三次握手:SYN(seq = ISN,ACK = 0)→ SYN+ACK(seq = 对端 ISN,ack = 本端 ISN + 1)→ ACK(seq = ISN + 1,ack = 对端 ISN + 1)。
- ★★ 两个 SYN 各占 1 个序号:锚点 ISN 1000 / 2000 → 客户端数据从 1001、服务端数据从 2001 起。
- ★ 建连耗时 1 个 RTT:第 3 个报文的 ACK 可以捎带第 1 个数据字节。
- ★★ 为什么三次:防"已失效的连接请求报文段"——两次握手会让服务端在收到迟到的旧 SYN 后单方面建连,白占资源。
- ★★ 第 3 次握手的作用:让服务端确认客户端的接收能力正常、确认对方收到了自己的 SYN+ACK。
- ★★ 四次挥手:FIN(seq)→ ACK(ack = FIN 序号 + 1)→ FIN(seq)→ ACK(ack = FIN 序号 + 1)。
- ★★ FIN 也占 1 个序号;锚点:客户端 FIN 在 1201、服务端 FIN 在 3001,第 5/6 个报文 ack 都是 1202。
- ★★ 为什么四次:半关闭——收到 FIN 只表示对方不发数据了,本端可能还有数据要发,所以 ACK 与 FIN 通常分开。
- ★ 若能合并则只要 3 个报文:但考试默认按 4 个报文作答。
- ★★ TIME_WAIT 的两条理由:① 保证最后一个 ACK 能到达(丢了可重发);② 让本次连接的旧报文段在网络中自然消亡。
- ★★ 2 MSL = 2 × 30 s = 60 s;MSL 是"报文最大生存时间",与 RTT 无关。
- ★★ 只有主动关闭方进 TIME_WAIT;半关闭状态是 FIN_WAIT_2(主动方)与 CLOSE_WAIT(被动方)。
- ★ 状态路径:客户端 CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED;服务端 LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED。
- ★ 三个独有状态:LISTEN(服务端)、SYN_SENT(客户端)、TIME_WAIT(主动关闭方)。
- ★ RST 三种场合:连接已不存在却收到数据、向未监听端口发起连接、半开连接检测。
- ★ SYN 洪泛与 SYN Cookie:攻击只发第 1 次握手占满半连接队列;Cookie 把状态编码进 ISN,第 3 个 ACK 之前不分配资源。
- ★ 连接是"两端的状态",中间路由器不感知——这是"半开连接"与"RST 复位"能存在的前提。
2. 高频陷阱
- 把"三次握手"说成"发三个 SYN":错。只有第 1 个报文带 SYN,第 2 个带 SYN+ACK,第 3 个只有 ACK。
- 把第 2 个报文的 ack 写成 1000:错。是 1001——SYN 占掉了 1000 这一格。
- 认为"服务端数据也从 1001 开始":错。两个方向序号独立——服务端从自己的 ISN + 1 = 2001 起。
- 把"为什么三次"答成"为了可靠":太笼统、不给分。要答"防失效的旧 SYN 浪费服务端资源"。
- 把"第 3 次握手的作用"答成"确认服务端收到了自己的 SYN":方向反了。第 1 次就已经让服务端知道客户端在发;第 3 次让"服务端"确认客户端收到了自己的 SYN+ACK。
- 认为 "ACK 位" 与 "ack 数字" 是一回事:错。前者是标志位,后者是 32 位的确认号。
- 挥手时忘记 FIN 占序号:错,最爱考的细节——第 5/6 个报文的 ack 都应是 1202。
- 把"4 次挥手"记成"4 个方向":错。是 4 个报文段,方向只有两个。
- 认为"挥手一定 4 个报文":不严谨。服务端无数据可发时可合并 ACK 与 FIN,只要 3 个报文。
- 把 TIME_WAIT 挂到被动关闭方:错。是主动关闭方。
- 认为"TIME_WAIT 是为了等对方数据":错。两条理由是"保证最后 ACK 到达"与"让旧报文消亡"。
- 把 2 MSL 算成 2 个 RTT:错。MSL 是报文段最大生存时间(秒级),常在 30 s,故 2 MSL = 60 s。
- 把 CLOSE_WAIT 与 FIN_WAIT_2 混用:要分清。FIN_WAIT_2 是主动方(已收到对方 ACK,等对方 FIN);CLOSE_WAIT 是被动方(已回 ACK,等本进程关闭)。
- 认为"SYN 洪泛靠加大队列就能解决":不对。攻击速率可无限提高,队列只能被更快占满——真正的解法是 SYN Cookie(不占资源)。
- 把 RST 与 FIN 混为一谈:要分清。FIN 是"正常关闭";RST 是"异常强制释放",不需要确认。
3. 解题模板("握手 / 挥手序号题")
① 握手序号推算:
设客户端 ISN = C, 服务端 ISN = S
第1个报文: seq = C, ack = 0, SYN=1 ACK=0
第2个报文: seq = S, ack = C + 1, SYN=1 ACK=1
第3个报文: seq = C + 1, ack = S + 1, SYN=0 ACK=1
握手完成后: 客户端数据序号从 C+1 起, 服务端数据序号从 S+1 起
② 挥手序号推算(已知双方下一个要发的序号):
第4个报文: seq = c, ack = s, FIN=1 ACK=1
-> 主动方下一个序号 = c + 1
第5个报文: seq = s, ack = c + 1, ACK=1
第6个报文: seq = s, ack = c + 1, FIN=1 ACK=1
-> 被动方下一个序号 = s + 1
第7个报文: seq = c + 1, ack = s + 1, ACK=1
★ 中间若被动方又发了 L 字节数据, 则第 6 个报文 seq 与第 7 个报文 ack 都要 + L
③ 状态题:
看到 SYN=1, ACK=0 -> 客户端在 SYN_SENT / 服务端即将进 SYN_RCVD
看到 SYN=1, ACK=1 -> 服务端在 SYN_RCVD
看到 ACK=1 无 SYN/FIN -> 进入 ESTABLISHED
看到 FIN=1 -> 主动方 FIN_WAIT_1 / 被动方 CLOSE_WAIT
★ 谁先发 FIN, 谁最后进 TIME_WAIT
④ 时延题:
建连 = 1 RTT (第3个 ACK 捎带数据)
拆连 = 1.5 RTT (服务端立即发 FIN)
TIME_WAIT = 2 MSL = 60 s, 与 RTT 无关4. 与相邻章节的接口
net/41-tcp.md(TCP 报文段):本章全部推算都建立在"序号是字节编号 + SYN/FIN 各占 1 个序号"之上;上一章"ACK 位连接建立后恒为 1"是本章识别握手阶段的依据。net/43-flow.md(流量控制):握手的第 2、3 个报文里的窗口字段,正是接收方第一次通告自己的接收窗口——下一章就从这个字段展开。net/44-congestion.md(拥塞控制):握手期间 cwnd 从 1 个 MSS 起算(初始窗口),这也正是"两次握手慢"的工程背景。net/50-dns.md与net/51-http.md:"建连多花 1 个 RTT"这件事,直接决定了 DNS 用 UDP(省掉握手)与 HTTP 连接复用的价值——HTTP 非持久连接每个对象都要重来一次三次握手,代价见net/51-http.md。os/10-process.md与net/02-syscall系列:套接字在操作系统里就是一个文件描述符——listen/accept/close都是系统调用;半连接队列与全连接队列由内核维护,这正是"连接是状态"的落地形式。
小结
- ★ 三次握手:SYN(ACK = 0)→ SYN+ACK → ACK;两个 SYN 各占 1 个序号。
- ★★ 锚点:客户端 ISN 1000 / 服务端 ISN 2000 → 第 2 个报文 ack 1001、第 3 个 ack 2001;客户端数据从 1001 起、服务端从 2001 起。
- ★ 建连 1 个 RTT——第 3 个 ACK 可捎带数据。
- ★★ 为什么三次:防失效的旧 SYN 让服务端建立半开连接、白占资源;第 3 次让服务端确认对方收到了自己的 SYN+ACK。
- ★★ 四次挥手:FIN → ACK → FIN → ACK;FIN 也占 1 个序号;锚点:客户端 FIN 在 1201、服务端 FIN 在 3001,第 5/6 个报文 ack 都是 1202。
- ★★ 为什么四次:半关闭——对方只是不发数据了,本端可能还有数据要发,故 ACK 与 FIN 分开;若能合并则只要 3 个报文。
- ★★ TIME_WAIT:只有主动关闭方进;等 2 MSL = 60 s;目的是保证最后 ACK 能到达、让旧报文段消亡。
- ★ 状态路径:客户端 FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT;服务端 CLOSE_WAIT → LAST_ACK;独有状态 LISTEN / SYN_SENT / TIME_WAIT。
- ★ 异常与安全:RST 三种场合;SYN 洪泛靠 SYN Cookie 化解(第 3 个 ACK 前不分配资源)。
下一篇:TCP 流量控制
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。