Appearance
UDP:报文结构与校验
概念
UDP(User Datagram Protocol,用户数据报协议)是传输层两个协议里"极简"的那一个:它把应用层交下来的数据前面加 8 个字节的首部就发出去,不建连接、不保证可靠、不排顺序、不管拥塞。
一句话说清它是什么:UDP 是"把报文打到网络上就完事"的传输层协议——它唯一的服务是"用端口号把数据交给正确的进程",外加一个可选的数据校验。
★ 本章要解决的三件事:
| 问题 | 对应内容 |
|---|---|
| 首部长什么样 | 固定 8 B、4 个字段 |
| 检验和怎么算 | 12 B 伪首部 + 16 位反码求和 |
| 什么时候用它 | 短报文、实时、可容忍丢失、广播/组播 |
与上一章的衔接:上一章(net/34-ipv6.md)说"IPv6 取消了首部检验和,传输层成了唯一一层校验"——这就是为什么 UDP 在 IPv6 上必须计算检验和,而在 IPv4 上可以偷懒置 0;"协议号 17 表示 UDP"这个值来自 net/30-ip.md,本章它会出现在伪首部里。
原理
一、首部 8 B、4 个字段
★ UDP 首部结构(必背,只有 8 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 位 │
├───────────────────────────────┼───────────────────────────────┤
│ UDP 长度 (首部+数据) │ 检验和 │ <- 4 B
│ 16 位 │ 16 位 │
├───────────────────────────────┴───────────────────────────────┤
│ 数据 (应用层交下来的报文) │
└───────────────────────────────────────────────────────────────┘
★ 首部固定 8 B, 没有"首部长度"字段 (它不可能变长)
★ 也没有"选项", 所以永远 8 B| 字段 | 位数 | 含义 | 易错点 |
|---|---|---|---|
| 源端口 | 16 | 发送方进程的端口 | 可选(不需要回复时可填 0) |
| 目的端口 | 16 | 接收方进程的端口 | 必需 |
| 长度 | 16 | 首部 + 数据的总字节数 | ★ 最小值 8(只有首部、没有数据) |
| 检验和 | 16 | 整个 UDP 报文的校验(算上了伪首部) | ★ IPv4 里可置 0 表示"不校验";IPv6 里必须算 |
★ 由字段宽度直接推出的三条结论:
| 结论 | 推导 |
|---|---|
| UDP 报文最短 8 B | 长度字段最小值 = 首部 8 B(数据长度 0 是合法的) |
| UDP 报文最长 65535 B | 长度字段 16 位 → |
| UDP 数据最长 65527 B |
⚠️ "面向报文"(message-oriented)是 UDP 最本质的语义:应用层交下来一个 100 B 的报文,UDP 就发一个 100 B 的 UDP 数据报;交下来 1000 B 就发一个 1000 B 的——UDP 既不合并(把两个小报文并成一个大报文)、也不拆分(把一个大报文切成两段)。 所以"UDP 保留报文边界":接收方一次
recvfrom拿到的就是一个完整的应用层报文。 TCP 正相反:它是字节流,不保留任何边界(见net/41-tcp.md)。
⚠️ "面向报文"带来的两个必考后果:① 应用层给的数据太长,UDP 交给 IP 层去分片——丢一片整个报文就废了;② 应用层给的数据太短,UDP 也照发——发 10 个小报文就是 10 次网络打扰,路由器与主机的开销都上去了。 所以"应用层自己掌握合适的报文大小"是 UDP 编程的基本要求。
二、检验和:12 B 伪首部 + 16 位反码求和
★ 伪首部(pseudo-header)是本章最需要看清楚的概念:
text
伪首部 12 B (只参与校验和计算, 不传输, 接收方自己拼出来):
┌───────────────────────────────────────────────┐
│ 源 IP 地址 (4 B) │
├───────────────────────────────────────────────┤
│ 目的 IP 地址 (4 B) │
├───────────────┬───────────────────────────────┤
│ 全 0 (1 B) │ 协议号 17 (1 B) │
├───────────────┴───────────────────────────────┤
│ UDP 长度 (2 B, 与首部里那个相同) │
└───────────────────────────────────────────────┘
★ 它不出现在网络上 —— 双方各自"凭空拼出"这 12 B 来算校验和
★ 为什么要有它: 让校验和覆盖"这两个地址 + 这个协议", 从而能查出
"包被送到了错误的目的地址/错误的协议" 这类差错
★ 协议号 17 就是 net/30-ip.md 里 IP 首部那个 "17 = UDP"★ 16 位反码求和(ones-complement sum)的三个步骤:
text
第①步 按 16 位一组(2 B)把"伪首部 + UDP 首部 + 数据"全部加起来
★ 若最后剩下 1 个字节, 就"高位补 0"当成一个 16 位字加上去
第②步 若和超过 16 位, 把高 16 位"回卷"(折返)加回低 16 位, 直到不再溢出
第③步 取反 (按位求反), 结果就是检验和
★ 若结果为 0, 则填 0xFFFF (因为 0 被保留表示"没有校验和")★ 锚点(本章定死):源 192.168.1.100:5000 → 目的 192.168.1.1:5001,数据 12 B
text
源 IP = 192.168.1.100 = 0xC0A8 0164
目的 IP = 192.168.1.1 = 0xC0A8 0101
协议号 17 = 0x11
UDP 首部: 源端口 5000 = 0x1388, 目的端口 5001 = 0x1389,
长度 = 8(首部) + 12(数据) = 20 = 0x0014, 检验和先用 0x0000
数据 12 B: 01 02 03 04 05 06 07 08 09 0A 0B 0C
拼出 32 B (12 + 8 + 12), 按 16 位切成 16 个字:
┌──────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 序号 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
│ 值 │ 0xC0A8 │ 0x0164 │ 0xC0A8 │ 0x0101 │ 0x0011 │ 0x0014 │
│ 含义 │ 源IP前 │ 源IP后 │ 目的前 │ 目的后 │ 0+协议 │ UDP长 │
├──────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ 序号 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │
│ 值 │ 0x1388 │ 0x1389 │ 0x0014 │ 0x0000 │ 0x0102 │ 0x0304 │
│ 含义 │ 源端口 │ 目的端 │ UDP长 │ 校验和 │ 数据 1 │ 数据 2 │
├──────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ 序号 │ 13 │ 14 │ 15 │ 16 │ │ │
│ 值 │ 0x0506 │ 0x0708 │ 0x090A │ 0x0B0C │ │ │
│ 含义 │ 数据 3 │ 数据 4 │ 数据 5 │ 数据 6 │ │ │
└──────┴────────┴────────┴────────┴────────┴────────┴────────┘
第①步 累加这 16 个字 = 118569 = 0x1CF29
第②步 回卷: 0x0001 + 0xCF2A = 0xCF2A (只回卷一次就够)
第③步 取反: ~0xCF2A = 0x30D5
-> 检验和 = 0x30D5
★ 验证: 把 0x30D5 也算进去再累加一次 -> 118569 + 12501 = 131070 = 0x1FFFE
回卷: 0x0001 + 0xFFFE = 0xFFFF -> 全 1 表示"校验通过"⚠️ "算出的检验和是 0 就填 0xFFFF"这条为什么必要:因为"检验和字段为 0"已经被占用为"发送方没有计算校验和"这个特殊含义(IPv4 允许不算、填 0;IPv6 不允许)——若真算出来恰好是
0x0000,接收方就无法区分"算出来正好是 0"和"发送方压根没算"。 按 RFC 768 的规定,这时发送方填0xFFFF(全 1 与全 0 在反码里互为相反数,校验逻辑仍然成立)。
⚠️ 为什么"反码求和 + 回卷 + 取反"能校验:因为它是一种"和为零"的校验——接收方把所有字(含检验和)相加,若结果全 1(反码意义上的 0),就说明没出错。 这种算法的好处是"与字节序无关"(大端小端算出来一样),坏处是"检错能力不如 CRC"(它查不出某些成对的差错),但对传输层来说够用了(链路层的 CRC 与
net/21-error.md讲的编码已经在那层兜着)。
三、端口:16 位的三段划分
端口号 16 位,范围 0 ~ 65535,按用途分三段:
| 段 | 范围 | 名称 | 说明 |
|---|---|---|---|
| ① | 0 ~ 1023 | 熟知端口(well-known) | 由 IANA 统一分配,固定给标准服务 |
| ② | 1024 ~ 49151 | 登记端口(registered) | 向 IANA 登记后使用,避免重名 |
| ③ | 49152 ~ 65535 | 动态端口(ephemeral) | 客户端临时用,用完即释放 |
★ 必须记住的熟知端口(本章只列常考的):
| 端口 | 协议 | 传输层 |
|---|---|---|
| 53 | DNS | UDP(查询)/ TCP(区域传送) |
| 67 / 68 | DHCP 服务端 / 客户端 | UDP |
| 69 | TFTP | UDP |
| 161 / 162 | SNMP / SNMP Trap | UDP |
| 123 | NTP | UDP |
| 520 | RIP | UDP |
⚠️ "客户端用动态端口、服务器用熟知端口"这个分工决定了 NAPT 表怎么建:内网主机的源端口是动态端口(如 1025),目的端口是服务器的熟知端口(如 80)——
net/32-nat.md的 NAPT 表项键正是"这一对端口 + 一对 IP"。⚠️ 一个常考的细节:一个端口只能被一个进程绑定(同一个传输层协议内),但 TCP 的 80 与 UDP 的 80 是两个互不相干的端口——因为端口空间按协议分开。
四、UDP 的四条性质与适用场合
★ UDP 与 TCP 的对照(这是本章最该记住的一张表):
| 对比项 | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接(三次握手) |
| 可靠性 | 尽最大努力交付 | 可靠(确认 + 重传 + 排序) |
| 报文边界 | ★ 保留(面向报文) | 不保留(面向字节流) |
| 首部开销 | 8 B | 20 B(最长 60 B) |
| 流量控制 | 无 | 有(滑动窗口) |
| 拥塞控制 | 无 | 有(慢开始、拥塞避免…) |
| 传输方式 | 单播 / 组播 / 广播 | 仅单播 |
| 顺序 | 不保证 | 保证按序交付 |
| 速度 | 快(没有建连与确认开销) | 慢(有握手、确认、重传) |
★ "UDP 支持多播与广播"的原因很直接:它没有连接状态——不需要"对端是谁、握手过没有"这些前提,所以一个报文可以同时发给一组接收者。 TCP 因为要维护"一条连接的收发状态",天然只能一对一。
★ UDP 的适用场合(记住"三快一广播"):
| 场合 | 例子 | 理由 |
|---|---|---|
| 短报文查询/应答 | DNS、SNMP | 一次请求一次应答,建连接的代价比传输本身还大 |
| 实时音视频 | RTP / VoIP / 直播 | 迟到的包毫无价值,宁可丢也不等 |
| 组播 / 广播 | DHCP、路由协议(RIP)、IPTV | TCP 做不到一对多 |
| 简单的文件传送 | TFTP | 协议极简,出错就重来 |
★ 不适合 UDP 的场合:大文件传输、需要可靠有序交付的业务(网页、邮件、文件下载)——这些必须用 TCP。
⚠️ "UDP 不可靠" ≠ "用 UDP 就一定丢数据":可靠性可以"在应用层自己补"——很多现代协议(QUIC、部分游戏协议)就是在 UDP 上自己实现了确认与重传。 选 UDP 的理由是"我要的是它的传输语义(无连接、保留边界、支持多播),可靠性我自己控制"。 这句话在考试里通常以"为什么 DNS 用 UDP"或"为什么视频直播用 UDP"的形式出现。
示例
例 1:C 实现——UDP 伪首部与检验和计算
参数:源
192.168.1.100:5000、目的192.168.1.1:5001、数据 12 B(01 02 … 0C)。
#include <stdio.h>
static unsigned words[32];
static int nw;
/* 16 位反码求和: 累加 -> 进位回卷 -> 返回 16 位和 (未取反) */
unsigned short ones_sum(const unsigned char *b, int len) {
unsigned sum = 0;
int i;
nw = 0;
for (i = 0; i + 1 < len; i += 2) {
unsigned w = (unsigned)((b[i] << 8) | b[i + 1]);
words[nw++] = w;
sum += w;
}
if (i < len) { /* 奇数字节: 高位补 0 当作一个字 */
unsigned w = (unsigned)(b[i] << 8);
words[nw++] = w;
sum += w;
}
while (sum >> 16) sum = (sum & 0xFFFFu) + (sum >> 16);
return (unsigned short)(sum & 0xFFFFu);
}
int main(void) {
unsigned char b[32];
unsigned char srcip[4] = {192, 168, 1, 100};
unsigned char dstip[4] = {192, 168, 1, 1};
unsigned char udp[20] = {0x13, 0x88, /* 源端口 5000 */
0x13, 0x89, /* 目的端口 5001 */
0x00, 0x14, /* 长度 20 */
0x00, 0x00, /* 校验和 置 0 */
0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};
const char *nm[16] = {"源 IP 前 16", "源 IP 后 16", "目的 IP 前 16", "目的 IP 后 16",
"全 0 + 协议 17", "UDP 长度 20", "源端口 5000", "目的端口 5001",
"UDP 长度 20", "校验和(置 0)", "数据 1", "数据 2",
"数据 3", "数据 4", "数据 5", "数据 6"};
unsigned short s, ck;
unsigned long raw = 0;
int i, k = 0;
for (i = 0; i < 4; i++) b[k++] = srcip[i];
for (i = 0; i < 4; i++) b[k++] = dstip[i];
b[k++] = 0x00; b[k++] = 0x11; /* 全 0 + 协议号 17 */
b[k++] = 0x00; b[k++] = 0x14; /* UDP 长度 20 */
for (i = 0; i < 20; i++) b[k++] = udp[i];
printf("① 参与校验的 16 位字 (共 %d B = %d 个字)\n", k, k / 2);
s = ones_sum(b, k);
for (i = 0; i < nw; i++) {
raw += words[i];
printf(" 字 %2d: 0x%04X %s\n", i + 1, words[i], nm[i]);
}
printf(" ---- 累加(未回卷) = %lu = 0x%lX\n", raw, raw);
printf("\n");
printf("② 进位回卷\n");
printf(" 0x%X 高 16 位 0x%04lX 回卷加回低 16 位 0x%04lX\n",
(unsigned)raw, (raw >> 16) & 0xFFFFu, raw & 0xFFFFu);
printf(" 回卷后和 = 0x%04X\n", s);
printf("\n");
printf("③ 取反得检验和\n");
ck = (unsigned short)(~s & 0xFFFFu);
printf(" ~0x%04X = 0x%04X\n", s, ck);
printf(" 检验和 = 0x%04X (若算出为 0x0000 则改填 0xFFFF)\n", ck);
printf("\n");
printf("④ 接收方验证: 把检验和填回再算一次\n");
b[18] = (unsigned char)((ck >> 8) & 0xFF); /* 校验和高字节: 12 + 6 */
b[19] = (unsigned char)(ck & 0xFF); /* 校验和低字节: 12 + 7 */
s = ones_sum(b, k);
printf(" 含检验和的 16 位和 = 0x%04X\n", s);
printf(" %s\n", s == 0xFFFFu ? "结果为 0xFFFF -> 校验通过" : "校验失败");
printf("\n");
printf("⑤ 首部开销对照 (载荷 100 B)\n");
printf(" UDP: 8 + 20(IP) + 100 = %d B, 开销率 %.2f%%\n",
8 + 20 + 100, 28 * 100.0 / 128.0);
printf(" TCP: 20 + 20(IP) + 100 = %d B, 开销率 %.2f%%\n",
20 + 20 + 100, 40 * 100.0 / 140.0);
printf(" ★ 载荷 1460 B 时 (MSS): UDP 效率 %.2f%%, TCP 效率 %.2f%%\n",
1460 * 100.0 / 1460.0, 1460 * 100.0 / 1500.0);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
① 参与校验的 16 位字 (共 32 B = 16 个字)
字 1: 0xC0A8 源 IP 前 16
字 2: 0x0164 源 IP 后 16
字 3: 0xC0A8 目的 IP 前 16
字 4: 0x0101 目的 IP 后 16
字 5: 0x0011 全 0 + 协议 17
字 6: 0x0014 UDP 长度 20
字 7: 0x1388 源端口 5000
字 8: 0x1389 目的端口 5001
字 9: 0x0014 UDP 长度 20
字 10: 0x0000 校验和(置 0)
字 11: 0x0102 数据 1
字 12: 0x0304 数据 2
字 13: 0x0506 数据 3
字 14: 0x0708 数据 4
字 15: 0x090A 数据 5
字 16: 0x0B0C 数据 6
---- 累加(未回卷) = 118569 = 0x1CF29
② 进位回卷
0x1CF29 高 16 位 0x0001 回卷加回低 16 位 0xCF29
回卷后和 = 0xCF2A
③ 取反得检验和
~0xCF2A = 0x30D5
检验和 = 0x30D5 (若算出为 0x0000 则改填 0xFFFF)
④ 接收方验证: 把检验和填回再算一次
含检验和的 16 位和 = 0xFFFF
结果为 0xFFFF -> 校验通过
⑤ 首部开销对照 (载荷 100 B)
UDP: 8 + 20(IP) + 100 = 128 B, 开销率 21.88%
TCP: 20 + 20(IP) + 100 = 140 B, 开销率 28.57%
★ 载荷 1460 B 时 (MSS): UDP 效率 100.00%, TCP 效率 97.33%⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);这句可能要执行两次:第一次回卷后仍可能再产生进位(例如和是0x2FFFF:回卷得0x0001 + 0xFFFF = 0x10000,还要再回卷一次);用while而不是if是标准写法。 锚点里 118569 只回卷了一次就到位。⑤里那句"UDP 效率 100.00%"是刻意留的对照:因为 1460 B 是 TCP 的 MSS(MTU 1500 − 20 IP − 20 TCP),UDP 没有 MSS 这个概念,它的载荷上限由"IP 数据报 65535 − 20 − 8"决定——这里的1460/1460只是为了在同一个载荷下对齐口径,真实 UDP 应用会自己选报文大小(如 DNS 的 512 B 或 EDNS 的 4096 B)。
例 2:Python——校验和逐步演算、端口表与开销账
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
def words_of(b):
out = []
i = 0
while i + 1 < len(b):
out.append((b[i] << 8) | b[i + 1])
i += 2
if i < len(b):
out.append(b[i] << 8)
return out
def ones_sum(ws):
raw = sum(ws)
s = raw
folds = 0
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
folds += 1
return raw, s, folds
print('=== ① UDP 首部 4 个字段 ===')
for a, b, c in [('源端口', '16 位', '发送方进程端口, 不需回复时可填 0'),
('目的端口', '16 位', '接收方进程端口, 必需'),
('长度', '16 位', '首部 + 数据; ★ 最小 8, 最大 65535'),
('检验和', '16 位', '★ 含伪首部; IPv4 可置 0, IPv6 必须算')]:
print(' ' + pad(a, 10) + pad(b, 10) + c)
print(' ★ 首部固定 8 B, 无"首部长度"与"选项"字段')
print(' ★ 数据最大 = 65535 - 8 = %d B' % (65535 - 8))
print()
print('=== ② 锚点: 12 B 伪首部 + 8 B 首部 + 12 B 数据 ===')
src = bytes([192, 168, 1, 100])
dst = bytes([192, 168, 1, 1])
pseudo = src + dst + bytes([0x00, 0x11]) + (20).to_bytes(2, 'big')
udp = bytes([0x13, 0x88, 0x13, 0x89, 0x00, 0x14, 0x00, 0x00,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C])
buf = pseudo + udp
print(' 伪首部 = 源IP 4 B + 目的IP 4 B + 全0 1 B + 协议号 17 1 B + UDP 长度 2 B = 12 B')
print(' 参与校验总长 = 12 + 8 + 12 = %d B -> %d 个字' % (len(buf), len(words_of(buf))))
print(' ' + pad('序号', 6) + pad('值', 10) + '来源')
label = ['源 IP 前 16', '源 IP 后 16', '目的 IP 前 16', '目的 IP 后 16',
'全 0 + 协议 17', 'UDP 长度', '源端口 5000', '目的端口 5001',
'UDP 长度', '检验和(置 0)', '数据 1', '数据 2', '数据 3', '数据 4',
'数据 5', '数据 6']
ws = words_of(buf)
raw, s, folds = ones_sum(ws)
for i, w in enumerate(ws):
print(' ' + pad(str(i + 1), 6) + pad('0x%04X' % w, 10) + label[i])
print(' 累加(未回卷) = %d = 0x%X' % (raw, raw))
print(' 回卷 %d 次 -> 和 = 0x%04X' % (folds, s))
ck = (~s) & 0xFFFF
print(' 取反 -> 检验和 = 0x%04X' % ck)
print(' ★ 若检验和算出 0x0000, 必须改填 0xFFFF (0 已被"不校验"占用)')
print()
print('=== ③ 接收方验证 (把检验和填回再算) ===')
check = pseudo + udp[:6] + ck.to_bytes(2, 'big') + udp[8:]
raw2, s2, folds2 = ones_sum(words_of(check))
print(' 含检验和的累加 = 0x%X, 回卷 %d 次后 = 0x%04X' % (raw2, folds2, s2))
print(' 0x%04X %s' % (s2, '-> 全 1, 校验通过' if s2 == 0xFFFF else '-> 校验失败'))
print(' ★ 反码求和的落点就是"和全 1 即通过", 且与字节序无关')
print()
print('=== ④ 端口三段划分 ===')
for a, b, c in [('0 ~ 1023', '熟知端口', 'IANA 统一分配, 固定给标准服务'),
('1024 ~ 49151', '登记端口', '登记后使用, 避免重名'),
('49152 ~ 65535', '动态端口', '客户端临时用, 用完释放')]:
print(' ' + pad(a, 16) + pad(b, 12) + c)
print(' 共 %d 个端口; 熟知端口占 %.2f%%' % (65536, 1024 * 100.0 / 65536))
print()
print('=== ⑤ 常用熟知端口 (UDP 相) ===')
for p, n, t in [('53', 'DNS', '查询用 UDP, 区域传送用 TCP'),
('67 / 68', 'DHCP 服务端 / 客户端', 'UDP'),
('69', 'TFTP', 'UDP'),
('123', 'NTP', 'UDP'),
('161 / 162', 'SNMP / Trap', 'UDP'),
('520', 'RIP', 'UDP')]:
print(' ' + pad(p, 12) + pad(n, 26) + t)
print()
print('=== ⑥ UDP 与 TCP 对照 ===')
for a, b, c in [('连接', '无连接', '面向连接 (三次握手)'),
('可靠性', '尽最大努力交付', '确认 + 重传 + 排序'),
('报文边界', '★ 保留 (面向报文)', '不保留 (面向字节流)'),
('首部', '8 B', '20 B (最长 60 B)'),
('流量控制', '无', '滑动窗口'),
('拥塞控制', '无', '慢开始 / 拥塞避免 / 快重传 / 快恢复'),
('传输方式', '单播 / 组播 / 广播', '仅单播')]:
print(' ' + pad(a, 12) + pad(b, 22) + c)
print()
print('=== ⑦ 首部开销账 ===')
print(' ' + pad('载荷(B)', 10) + pad('UDP 总长', 12) + pad('UDP 开销率', 14)
+ pad('TCP 总长', 12) + 'TCP 开销率')
for pl in [12, 100, 512, 1000, 1460]:
u = pl + 8 + 20
t = pl + 20 + 20
print(' ' + pad(str(pl), 10) + pad(str(u), 12)
+ pad('%.2f%%' % ((u - pl) * 100.0 / u), 14)
+ pad(str(t), 12) + '%.2f%%' % ((t - pl) * 100.0 / t))
print(' ★ 载荷越大, 两种协议的开销率都越小 -> 小报文才需要在意 8 B 还是 20 B')
print(' ★ MTU 1500 下: UDP 可塞 %d B 载荷, TCP 受 MSS 限制只能 %d B'
% (1500 - 20 - 8, 1500 - 20 - 20))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① UDP 首部 4 个字段 ===
源端口 16 位 发送方进程端口, 不需回复时可填 0
目的端口 16 位 接收方进程端口, 必需
长度 16 位 首部 + 数据; ★ 最小 8, 最大 65535
检验和 16 位 ★ 含伪首部; IPv4 可置 0, IPv6 必须算
★ 首部固定 8 B, 无"首部长度"与"选项"字段
★ 数据最大 = 65535 - 8 = 65527 B
=== ② 锚点: 12 B 伪首部 + 8 B 首部 + 12 B 数据 ===
伪首部 = 源IP 4 B + 目的IP 4 B + 全0 1 B + 协议号 17 1 B + UDP 长度 2 B = 12 B
参与校验总长 = 12 + 8 + 12 = 32 B -> 16 个字
序号 值 来源
1 0xC0A8 源 IP 前 16
2 0x0164 源 IP 后 16
3 0xC0A8 目的 IP 前 16
4 0x0101 目的 IP 后 16
5 0x0011 全 0 + 协议 17
6 0x0014 UDP 长度
7 0x1388 源端口 5000
8 0x1389 目的端口 5001
9 0x0014 UDP 长度
10 0x0000 检验和(置 0)
11 0x0102 数据 1
12 0x0304 数据 2
13 0x0506 数据 3
14 0x0708 数据 4
15 0x090A 数据 5
16 0x0B0C 数据 6
累加(未回卷) = 118569 = 0x1CF29
回卷 1 次 -> 和 = 0xCF2A
取反 -> 检验和 = 0x30D5
★ 若检验和算出 0x0000, 必须改填 0xFFFF (0 已被"不校验"占用)
=== ③ 接收方验证 (把检验和填回再算) ===
含检验和的累加 = 0x1FFFE, 回卷 1 次后 = 0xFFFF
0xFFFF -> 全 1, 校验通过
★ 反码求和的落点就是"和全 1 即通过", 且与字节序无关
=== ④ 端口三段划分 ===
0 ~ 1023 熟知端口 IANA 统一分配, 固定给标准服务
1024 ~ 49151 登记端口 登记后使用, 避免重名
49152 ~ 65535 动态端口 客户端临时用, 用完释放
共 65536 个端口; 熟知端口占 1.56%
=== ⑤ 常用熟知端口 (UDP 相) ===
53 DNS 查询用 UDP, 区域传送用 TCP
67 / 68 DHCP 服务端 / 客户端 UDP
69 TFTP UDP
123 NTP UDP
161 / 162 SNMP / Trap UDP
520 RIP UDP
=== ⑥ UDP 与 TCP 对照 ===
连接 无连接 面向连接 (三次握手)
可靠性 尽最大努力交付 确认 + 重传 + 排序
报文边界 ★ 保留 (面向报文) 不保留 (面向字节流)
首部 8 B 20 B (最长 60 B)
流量控制 无 滑动窗口
拥塞控制 无 慢开始 / 拥塞避免 / 快重传 / 快恢复
传输方式 单播 / 组播 / 广播 仅单播
=== ⑦ 首部开销账 ===
载荷(B) UDP 总长 UDP 开销率 TCP 总长 TCP 开销率
12 40 70.00% 52 76.92%
100 128 21.88% 140 28.57%
512 540 5.19% 552 7.25%
1000 1028 2.72% 1040 3.85%
1460 1488 1.88% 1500 2.67%
★ 载荷越大, 两种协议的开销率都越小 -> 小报文才需要在意 8 B 还是 20 B
★ MTU 1500 下: UDP 可塞 1472 B 载荷, TCP 受 MSS 限制只能 1460 B七条结论:
- ★ UDP 首部固定 8 B、4 个字段:源端口 / 目的端口 / 长度 / 检验和——没有"首部长度",也没有"选项"。
- ★ 长度字段的最小值是 8:数据长度可以为 0(此时长度字段 = 8)。
- ★ 检验和锚点:16 个字累加得
,回卷得 ,取反得 ——含检验和再算得 即通过。 - ★ 伪首部 12 B 是"凭空拼出来的",不在网络上传输:它的作用是让校验和覆盖"源 IP + 目的 IP + 协议号",从而能查出"送错地址/送错协议"这类差错。
- ★ "算出 0 就填 0xFFFF"——因为 0 已被"发送方未计算校验和"这个含义占用(IPv4 允许不算,IPv6 强制要算)。
- ★ 端口三段:熟知 0 ~ 1023 / 登记 1024 ~ 49151 / 动态 49152 ~ 65535(熟知端口占
)。 - ★ 开销账:载荷 12 B 时 UDP 总 40 B、开销率 70.00%;载荷 1460 B 时降到 1.88%——"8 B 还是 20 B"只在小报文上才值得计较。
考点
考点
1. 必背结论
- ★ UDP 首部固定 8 B、4 个字段:源端口 16 位 + 目的端口 16 位 + 长度 16 位 + 检验和 16 位。
- ★ 长度字段 = 首部 + 数据,最小 8、最大 65535;数据最大
B。 - ★ UDP 四条性质:无连接 / 尽最大努力交付 / 面向报文(保留边界)/ 首部开销小(8 B);支持单播、组播、广播。
- ★ 检验和的伪首部 12 B:源 IP 4 + 目的 IP 4 + 全 0 1 + 协议号 17 1 + UDP 长度 2;伪首部不传输,双方各自拼出。
- ★ 反码求和三步:按 16 位累加 → 高 16 位回卷加回低 16 位(可能多次)→ 取反;若结果为 0 则填 0xFFFF。
- ★★ 检验和锚点:16 个字累加
,回卷得 ,取反得 ;把 0x30D5 填回再算得 0xFFFF(校验通过)。 - ★ 检验和的两种"免算"规则:IPv4 里检验和字段可置 0 表示"不校验";IPv6 里必须计算。
- ★ 校验和与字节序无关(这是反码求和的性质之一)。
- ★ 端口三段:0 ~ 1023 熟知 / 1024 ~ 49151 登记 / 49152 ~ 65535 动态。
- ★ 常用熟知端口:53 DNS、67/68 DHCP、69 TFTP、123 NTP、161/162 SNMP、520 RIP(都走 UDP)。
- ★ UDP vs TCP 首部:8 B vs 20 B(最长 60 B);UDP 无流量控制、无拥塞控制、保留报文边界、支持一对多。
- ★ UDP 适用场合:短报文查询/应答(DNS、SNMP)、实时音视频(RTP)、组播广播(DHCP、RIP)、简单文件传送(TFTP)。
- ★ "不可靠"可由应用层补(QUIC 等就是在 UDP 上自建可靠性)。
2. 高频陷阱
- 把 UDP 首部记成 20 B:错。是 8 B(20 B 是 TCP 的固定首部)。
- 认为 UDP 首部里有"首部长度"字段:错。UDP 首部固定 8 B,不需要这个字段——有"首部长度"的是 IP 与 TCP。
- 把伪首部当成"真的在网络上传输的字段":错。伪首部只参与校验和计算,不传输——接收方自己按同样的规则拼出 12 B。
- 算校验和时忘了伪首部:错。必须含 12 B 伪首部——漏掉它算出的值一定对不上。
- 把伪首部里的协议号写成 6:错。6 是 TCP,UDP 是 17。
- 回卷只做一次(用
if而不是while):有风险。回卷后仍可能再产生进位——标准写法是循环回卷。 - "检验和算出 0"就填 0x0000:错。要填 0xFFFF——0 已被"不校验"占用。
- 认为"UDP 校验和是可选的"这一说法在 IPv6 上也成立:错。IPv6 里 UDP 必须计算校验和(因为 IPv6 取消了网络层检验和,传输层成了唯一一层校验)。
- 认为"UDP 会把小报文合并发送":错。UDP 面向报文,一个报文一个数据报——会合并的是 TCP(字节流,无边界)。
- 认为"UDP 会拆分大报文":错。UDP 不拆分——太大的报文交给 IP 层分片,丢一片就整个报废。
- 认为"UDP 支持可靠传输":错。它只做"尽力而为";可靠性要么靠应用层补,要么改用 TCP。
- 把"端口占用"说成"TCP 用了 80 端口,UDP 就不能用了":错。端口空间按协议分开——TCP 80 与 UDP 80 互不相干。
- 把动态端口范围记成 1024 ~ 65535:错。动态端口是 49152 ~ 65535;1024 ~ 49151 是登记端口。
- 认为"UDP 比 TCP 一定更快":要限定条件。UDP 少了建连与确认开销,但若应用层自己补可靠性与流控,未必更快——准确说法是"UDP 的传输控制更少"。
- 把"面向报文"理解成"面向连接":错,完全是两回事——"面向报文"说的是保留应用层报文边界,与连接无关。
3. 解题模板("UDP 计算与判断题")
① 首部字段题:
8 B = 源端口 2 + 目的端口 2 + 长度 2 + 检验和 2
长度 = 8 + 数据长度 -> 数据长度 = 长度 - 8
★ 数据长度为 0 时, 长度字段 = 8 (合法)
② 校验和计算题(四步):
(1) 拼伪首部 12 B: 源IP(4) + 目的IP(4) + 00 + 协议号(1) + UDP长度(2)
(2) 接上 UDP 首部(检验和字段先置 0) + 数据, 按 16 位切字
★ 若总长是奇数, 末尾补一个 0 字节 (当作低位 0)
(3) 全部相加 -> 高 16 位回卷加回低 16 位 (循环直到不溢出)
(4) 取反 -> 检验和; 若得 0x0000 则填 0xFFFF
③ 校验和验证题:
把收到的检验和填回字段, 再按同样规则算一遍
结果 = 0xFFFF -> 通过; 否则丢弃
④ 端口题:
0 ~ 1023 熟知 / 1024 ~ 49151 登记 / 49152 ~ 65535 动态
53 DNS, 67/68 DHCP, 69 TFTP, 123 NTP, 161/162 SNMP, 520 RIP
⑤ 协议选择题(判断该用 UDP 还是 TCP):
要可靠有序、传大文件、网页 -> TCP
要实时、要组播广播、短报文一问一答、自己实现可靠性 -> UDP4. 与相邻章节的接口
net/30-ip.md(IP 数据报):"协议号 17 = UDP"就写在 IP 首部的协议字段里,本章它又出现在伪首部中;"首部检验和只校验首部"与本章"UDP 检验和校验首部 + 数据 + 伪首部"形成分工对照。net/34-ipv6.md(IPv6):"IPv6 取消首部检验和"直接推出"IPv6 上 UDP 必须计算校验和"——这是两章之间最硬的一条因果。net/32-nat.md(NAT):"NAPT 表项键 = 内网 IP + 内网端口 + 目的 IP + 目的端口"里的端口,正是本章的 UDP/TCP 端口;NAT 改了 IP 与端口,所以 UDP 的伪首部校验和(含 IP 与端口)必须重算。net/41-tcp.md(TCP):TCP 与 UDP 是传输层的两个协议——下一章讲的首部 20 B、序号、确认号、窗口、面向字节流,与本章逐项对照;两者共用同一套"伪首部 + 反码求和"的校验和算法。net/22-window.md(滑动窗口):UDP 没有滑动窗口——"流量控制"与"可靠传输"这两件事 UDP 一概不做,正是下一章 TCP 要做齐的。net/42-handshake.md(连接管理):"UDP 无连接"与 TCP 的三次握手是同一枚硬币的两面——理解 UDP 的"省掉了什么",才能理解 TCP 的握手为什么值得。net/33-routing.md(路由协议):RIP 跑在 UDP 520 上(本章列的熟知端口之一);"RIP 靠 30 s 周期通告兜底不可靠的 UDP"正是"应用层自己补可靠性"的实例。
小结
- ★ UDP 首部固定 8 B、4 个字段:源端口 + 目的端口 + 长度 + 检验和;无首部长度、无选项。
- ★ 长度 = 首部 + 数据,最小 8、最大 65535;数据最大 65527 B。
- ★ 四条性质:无连接 / 尽最大努力交付 / 面向报文(保留边界)/ 开销小;支持单播、组播、广播。
- ★ 伪首部 12 B:源 IP + 目的 IP + 全 0 + 协议号 17 + UDP 长度;不传输,双方各自拼出。
- ★ 校验和三步:16 位累加 → 高位回卷 → 取反;算出 0 就填 0xFFFF。
- ★★ 锚点:累加
→ 回卷 → 取反 ;填回再算得 即通过。 - IPv4 里检验和可置 0(不算);IPv6 里必须算。
- ★ 端口三段:0 ~ 1023 熟知 / 1024 ~ 49151 登记 / 49152 ~ 65535 动态;53 DNS、67/68 DHCP、69 TFTP、123 NTP、161/162 SNMP、520 RIP。
- ★ 与 TCP 的差别:8 B vs 20 B、无流控无拥塞控制、保留报文边界、可一对多。
- ★ 适用场合:短报文问答、实时音视频、组播广播、简单文件传送;不可靠可以由应用层补。
下一篇:TCP 报文段与可靠传输
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。