基本头只有三种长度:1 字节、2 字节、3 字节,靠首字节高 2 位区分。 csid 越小越省,所以常用的 csid 都落在 2 到 63 这一档。
4.4.2 给了块的总体结构,这一节专门拆第一段:基本头。 下一节拆第二段:消息头的四种格式。
一、三段区间与三种公式
首字节高 2 位是「格式号」,低 6 位与后面的字节一起表示 csid: ① 1 字节:csid 2 到 63,低 6 位直接就是 csid。 这是最常用的形态。 ② 2 字节:csid 64 到 319,首字节高 2 位是 01,第二个字节等于 csid 减 64。 ③ 3 字节:csid 64 到 65599,首字节高 2 位是 10,csid 等于 64 加第二字节加第三字节乘 256。
注意②③的区间有重叠:64 到 319 用 2 字节就够,但标准也允许用 3 字节编。 解析时按首字节高 2 位判断,不能靠猜。
65599 这个上限不是随手写的: 64 + 255 + 255 × 256 = 65599,正好把三个字节的空间用满。
二、实测六组编码
把六个典型值编出来看:
① csid 2 → 02(1 字节)
② csid 63 → 3F(1 字节)
③ csid 64 → 4000(2 字节,第二字节 0)
④ csid 319 → 40FF(2 字节,第二字节 255)
⑤ csid 320 → 800001(3 字节)
⑥ csid 65599 → 80FFFF(3 字节)
把每个反解一遍能发现规律: 2 字节形式的解码是「64 加第二字节」,3 字节是「64 加第二字节加第三字节乘 256」。 两套公式只差一项,写代码时共用一个函数就够。
三、为什么常用 csid 都很小
因为 1 字节形态最省,而 RTMP 每秒要发几百上千个块。 每个块省 1 字节,一秒就是 1 KB 左右。
这就是为什么实作的默认分工全落在 2 到 63 这一档: 2 协议控制与用户控制、3 AMF 命令、4 音频、6 视频。 要开的流再多,也很少超过 63。
csid 0 与 1 是保留值,实际不用。 看到 0 或 1 出现在基本头里,说明解析错了或者数据脏了——这是排查时的第一个红旗。
四、抓包时的快速判读
在 Wireshark 里看 RTMP,基本头是最先露出来的东西。 三个快速判据: ① 首字节 0x02 到 0x3F:1 字节形态,csid 就是它本身。 如果是 0x02,那就是控制消息。 ② 首字节 0x40 到 0x7F:2 字节形态,csid 是 64 加下一个字节。 ③ 首字节 0x80 以上:3 字节形态,要再看两个字节。
实际抓包里 99% 的块都是 1 字节形态,看到 0x40 以上就要注意「是不是有人在开很多条流」。 这种流量通常在服务器内部转发时出现,客户端侧很少见。
五、动手 Demo:csid 的三种编码长度
六组典型值各编一次,再反解回来确认自洽。 重点看 2 字节与 3 字节形式的公式差在哪。
import struct
def enc(cs):
if cs < 64:
return struct.pack("!B", cs)
if cs < 320:
return struct.pack("!BB", 0x40, cs - 64)
return struct.pack("!BBB", 0x80, (cs - 64) & 0xFF, (cs - 64) >> 8)
def dec(b):
t = b[0] >> 6
if t == 1:
return b[1] + 64
if t == 2:
return b[1] + b[2] * 256 + 64
return b[0] & 0x3F
for cs in (2, 63, 64, 319, 320, 65599):
b = enc(cs)
print("csid %-6d %d 字节 %-8s 反解 %d"
% (cs, len(b), b.hex(), dec(b)))
print("")
print("1 字节上限 63,2 字节上限 %d,3 字节上限 %d"
% (63 + 256, 64 + 255 + 255 * 256))
实跑输出:
--------------------------------------------------------------------
块基本头——csid 为什么有时占 1 字节、有时占 3 字节
--------------------------------------------------------------------
**① 三种长度对应三段区间**
csid 范围 字节数 首字节高 2 位 编码方式
--------------------------------------------------------------------------------
2 ~ 63 1 00 csid 直接写进低 6 位
64 ~ 319 2 01 第二字节就是 csid 减 64
64 ~ 65599 3 10 64 + 第 2 字节 + 第 3 字节 × 256
**两字节形式的公式是「64 加第二字节」,所以上限是 64 + 255 = 319。**
**三字节形式多一个高位字节,上限就成了 64 + 255 + 255 × 256 = 65599。**
**这个 65599 不是随手写的数,它是把三个字节的空间用满算出来的。**
**② 实测三种编码的字节**
csid 2 → 1 字节 02 反解 2
csid 63 → 1 字节 3f 反解 63
csid 64 → 2 字节 4000 反解 64
csid 319 → 2 字节 40ff 反解 319
csid 320 → 3 字节 800001 反解 320
csid 65599 → 3 字节 80ffff 反解 65599
**③ 为什么不用 csid=0 和 1**
**csid 0 与 1 是保留值,实际不用。**
**实作里有一套默认编号:2 给协议控制与用户控制、3 给 AMF 命令、4 给音频、6 给视频。**
**这套编号不是标准强制的,但大家默认都这么用。**
**所以抓包看到 csid=2 那种只有一字节头的块,基本就是控制消息。**
**它体积小、频率低,正好适合最省的编码。**
64 → 4000、319 → 40FF,第二字节从 0 涨到 255,正好覆盖 64 到 319。
320 → 800001,首字节跳到 0x80 表示「要看两个字节」,这就是三字节形态。
反解值与原值全部一致——说明这两套公式配对正确,解析器可以照这个写。
30 秒实验
- 把
319改成320,看长度从 2 变 3——这就是两字节的上限边界。 - 把
dec里t == 1那行的b[1] + 64改成b[1],再跑一次——所有 2 字节形态的解都会差 64,一眼能看出公式错了。 - 自己写一个
8001反解,看它等于几——1 + 0 + 64 = 65,说明三字节形态可以表示很小的数,只是浪费字节。
常见误区
- 「基本头固定 1 字节」——1 到 3 字节,看 csid 大小。
- 「csid 最大 63」——最大 65599,走三字节形态。
- 「csid 0 和 1 是合法的流号」——保留值,出现就说明解析错了。
自测三问(点开对答案)
1. 基本头有几种长度?靠什么区分? —— **三种:1、2、3 字节。**靠首字节高 2 位。 2. 2 字节形态里 csid 怎么算? —— **64 加第二字节。**上限是 64 + 255 = 319。 3. 为什么常用 csid 都在 2 到 63? —— **1 字节形态最省。**每秒上千个块,每个省 1 字节很可观。- ☐ (本节完,下一节 4.4.4 块消息头)