RTMP协议详解(二):块流协议

RTMP协议详解(二):块流协议

前言

上一篇讲了RTMP握手,握手完成后就进入了真正的数据传输阶段。但RTMP不是简单地把数据直接发出去,而是通过一套块流(Chunk Stream)机制来传输。

本文将深入讲解块流协议的设计原理、数据结构和完整的收发流程。


一、为什么需要分块?

问题的根源:TCP是一条单车道

RTMP运行在TCP之上,而TCP连接就像一条单行道公路

markdown 复制代码
推流端 ════════════════════════════════════▶ 服务器
       (所有数据必须排队,一个一个发送)

直播场景中,同时需要传输多种数据:

数据类型 大小 频率
视频帧(I帧) 50KB 每秒1-2次
视频帧(P帧) 5KB 每秒25-60次
音频帧 200字节 每秒25-50次
控制消息 几十字节 偶发

不分块的问题:大卡车堵路

把大视频帧比作大卡车,小音频帧比作小轿车,它们共用同一条单行道:

sequenceDiagram participant P as 推流端 participant S as 服务器 P->>S: 视频帧I帧 (50KB) Note over S: 等待传输50KB... Note over P: 音频帧在排队等待 P->>S: 音频帧1 (200B) Note over S: 音频延迟了500ms!

实际延迟计算(假设带宽100KB/秒):

markdown 复制代码
发送50KB视频帧:
    耗时 = 50KB ÷ 100KB/秒 = 500ms

音频帧被迫等待 500ms!
而实时直播要求音视频延迟 < 50ms

不分块带来的问题

问题 说明
队头阻塞 大帧阻塞后续所有数据
音视频不同步 音频等待视频传完才能发送
内存浪费 需要一次性缓冲整帧数据
无法插入紧急消息 关键控制消息无法优先发送

分块的解决方案

把大帧拆成多个小块(默认128字节),音视频交替发送

sequenceDiagram participant P as 推流端 participant S as 服务器 P->>S: 视频Chunk1 (128B) P->>S: 音频Chunk1 (128B) P->>S: 视频Chunk2 (128B) P->>S: 音频Chunk2 (72B) P->>S: 视频Chunk3 (128B) Note over S: 音视频交替传输,延迟极低!

分块后的延迟

markdown 复制代码
发送视频Chunk1 (128字节):
    耗时 = 128B ÷ 100KB/秒 ≈ 1.3ms

音频在 1.3ms 后即可发送(原来是500ms)
提升:快了约 400倍

分块的三大优势

graph LR A[分块传输] --> B[多路复用<br/>音视频交替发送] A --> C[降低延迟<br/>小块快速传输] A --> D[流量控制<br/>动态调整块大小]

二、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

作用:接收方定期向发送方汇报已接收的累计字节数,实现流量控制。

sequenceDiagram participant S as 服务器 participant C as 客户端 Note over S,C: Window Ack Size = 5MB S->>C: 发送数据... Note over C: 累计收到 5MB C->>S: Acknowledgement (seq=5MB) S->>C: 继续发送... Note over C: 累计收到 10MB C->>S: Acknowledgement (seq=10MB)

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 - -

接收流程
graph TD A[TCP收到数据] --> B[读取Basic Header] B --> C[解析 fmt 和 csid] C --> D[查找该CSID的Message状态] D --> E{fmt是多少?} E -->|0| F[读取11字节Message Header<br/>更新所有字段] E -->|1| G[读取7字节Message Header<br/>更新时间/长度/类型] E -->|2| H[读取3字节Message Header<br/>更新时间戳] E -->|3| I[不读Message Header<br/>继承前一个Chunk的信息] F --> J[读取Chunk Data] G --> J H --> J I --> J J --> K{Message是否完整?<br/>已接收字节 == length?} K -->|否| L[更新已接收字节数\n等待下一个Chunk] K -->|是| M[完整Message交给应用层处理] L --> A

接收示例(结合多路复用)

以下是收到交替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字节)

完整收发流程

sequenceDiagram participant APP1 as 发送应用层 participant CS as Chunk发送层 participant NET as 网络(TCP) participant CR as Chunk接收层 participant APP2 as 接收应用层 APP1->>CS: Message(视频,5000B,ts=1000) APP1->>CS: Message(音频,200B,ts=1000) CS->>NET: [V1: Type0, 128B] CS->>NET: [A1: Type0, 128B] CS->>NET: [V2: Type3, 128B] CS->>NET: [A2: Type3, 72B] CS->>NET: [V3-V40: Type3, ...] NET->>CR: 按顺序接收Chunk Note over CR: 维护CSID状态表,拼装Message CR->>APP2: 完整音频帧(200B) CR->>APP2: 完整视频帧(5000B)

六、常见问题 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。

相关推荐
修己xj21 小时前
一锹黄土,几缕炊烟:关于生命意义的思考
程序员
极光技术熊1 天前
全栈项目部署实战指南:Java / Python / Vue / React 一站式搞定
程序员·架构
程序员老申1 天前
第三篇 5 天 12 个 commit:踩坑实录与代码演进
后端·程序员
LeahDizon1 天前
AI Coding 协作实践方案
程序员·github·代码规范
KevinWang_1 天前
AE 基本操作
程序员
爱勇宝1 天前
CEO通知5100名员工:今年不涨薪了,钱要投给AI!
前端·后端·程序员
字节跳动数据库1 天前
文章分享——好代码 - 半点没用的话题
人工智能·程序员
AskHarries1 天前
手写一个最小 Agent
程序员
CodeSheep1 天前
又是梁文锋,有点猛啊。
前端·后端·程序员
SimonKing1 天前
低调低调,白嫖文生图,文生视频模型,无Token限制
java·后端·程序员