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

块流就是把一条消息切碎,让多路数据能在一条 TCP 上交错。 默认块大小 128 字节,调大它省的其实不是带宽,而是块数与解析开销。

📝 本节和前面几节的关系

4.2 讲的是「命令层」,从这一节起进入「传输层」。 4.4.2 讲块的格式,4.4.3 到 4.4.5 逐段拆头,4.4.6 讲交错重组。

一、为什么要切

一条 TCP 连接要同时送四类东西:命令、音频、视频、控制。 如果每条消息都整块发,那么一条 20 KB 的视频帧会独占连接,音频就只能等它写完。

等多久? 按 2 Mbps 算,20 KB 大约要 80 毫秒。 音频的帧间隔是 20 毫秒左右,所以一次视频大帧就能让声音断掉四拍。 这就是必须切块的理由。

切成 128 字节之后,音频就能在视频块之间插队。 代价是每个块要带一个头部,但那只有 1 字节(后续块),便宜得可以忽略。

二、块大小影响的是块数

很多人以为「块调大能省带宽」,这个说法是错的。 实测一条 20 KB 的消息: 按 128 切是 160 块、块头共 171 字节,占 0.83%;按 4096 切只有 5 块、块头 16 字节,占 0.08%。 两者差十倍,但绝对值都很小。

真正的差别在块数:160 块对 5 块,差了 32 倍。 接收端每处理一个块,都要走一次「查 csid、拼缓冲、更新状态」的流程。 块数多就是纯粹的 CPU 与调度开销,跟带宽没关系。

所以实际推流都会在 connect 之后发一条「设置块大小」,常见值是 4096 或 60000。 但调太大也有代价:交错粒度变粗,音频要等更久才能插进来。

三、块大小是单向的

这一点特别容易误解:我发给你 4096,只约束「我发给你的块」。 你要发给我,还得自己再设一次。

所以抓包时常见「两个方向的块大小不一样」,这不是配错。 推流端设 4096(因为视频帧大),服务器回推流端可能还是 128(命令都很小)。

还有一条硬规定:设置块大小这条消息本身,必须用「旧块大小」切。 因为它只有 4 字节消息体,一条块就装完了,实际上不会触发切换——但规则要记住,否则自己实现时容易写成「先改再发」。 正确顺序是「按旧尺寸发完这条,之后才按新尺寸」。

四、块流 ID 是车道

每条块流用 csid 标识,取值 2 到 65599。 实作的默认分工是:2 走协议控制与用户控制、3 走 AMF 命令、4 走音频、6 走视频。

csid 的作用等价于「车道」: 同一条 csid 上的块必须按顺序发(因为消息头的压缩依赖「上一次的值」);不同 csid 之间可以任意交错。 这就是为什么音频能插在视频大帧中间——它们走的是不同车道。

一句话概括:csid 是车道、块是车厢、消息是货。 多车道可以并行跑,但同一车道上的车厢不能超车。

五、动手 Demo:128 字节一块的老规矩

先看一条 300 字节消息怎么切成 3 块,再算块大小对块数的影响。 重点看「块头占比」与「块数」哪个才是真正的成本。

msg = bytes(range(256)) * 80       # 凑一条 20480 字节的大消息
csize = 128
n = (len(msg) + csize - 1) // csize
print("消息 %d 字节,按 %d 切,%d 块" % (len(msg), csize, n))

parts = [len(msg[i:i + csize]) for i in range(0, len(msg), csize)]
print("前 3 块载荷:%s 末块 %d" % (parts[:3], parts[-1]))

print("")
for cs in (128, 512, 1024, 4096, 60000):
    cnt = (len(msg) + cs - 1) // cs
    over = 12 + (cnt - 1) * 1     # 首块 12 字节头,后续各 1 字节
    print("块大小 %-6d 块数 %-4d 块头 %-4d 字节 占 %.2f%%"
          % (cs, cnt, over, over * 100.0 / len(msg)))

print("")
print("块数从 %d 降到 %d,少了 %.0f 倍"
      % (160, 5, 160 / 5.0))

实跑输出:

--------------------------------------------------------------------
块流——128 字节一块的老规矩
--------------------------------------------------------------------
**① 默认块大小是 128 字节**

  一条 300 字节的消息,按 128 切,变成 3 块
  各块载荷:128、128、44

  **为什么要切?因为 RTMP 要在一条 TCP 连接上「同时」送多路数据。**
  **如果一条大消息不切,它就会独占整条连接,音频要等视频发完才能走。**
  **切块之后可以交错发送,语音就不会被视频大帧堵住。**

**② 块大小真正影响的是「块数」**

  块大小     块数     块头总量     占消息
  -------------------------------------------
  128        160      171 字节     0.83%
  512        40       51 字节      0.25%
  1024       20       31 字节      0.15%
  4096       5        16 字节      0.08%
  60000      1        12 字节      0.06%

  **先看清一件事:块头总量只占百分之零点几,所以「调大块省带宽」是错的。**
  **块头本身太便宜了——后续块只有 1 字节。**

  **真正的收益在块数:一条 20 KB 的视频消息,按 128 切是 160 块。**
  **按 4096 切只有 5 块,少了 32 倍。**
  **接收端每处理一个块,都要走一次「查 csid、拼缓冲、更新状态」的流程。**
  **块数多就是纯粹的 CPU 与调度开销,跟带宽没关系。**

  **所以实际推流会在 connect 之后发一条「设置块大小」的消息把它调大,常见值是 4096 或 60000。**
  **但调得太大也有代价:交错粒度变粗,音频要等更久才能插进来。**

**③ 一个容易误解的地方**

  **块大小是「单向生效」的:我发给你 4096,只约束我发给你的块;你发给我还要单独设一次。**
  **所以抓包时两边的块大小常常不一样,别以为是配错了。**

  **还有:改块大小这条消息本身必须用小尺寸发。**
  **标准规定它自己要用「旧块大小」切**——**否则接收方还不知道新尺寸,就会解错。**

20 KB 的消息按 128 切是 160 块、按 4096 切是 5 块。 块头占比从 0.83% 降到 0.08%——看着差十倍,但绝对值都小到可以忽略。 所以「调大块」的真实收益是块数少 32 倍,省的是 CPU 与调度,不是带宽。

30 秒实验

  1. 把消息改成 300 字节,看各个块大小下块数怎么变——512 以上都只有 1 块,这就是「小块无所谓」的边界。
  2. 把首块的 12 字节头改成 1 字节(想象成格式 3),再算占比——会低到几乎为零,说明块头真的不是瓶颈。
  3. 把块大小设成 1,算块数——20480 块,这时块头就占到 50% 了,这是理论上的下限。

常见误区

  1. 「调大块能省带宽」——块头只占 0.8%,省的是解析开销。
  2. 「块大小一设,两个方向都生效」——单向的,各设各的。
  3. 「块越大越好」——交错粒度变粗,音频要等更久。
自测三问(点开对答案) 1. 为什么要切块? —— **为了让多路数据在一条 TCP 上交错**,避免音频被视频大帧堵住。 2. 调大块大小真正的收益是什么? —— **块数大幅减少**(160 → 5),省的是 CPU 与调度,不是带宽。 3. 块大小是单向还是双向生效? —— **单向。**我设的只约束我发出的块。

相关笔记