📚 第六章 TCP与UDP
📄 一句话概括

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 秒实验

  1. 算一下:UDP 承载 100 字节数据,长度字段填多少?
  2. 想一下:为什么伪首部不真的发送出去?
  3. 想一下:为什么 IPv6 里 UDP 校验和不能为 0?

常见误区

  1. 以为「长度是只有数据」。 含头,8 加数据长度。
  2. 以为「校验和只算 UDP 段」。 还加伪首部,否则「送错地方」查不出来。
  3. 以为「校验和为 0 是算出来 0」。 是「没算」的标记,IPv6 里不允许。
自测三问(点开对答案) 1. UDP 首部有哪四个字段? —— **源端口、目的端口、长度、校验和**,一共 8 字节。 2. 长度字段算的是什么? —— **首部加数据的总字节数**,和 IP 的「总长度」口径一致。 3. 伪首部有什么用? —— **把地址也纳入校验**,这样「送错地方」的包也能被发现。

下一节拆 TCP 首部:20 个字节,8 个标志位。

相关笔记