多扩展不是「塞更多项」,而是「每项能装更长」。 单项长度从 4 位的 16 字节上限,变成 1 字节的 255 字节。
3.5.1 的单扩展头受 4 位长度限制,一项最多 16 字节。 这一节看 RFC 8285 怎么把限制放开,以及为什么实际部署里多扩展更常见。
一、区别只在 profile
两种扩展的结构完全一样:都是「4 字节扩展头 + 4N 字节内容」。 不同的只有 profile 那 2 个字节。
0xBEDE 走单扩展,0x1000 到 0x100F 走多扩展。
低 4 位那 16 个值有分工:
0x1000 表示「应用层自己读」,0x1001 到 0x100F 是「中间层专用」。
这个设计是为了让中间设备(比如 SFU 服务器)知道:哪些扩展自己该处理、哪些原样透传。
写错这一位,扩展要么被吃掉、要么被重复处理。
还有一个约定:多扩展的 profile 必须满足 (profile & 0xFFF0) == 0x1000。
收到不满足这个条件的 profile,正确做法是整个扩展跳过,按「不认识的扩展」处理——而不是猜着解析。
二、元素格式变长了
多扩展的每一项是「1 字节 id + 1 字节长度 + 数据 + 填充」。 和单扩展比,差别只有一处:长度不再挤在 id 的低 4 位,而是单独占一个字节,能表达 0 到 255。
实测三项的长度分别是 1、20、10 字节: 第二项 20 字节,用单扩展根本装不下——单扩展的长度上限是 16。 这就是 RFC 8285 出现的直接原因。 整包算一下:12 固定头 + 4 扩展头 + 40 扩展内容 + 1+30 载荷 = 87 字节。 扩展内容比载荷还多,这在多扩展场景里很常见。
三、填充字节要跳过
每个元素要补齐到 4 字节边界,补的都是 0。
所以解析时会遇到 id=0 的字节。
RFC 8285 明确规定:id=0 是填充,必须跳过。 跳过的方式是直接跳到下一个 4 字节边界,而不是「按长度字段往前挪」。 因为填充字节的第二个字节不是长度,可能是任意值。
这一点如果不处理会怎样? 解析器会把 0 当成一个 id、把下一个字节当成长度,然后整段错位——表现是「解出一堆 id=0 的项,指针跑到扩展区之外」。 这类错很典型:不是崩,而是安静地读出垃圾。
四、什么时候该用扩展
多扩展用得比单扩展多得多,因为 WebRTC 要在 RTP 头里带一堆元数据: 音频电平、视频方向、绝对发送时间、帧标记。 一项最多 16 字节根本不够。
但扩展不是免费的: 刚才那个例子里,30 字节载荷配了 40 字节扩展,开销超过载荷本身。 所以有一条判断标准: 「包少、每包大」的场景可以用扩展(比如关键帧、大块数据);「包小、包密」的场景要克制(比如音频,一包才二十几字节)。
实践中还有两条经验: ① 能放在别处就别放头里:能用 RTCP 传的统计信息,不要塞进 RTP 扩展。 ② 扩展要能省就省:同一个值重复发的(比如恒定的视频方向),只在变化时发一次就够了。
最后提一句兼容性: 老设备遇到不认识的 profile 会怎么办? 标准要求是「按长度字段整体跳过」——所以解析代码里那句「跳过 4+4N 字节」是兼容性的关键,漏了就会把扩展当载荷交给解码器。
五、动手 Demo:一个扩展头里塞三项
构造三项扩展:1 字节、20 字节、10 字节,用 0x1000 的 profile。
每项补齐到 4 字节边界,然后逐项解回来。
import struct
prof = 0x1000
items = [(1, b"\x7f"), (2, bytes(range(20))), (3, b"ABCDEFGHIJ")]
body = b""
for eid, data in items:
body += struct.pack("!BB", eid, len(data)) + data
body += b"\x00" * ((4 - len(body) % 4) % 4) # 补齐到 4 字节
ext = struct.pack("!HH", prof, len(body) // 4) + body
hdr = struct.pack("!BBHII", 0x90, 96, 1001, 3600 * 8, 0x1A2B3C4D)
pkt = hdr + ext + b"\x41" + b"w" * 30
print("扩展内容 %d 字节,整包 %d 字节" % (len(body), len(pkt)))
off, n = 16, 0
while off < 16 + len(body):
eid = pkt[off]
if eid == 0: # 填充字节
off = 16 + ((off - 16) // 4 + 1) * 4
continue
elen = pkt[off + 1]
data = pkt[off + 2:off + 2 + elen]
n += 1
print("第 %d 项 id=%d 长度=%d 数据=%s" % (n, eid, elen, data[:8].hex()))
off += 2 + elen
off += (4 - (off - 16) % 4) % 4
print("共 %d 项,指针停在 %d(扩展区结束在 %d)" % (n, off, 16 + len(body)))
实跑输出:
--------------------------------------------------------------------
RTP 多扩展头——一个扩展头里塞进好几项
--------------------------------------------------------------------
**① 单扩展与多扩展的区别只在 profile**
方式 profile 结构
------------------------------------------------------------------------------
单扩展(RFC 3550) 0xBEDE id+len 各占半字节,长度最大 16
多扩展(RFC 8285) 0x1000~0x100F 每个元素自带 1 字节长度,可到 255
**所以「多扩展」不是「塞更多项」,而是「每项能装更长」。**
**并且靠 profile 的低 4 位区分「哪一层要读它」。**
**② 真构造一个带三项的多扩展包**
扩展内容 40 字节,整包 87 字节
**③ 逐项解回来**
第 1 项:id=1 长度=1 数据=7f
第 2 项:id=2 长度=20 数据=0001020304050607…
第 3 项:id=3 长度=10 数据=4142434445464748…
共 3 项,解析指针停在 56(扩展区结束在 56)
**④ 对比:同样的数据用单扩展装会怎样**
**第二项有 20 字节,而单扩展的长度字段只有 4 位(表示 1~16)。**
**装不下——只能切成两项,或者改用多扩展。**
**这就是 RFC 8285 出现的直接原因:4 位长度不够用了。**
**实际部署里,多扩展用得比单扩展多得多。**
**因为 WebRTC 要在 RTP 头里带一堆元数据:音频电平、视频方向、绝对发送时间、帧标记。**
**一项 16 字节根本装不下。**
**但多用扩展的代价是头变大:**
**这里一个 30 字节的载荷配了 40 字节的扩展,开销超过载荷本身。**
**所以扩展只该用在「包少、每包大」的场景;包小又密的场景(比如音频)要克制。**
三项都对上了:id=1 长 1、id=2 长 20、id=3 长 10。第二项那 20 字节,单扩展绝对装不下。 扩展内容 40 字节、整包 87 字节——40 比载荷还厚,这是多扩展的常态代价。 解析指针最后正好停在 56,等于扩展区结束位置,一个字节没多读也没少读——这是「解析对了」的判据。
30 秒实验
- 删掉
if eid == 0那个分支,看解出多少项、指针停在哪——会解出一堆 id=0 的假项,指针冲出扩展区。 - 把填充从
len(body) % 4改成len(data) % 4,再跑一遍——填充位置会错,第二项之后全乱。 - 把第二项改成 300 字节,算一下长度字段还够不够——1 字节最多 255,超了就要改用别的办法。
常见误区
- 「多扩展能塞更多项」——项数没上限,真正的区别是「每项能更长」。
- 「填充字节按长度字段跳过」——填充字节没有长度字段,要按 4 字节边界跳。
- 「扩展越多越好」——包小时开销会超过载荷,音频场景要克制。
自测三问(点开对答案)
1. 单扩展与多扩展的本质区别是什么? —— **元素长度字段的宽度。**单扩展 4 位(1~16 字节),多扩展 1 字节(0~255)。 2. profile 为什么有 `0x1000` 到 `0x100F` 十六个值? —— **低 4 位区分「应用层读」还是「中间层读」。**`0x1000` 是应用层自用。 3. 解析时遇到 id=0 该怎么办? —— **当作填充跳过,直接跳到下一个 4 字节边界。**不能按长度字段挪。- ☐ (本章完,下一章 4.1 RTMP简介)