Appearance
TCP 报文段与可靠传输
概念
TCP(Transmission Control Protocol,传输控制协议)是传输层的"重装版":它在不可靠的 IP 之上,用"序号 + 确认 + 超时重传 + 窗口"这一套机制,造出一条"看起来像一根可靠管道"的连接。
一句话说清它与 UDP 的分工:UDP 只负责"把报文交给对的进程",TCP 还要负责"保证交到的内容一字不差、顺序不乱、快慢可控"。 代价就是首部从 8 B 涨到 20 B,以及建连接、确认、重传这一整套开销。
★ 本章要解决的三件事:
| 问题 | 对应内容 |
|---|---|
| 首部长什么样 | 固定 20 B、10 个字段 + 选项(最长 40 B) |
| 序号与确认号是什么意思 | 字节流编号 + 累积确认(含"SYN/FIN 各占一个序号") |
| 怎么做到可靠 | 超时重传(自适应 RTO)+ 快速重传 + 累积确认 |
与上一章的衔接:上一章(net/40-udp.md)说"UDP 面向报文、保留边界、无流控无拥塞控制"——本章的 TCP 在每一项上都做了相反的选择;而两者共用同一套"12 B 伪首部 + 16 位反码求和"的检验和算法(连伪首部里的协议号从 17 换成 6,其余规则不变)。
原理
一、首部:固定 20 B,最长 60 B
★ 首部结构(必背):
text
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────────────────────────────┬───────────────────────────────┐
│ 源端口 │ 目的端口 │ <- 4 B
│ 16 位 │ 16 位 │
├───────────────────────────────┴───────────────────────────────┤
│ 序号 │ <- 4 B
│ 32 位 │
├───────────────────────────────────────────────────────────────┤
│ 确认号 │ <- 4 B
│ 32 位 │
├───────┬───────┬───────────────────────────────┬───────────────┤
│数据偏移│ 保留 │ 标志位 (6 个) │ 窗口 │ <- 4 B
│ 4 位 │ 6 位 │ U A P R S F │ 16 位 │
│ │ │ R C S S T I │ │
│ │ │ G K H T N N │ │
├───────────────────────────────┬───────────────────────────────┤
│ 检验和 │ 紧急指针 │ <- 4 B
│ 16 位 │ 16 位 │
├───────────────────────────────┴───────────────────────────────┤
│ 选项 (0 ~ 40 B, 不用则首部就是 20 B) │
└───────────────────────────────────────────────────────────────┘
★ 固定部分 20 B = 5 行 x 4 B; 带选项时最长 60 B| 字段 | 位数 | 含义 | 易错点 |
|---|---|---|---|
| 源端口 | 16 | 发送方进程端口 | — |
| 目的端口 | 16 | 接收方进程端口 | — |
| 序号(seq) | 32 | 本报文段数据部分第一个字节的序号 | ★ 是"字节编号"不是"报文编号" |
| 确认号(ack) | 32 | 期望收到的下一个字节的序号 | ★ 累计确认:表示"该序号之前的全部字节都已收到" |
| 数据偏移 | 4 | 首部有多少个 4 B | ★ 单位是 4 B;最小 5(20 B),最大 15(60 B) |
| 保留 | 6 | 置 0 | — |
| 标志位 | 6 | URG / ACK / PSH / RST / SYN / FIN | 见下一小节 |
| 窗口 | 16 | 接收方还能接收的字节数(接收窗口) | ★ 单位 1 B;最大 65535 |
| 检验和 | 16 | 首部 + 数据 + 伪首部(协议号 6) | ★ 与 UDP 用同一套算法 |
| 紧急指针 | 16 | URG = 1 时指出紧急数据的末尾位置 | 现代很少用 |
★ 由字段宽度直接推出的三条结论:
| 结论 | 推导 |
|---|---|
| TCP 首部固定 20 B、最长 60 B | 数据偏移 4 位 → 最大值 15 → |
| 窗口字段最大 65535 B | 窗口 16 位 → |
| 选项最长 40 B |
★ 六个标志位(必背):
| 位 | 名称 | 置 1 表示 |
|---|---|---|
| URG | 紧急 | 紧急指针有效,数据需要优先处理(很少用) |
| ACK | 确认 | 确认号字段有效——连接建立之后,所有报文段的 ACK 位恒为 1 |
| PSH | 推送 | 尽快把数据交给应用层,不要等缓冲区满 |
| RST | 复位 | 连接出现异常,强制释放(拒绝连接、半开连接检测) |
| SYN | 同步 | 请求建立连接,同时同步初始序号 |
| FIN | 终止 | 发送方数据已发完,请求释放连接 |
⚠️ "ACK 位恒为 1"这句话要理解准确:它说的是"建立了连接之后"——只有最开始那个 SYN 报文段的 ACK 位是 0(因为此时还没有任何东西可确认)。 之后的每一个报文段都置 ACK = 1,所以"ACK 位"这个信息量只体现在第一个报文段上。
二、序号与确认号:一切按"字节"算
★ 三条基本规则(必背):
text
① 序号 = 本报文段"数据部分"第一个字节在整个字节流里的编号
★ 不是"第几个报文段"! 是"第几个字节"
② 确认号 = 期望对方发来的下一个字节的编号
★ 隐含"该序号之前的字节我全收到了" -> 这就是"累积确认"
③ ★ SYN 占 1 个序号, FIN 也占 1 个序号
(它们虽然不携带数据, 但要"占用一格", 以便被确认)★ 锚点(本章定死):客户端 ISN = 1000,随后发 200 B、再发 500 B
text
① 连接请求 (SYN): 序号 = 1000, 不携带数据
★ SYN 占掉序号 1000 这一格
-> 数据部分的第一个字节从 1001 开始
② 客户端发 200 B 数据: 序号 = 1001
占用字节编号 [1001, 1200] (因为 1001 + 200 - 1 = 1200)
-> 下一个要发的字节编号 = 1201
-> 服务端的确认号 = 1201
③ 客户端再发 500 B 数据: 序号 = 1201
占用字节编号 [1201, 1700]
-> 下一个序号 = 1701
★ 若中间第 2 段丢了 (序号 1201 那段), 服务端会一直回 确认号 = 1201
直到收到 1201 开始的字节 -> "重复 ACK" 的由来★ 累积确认的含义(考试最爱考的一句):确认号 1201 表示"序号
⚠️ 累积确认带来一个必然的代价:若中间一段丢失,后面的段虽然收到了,确认号也只能停在丢失处——发送方必须重传丢失段,并且"后面那些已经到达的段"在接收方缓冲区里排队等着。 这正是"快速重传"要解决的问题(见本节第四部分)。
★ 一个常考的边角:序号的空间是
⚠️ 为什么初始序号(ISN)必须是随机值:若 ISN 固定,攻击者可以盲猜序号伪造报文、插入数据。 所以 TCP 的 ISN 由时间驱动的算法生成,每次都不同——这也是"为什么抓包看不到序号从 0 开始"的原因。
三、可靠传输:超时重传与自适应 RTO
"可靠"这件事在 TCP 里由三件工具合起来完成:
| 工具 | 作用 |
|---|---|
| 累积确认 | 让发送方知道"到哪为止对方收到了" |
| 超时重传 | 对方没确认(可能丢包了)就重发——靠 RTO 计时器 |
| 快速重传 | 收到 3 个重复 ACK 就立刻重发,不必等超时 |
★ RTO 不能拍一个固定值:定小了会"没丢也重传"(浪费带宽、还可能加剧拥塞);定大了丢包后要等很久。 所以 RTO 必须"跟着实际 RTT 走",这就是自适应重传算法:
其中
★ 锚点(本章定死):四个 RTT 样本 100、110、90、120 ms(单位 ms)
text
初值 (按第一个样本初始化): SRTT = 100, RTTVAR = 100 / 2 = 50
RTO = 100 + 4 x 50 = 300
样本 R = 110:
RTTVAR = (3/4) x 50 + (1/4) x |100 - 110| = 37.5 + 2.5 = 40.0000
SRTT = (7/8) x 100 + (1/8) x 110 = 87.5 + 13.75 = 101.2500
RTO = 101.2500 + 4 x 40.0000 = 261.2500
样本 R = 90:
RTTVAR = (3/4) x 40 + (1/4) x |101.25 - 90| = 30 + 2.8125 = 32.8125
SRTT = (7/8) x 101.25 + (1/8) x 90 = 88.59375 + 11.25 = 99.8438
RTO = 99.8438 + 4 x 32.8125 = 231.0938
样本 R = 120:
RTTVAR = (3/4) x 32.8125 + (1/4) x |99.84375 - 120| = 24.6094 + 5.0391 = 29.6484
SRTT = (7/8) x 99.84375 + (1/8) x 120 = 87.3633 + 15 = 102.3633
RTO = 102.3633 + 4 x 29.6484 = 220.9570
★ 对照: 若简单用 "2 x 最新 RTT" 当 RTO
2 x 110 = 220, 2 x 90 = 180, 2 x 120 = 240
-> 在 180 ~ 240 之间跳, 抖动 60 ms
而平滑后的 RTO: 261.2500 -> 231.0938 -> 220.9570 (平滑下降, 不随单个样本发抖)⚠️ 公式里的两个"坑":①
用的是"旧的 SRTT"(必须先更新 RTTVAR 再更新 SRTT,顺序反了数值就不同);② 初始条件是 SRTT = 第一个样本、RTTVAR = 半个样本(RFC 6298 的规定)。 考试若给"初始 SRTT 与 RTTVAR",就用给的值;没给就按上面这两条推。 ⚠️ Karn 算法(与 RTO 配套的一条规则):"重传过的报文段,不用它的确认来测 RTT"——因为收到确认时你无法判断它确认的是"原报文"还是"重传报文",测出来的 RTT 会失真。 Karn 算法还规定"每重传一次,RTO 翻倍"(退避),所以连续丢包时 RTO 会指数增长,避免"越拥塞越猛发"。
四、快速重传与选择确认
★ 快速重传(fast retransmit)的思路:不必等 RTO 超时——只要收到 3 个重复 ACK,就说明"后面有 3 个报文段都到了、只缺中间那一段",立刻重传它。 ("3 个"是个经验值:它能排除"乱序到达"造成的假丢失。)
★ 锚点:发送 4 个报文段,第 2 个丢了
text
发送: 段1 [1,100] 段2 [101,200] 段3 [201,300] 段4 [301,400]
★ 段2 在路上丢了
接收方:
收到 段1 -> 期望 101, 回 ACK = 101 (第 1 次)
收到 段3 -> 不连续, 仍回 ACK = 101 (重复 ACK 1)
收到 段4 -> 不连续, 仍回 ACK = 101 (重复 ACK 2)
收到 段5 -> 不连续, 仍回 ACK = 101 (重复 ACK 3)
★ 发送方数到"3 个重复 ACK" -> 立即重传段2, 不等 RTO 到期
★ 段3、段4 虽已到达, 但"交付不了"(前件不连续) -> 缓存在接收方窗口里
★ 若开启 SACK 选项, 接收方可以明确告诉发送方"我收到了 201~400"
-> 发送方就只重传 101~200, 不必连后面的也重传★ 累积确认 vs 选择确认(SACK):
| 机制 | 确认方式 | 代价 |
|---|---|---|
| 累积确认(TCP 基本机制) | 只说"到哪为止连续收到了" | 丢一段后面全要重传 |
| SACK(选择确认,选项形式) | 额外告诉对方"我还零散收到了哪些区间" | 需要双方都支持该选项 |
⚠️ TCP 的"选择重传"是靠 SACK 选项实现的,不是内建机制——这一点与
net/22-window.md里讲的"SR 协议"不同:SR 是链路层的帧级协议,SACK 是传输层的选项。 408 会把两者放在一起对比,注意别混。
五、MSS、窗口与吞吐率
★ MSS(Maximum Segment Size,最大报文段长度)指"TCP 报文段里数据部分的最大长度"——它不包含首部:
★ MSS 与 MTU 的关系(常考的辨析):
| 概念 | 含义 | 包含什么 |
|---|---|---|
| MTU | 链路层能承载的 IP 数据报最大长度 | IP 首部 + TCP 首部 + 数据 |
| MSS | TCP 数据部分的最大长度 | 只有数据 |
| 关系 | 差 40 B | |
| 实际协商 | 双方在 SYN 报文里各自声明自己的 MSS,取较小者 | 避免分片 |
★ 吞吐率受窗口与 RTT 共同限制:
★ 锚点:窗口 65535 B、RTT 40 ms
text
吞吐率 = 65535 B / 0.04 s = 1638375 B/s = 13107000 bit/s = 13.107 Mbps
★ 想跑满 1 Gbps 需要多大的窗口?
窗口 = 1e9 bit/s x 0.04 s / 8 = 5000000 B = 5 MB (约 4.77 MiB)
★ 但窗口字段只有 16 位 -> 最大 65535 B -> 差 76 倍!
-> 这就是"窗口扩大选项"(RFC 1323) 的动因: 可把窗口放大到 2^30⚠️ "带宽时延积"(Bandwidth-Delay Product)这个词要会算:它 = 带宽 × RTT,表示"一条链路上"在途"的数据量上限"——窗口必须 ≥ 带宽时延积,才能跑满带宽。 锚点里 1 Gbps × 40 ms = 40 Mbit = 5 MB,正是这个意思。
★ 流量控制与拥塞控制的分工(本章只做界分,详见后续章节):
| 机制 | 谁决定 | 针对什么 | 体现为 |
|---|---|---|---|
| 流量控制 | 接收方 | "接收方来不来得及处理" | 窗口字段的值(接收窗口 rwnd) |
| 拥塞控制 | 发送方 | "网络扛不扛得住" | 拥塞窗口 cwnd |
| 实际可发量 | 两者取小 | net/43-flow.md 与 net/44-congestion.md |
示例
例 1:C 实现——首部解析、序号推算与吞吐率
参数:一个 20 B 的 TCP 首部(SYN 报文段);数据推算用 ISN = 1000、200 B、500 B;吞吐率用窗口 65535 B、RTT 40 ms。
#include <stdio.h>
void parse(const unsigned char *h) {
int sport = (h[0] << 8) | h[1];
int dport = (h[2] << 8) | h[3];
unsigned seq = ((unsigned)h[4] << 24) | ((unsigned)h[5] << 16)
| ((unsigned)h[6] << 8) | (unsigned)h[7];
unsigned ack = ((unsigned)h[8] << 24) | ((unsigned)h[9] << 16)
| ((unsigned)h[10] << 8) | (unsigned)h[11];
int doff = (h[12] >> 4) & 0x0F;
int urg = (h[13] >> 5) & 1, ackf = (h[13] >> 4) & 1, psh = (h[13] >> 3) & 1;
int rst = (h[13] >> 2) & 1, syn = (h[13] >> 1) & 1, fin = h[13] & 1;
int win = (h[14] << 8) | h[15];
int csum = (h[16] << 8) | h[17];
int urp = (h[18] << 8) | h[19];
printf("① TCP 首部字段解析 (20 B 固定部分)\n");
printf(" 源端口 = %d; 目的端口 = %d\n", sport, dport);
printf(" 序号 = %u; 确认号 = %u\n", seq, ack);
printf(" 数据偏移 = %d x 4 = %d B\n", doff, doff * 4);
printf(" 标志位: URG=%d ACK=%d PSH=%d RST=%d SYN=%d FIN=%d\n",
urg, ackf, psh, rst, syn, fin);
printf(" 窗口 = %d B; 检验和 = 0x%04X; 紧急指针 = %d\n", win, csum, urp);
printf(" ★ 首部 %d B + 数据 0 B; SYN=1 且 ACK=0 -> 这是连接请求报文段\n", doff * 4);
printf("\n");
}
int main(void) {
unsigned char h[20] = {0xC0, 0x00, 0x00, 0x50, 0x00, 0x00, 0x03, 0xE8,
0x00, 0x00, 0x00, 0x00, 0x50, 0x02, 0xFF, 0xFF,
0x12, 0x34, 0x00, 0x00};
unsigned isn = 1000, next;
parse(h);
printf("② 序号推算 (SYN 与 FIN 各占 1 个序号)\n");
printf(" SYN 的序号 = %u, 不携带数据, 但占掉序号 %u\n", isn, isn);
next = isn + 1;
printf(" -> 数据部分从 %u 开始\n", next);
printf(" 发 200 B: 序号 = %u, 占用 [%u, %u]\n", next, next, next + 199);
next += 200;
printf(" -> 下一个序号 = %u; 接收方确认号 = %u\n", next, next);
printf(" 再发 500 B: 序号 = %u, 占用 [%u, %u]\n", next, next, next + 499);
next += 500;
printf(" -> 下一个序号 = %u\n", next);
printf(" ★ 序号是\"字节编号\"不是\"报文编号\"\n");
printf("\n");
printf("③ MSS 与效率\n");
printf(" MSS = MTU 1500 - IP 20 - TCP 20 = %d B\n", 1500 - 20 - 20);
printf(" MSS 段效率 = 1460 / 1500 = %.2f%%\n", 1460 * 100.0 / 1500.0);
printf(" 载荷 100 B: 20 + 20 + 100 = %d B, 开销率 %.2f%%\n",
20 + 20 + 100, 40 * 100.0 / 140.0);
printf(" ★ MSS 只算\"数据部分\", MTU 算\"整包\"\n");
printf("\n");
printf("④ 吞吐率与窗口账 (窗口 65535 B, RTT 40 ms)\n");
printf(" 吞吐率 = 65535 / 0.04 = %.0f B/s = %.3f Mbps\n",
65535 / 0.04, 65535.0 * 8 / 0.04 / 1e6);
printf(" 跑满 1000 Mbps 需要窗口 = 1e9 x 0.04 / 8 = %.0f B\n", 1e9 * 0.04 / 8);
printf(" 但窗口字段 16 位 -> 最大 65535 B, 差 %.1f 倍\n",
(1e9 * 0.04 / 8) / 65535.0);
printf(" ★ 所以要\"窗口扩大选项\" (可放大到 2^30)\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
① TCP 首部字段解析 (20 B 固定部分)
源端口 = 49152; 目的端口 = 80
序号 = 1000; 确认号 = 0
数据偏移 = 5 x 4 = 20 B
标志位: URG=0 ACK=0 PSH=0 RST=0 SYN=1 FIN=0
窗口 = 65535 B; 检验和 = 0x1234; 紧急指针 = 0
★ 首部 20 B + 数据 0 B; SYN=1 且 ACK=0 -> 这是连接请求报文段
② 序号推算 (SYN 与 FIN 各占 1 个序号)
SYN 的序号 = 1000, 不携带数据, 但占掉序号 1000
-> 数据部分从 1001 开始
发 200 B: 序号 = 1001, 占用 [1001, 1200]
-> 下一个序号 = 1201; 接收方确认号 = 1201
再发 500 B: 序号 = 1201, 占用 [1201, 1700]
-> 下一个序号 = 1701
★ 序号是"字节编号"不是"报文编号"
③ MSS 与效率
MSS = MTU 1500 - IP 20 - TCP 20 = 1460 B
MSS 段效率 = 1460 / 1500 = 97.33%
载荷 100 B: 20 + 20 + 100 = 140 B, 开销率 28.57%
★ MSS 只算"数据部分", MTU 算"整包"
④ 吞吐率与窗口账 (窗口 65535 B, RTT 40 ms)
吞吐率 = 65535 / 0.04 = 1638375 B/s = 13.107 Mbps
跑满 1000 Mbps 需要窗口 = 1e9 x 0.04 / 8 = 5000000 B
但窗口字段 16 位 -> 最大 65535 B, 差 76.3 倍
★ 所以要"窗口扩大选项" (可放大到 2^30)⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
0x50这个字节同时编码了"数据偏移"与"保留位":高 4 位0101= 5(表示 20 B),低 4 位是保留位(置 0)——所以0x50就是"标准无选项 TCP 首部"的标记。 若带 12 B 时间戳选项,这个字节要变成0x80(偏移 8 → 32 B)。SYN=1 且 ACK=0是"连接请求"的指纹:连接建立之后的任何报文段 ACK 位都是 1,所以"ACK=0"这个特征本身就说明它是整个连接的第一个报文段。 这条判断(配合 SYN / FIN)是net/42-handshake.md识别握手各阶段的依据。
例 2:Python——RTO 迭代、快速重传与吞吐率账
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
print('=== ① TCP 首部 20 B 的十个字段 ===')
for a, b, c in [('源端口', '16 位', '发送方进程端口'),
('目的端口', '16 位', '接收方进程端口'),
('序号', '32 位', '★ 本报文段数据第一个字节的编号'),
('确认号', '32 位', '★ 期望收到的下一个字节编号 (累积确认)'),
('数据偏移', '4 位', '★ 单位 4 B; 最小 5 (20 B), 最大 15 (60 B)'),
('保留', '6 位', '置 0'),
('标志位', '6 位', 'URG / ACK / PSH / RST / SYN / FIN'),
('窗口', '16 位', '接收窗口, 最大 65535 B'),
('检验和', '16 位', '首部 + 数据 + 伪首部 (协议号 6)'),
('紧急指针', '16 位', 'URG = 1 时有效'),
('选项', '0 ~ 40 位/字节', '最长 40 B; 于是首部最长 60 B')]:
print(' ' + pad(a, 12) + pad(b, 14) + c)
print(' ★ 固定 20 B; 序列号是"字节编号", 不是"报文编号"')
print()
print('=== ② 六个标志位 ===')
for a, b in [('URG', '紧急指针有效, 数据优先处理'),
('ACK', '确认号有效; ★ 连接建立后恒为 1'),
('PSH', '尽快交给应用层, 不等缓冲满'),
('RST', '复位连接 (拒绝 / 异常)'),
('SYN', '请求建立连接, 同步初始序号'),
('FIN', '数据发完, 请求释放连接')]:
print(' ' + pad(a, 8) + b)
print(' ★ 只有第一个 SYN 报文段的 ACK = 0; 之后一律 ACK = 1')
print()
print('=== ③ RTO 自适应计算 (alpha=1/8, beta=1/4, K=4) ===')
alpha, beta, K = 1 / 8, 1 / 4, 4
R = [100.0, 110.0, 90.0, 120.0]
srtt, rttvar = R[0], R[0] / 2
rows = [('初值', '—', R[0] / 2, srtt, rttvar, srtt + K * rttvar)]
for r in R[1:]:
rttvar = (1 - beta) * rttvar + beta * abs(srtt - r)
srtt = (1 - alpha) * srtt + alpha * r
rows.append(('样本', '%.0f' % r, abs(R[0] - r), srtt, rttvar, srtt + K * rttvar))
print(' ' + pad('阶段', 8) + pad('RTT 样本', 12) + pad('SRTT', 14) + pad('RTTVAR', 14) + 'RTO')
for st, r, a, s_, v, rto in rows:
print(' ' + pad(st, 8) + pad(r, 12) + pad('%.4f' % s_, 14) + pad('%.4f' % v, 14)
+ '%.4f' % rto)
print(' ★ 顺序不能反: 先用"旧 SRTT"更新 RTTVAR, 再更新 SRTT')
print()
print(' 对照 —— 若简单用"2 x 最新 RTT"当 RTO:')
naive = [2 * r for r in R[1:]]
for r, n in zip(R[1:], naive):
print(' RTT %6.0f -> RTO %6.0f' % (r, n))
print(' 抖动幅度 = %.0f - %.0f = %.0f ms' % (max(naive), min(naive), max(naive) - min(naive)))
smooth = [row[5] for row in rows[1:]]
print(' 平滑后的 RTO = %.4f / %.4f / %.4f (单调平滑, 不随单个样本发抖)'
% (smooth[0], smooth[1], smooth[2]))
print()
print('=== ④ 快速重传 (段 2 丢失) ===')
sends = [('段1', 1, 100), ('段2', 101, 200), ('段3', 201, 300), ('段4', 301, 400),
('段5', 401, 500)]
print(' 发送: ' + ', '.join('%s [%d,%d]' % s for s in sends))
print(' ★ 段2 [101,200] 在路上丢了')
want = 101 # 连续前缀的下一个字节: 收到段1 [1,100] 后 = 101
print(' ' + pad('收到', 10) + pad('是否连续', 12) + pad('回 ACK', 10) + '说明')
got = [('段1', True), ('段3', False), ('段4', False), ('段5', False)]
dup = 0
for name, ok in got:
if ok:
note = '正常确认 (连续前缀到 100)'
else:
dup += 1
note = '重复 ACK %d (前缀仍到 100)' % dup
print(' ' + pad(name, 10) + pad('连续' if ok else '不连续', 12)
+ pad(str(want), 10) + note)
print(' 收到第 3 个重复 ACK -> 立即重传段2 [101,200], 不等 RTO 超时')
print(' ★ 段3/4/5 已到达但"交付不了", 缓存在接收方窗口里')
print(' ★ 若开 SACK, 接收方可告知"已零散收到 201~500", 发送方只补 101~200')
print()
print('=== ⑤ MSS、效率与吞吐率 ===')
print(' MSS = MTU 1500 - IP 20 - TCP 20 = %d B' % (1500 - 20 - 20))
print(' ' + pad('载荷(B)', 10) + pad('TCP 总长', 12) + pad('开销率', 10) + '说明')
for pl in [100, 512, 1000, 1460]:
print(' ' + pad(str(pl), 10) + pad(str(pl + 40), 12)
+ pad('%.2f%%' % (40 * 100.0 / (pl + 40)), 10)
+ ('达到 MSS' if pl == 1460 else ''))
print()
RTT = 0.04
print(' RTT = %.0f ms 时的吞吐率:' % (RTT * 1000))
print(' ' + pad('窗口(B)', 12) + pad('吞吐率(B/s)', 16) + pad('吞吐率(Mbps)', 16) + '说明')
for w, note in [(65535, '窗口字段上限'), (65535 * 8, '带窗口扩大选项'),
(5000000, '跑满 1 Gbps 所需')]:
print(' ' + pad(str(w), 12) + pad('%.0f' % (w / RTT), 16)
+ pad('%.3f' % (w * 8 / RTT / 1e6), 16) + note)
print(' 带宽时延积 = 1 Gbps x 40 ms = 40 Mbit = %.0f B' % (1e9 * RTT / 8))
print(' ★ 窗口 >= 带宽时延积 才可能跑满带宽; 65535 B 只够 %.1f Mbps'
% (65535 * 8 / RTT / 1e6))
print()
print('=== ⑥ 三种可靠性工具的分工 ===')
for a, b, c in [('累积确认', '只说"连续收到哪儿"', '丢一段则后面全要重传'),
('超时重传', 'RTO 到期未确认就重发', 'RTO 靠 SRTT/RTTVAR 自适应'),
('快速重传', '3 个重复 ACK 就重发', '不等超时; SACK 可减少重传量')]:
print(' ' + pad(a, 12) + pad(b, 28) + c)
print(' ★ 选择重传靠 SACK 选项实现, 与 net/22-window.md 的"SR 协议"不是一回事')
print()
print('=== ⑦ TCP 与 UDP 首部对照 ===')
for a, b, c in [('固定长度', '8 B', '20 B'), ('最长', '8 B (无选项)', '60 B (选项 40 B)'),
('字段数', '4 个', '10 个'),
('伪首部协议号', '17', '6'),
('校验和算法', '16 位反码求和', '16 位反码求和 (相同)'),
('校验和可选性', 'IPv4 可置 0, IPv6 必须算', '必须计算')]:
print(' ' + pad(a, 16) + pad(b, 26) + c)
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① TCP 首部 20 B 的十个字段 ===
源端口 16 位 发送方进程端口
目的端口 16 位 接收方进程端口
序号 32 位 ★ 本报文段数据第一个字节的编号
确认号 32 位 ★ 期望收到的下一个字节编号 (累积确认)
数据偏移 4 位 ★ 单位 4 B; 最小 5 (20 B), 最大 15 (60 B)
保留 6 位 置 0
标志位 6 位 URG / ACK / PSH / RST / SYN / FIN
窗口 16 位 接收窗口, 最大 65535 B
检验和 16 位 首部 + 数据 + 伪首部 (协议号 6)
紧急指针 16 位 URG = 1 时有效
选项 0 ~ 40 位/字节最长 40 B; 于是首部最长 60 B
★ 固定 20 B; 序列号是"字节编号", 不是"报文编号"
=== ② 六个标志位 ===
URG 紧急指针有效, 数据优先处理
ACK 确认号有效; ★ 连接建立后恒为 1
PSH 尽快交给应用层, 不等缓冲满
RST 复位连接 (拒绝 / 异常)
SYN 请求建立连接, 同步初始序号
FIN 数据发完, 请求释放连接
★ 只有第一个 SYN 报文段的 ACK = 0; 之后一律 ACK = 1
=== ③ RTO 自适应计算 (alpha=1/8, beta=1/4, K=4) ===
阶段 RTT 样本 SRTT RTTVAR RTO
初值 — 100.0000 50.0000 300.0000
样本 110 101.2500 40.0000 261.2500
样本 90 99.8438 32.8125 231.0938
样本 120 102.3633 29.6484 220.9570
★ 顺序不能反: 先用"旧 SRTT"更新 RTTVAR, 再更新 SRTT
对照 —— 若简单用"2 x 最新 RTT"当 RTO:
RTT 110 -> RTO 220
RTT 90 -> RTO 180
RTT 120 -> RTO 240
抖动幅度 = 240 - 180 = 60 ms
平滑后的 RTO = 261.2500 / 231.0938 / 220.9570 (单调平滑, 不随单个样本发抖)
=== ④ 快速重传 (段 2 丢失) ===
发送: 段1 [1,100], 段2 [101,200], 段3 [201,300], 段4 [301,400], 段5 [401,500]
★ 段2 [101,200] 在路上丢了
收到 是否连续 回 ACK 说明
段1 连续 101 正常确认 (连续前缀到 100)
段3 不连续 101 重复 ACK 1 (前缀仍到 100)
段4 不连续 101 重复 ACK 2 (前缀仍到 100)
段5 不连续 101 重复 ACK 3 (前缀仍到 100)
收到第 3 个重复 ACK -> 立即重传段2 [101,200], 不等 RTO 超时
★ 段3/4/5 已到达但"交付不了", 缓存在接收方窗口里
★ 若开 SACK, 接收方可告知"已零散收到 201~500", 发送方只补 101~200
=== ⑤ MSS、效率与吞吐率 ===
MSS = MTU 1500 - IP 20 - TCP 20 = 1460 B
载荷(B) TCP 总长 开销率 说明
100 140 28.57%
512 552 7.25%
1000 1040 3.85%
1460 1500 2.67% 达到 MSS
RTT = 40 ms 时的吞吐率:
窗口(B) 吞吐率(B/s) 吞吐率(Mbps) 说明
65535 1638375 13.107 窗口字段上限
524280 13107000 104.856 带窗口扩大选项
5000000 125000000 1000.000 跑满 1 Gbps 所需
带宽时延积 = 1 Gbps x 40 ms = 40 Mbit = 5000000 B
★ 窗口 >= 带宽时延积 才可能跑满带宽; 65535 B 只够 13.1 Mbps
=== ⑥ 三种可靠性工具的分工 ===
累积确认 只说"连续收到哪儿" 丢一段则后面全要重传
超时重传 RTO 到期未确认就重发 RTO 靠 SRTT/RTTVAR 自适应
快速重传 3 个重复 ACK 就重发 不等超时; SACK 可减少重传量
★ 选择重传靠 SACK 选项实现, 与 net/22-window.md 的"SR 协议"不是一回事
=== ⑦ TCP 与 UDP 首部对照 ===
固定长度 8 B 20 B
最长 8 B (无选项) 60 B (选项 40 B)
字段数 4 个 10 个
伪首部协议号 17 6
校验和算法 16 位反码求和 16 位反码求和 (相同)
校验和可选性 IPv4 可置 0, IPv6 必须算 必须计算七条结论:
- ★ TCP 首部固定 20 B、带选项最长 60 B:数据偏移单位是 4 B(
0x50就是"无选项"的标记)。 - ★ 序号是"字节编号"不是"报文编号":确认号 = "期望的下一个字节编号",天然是累积确认。
- ★★ 锚点推算:SYN(序号 1000)占掉一格 → 数据从 1001 起;发 200 B 占 [1001, 1200]、下一序号 1201、对方确认号 1201;再发 500 B 占 [1201, 1700]。
- ★ RTO 锚点:样本 100 → 110 → 90 → 120 得到
——平滑效果就是"不随单个样本发抖"(朴素算法会在 之间跳,抖动 60 ms)。 - ★ 计算顺序不能反:先用旧 SRTT 更新 RTTVAR,再更新 SRTT;初值 SRTT = 首样本、RTTVAR = 首样本的一半。
- ★ 快速重传的触发点在"3 个重复 ACK":段 3/4/5 到达后只能回同一个 ACK = 101——这是"累积确认"的直接后果,也是 SACK 要补的短板。
- ★ MSS = MTU − 40 = 1460 B;窗口 65535 B / RTT 40 ms 只有 13.107 Mbps——1 Gbps 需要 5 MB 窗口,所以必须有"窗口扩大选项"。
考点
考点
1. 必背结论
- ★ TCP 首部固定 20 B、最长 60 B;共 10 个字段 + 选项(最长 40 B)。
- ★ 数据偏移单位 4 B:最小 5(20 B)、最大 15(60 B)——与 IP 的"首部长度"是同一种设计。
- ★ 窗口字段 16 位 → 最大 65535 B(高带宽时延积链路需要"窗口扩大选项")。
- ★ 六个标志位:URG(紧急指针有效)/ ACK(确认号有效,连接建立后恒 1)/ PSH(推送)/ RST(复位)/ SYN(建立连接)/ FIN(释放连接)。
- ★ 检验和含伪首部(协议号 6),算法与 UDP 完全相同(16 位反码求和,算出 0 也填 0xFFFF)。
- ★★ 序号 = 本报文段数据第一个字节的编号;确认号 = 期望收到的下一个字节编号(累积确认)。
- ★★ SYN 与 FIN 各占 1 个序号(不携带数据也要占一格)。
- ★★ 锚点推算:ISN = 1000 → SYN 占 1000 → 数据从 1001;发 200 B 占 [1001, 1200]、下一序号 1201;再发 500 B 占 [1201, 1700]。
- ★ 初始序号 ISN 必须随机(防伪造);序号 32 位、可回绕。
- ★ 可靠传输三工具:累积确认 + 超时重传 + 快速重传;快速重传的触发是"3 个重复 ACK"。
- ★ RTO 自适应公式:
、 、 , 、 。 - ★★ RTO 锚点:样本 100/110/90/120 → RTO
;初值 SRTT = 首样本、RTTVAR = 首样本的一半。 - ★ 顺序要求:先更新 RTTVAR(用旧 SRTT),再更新 SRTT。
- ★ Karn 算法:重传过的报文段不用于测量 RTT;每次重传 RTO 翻倍(退避)。
- ★ SACK = 选择确认选项:让接收方说明"零散收到了哪些区间",避免整片重传。
- ★ MSS = MTU − 20 − 20 = 1460 B(MSS 只算数据部分,MTU 算整包);双方在 SYN 里各报 MSS,取较小者。
- ★ 吞吐率
;带宽时延积 = 带宽 × RTT;窗口 ≥ 带宽时延积 才可能跑满带宽。 - ★ 锚点:窗口 65535 B、RTT 40 ms → 13.107 Mbps;跑满 1 Gbps 需 5 MB 窗口。
- ★ 流量控制 vs 拥塞控制:前者由接收方决定(rwnd),后者由发送方决定(cwnd);实际可发量 =
。
2. 高频陷阱
- 把 TCP 首部记成 8 B:错。是 20 B 固定(8 B 是 UDP)。
- 把"数据偏移"字段当成字节数:错。单位是 4 B——值 5 表示 20 B。
- 认为 TCP 首部长度是固定的:不准确。固定部分是 20 B,但带选项最长可达 60 B——"20 B 固定"指的是"无选项时"。
- 把"序号"当成"报文段编号":错。它是字节编号——所以一个报文段的序号是"它第一个数据字节的编号"。
- 忘记"SYN 占 1 个序号":错,这是最爱考的细节——锚点里 SYN 序号 1000 之后,数据从 1001 开始。
- 忘记"FIN 也占 1 个序号":错。FIN 同样占一格——所以四次挥手后确认号也要加 1。
- 确认号理解成"我收到了这个编号":错。它是"期望收到的下一个编号"——收到 1001~1200 后,确认号是 1201 而不是 1200。
- 认为"确认号只能确认一个字节":错。它是累积确认——确认号 1201 表示"
的字节全收到了"。 - 把"ACK 位"与"确认号"混为一谈:要分清。ACK 位是"确认号是否有效"的标志;确认号是那个具体的数字。
- 认为"RTO 是固定值":错。RTO 由 SRTT 与 RTTVAR 自适应算出——这正是"自适应重传"。
- RTTVAR 公式里忘了绝对值:错。是
——不加绝对值会让方差算出负数。 - 更新的顺序写反:错。先用旧 SRTT 更新 RTTVAR,再更新 SRTT——顺序反了数值不同。
- 认为"重传过的报文段也能用来测 RTT":错。Karn 算法明确规定不能——因为无法判断确认的是原报文还是重传报文。
- 认为"快速重传要等 RTO 超时":错。收到 3 个重复 ACK 就立即重传——这是它的全部意义。
- 把"3 个重复 ACK"记成"2 个"或"任意个":错。标准是 3 个(用来排除乱序造成的假丢失)。
- 把 MSS 与 MTU 等同:错。MSS = MTU − 40(IPv4 + 无选项 TCP);MSS 只是数据部分长度。
- 把 TCP 的 SACK 与链路层的 SR 协议混为一谈:要分清。SACK 是 TCP 的一个选项;SR 是链路层的滑动窗口协议(见
net/22-window.md)。 - 认为"窗口字段能表达任意大的窗口":错。只有 16 位、最大 65535 B——再大要靠窗口扩大选项。
3. 解题模板("TCP 计算题")
① 首部解析题:
前 4 B = 源端口 + 目的端口
接着 4 B = 序号; 再 4 B = 确认号
第 13 字节高 4 位 x 4 = 首部长度
第 14 字节的 6 个低位: URG ACK PSH RST SYN FIN
接着 2 B = 窗口; 再 2 B = 检验和; 再 2 B = 紧急指针
② 序号推算题(牢记 SYN/FIN 各占 1):
设 ISN = X
SYN 后: 数据第一个字节序号 = X + 1
发 L 字节: 占用 [X+1, X+L], 下一序号 = X + L + 1
接收方确认号 = X + L + 1
再发 M 字节: 占用 [X+L+1, X+L+M]
收到区间 [a, b] 时的确认号 = b + 1 (连续情形)
乱序/缺失时 -> 重复回"缺失处的起始序号"
③ RTO 计算题:
初值: SRTT = 首个 RRT 样本, RTTVAR = 首个样本 / 2
每来一个样本 R:
RTTVAR = (1 - beta) x RTTVAR + beta x |SRTT - R| <- 用旧 SRTT
SRTT = (1 - alpha) x SRTT + alpha x R
RTO = SRTT + 4 x RTTVAR
★ alpha = 1/8, beta = 1/4
④ 吞吐率题:
吞吐率 = 窗口 / RTT (单位配平: bit/s 与 B/s 差 8)
带宽时延积 = 带宽 x RTT -> 窗口至少要这么大才能跑满
MSS = MTU - 40
⑤ 标志位判断题:
SYN=1, ACK=0 -> 连接请求 (第一次握手)
SYN=1, ACK=1 -> 连接接受 (第二次握手)
ACK=1, FIN=1 -> 释放请求
RST=1 -> 复位
★ 连接建立后, 所有报文段 ACK = 14. 与相邻章节的接口
net/40-udp.md(UDP):首部 8 B vs 20 B、面向报文 vs 面向字节流、无流控 vs 有流控——本章的每一项都是上一章的对照项;两者的检验和算法完全相同(伪首部里的协议号 17 → 6)。net/42-handshake.md(三次握手与四次挥手):本章的"SYN 与 FIN 各占 1 个序号"是下一章算"握手后序号从哪开始"的关键;"SYN=1 且 ACK=0"这个指纹正是下一章识别第一次握手的依据。net/43-flow.md(流量控制):本章的"窗口字段 16 位、最大 65535"是下一章"零窗口与窗口探测"的前提;窗口扩大选项的作用也在那一章展开。net/44-congestion.md(拥塞控制):本章"重传的 RTO 会翻倍(退避)"与下一章的"超时后 cwnd 回到慢开始"是同一次事件的两个侧面;"3 个重复 ACK"既触发快速重传,也触发快速恢复。net/22-window.md(滑动窗口):链路层的停止-等待 / GBN / SR 是同一套思想的"简化版";TCP 的累积确认对应 GBN、SACK 对应 SR——但 TCP 的窗口按"字节"滑动,而链路层按"帧"滑动,这是最本质的差别。net/30-ip.md(IP 数据报):MSS 与 MTU 的 40 B 差额,正是"IP 首部 20 B + TCP 首部 20 B"——两章的字段在这里合流;"IP 分片有害"也正靠 MSS 协商来避免。net/21-error.md(差错控制):"TCP 检验和能检错、不能纠错"与链路层 CRC 的分工一致——纠错靠的是"重传"这个更高层的动作,而不是编码本身。
小结
- ★ TCP 首部固定 20 B、最长 60 B;10 个字段 + 选项;数据偏移单位 4 B(
0x50标识无选项)。 - ★ 六个标志位:URG / ACK / PSH / RST / SYN / FIN;连接建立后 ACK 恒为 1。
- ★ 检验和含伪首部(协议号 6),算法同 UDP。
- ★★ 序号 = 数据第一个字节的编号;确认号 = 期望的下一个字节编号——累积确认。
- ★★ SYN 与 FIN 各占 1 个序号;锚点:ISN 1000 → 数据从 1001;发 200 B 占 [1001, 1200],下一序号与对方确认号都是 1201。
- ★ 可靠传输三工具:累积确认 / 超时重传(自适应 RTO)/ 快速重传(3 个重复 ACK)。
- ★★ RTO 公式:
, 、 ;锚点得 。 - ★ 顺序:先更新 RTTVAR(用旧 SRTT)再更新 SRTT;Karn 算法让重传段不参与 RTT 测量、且 RTO 翻倍退避。
- ★ MSS = MTU − 40 = 1460 B;吞吐率 = 窗口 / RTT,窗口 65535 B / 40 ms = 13.107 Mbps。
- ★ 流量控制(rwnd,接收方定)与拥塞控制(cwnd,发送方定)取小——分别在
net/43-flow.md与net/44-congestion.md展开。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。