🎬 第四章 RTMP流媒体协议
📄 一句话概括

基本头只有三种长度: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 秒实验

  1. 把 319 改成 320,看长度从 2 变 3——这就是两字节的上限边界。
  2. 把 dec 里 t == 1 那行的 b[1] + 64 改成 b[1],再跑一次——所有 2 字节形态的解都会差 64,一眼能看出公式错了。
  3. 自己写一个 8001 反解,看它等于几——1 + 0 + 64 = 65,说明三字节形态可以表示很小的数,只是浪费字节。

常见误区

  1. 「基本头固定 1 字节」——1 到 3 字节,看 csid 大小。
  2. 「csid 最大 63」——最大 65599,走三字节形态。
  3. 「csid 0 和 1 是合法的流号」——保留值,出现就说明解析错了。
自测三问(点开对答案) 1. 基本头有几种长度?靠什么区分? —— **三种:1、2、3 字节。**靠首字节高 2 位。 2. 2 字节形态里 csid 怎么算? —— **64 加第二字节。**上限是 64 + 255 = 319。 3. 为什么常用 csid 都在 2 到 63? —— **1 字节形态最省。**每秒上千个块,每个省 1 字节很可观。

相关笔记