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

协议控制消息有五种:改块大小、放弃消息、确认、窗口确认、限带宽。 它们都走 csid 2、流 ID 0,消息体固定 4 或 5 字节。

📝 本节和前面几节的关系

4.4.6 讲完传输流程,这一节开始看「流程里传的具体是什么」。 4.4.8 讲用户控制消息,4.4.9 讲音视频与元数据。

一、五种消息各自管什么

类型号 1 到 6(不含 4)都属于这一类: ① 类型 1 Set Chunk Size:改块大小,消息体 4 字节。 单向生效。 ② 类型 2 Abort:放弃某条还没发完的消息,消息体 4 字节(流 ID)。 ③ 类型 3 Ack:告诉对端「我累计收到多少字节」,4 字节。 ④ 类型 5 Window Ack Size:定「收到多少字节该回一个 Ack」,4 字节。 ⑤ 类型 6 Set Peer Bandwidth:限速,5 字节——前 4 字节是带宽,最后 1 字节是策略。

五个里最常用的是类型 1,因为不调块大小,20 KB 的视频帧会被切成 160 块。 其余四个是流控与容错,很多简陋实现根本不发。

二、三种确认机制的关系

这三条容易被混在一起,其实分工很清楚: ① Window Ack Size 定「节奏」:比如设 5000000,意思是对端每收到 5 MB 就该回一个 Ack。 ② Ack 是「回执」:带的是累计字节数,不是增量。 所以收到 5 MB 回 5000000,收到 10 MB 回 10000000。 ③ Set Peer Bandwidth 是「限速」:反过来告诉对端「你别发太快」。 策略字节有 3 个值:0 硬限、1 软限、2 动态。

这三条合起来,才让 RTMP 在 TCP 之上还有一层自己的流控。 但很多实现完全不发它们,也照样能跑——因为 TCP 自己有滑动窗口。 区别在弱网下:缺了 RTMP 层的确认,发送方就不知道对方积压了多少,容易一路猛推最后爆缓冲。

三、为什么 csid 固定是 2

因为控制消息要「最先被解出来」。 如果它跟音视频混在一条 csid 上,接收端必须先解完前面的块才知道这条是控制消息。 而「设置块大小」又恰好会改变后面所有块的解析方式——这就成了死循环。

所以规矩是:控制消息走 csid 2、流 ID 0,永远独占一条车道。 这样接收端可以「一看到 csid 2 就优先处理」,不依赖任何上下文。

流 ID 填 0 也是同一个道理:控制消息不属于任何一条流,它作用于整条连接。 填 1 的话,语义就变成「只影响流 1」,那是错的。

四、Set Peer Bandwidth 的那 5 字节

它是唯一一个 5 字节的协议控制消息,因为多了一个策略字节: ① 前 4 字节:带宽,单位字节每秒。 注意不是比特,是字节——所以 5000000 大约是 40 Mbps。 ② 第 5 字节:策略。0 硬限(必须遵守)、1 软限(可协商)、2 动态(按对端能力调整)。

单位这件事常被搞错。 如果按比特理解,5000000 就变成 5 Mbps,小了一个数量级。 判断办法是看实际行为:发完这条之后对端的发送速率掉到哪个量级,就知道它按哪个单位算的。

五、动手 Demo:改块大小、要确认、限带宽

把前四种各构造一条,看它们的字节。 再逐字节拆一条 Set Chunk Size,确认流 ID 是 0。

import struct

def ctrl(mtype, body, ts=0):
    basic = struct.pack("!B", 2)              # csid 2
    mh = (struct.pack("!I", ts)[1:]
          + struct.pack("!I", len(body))[1:]
          + struct.pack("!B", mtype)
          + struct.pack("<I", 0))           # 流 ID 0
    return basic + mh + body

items = [
    (1, "Set Chunk Size", struct.pack("!I", 4096)),
    (2, "Abort", struct.pack("!I", 1)),
    (3, "Ack", struct.pack("!I", 123456)),
    (5, "Window Ack Size", struct.pack("!I", 5000000)),
    (6, "Set Peer BW", struct.pack("!I", 5000000) + b"\x02"),
]
for t, name, body in items:
    p = ctrl(t, body)
    print("类型 %d %-16s 消息体 %d 字节,块 %d 字节"
          % (t, name, len(body), len(p)))

print("")
p = ctrl(1, struct.pack("!I", 4096))
print("Set Chunk Size 全字节 %s" % p.hex())
print("csid = %d,类型 = %d,流 ID = %d,值 = %d"
      % (p[0] & 0x3F, p[7], struct.unpack("<I", p[8:12])[0],
         struct.unpack("!I", p[12:16])[0]))

实跑输出:

--------------------------------------------------------------------
协议控制消息——改块大小、要确认、限带宽
--------------------------------------------------------------------
**① 五种消息,作用各不相同**

  类型     名称                   消息体     作用
  -------------------------------------------------------------------------
  1        Set Chunk Size         4 字节     改块大小,单向生效
  2        Abort                  4 字节     放弃某条未发完的消息
  3        Ack                    4 字节     告诉对端「我收到多少字节」
  5        Window Ack Size        4 字节     多久回一次确认
  6        Set Peer Bandwidth     5 字节     限速,末尾 1 字节是策略

**② 真构造前四种,看字节**

  类型 1 Set Chunk Size     → 16 字节 02000000000004010000000000001000
  类型 2 Abort              → 16 字节 02000000000004020000000000000001
  类型 3 Ack                → 16 字节 0200000000000403000000000001e240
  类型 5 Window Ack Size    → 16 字节 020000000000040500000000004c4b40

**③ 逐字节拆一条 Set Chunk Size**

  基本头 0x02:格式 0、csid 2
  时间戳 0、长度 4、类型 1、流 ID 0
  消息体 4 字节,值为 4096

  **注意流 ID 是 0:控制消息不属于任何流,它作用于整条连接。**
  **同理 csid 固定用 2,因为它要最先被解出来。**

**④ 三种确认机制的关系**

  **① Window Ack Size 定「多大的窗口」**:**收到这么多字节就该回一个 Ack。**
  **② Ack 是「我收到了这么多」**:**累计字节数,不是增量。**
  **③ Set Peer Bandwidth 是「你别发太快」**:**它反过来限制对端的发送速率。**

  **这三条配合起来,才让 RTMP 在 TCP 之上还有一层自己的流控。**
  **很多简陋实现完全不发这几条,也照样能跑——因为 TCP 本身有滑动窗口。**
  **但在弱网下,缺了 RTMP 层的确认,发送方就不知道对方积压了多少。**
  **结果是一路猛推,最后爆缓冲。**

五条控制消息都很短:消息体 4 或 5 字节,块 16 到 17 字节。 所有控制消息的流 ID 都是 0、csid 都是 2,这样接收端可以优先处理、不依赖上下文。 Set Peer Bandwidth 比别的多 1 字节,就是那个策略字节。它决定「这个限速是硬要求还是建议」。

30 秒实验

  1. 把 Set Chunk Size 的值改成 128,看是不是「改回默认」——它就是一条普通消息,值可以反复改。
  2. 把 Set Peer BW 的策略字节改成 0,看长度变不变——不变,策略只影响语义不影响长度。
  3. 把 ctrl 里的流 ID 从 0 改成 1,看会不会出错——结构上没问题,但语义上就错了,控制消息不该属于某条流。

常见误区

  1. 「协议控制消息要走流 1」——流 ID 必须是 0,它作用于整条连接。
  2. 「Ack 带的是增量字节数」——是累计字节数。
  3. 「Set Peer Bandwidth 单位是比特」——是字节每秒。
自测三问(点开对答案) 1. 协议控制消息有哪五种? —— **Set Chunk Size、Abort、Ack、Window Ack Size、Set Peer Bandwidth。** 2. 为什么控制消息固定走 csid 2? —— **要能被优先解出来。**改块大小会影响后面所有块的解析。 3. Set Peer Bandwidth 的 5 字节怎么分? —— **4 字节带宽(字节每秒)+ 1 字节策略。**

相关笔记