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

RTMP 有四个层次:连接、流、消息、块,一层套一层。 最容易混的是 csid 与 streamId——一个管传输车道,一个管业务归属。

📝 本节和前面几节的关系

4.2.1 到 4.2.4 走完了一条会话的全流程,这一节把名词固化成一套说法。 4.4 拆块流时会反复用到 csid、message、chunk 这三个词。

一、四层结构

从外到内是:连接 → 流 → 消息 → 块。 实测一条推流会话的量级是:1 条连接、1 条流、42 条消息、97 个块。 层数越往下,数量越多。

各层的职责: ① 连接:TCP 一条,负责握手与鉴权。 ② 流:一路音视频,用 streamId 标识。 ③ 消息:一条完整命令或一帧数据,带类型号与长度。 ④ 块:消息被切开后的一段,用于在一条连接上交错多路。

抓包工具默认显示的是「块」,而分析时要看的是「消息」。 这一步转换就是 RTMP 分析最费劲的地方——好在 Wireshark 有 RTMP 解析器,能帮你把块拼回消息。

二、csid 与 streamId 不是一回事

这是全章最容易混的一对: ① csid(块流 ID):传输层概念,取值 2 到 65599。 它决定「这一块走哪条车道」,同一 csid 的块必须按顺序。 ② streamId:应用层概念,来自 createStream 的响应。 它决定「这条消息属于哪条流」。

两者的关系是「多对多」: 一个 csid 可以承载多条 streamId 的消息(比如所有命令都走 csid 3);同一条 streamId 的消息也可以走不同 csid(音频一条、视频一条)。

实作的默认编号是:csid 2 走协议控制与用户控制、3 走 AMF 命令、4 走音频、6 走视频。 所以「csid 4」常被当成「音频车道」,streamId 才是「哪一路音频」。

三、chunk 与 message

一条消息可能被切成很多块,也可能只占一块。 默认块大小 128 字节,所以一条 20 KB 的视频消息要切成 160 块。

切块的目的只有一个:交错。 如果把 20 KB 一次性写进 TCP,音频就要等它写完才能插进来,表现是声音一顿一顿。 切成 128 字节的小块,音频就能在视频块之间插队。

所以「块」是延迟与开销的权衡点: 块小→交错细、延迟低,但块数多;块大→开销小,但交错粒度粗。 实用值是 4096,兼顾两头。

四、app、流名与 URL

一个 RTMP 地址能拆出五段,每一段都有专门的用处: 协议、主机、端口、app、流名。

拿 rtmp://127.0.0.1:1935/live/test?token=abc 来拆: app 是 live,流名是 test,参数是 token=abc。 但注意:服务器收到的流名往往是 test?token=abc 整段,问号后面的部分由服务器自己切。

两个常见误解: ① 「app 就是目录名」:多数实现确实映射到一个目录,但也可以纯配置,跟文件系统无关。 ② 「流名必须唯一」:同一个 app 下唯一,跨 app 可以重名。 所以 live/test 与 vod/test 是两个不同的流。

还有一个名词经常一起出现:tcUrl。 它就是「去掉流名的那一段完整地址」,服务器常用它做鉴权与路由,所以它必须与 app 一致。

五、动手 Demo:四层结构与一个 URL 的拆解

先把一条会话的四层数量摆出来,再拆一个带参数的地址。 最后验证 csid 与 streamId 的多对多关系。

layers = [("连接", "TCP", 1), ("流", "Stream", 1),
          ("消息", "Message", 42), ("块", "Chunk", 97)]
for name, unit, n in layers:
    print("%-4s %-8s %d" % (name, unit, n))

url = "rtmp://127.0.0.1:1935/live/test?token=abc"
body = url.split("://", 1)[1]
hostport, _, path = body.partition("/")
app, _, stream = path.partition("/")
print("主机端口 = %s" % hostport)
print("app = %s" % app)
print("流名 = %s" % stream)

# csid 与 streamId 的多对多:同一条 csid 承载多个 streamId
msgs = [(4, 1), (4, 2), (6, 1)]     # (csid, streamId)
from collections import defaultdict
by_csid = defaultdict(set)
for cs, sid in msgs:
    by_csid[cs].add(sid)
for cs, sids in by_csid.items():
    print("csid %d 承载 streamId %s" % (cs, sorted(sids)))

实跑输出:

--------------------------------------------------------------------
名词解释——连接、流、消息、块四层
--------------------------------------------------------------------
**① 四层结构,一层套一层**

  层       单位        数量(实测一条推流)     标识
  -------------------------------------------------------------
  连接     TCP         1 条                     IP+端口
  流       Stream      1 条                     streamId=1
  消息     Message     42 个                    type + 长度
  块       Chunk       97 个                    csid

  **这四层的关系是:块组成消息、消息按类型分流、流属于连接。**
  **抓包工具默认显示的是「块」,而分析时要看的是「消息」。**
  **这一步转换就是 RTMP 分析最费劲的地方。**

**② 最容易混的三组词**

  **① csid 与 streamId 完全不是一回事。**
  **csid 是「块流 ID」,属于传输层,取值 2 到 65599;streamId 是「消息属于哪条流」,属于应用层。**
  **一个 csid 可以承载多条 streamId 的消息,反之亦然。**
  **② chunk 与 message。**
  **message 是一条完整的命令或一帧数据;chunk 是它被切开后的一段。**
  **默认块大小 128 字节,所以一条 20 KB 的视频消息要切成 160 个块。**
  **③ app 与 streamName。**
  **app 是「应用名」,对应 URL 第一段路径(如 live)。**
  **streamName 是「流名」,对应第二段(如 test)。**
  **`rtmp://host/live/test` 里 live 是 app、test 是流名。**

**③ 一个 URL 能拆出多少东西**

  拿一个带参数的地址来拆:
  rtmp://127.0.0.1:1935/live/test?token=abc
  协议 rtmp/主机 127.0.0.1/端口 1935/app live/流名 test/参数 token=abc

  **很多人以为流名就是 test,其实服务器收到的往往是 `test?token=abc` 整段。**
  **鉴权参数就这么跟着流名一起传过来,服务器自己切。**
  **所以「流名里能不能带问号」这个问题,答案取决于服务器实现,不取决于协议。**

四层数量是 1 → 1 → 42 → 97,越往下越碎,这就是「分块」的直接体现。 拆地址时用 partition 比 split 稳:流名里可能还有斜杠,split 会把最后一段吃掉。 csid 4 同时承载 streamId 1 与 2,说明「车道」与「货主」确实是两个独立维度。

30 秒实验

  1. 把地址里的 app 改成 live2,再跑一次看输出怎么变——app 与流名的分界永远是第一个斜杠。
  2. 把块大小改成 4096 再算一遍块数,看「97」会掉到多少——块数直接由块大小决定。
  3. 把 msgs 里加上 (3, 1),看 csid 3 与 4 是否都挂着 streamId 1——这正是「命令走一条车道、音视频走另一条」的真实情况。

常见误区

  1. 「csid 就是 streamId」——一个是传输车道,一个是业务归属,取值与含义都不同。
  2. 「一条消息就是一个块」——20 KB 的消息要切 160 块。
  3. 「流名不能带参数」——常带 ?token=,服务器自己切。
自测三问(点开对答案) 1. RTMP 的四层结构是什么? —— **连接、流、消息、块。**从外到内,数量逐层增加。 2. csid 与 streamId 各管什么? —— **csid 管「走哪条车道」,streamId 管「属于哪条流」。**两者多对多。 3. 为什么要切块? —— **为了在同一连接上交错多路数据**,让音频不被视频大帧堵住。

相关笔记