RTMP协议详解(二):块流协议
前言
上一篇讲了RTMP握手,握手完成后就进入了真正的数据传输阶段。但RTMP不是简单地把数据直接发出去,而是通过一套块流(Chunk Stream)机制来传输。
本文将深入讲解块流协议的设计原理、数据结构和完整的收发流程。
一、为什么需要分块?
问题的根源:TCP是一条单车道
RTMP运行在TCP之上,而TCP连接就像一条单行道公路:
markdown
推流端 ════════════════════════════════════▶ 服务器
(所有数据必须排队,一个一个发送)
直播场景中,同时需要传输多种数据:
| 数据类型 | 大小 | 频率 |
|---|---|---|
| 视频帧(I帧) | 50KB | 每秒1-2次 |
| 视频帧(P帧) | 5KB | 每秒25-60次 |
| 音频帧 | 200字节 | 每秒25-50次 |
| 控制消息 | 几十字节 | 偶发 |
不分块的问题:大卡车堵路
把大视频帧比作大卡车,小音频帧比作小轿车,它们共用同一条单行道:
实际延迟计算(假设带宽100KB/秒):
markdown
发送50KB视频帧:
耗时 = 50KB ÷ 100KB/秒 = 500ms
音频帧被迫等待 500ms!
而实时直播要求音视频延迟 < 50ms
不分块带来的问题:
| 问题 | 说明 |
|---|---|
| 队头阻塞 | 大帧阻塞后续所有数据 |
| 音视频不同步 | 音频等待视频传完才能发送 |
| 内存浪费 | 需要一次性缓冲整帧数据 |
| 无法插入紧急消息 | 关键控制消息无法优先发送 |
分块的解决方案
把大帧拆成多个小块(默认128字节),音视频交替发送:
分块后的延迟:
markdown
发送视频Chunk1 (128字节):
耗时 = 128B ÷ 100KB/秒 ≈ 1.3ms
音频在 1.3ms 后即可发送(原来是500ms)
提升:快了约 400倍
分块的三大优势
二、Message 和 Chunk 的区别
核心概念
Message(消息)= 完整的逻辑数据
Chunk(块) = Message 的传输片段
这两个概念对应不同的层次:
| 维度 | Message | Chunk |
|---|---|---|
| 层次 | 应用层(业务逻辑) | 传输层(网络传输) |
| 大小 | 不固定(几字节~几MB) | 固定(默认128字节) |
| 完整性 | 必须完整才有意义 | 只是传输片段 |
| 类比 | 一封完整的信 | 信被撕成的碎片 |
Message 是什么
Message 是业务层面上完整的一个数据单元:
| Message类型 | 内容 | 典型大小 |
|---|---|---|
| 视频帧(I帧) | 完整的H.264关键帧 | 50KB |
| 视频帧(P帧) | H.264参考帧 | 5KB |
| 音频帧 | 完整的AAC音频包 | 200字节 |
| 元数据 | 视频宽高、码率信息 | 500字节 |
| 控制命令 | connect、publish命令 | 几百字节 |
Message必须完整接收才有意义------收到半帧视频数据是无法解码播放的。
Chunk 是什么
Chunk 是 Message 的运输工具,负责把Message拆碎传输:
markdown
一个视频Message(50KB)
↓ 拆成128字节的Chunk
┌─────┬─────┬─────┬─────┬─────┐
│ C1 │ C2 │ C3 │ ... │ C391│
│128B │128B │128B │ │ 8B │
└─────┴─────┴─────┴─────┴─────┘
关系图

接收端如何重组
关键问题:接收端怎么知道哪些Chunk属于同一个Message?
每个Chunk都带有标识信息:
- Chunk Stream ID (CSID):标识这些Chunk属于哪个流(类似收件地址)
- Message Length:Message的总字节数(知道什么时候收齐了)
三、Chunk 的详细结构
总体结构
每个Chunk由三部分组成:
scss
┌─────────────────┬────────────────────┬───────────────┐
│ Basic Header │ Message Header │ Chunk Data │
│ (1-3 字节) │ (0/3/7/11 字节) │ (≤128 字节) │
└─────────────────┴────────────────────┴───────────────┘
| 部分 | 作用 | 大小 |
|---|---|---|
| Basic Header | 标识Chunk类型(fmt)和所属流(csid) | 1-3字节 |
| Message Header | 描述Message的时间戳、长度、类型等 | 0/3/7/11字节 |
| Extended Timestamp | 时间戳超过0xFFFFFF时的扩展字段 | 0或4字节 |
| Chunk Data | Message的实际数据片段 | ≤ chunk_size |
Part 1:Basic Header(基本头部)
作用
Basic Header传递两个最关键的信息:
- fmt:Chunk类型(决定Message Header的长度)
- csid:Chunk Stream ID(这个Chunk属于哪个流)
三种编码形式
根据csid的大小,Basic Header有1字节、2字节、3字节三种形式:
形式1:1字节(最常用)
arduino
比特: 7 6 5 4 3 2 1 0
┌─────┬───────────┐
│ fmt │ csid │
│2 bit│ 6 bits │
└─────┴───────────┘
- csid范围:2-63
- 示例:
0xC5= 二进制11 000101→ fmt=3, csid=5
形式2:2字节
bash
第1字节:
┌─────┬───────────┐
│ fmt │ 0 │ csid字段=0,表示后面还有1字节
└─────┴───────────┘
第2字节:
┌───────────────────┐
│ csid - 64 │
└───────────────────┘
- csid范围:64-319
- 计算:
csid = 第2字节 + 64
形式3:3字节
scss
第1字节:
┌─────┬───────────┐
│ fmt │ 1 │ csid字段=1,表示后面还有2字节
└─────┴───────────┘
第2-3字节(小端字节序):
┌───────────────────┬───────────────────┐
│ 低8位 │ 高8位 │
│ (csid-64) & 0xFF│ (csid-64) >> 8 │
└───────────────────┴───────────────────┘
- csid范围:64-65599
三种形式总结:
| csid范围 | Basic Header长度 | 第1字节的csid字段 |
|---|---|---|
| 2-63 | 1字节 | csid本身 |
| 64-319 | 2字节 | 0 |
| 320-65599 | 3字节 | 1 |
注意:csid = 0 和 1 是保留值,用于扩展编码,实际流ID从2开始。
常用 CSID 分配
RTMP协议约定了常用CSID的用途:
| CSID | 用途 |
|---|---|
| 2 | 控制消息(SetChunkSize、Abort等) |
| 3 | 命令消息(connect、publish、play) |
| 4 | 音频数据 |
| 5 | 视频数据 |
| 6+ | 自定义流 |
Part 2:Message Header(消息头部)
作用
Message Header描述这个Chunk所属Message的元信息。
4种类型由 fmt 决定

Type 0:完整头部(11字节)
使用场景:新消息的第一个Chunk,或者流ID发生变化时。
python
字节偏移: 0 1 2 3 4 5 6 7 8 9 10
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ Timestamp │ Msg Length │Type│ Stream ID │
│ (3 bytes) │ (3 bytes) │(1B)│ (4 bytes) │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
字段详解:
| 字段 | 字节数 | 字节序 | 说明 |
|---|---|---|---|
| Timestamp | 3 | 大端(BE) | 绝对时间戳(毫秒) |
| Message Length | 3 | 大端(BE) | Message完整字节数 |
| Message Type ID | 1 | - | 消息类型(0x08=音频,0x09=视频) |
| Message Stream ID | 4 | 小端(LE) | 消息流ID |
注意:前三个字段用大端字节序,但 Message Stream ID 用小端字节序,这是Adobe的历史遗留问题。
Type 1:省略 Stream ID(7字节)
使用场景:同一Stream上的新消息,但长度或类型与上一个不同。
python
字节偏移: 0 1 2 3 4 5 6
┌────┬────┬────┬────┬────┬────┬────┐
│ Time Delta │ Msg Length │Type│
│ (3 bytes) │ (3 bytes) │(1B)│
└────┴────┴────┴────┴────┴────┴────┘
与Type 0的区别:
- 没有 Message Stream ID(继承前一个Chunk的值)
- Timestamp 改为 Time Delta(与上一个Message的时间差)
Time Delta 示例:
| 消息序号 | 绝对时间戳 | Time Delta |
|---|---|---|
| Message 1 (Type 0) | 1000ms | - |
| Message 2 (Type 1) | 1033ms | 33ms |
| Message 3 (Type 1) | 1066ms | 33ms |
Type 2:只有时间戳(3字节)
使用场景:后续Chunk的长度和类型不变,只有时间戳变化。
css
字节偏移: 0 1 2
┌────┬────┬────┐
│ Time Delta │
│ (3 bytes) │
└────┴────┴────┘
Type 3:无头部(0字节)
使用场景:同一个Message内部的后续Chunk,完全继承前一个Chunk的所有信息。
css
(无Message Header,直接是Chunk Data)
Type 2 vs Type 3:
| 场景 | 建议类型 |
|---|---|
| 同一个Message的多个Chunk | Type 3(时间戳不变) |
| 音频流(每帧间隔完全固定) | Type 3(间隔恒定) |
| 帧率不稳定的视频 | Type 2(时间delta不固定) |
4种类型对比
| Chunk类型 | 头部大小 | 包含字段 | 典型使用场景 |
|---|---|---|---|
| Type 0 | 11字节 | 时间戳+长度+类型+StreamID | 流的第一个Message |
| Type 1 | 7字节 | 时间增量+长度+类型 | 同流新消息(StreamID不变) |
| Type 2 | 3字节 | 时间增量 | 消息多chunk时(长度不变) |
| Type 3 | 0字节 | 无 | 消息的连续chunk |
Part 3:Extended Timestamp(扩展时间戳)
为什么需要扩展时间戳
Message Header中的Timestamp字段只有3字节 ,最大值为 0xFFFFFF = 16,777,215 ms ≈ 4.66小时。
如果直播时长超过4.66小时,3字节就放不下了,需要扩展时间戳。
结构
scss
完整Chunk结构(带扩展时间戳):
┌──────────────┬────────────────┬──────────────────┬──────────────┐
│ Basic Header │ Message Header │Extended Timestamp│ Chunk Data │
│ (1-3字节) │ (0/3/7/11字节)│ (4字节) │ (≤128字节) │
└──────────────┴────────────────┴──────────────────┴──────────────┘
Extended Timestamp字段:
| 字段 | 字节数 | 字节序 | 说明 |
|---|---|---|---|
| Extended Timestamp | 4 | 大端 | 真实时间戳,最大约49天 |
触发条件

Type 3 chunk 的特殊规则
Type 3没有Message Header,怎么判断是否有Extended Timestamp?
规则:如果前一个同CSID的Chunk使用了Extended Timestamp,这个Type 3 Chunk也必须包含Extended Timestamp。
| 前一个Chunk | Type 3 Chunk |
|---|---|
| 有Extended Timestamp | 必须有Extended Timestamp |
| 没有Extended Timestamp | 没有Extended Timestamp |
完整结构总结
| Chunk类型 | Basic Header | Message Header | Ext Timestamp | Chunk Data | 最小总大小 |
|---|---|---|---|---|---|
| Type 0 | 1-3字节 | 11字节 | 0/4字节 | ≤128字节 | 12字节 |
| Type 1 | 1-3字节 | 7字节 | 0/4字节 | ≤128字节 | 8字节 |
| Type 2 | 1-3字节 | 3字节 | 0/4字节 | ≤128字节 | 4字节 |
| Type 3 | 1-3字节 | 0字节 | 0/4字节 | ≤128字节 | 1字节 |
四、Message Type ID 详解
Message Header中有1字节的 Message Type ID,标识这个Message的内容类型。
所有消息类型
| Type ID | 十六进制 | 名称 | 用途 |
|---|---|---|---|
| 1 | 0x01 | Set Chunk Size | 协商新的chunk大小 |
| 2 | 0x02 | Abort Message | 放弃某个消息 |
| 3 | 0x03 | Acknowledgement | 确认收到的字节数 |
| 4 | 0x04 | User Control Message | 流状态通知 |
| 5 | 0x05 | Window Ack Size | 设置ACK窗口大小 |
| 6 | 0x06 | Set Peer Bandwidth | 限制对端发送带宽 |
| 8 | 0x08 | Audio | 音频数据 |
| 9 | 0x09 | Video | 视频数据 |
| 15 | 0x0F | Data Message (AMF3) | AMF3编码数据 |
| 18 | 0x12 | Data Message (AMF0) | AMF0编码数据(元数据) |
| 20 | 0x14 | Command (AMF0) | AMF0编码命令 |
| 22 | 0x16 | Aggregate | 聚合消息 |
协议控制消息(Type 1-6)
Type 1:Set Chunk Size
作用:双方协商新的chunk大小,发送后立即生效。
| 属性 | 值 |
|---|---|
| 默认值 | 128字节 |
| 最大值 | 2,147,483,647字节 |
| 常用值(低延迟) | 128-512字节 |
| 常用值(高带宽) | 4096-60000字节 |
消息格式(4字节):
java
┌──────────────────────────────────────────┐
│ Chunk Size (4字节, 大端) │
└──────────────────────────────────────────┘
Type 3:Acknowledgement
作用:接收方定期向发送方汇报已接收的累计字节数,实现流量控制。
Type 5:Window Ack Size
作用:设置确认窗口大小(每收到多少字节发一次ACK)。
| 属性 | 值 |
|---|---|
| 默认值 | 2,500,000字节(2.5MB) |
| 作用 | 防止接收方缓冲区溢出 |
Type 6:Set Peer Bandwidth
作用:限制对端的发送速率。
Limit Type:
| 值 | 类型 | 说明 |
|---|---|---|
| 0 | Hard | 严格限制,不能超过 |
| 1 | Soft | 软限制,可临时超过 |
| 2 | Dynamic | 动态调整 |
音视频数据消息(Type 8/9)
Type 8:Audio(音频)
Payload第1字节格式:
scss
7 6 5 4 3 2 1 0
┌─────────┬─────┬───┬───┐
│ Format │Rate │Size│ Ch│
│ (4 bit) │(2b) │(1b)│(1b)│
└─────────┴─────┴───┴───┘
| 字段 | 常用值 | 说明 |
|---|---|---|
| Format | 10=AAC, 2=MP3 | 音频编码格式 |
| Rate | 3=44.1kHz | 采样率 |
| Size | 1=16bit | 采样精度 |
| Ch | 1=立体声 | 声道数 |
AAC的第2字节(PacketType):
| 值 | 含义 |
|---|---|
| 0x00 | AAC Sequence Header(解码器配置,必须先发) |
| 0x01 | AAC Raw Data(实际音频数据) |
Type 9:Video(视频)
Payload第1字节格式:
arduino
7 6 5 4 3 2 1 0
┌─────────┬─────────┐
│FrameType│ CodecID │
│ (4 bit) │ (4 bit) │
└─────────┴─────────┘
Frame Type:
| 值 | 名称 | 说明 |
|---|---|---|
| 1 | Key Frame | 关键帧(I帧),可独立解码 |
| 2 | Inter Frame | 非关键帧(P/B帧),依赖其他帧 |
| 3 | Disposable | 可丢弃帧 |
Codec ID:
| 值 | 编码 | 说明 |
|---|---|---|
| 7 | H.264 (AVC) | 目前最常用 |
| 4 | On2 VP6 | Flash时代常用 |
H.264的第2字节(AVCPacketType):
| 值 | 含义 |
|---|---|
| 0 | AVC Sequence Header(SPS/PPS配置,必须先发) |
| 1 | AVC NALU(实际视频数据) |
| 2 | AVC End of Sequence |
命令和数据消息(Type 18/20)
Type 18:Data Message(元数据)
作用:传输音视频的元数据信息,在推流开始时发送。
常见内容:
json
{
"width": 1920,
"height": 1080,
"videocodecid": "avc1",
"audiocodecid": "mp4a",
"framerate": 30,
"videodatarate": 2000
}
Type 20:Command(命令)
作用:传输RTMP命令,控制连接和流的状态。
常用命令列表:
| 命令名 | 方向 | 作用 |
|---|---|---|
| connect | 客户端→服务器 | 建立连接,协商参数 |
| createStream | 客户端→服务器 | 创建一个逻辑流 |
| publish | 客户端→服务器 | 开始推流 |
| play | 客户端→服务器 | 开始拉流 |
| deleteStream | 客户端→服务器 | 关闭流 |
| _result | 服务器→客户端 | 命令执行成功的响应 |
| _error | 服务器→客户端 | 命令执行失败的响应 |
| onStatus | 服务器→客户端 | 流状态变化通知 |
五、Chunk 的发送和接收流程
发送端:Message → Chunk
Chunk Type 的选择策略
发送端需要根据上下文选择最节省带宽的Chunk Type
带宽节省分析
以发送一个5000字节视频帧为例(chunk_size=128):
全部使用 Type 0(极端低效):
40个chunk × 11字节头 = 440字节头部开销
Type 0 + Type 3 组合(实际使用):
ini
1个Type 0头(11字节)+ 39个Type 3头(1字节)= 50字节头部开销
节省:440 - 50 = 390字节(减少88%的头部开销)
实际发送示例
场景:首次发送一个5000字节的H.264视频帧(时间戳1000ms)
ini
视频帧 5000字节,chunk_size=128
需要 ceil(5000/128) = 40个Chunk
Chunk V1 (Type 0,因为是该CSID的第一个Message):
Basic Header: 0x05 → fmt=0, csid=5
Message Header: timestamp=1000, length=5000, type=0x09, stream_id=1
Data: 第1-128字节
Chunk V2 ~ V40 (Type 3,同一个Message的后续Chunk):
Basic Header: 0xC5 → fmt=3, csid=5
Message Header: 无
Data: 对应128字节(最后一个Chunk为8字节)
接收端:Chunk → Message
核心数据结构:Chunk Stream 表
接收端维护一张表,每个CSID对应一个正在拼装的Message状态:
| CSID | type_id | length | 已接收 | 时间戳 | payload |
|---|---|---|---|---|---|
| 4 | 0x08 | 200 | 128 | 1000ms | 音频数据... |
| 5 | 0x09 | 5000 | 256 | 1000ms | 视频数据... |
| 3 | - | - | 0 | - | - |
接收流程
接收示例(结合多路复用)
以下是收到交替Chunk的处理过程:
收到 [0x05][11字节头][128字节数据](视频Chunk V1):
ini
解析:fmt=0, csid=5
更新状态表:
csid=5: {type=0x09, length=5000, received=128, timestamp=1000}
消息完整?5000 ≠ 128,等待更多
收到 [0x04][11字节头][128字节数据](音频Chunk A1):
ini
解析:fmt=0, csid=4
更新状态表:
csid=4: {type=0x08, length=200, received=128, timestamp=1000}
消息完整?200 ≠ 128,等待更多
收到 [0xC5][128字节数据](视频Chunk V2):
ini
解析:fmt=3, csid=5
继承状态:csid=5的信息不变
更新:received = 128 + 128 = 256
消息完整?5000 ≠ 256,继续等待
收到 [0xC4][72字节数据](音频Chunk A2):
ini
解析:fmt=3, csid=4
更新:received = 128 + 72 = 200
消息完整?200 == 200
→ 交给应用层:完整音频帧(200字节)
完整收发流程
六、常见问题 FAQ
Q1:chunk_size 越大越好吗?
A: 不是,需要根据场景权衡:
| chunk_size | 延迟 | 带宽利用率 | 适用场景 |
|---|---|---|---|
| 小(128-512B) | 低 | 差(头部开销大) | 实时直播 |
| 大(4096-60000B) | 高 | 好(头部开销小) | 点播、高带宽 |
实际开销对比(5000字节消息):
| chunk_size | Chunk数量 | 头部开销 | 占比 |
|---|---|---|---|
| 128B | 40个 | 50字节 | 1% |
| 1024B | 5个 | 14字节 | 0.28% |
| 4096B | 2个 | 12字节 | 0.24% |
Q2:多个音视频流怎么处理?
A: 每个流使用不同的CSID:
ini
直播间1: 视频CSID=5, 音频CSID=4
直播间2: 视频CSID=7, 音频CSID=6
接收端通过CSID区分,分别维护各自的Message状态。
Q3:为什么Stream ID用小端字节序?
A: Adobe的历史遗留设计错误。RTMP规范中其他所有多字节字段都用大端(网络字节序),只有Message Stream ID使用小端。这被认为是Adobe当年实现时的bug,但因为兼容性原因一直保留到现在。
Q4:Type 0 的时间戳是绝对时间还是相对时间?
A: Type 0 用绝对时间戳 (相对于流开始的毫秒数),Type 1/2 用时间增量(delta)。
| Chunk类型 | 时间戳含义 |
|---|---|
| Type 0 | 绝对时间戳(从0开始累加) |
| Type 1 | 与上一个Message的时间差 |
| Type 2 | 与上一个Message的时间差 |
总结
块流协议解决了TCP单通道传输多种数据时的阻塞问题:
分块传输 :把大Message拆成128字节的小Chunk
多路复用 :不同CSID的Chunk交替发送
4种类型 :Type 0/1/2/3 从完整到无头,节省带宽
状态维护:接收端按CSID分别拼装Message
下一篇
RTMP协议详解(三):AMF编码
握手完成、数据分块之后,RTMP命令(connect、publish、play)的内容是如何编码的?下一篇将讲解Adobe的私有序列化格式AMF。