2. 流媒体协议基础
RTMP这协议,说实话,年头不短了。Adobe在2009年把它开放出来,到现在十几年过去,直播行业里它依然是"老大哥"。我最早接触RTMP是在2015年做互动直播项目,那时候HLS延迟太高,WebRTC还没普及,RTMP几乎是唯一选择。
今天咱们就把RTMP的底裤扒干净。从握手到连接,从数据包结构到流控制,每个细节我都会结合实战经验来讲。你想想看,搞懂了这些,后面做协议适配才能游刃有余。
2.1 RTMP握手:三次握手的变种
RTMP的握手,说白了就是客户端和服务器互相确认身份的过程。它跟TCP的三次握手有点像,但更复杂一些。
握手分为三个阶段:
- C0/S0:版本声明。客户端发一个字节,告诉服务器"我用的是RTMP版本X"。服务器回一个字节确认。
- C1/S1:随机数据。客户端发1536字节的随机数据,服务器也发1536字节。这里面包含时间戳和随机数。
- C2/S2:回声确认。客户端把S1的内容原样返回,服务器把C1的内容原样返回。
关键点:握手是串行的。客户端必须收到S1才能发C2,收到S2才算握手完成。但有些实现允许"优化握手",把C0+C1一起发,减少一次RTT。
我记得有一次排查推流失败问题,抓包发现客户端发了C0+C1,但服务器只回了S0+S1,然后就没然后了。后来发现是服务器版本号不匹配——客户端声明版本3,服务器只支持版本2。嗯,这种坑在老旧设备上特别常见。
2.2 连接建立:从NetConnection到NetStream
握手完成后,RTMP会建立两条逻辑通道:
- NetConnection:应用层连接。客户端发"connect"命令,带上应用名(比如"live"),服务器验证后返回成功。
- NetStream:流通道。在NetConnection之上创建,用于传输音视频数据。
连接流程大致是这样:
客户端 → 服务器: connect("rtmp://example.com/live")
服务器 → 客户端: _result(连接成功)
客户端 → 服务器: createStream()
服务器 → 客户端: _result(stream ID = 1)
客户端 → 服务器: publish("mystream", "live")
服务器 → 客户端: onStatus("NetStream.Publish.Start")
实战技巧:我建议在connect命令里带上"tcUrl"参数,它包含了完整的RTMP URL。有些CDN会根据tcUrl做路由,不带的话可能被拒绝。
2.3 数据包结构:RTMP的"信封"
RTMP的数据包分两层:Chunk(块)和Message(消息)。一个Message可能被拆成多个Chunk发送,接收端再重组。
2.3.1 Chunk基本格式
每个Chunk包含:
| 字段 | 大小 | 说明 |
|---|---|---|
| Basic Header | 1-3字节 | 包含chunk stream ID和chunk type |
| Message Header | 0, 3, 7, 11字节 | 消息的时间戳、长度、类型ID |
| Extended Timestamp | 0或4字节 | 当时间戳超过0xFFFFFF时使用 |
| Chunk Data | 可变 | 实际数据 |
Chunk Type有4种,用Basic Header里的前2位表示:
- Type 0 (00):11字节Message Header,完整信息。用于流的第一个chunk或时间戳变化时。
- Type 1 (01):7字节Message Header,省略了message stream ID。用于同一流中连续的消息。
- Type 2 (10):3字节Message Header,只包含时间戳增量。用于同一消息的后续chunk。
- Type 3 (11):0字节Message Header,完全复用前一个chunk的头部。用于批量发送。
注意:Type 3虽然省带宽,但如果前一个chunk丢失,整个流都会解析失败。我曾经遇到一个推流端疯狂用Type 3,结果网络抖动时画面全花。后来我强制要求每30个chunk至少插入一个Type 0,问题就解决了。
2.3.2 Message类型
RTMP定义了多种Message类型,常用的有:
| 类型ID | 名称 | 用途 |
|---|---|---|
| 0x01 | Set Chunk Size | 设置chunk大小,默认128字节 |
| 0x02 | Abort Message | 中断消息传输 |
| 0x03 | Acknowledgement | 确认收到数据量 |
| 0x04 | User Control | 用户控制事件(如流开始/结束) |
| 0x05 | Window Acknowledgement Size | 设置确认窗口大小 |
| 0x06 | Set Peer Bandwidth | 设置对端带宽限制 |
| 0x08 | Audio | 音频数据 |
| 0x09 | Video | 视频数据 |
| 0x12 | Data Message (AMF0) | 元数据(如onMetaData) |
| 0x14 | Command Message (AMF0) | 命令(如connect, publish) |
2.4 流控制:别让数据撑爆缓冲区
RTMP的流控制机制,说白了就是"你发慢点,我快撑不住了"。它主要通过三个参数来调节:
- Chunk Size:每个chunk的最大字节数。默认128字节,可以通过Set Chunk Size消息调整。我一般设成4096或8192,减少chunk数量,降低CPU开销。
- Window Acknowledgement Size:接收方告诉发送方"你每发X字节,我就确认一次"。默认是5000000字节(约5MB)。
- Peer Bandwidth:发送方告诉接收方"我的带宽上限是X,你别超"。这个值可以设为"硬限制"或"软限制"。
为什么会需要这些?你想想看,如果推流端以10Mbps的速度发数据,而拉流端只有2Mbps的带宽,不控制的话缓冲区瞬间就爆了。RTMP通过Acknowledgment消息来同步收发速率:
发送方 → 接收方: 发送数据(累计X字节)
接收方 → 发送方: Acknowledgement(已收到X字节)
发送方: 收到确认后继续发送
我曾经在弱网环境下调试RTMP推流,发现手机端频繁发Acknowledgment,但服务器端就是不调整发送速率。后来查代码发现,服务器把Window Acknowledgement Size设成了0——等于关闭了流控制。嗯,这种配置错误在自研服务器里特别常见。
2.5 握手与连接流程总览
下面这张图把RTMP从握手到推流的完整流程串起来了:
2.6 实战中的坑与建议
搞了这么多年RTMP,我总结几个常见问题:
- 握手超时:有些设备握手时C2发得慢,服务器等不及就断开了。我建议把握手超时设到10秒以上。
- Chunk Size太小:默认128字节,推高清流时CPU开销巨大。我一般设成4096,平衡延迟和性能。
- AMF编码错误:RTMP用AMF0或AMF3编码命令。有些库混用两种格式,导致解析失败。我建议统一用AMF0,兼容性最好。
- 时间戳回绕:RTMP时间戳是24位,最大0xFFFFFF。超过后会回绕到0。我见过播放器因为时间戳回绕导致画面卡死。解决方案是用Extended Timestamp扩展成32位。
我的习惯:每次做RTMP适配,我都会先抓包看握手和连接过程。用Wireshark配合rtmpdump,一眼就能看出问题出在哪个阶段。你想想看,如果握手都没过,后面分析音视频数据就是浪费时间。