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 秒实验
- 把地址里的 app 改成
live2,再跑一次看输出怎么变——app 与流名的分界永远是第一个斜杠。 - 把块大小改成 4096 再算一遍块数,看「97」会掉到多少——块数直接由块大小决定。
- 把
msgs里加上(3, 1),看 csid 3 与 4 是否都挂着 streamId 1——这正是「命令走一条车道、音视频走另一条」的真实情况。
常见误区
- 「csid 就是 streamId」——一个是传输车道,一个是业务归属,取值与含义都不同。
- 「一条消息就是一个块」——20 KB 的消息要切 160 块。
- 「流名不能带参数」——常带
?token=,服务器自己切。
自测三问(点开对答案)
1. RTMP 的四层结构是什么? —— **连接、流、消息、块。**从外到内,数量逐层增加。 2. csid 与 streamId 各管什么? —— **csid 管「走哪条车道」,streamId 管「属于哪条流」。**两者多对多。 3. 为什么要切块? —— **为了在同一连接上交错多路数据**,让音频不被视频大帧堵住。- ☐ (本节完,下一节 4.3.1 直播推流)