RTMP协议基础:协议原理、握手流程与消息类型
RTMP(Real-Time Messaging Protocol)最初由Macromedia设计,后由Adobe公开,是直播推流领域最经典的“老将”。尽管如今SRT、WebRTC等新协议不断涌现,但国内绝大多数CDN、云厂商和OBS、FFmpeg等工具链对RTMP的支持依然最完善。理解RTMP的底层原理,是排查推流卡顿、断连、延迟飙升的第一步。
2.1 核心原理:基于TCP的长连接与分块传输
RTMP本身并不定义视频编码格式,它只负责把已经编码好的H.264视频流、AAC音频流以及元数据(如分辨率、帧率)封装成自己的消息格式,再通过TCP连接传输到服务器。其核心设计可以概括为三点:
- 单一长连接:RTMP推流通常只建立一个TCP连接(默认端口1935),所有控制命令、音视频数据都在这条连接上复用传输,避免了频繁建连的开销。
- 分块(Chunk)机制:为了降低大视频帧阻塞小控制消息的风险,RTMP将消息切割成更小的Chunk(默认每个Chunk最多128字节),多个消息的Chunk可以交错发送,实现类似“优先级调度”的效果。
- 流式ID复用:同一个连接内通过Stream ID区分不同的媒体流(如视频流、音频流、数据流),通过Message Type ID区分消息类型。
关键理解:RTMP是“有状态”的协议。推流端和服务器之间需要维护会话状态(如是否已握手、是否已发布流、当前时间戳基线),任何一方的状态异常都会导致连接被重置。
2.2 握手流程:从C0/C1到S2的“三次往返”
RTMP握手是推流端与服务器建立信任的第一步。握手过程共需交换6个数据块:C0/S0(版本号)、C1/S1(时间戳+随机数)、C2/S2(对端S1/C1的回应)。握手完成后,才能开始发送真正的命令消息。
握手的关键细节在于:
- C0和S0各占1字节,表示协议版本,目前几乎都是3(RTMP版本3)。
- C1和S1各占1536字节,前4字节为时间戳,后4字节为零,其余为随机字节。
- C2和S2各占1536字节,需要回显对端S1/C1的时间戳和随机数,用于验证连接是否有效。
在实际推流中,OBS或FFmpeg会先发送C0+C1,然后等待S0+S1,再发送C2,最后等待S2。虽然规范要求严格按顺序,但很多服务器实现允许客户端将C0+C1+C2一起发送(“优化握手”),以减少一次RTT。
排障提示:如果推流日志中出现 Handshake failed 或 Timeout waiting for S1,通常意味着服务器端口未开放、防火墙拦截了TCP 1935端口,或者服务器不支持RTMP(如误用了RTMPS端口)。
2.3 消息类型:控制消息与媒体数据的“身份标签”
RTMP通过Message Type ID区分不同用途的消息。理解这些ID,能帮助你在抓包或查看服务器日志时快速定位问题。下表列出了最常见的消息类型:
| Message Type ID | 消息名称 | 方向 | 典型用途 |
|---|---|---|---|
| 1 | Set Chunk Size | 双向 | 协商后续Chunk的最大字节数(默认128,可调大) |
| 2 | Abort Message | 双向 | 通知对方丢弃指定Chunk Stream中未完成的消息 |
| 3 | Acknowledgement | 双向 | 接收方确认已收到指定字节数,用于流量控制 |
| 4 | User Control Message | 双向 | 流开始/结束、缓冲清空、Ping请求等事件 |
| 5 | Window Acknowledgement Size | 双向 | 声明发送方的窗口大小(如2500000字节) |
| 6 | Set Peer Bandwidth | 双向 | 限制对端发送带宽 |
| 8 | Audio Message | 推流→服务器 | 承载AAC音频数据(含AudioSpecificConfig序列头) |
| 9 | Video Message | 推流→服务器 | 承载H.264视频数据(含AVC序列头与参数集) |
| 18 | AMF0 Command Message | 双向 | 命令交互:connect、createStream、publish、play等 |
| 20 | AMF0 Data Message | 推流→服务器 | 发送onMetaData(分辨率、帧率、码率等元数据) |
其中,Message Type ID 8和9是推流中最核心的媒体数据消息。每个视频关键帧(IDR帧)通常以AVC序列头(包含SPS/PPS)开始,然后才是实际的I帧或P帧数据。如果服务器收到的第一个视频消息不是AVC序列头,往往会直接拒绝推流或导致播放端黑屏。
2.4 典型推流命令序列:从connect到publish
一次标准的RTMP推流,在握手完成后,会依次发送以下AMF0命令(以OBS推流到Nginx-RTMP为例):
1. connect("rtmp://192.168.1.10/live", app="live", tcUrl="rtmp://192.168.1.10/live")
<- 服务器回应 _result(connect success) + Window Ack Size + Set Peer Bandwidth
2. createStream()
<- 服务器回应 _result(stream_id=1)
3. publish("mystream", "live")
<- 服务器回应 onFCPublish + onStatus(NetStream.Publish.Start)
4. 发送 @setDataFrame (onMetaData) // 包含 width=1920, height=1080, framerate=30...
5. 发送 Video Message (AVC sequence header) // SPS/PPS
6. 发送 Audio Message (AAC sequence header) // AudioSpecificConfig
7. 循环发送 Video Message (H.264 NAL units) 和 Audio Message (AAC frames)
注意第4步的onMetaData必须在音视频序列头之前发送。如果元数据中的width、height与后续实际编码的SPS不一致,部分播放器会出现画面拉伸或无法解码的问题。
2.5 常见误区与复盘:为什么握手成功却推不上流?
误区一:忽略Chunk Size协商。默认Chunk Size为128字节,如果推流端不发送Set Chunk Size消息,服务器会按128字节解析。对于高码率推流(如8Mbps),大量小Chunk会增加CPU开销和TCP小包数量,容易触发服务器限流。建议在connect后主动发送Set Chunk Size为4096或更大。
误区二:AMF0命令中的对象键顺序敏感。有些服务器实现(特别是老版本Nginx-RTMP)对connect命令中app和tcUrl的解析顺序有严格要求。如果tcUrl中的路径与app不匹配,服务器会返回NetConnection.Connect.Rejected。
误区三:时间戳回绕或跳跃。RTMP时间戳是32位无符号整数,约49.7天回绕一次。对于长时间直播,需要正确处理回绕。更常见的问题是时间戳跳跃——如果推流端因编码延迟导致时间戳突然增加几百毫秒,服务器会认为网络抖动而触发缓冲或断开。
复盘案例:某次线上推流每隔几分钟就断连一次,抓包发现推流端在发送视频消息时,时间戳从0x0000A3B2突然跳到0x0001F4A0(相差约86秒)。原因是采集端在系统休眠恢复后,未重置PTS基准。修复方法:在编码器重启或系统休眠恢复时,主动发送新的AVC序列头并重置时间戳基线。
最后需要强调的是,RTMP虽然基于TCP,但TCP的可靠性并不等于直播的流畅性。TCP重传导致的延迟累积、拥塞窗口收缩引发的发送暂停,都会表现为推流端“发送缓冲区满”或服务器端“接收超时”。理解RTMP的消息分块和流控制机制,是后续分析推流卡顿和延迟问题的基石。