15、RTMP协议入门:RTMP握手、AMF编码、消息类型、Chunk流机制
RTMP协议,说白了就是直播推流的老祖宗。我最早接触它的时候,还在用Flash播放器看视频。现在虽然HLS、WebRTC这些新协议很火,但RTMP在推流端依然是绝对的主力。你想想看,各大直播平台、OBS、FFmpeg,底层走的都是RTMP。
这一节,咱们就把它扒开看看。RTMP握手怎么玩?AMF编码是什么鬼?消息类型有哪些?Chunk流又是怎么分片的?嗯,一个一个来。
15.1 RTMP握手:先打个招呼
RTMP握手是客户端和服务器建立连接的第一步。说白了,就是双方互相确认一下「嘿,你也是RTMP协议吧?」。握手过程分三个阶段:
- C0/S0:版本号。客户端发一个字节,告诉服务器我用的是RTMP版本(通常是3)。服务器回一个字节确认。
- C1/S1:客户端发1536字节的随机数据。服务器也回1536字节。这里包含时间戳和随机数。
- C2/S2:客户端收到S1后,把S1的时间戳和随机数原样返回。服务器收到C1后也一样。
我刚开始做推流SDK时,就栽在握手这一步。当时服务器一直断开连接,我查了半天,发现是C2里时间戳没对齐。嗯,细节决定成败。
握手核心要点:
- C0和S0固定为0x03(RTMP版本3)
- C1和S1共1536字节,前4字节是时间戳,后4字节是随机数,其余填充0
- C2和S2是回显,必须原样返回对方的时间戳和随机数
避坑指南:我曾经遇到过服务器只支持简单握手(Simple Handshake),不支持复杂握手。如果你用FFmpeg推流连不上,试试加个 -rtmp_handshake simple 参数。
15.2 AMF编码:数据怎么打包?
AMF(Action Message Format)是Adobe搞出来的一种序列化格式。RTMP用它来传输命令、元数据这些控制信息。说白了,就是把数据变成二进制流,方便网络传输。
AMF有两种版本:AMF0和AMF3。RTMP主要用AMF0,偶尔也用AMF3。常见的类型有:
| 类型标记 | AMF0类型 | 说明 |
|---|---|---|
| 0x00 | Number | 双精度浮点数(8字节) |
| 0x01 | Boolean | 布尔值(1字节) |
| 0x02 | String | 字符串(2字节长度 + 内容) |
| 0x03 | Object | 键值对集合,以0x000009结束 |
| 0x08 | ECMA Array | 带长度的键值对集合 |
| 0x0A | Null | 空值 |
举个例子,你要发送一个命令「connect」,AMF编码后大概长这样:
// 伪代码:AMF编码 "connect"
0x02 // String类型
0x00 0x07 // 字符串长度7
0x63 0x6F 0x6E 0x6E 0x65 0x63 0x74 // "connect" 的ASCII码
我个人习惯在调试时,用Wireshark抓包看AMF数据。你想想看,如果编码错了,服务器根本不知道你在说什么。我曾经遇到过AMF Object的结束标记写成了0x000000,结果服务器一直解析不完,连接超时。
15.3 消息类型:RTMP在聊什么?
RTMP消息分为好几类。握手完成后,客户端和服务器就开始互发消息。消息类型由消息头里的Type ID决定:
| Type ID | 消息类型 | 用途 |
|---|---|---|
| 1 | Set Chunk Size | 设置Chunk大小 |
| 2 | Abort Message | 中断消息 |
| 3 | Acknowledgement | 确认收到数据 |
| 4 | User Control | 用户控制事件(如流开始/结束) |
| 5 | Window Acknowledgement Size | 设置确认窗口大小 |
| 6 | Set Peer Bandwidth | 设置对端带宽 |
| 8 | Audio | 音频数据 |
| 9 | Video | 视频数据 |
| 15 | Data Message (AMF3) | 元数据(如onMetaData) |
| 16 | Shared Object (AMF3) | 共享对象 |
| 17 | Command Message (AMF3) | 命令(如connect、play) |
| 18 | Data Message (AMF0) | 元数据(AMF0版本) |
| 20 | Command Message (AMF0) | 命令(AMF0版本) |
你看,音频视频数据分别用Type ID 8和9。命令消息用20(AMF0)或17(AMF3)。我一般用AMF0,兼容性更好。
注意:消息类型和Chunk Stream ID是两码事。消息类型告诉你怎么解析数据,Chunk Stream ID告诉你怎么重组分片。别搞混了。
15.4 Chunk流机制:大包怎么拆?
RTMP底层走的是TCP,但TCP是流式传输,没有消息边界。所以RTMP搞了个Chunk机制,把大消息切成小块,一块一块发。接收端再拼回去。
Chunk的基本结构:
- Basic Header:1-3字节,包含Chunk Stream ID和Chunk Type
- Message Header:0、3、7、11字节四种格式,包含时间戳、消息长度、消息类型
- Extended Timestamp:可选,当时间戳超过0xFFFFFF时用
- Chunk Data:实际数据,长度不超过Chunk Size
Chunk Type有四种:
| Type | Message Header长度 | 说明 |
|---|---|---|
| 0 | 11字节 | 完整消息头,用于新消息或时间戳变化 |
| 1 | 7字节 | 不含消息流ID,其他字段与Type 0相同 |
| 2 | 3字节 | 只含时间戳增量,其他字段继承 |
| 3 | 0字节 | 完全继承上一个Chunk的头部 |
为什么会这样设计?说白了就是为了省带宽。你想想看,视频帧的音频数据,消息类型、流ID这些字段几乎不变。用Type 3的Chunk,每次只发1字节的Basic Header,省了多少开销。
我做过一个优化:把Chunk Size从默认的128字节改成4096字节。推流时CPU占用率降了15%。但注意,改太大也不行,网络丢包时重传成本高。
Chunk流的核心逻辑:
- 发送方把消息按Chunk Size切成多个Chunk
- 第一个Chunk用Type 0,后续用Type 1/2/3
- 接收方根据Chunk Stream ID和消息长度重组
- 如果某个Chunk丢了,整个消息都得重传
15.5 知识体系总览
下面这张图,把RTMP协议的核心流程串起来了。从握手开始,到AMF编码、消息类型、Chunk分片,最后到音视频数据发送。我建议你把它存下来,写代码时对照着看。
15.6 实战中的坑
最后分享几个我踩过的坑:
- 握手超时:RTMP握手必须在几秒内完成。我曾经在弱网环境下测试,握手一直卡在C2阶段。后来加了超时重试机制,问题解决。
- AMF编码大小端:AMF0的Number是大端(Big Endian)。如果你用C/C++写,记得用htonl/htons转换。我见过有人直接memcpy,结果服务器解析出NaN。
- Chunk Size协商:客户端和服务器通过Set Chunk Size消息协商分片大小。默认128字节,但推流时建议调大。我一般设4096,兼顾延迟和CPU。
- 消息重组:接收方必须等同一个消息的所有Chunk到齐才能处理。如果中间丢了一个Chunk,整个消息都得丢弃。我曾经因为网络抖动,视频花屏,查了半天才发现是Chunk重组逻辑有bug。
调试小技巧:用Wireshark抓包时,过滤条件写 rtmp 或 rtmpt。然后看每个Chunk的Basic Header,确认Chunk Type是否正确。我习惯先抓一个完整的推流过程,对照协议文档逐帧分析。
好了,RTMP协议的核心就这些。握手、AMF编码、消息类型、Chunk流,这四个东西搞明白,你就能自己写一个简单的RTMP推流器了。嗯,下一节咱们聊聊怎么用代码实现它。