UDP 首部只有 8 个字节、4 个字段:所有传输层协议里最简单的。 「长度」算的是首部加数据,不是只有数据,这一点最容易记错。
6.3 讲了 UDP 的取舍,这一节逐字节看它的结构。 4.7 讲 IPv4 首部时用过同样的方法,这里的伪首部要靠那一节的地址概念。
一、四个字段
| 字段 | 字节 | 作用 |
|---|---|---|
| 源端口 | 0~1 | 谁发的,回程用 |
| 目的端口 | 2~3 | 交给哪个程序 |
| 长度 | 4~5 | 首部加数据的总字节 |
| 校验和 | 6~7 | 整个包的校验值 |
每一行都和「TCP 有、UDP 没有」的那套东西对应: 没有序列号,所以不排序;没有确认号,所以不重传;没有窗口,所以不做流控制。 四个字段就是「够用就好」的全部内容。 第三行那句「首部加数据」要特别注意:它和 IP 首部的「总长度」口径一致,都是「含头」。 所以「长度是 13」意味着「8 字节头加 5 字节数据」,不是「5 字节数据」。
二、校验和与伪首部
UDP 的校验和不是「只算 UDP 这一段」: 它还要把「IP 首部里的一部分」也算进去——源地址、目的地址、协议号、UDP 长度。 这四样加上 UDP 段本身,叫「伪首部」。 为什么要造假首部:因为「光校验数据」防不住「送错地方」。 如果只有数据校验,那么「本该发给 A 的包错发给了 B」是查不出来的——因为数据本身没错。 加入地址之后,「送错」也会被校验和发现。 注意伪首部「不真的发送」:它只在计算校验和时临时拼出来,算完就丢。 所以「抓包看不到伪首部」是正常的,它只是「参与计算的一个中间量」。
三、校验和为 0 的特殊含义
在 IPv4 里,UDP 的校验和填 0 表示「发送方没算」,接收方就不校验。 所以「校验和为 0」不等于「校验结果是 0」,它是「跳过校验」的标记。 而 IPv6 里这是非法的:因为 v6 取消了 IP 层的首部校验和, 传输层再不算,就彻底没人管数据对不对了。 所以「v6 强制要求算 UDP 校验和」不是「更严格」,而是「必须有人兜底」。 工程上还有个细节:算出来的校验和如果正好是 0,要写成全 1(0xFFFF), 否则会和「没算」混淆。 这种「用特殊值表示特殊情况」的做法,在协议里到处都是。
四、动手 Demo:UDP 首部只有 8 个字节
构造一个真实的 UDP 首部,看它的十六进制长什么样。 前四行十六进制就是四个字段。
import struct
payload = b'hello'
hdr = struct.pack('!HHHH', 50000, 53, 8 + len(payload), 0)
raw = hdr + payload
for i in range(0, len(raw), 16):
print('%04x %s' % (i, ' '.join('%02x' % b for b in raw[i:i+16])))
实跑输出:
--------------------------------------------------------------------
UDP 首部只有 8 个字节
--------------------------------------------------------------------
**UDP 首部就四个字段**,**这是所有传输层协议里最简单的。**
字段 字节位置 本包的值
----------------------------------------------
**源端口** **0~1** **50000**
**目的端口** **2~3** **53**
**长度** **4~5** **13**
**校验和** **6~7** **0(未计算)**
**实际发出的 13 个字节**(8 字节头加 5 字节数据):
0000 c3 50 00 35 00 0d 00 00 68 65 6c 6c 6f .P.5....hello
**前四行十六进制就是四个字段**:**c350 是 50000,0035 是 53,000d 是 13,0000 是校验和占位。**
**注意「长度」这一项算的是「首部加数据」,不是「只有数据」**——
**这一点和 IP 首部的「总长度」一致,容易记混。**
**校验和这里填 0 是有意的**:**在 IPv4 里「校验和为 0」表示「发送方没算」**,**接收方就不校验。**
**而 IPv6 里这是非法的**——**v6 强制要求算 UDP 校验和**,
**因为它取消了 IP 层的校验和,这一层再不算就没人管了。**
c350 是十进制的 50000,0035 是 53:这两个数就是「源端口」和「目的端口」。 000d 是 13,也就是「8 加 5」——这是「含头」的直接证据。 0000 那个校验和是有意留空的:在 IPv4 里它表示「不算校验」, 所以这个包是「能发出去但没人校验」的。 真实实现里当然会算,这里只是为了看清楚字段位置。 最后 5 个字节 68 65 6c 6c 6f 是「hello」的 ASCII:传输层首部之后紧跟着的就是应用数据, 中间没有任何分隔符。 所以「怎么知道数据从哪儿开始」全靠首部里的长度——这也是「长度字段不能省」的原因。
30 秒实验
- 算一下:UDP 承载 100 字节数据,长度字段填多少?
- 想一下:为什么伪首部不真的发送出去?
- 想一下:为什么 IPv6 里 UDP 校验和不能为 0?
常见误区
- 以为「长度是只有数据」。 含头,8 加数据长度。
- 以为「校验和只算 UDP 段」。 还加伪首部,否则「送错地方」查不出来。
- 以为「校验和为 0 是算出来 0」。 是「没算」的标记,IPv6 里不允许。
自测三问(点开对答案)
1. UDP 首部有哪四个字段? —— **源端口、目的端口、长度、校验和**,一共 8 字节。 2. 长度字段算的是什么? —— **首部加数据的总字节数**,和 IP 的「总长度」口径一致。 3. 伪首部有什么用? —— **把地址也纳入校验**,这样「送错地方」的包也能被发现。下一节拆 TCP 首部:20 个字节,8 个标志位。
- ☐ (本节完,下一节 6.7 TCP首部格式)