2. RTMP协议深度解析:握手、连接、流控制与数据包结构

RTMP这协议,说实话,年头不短了。Adobe在2009年把它开放出来,到现在十几年过去,直播行业里它依然是"老大哥"。我最早接触RTMP是在2015年做互动直播项目,那时候HLS延迟太高,WebRTC还没普及,RTMP几乎是唯一选择。

今天咱们就把RTMP的底裤扒干净。从握手到连接,从数据包结构到流控制,每个细节我都会结合实战经验来讲。你想想看,搞懂了这些,后面做协议适配才能游刃有余。

2.1 RTMP握手:三次握手的变种

RTMP的握手,说白了就是客户端和服务器互相确认身份的过程。它跟TCP的三次握手有点像,但更复杂一些。

握手分为三个阶段:

  1. C0/S0:版本声明。客户端发一个字节,告诉服务器"我用的是RTMP版本X"。服务器回一个字节确认。
  2. C1/S1:随机数据。客户端发1536字节的随机数据,服务器也发1536字节。这里面包含时间戳和随机数。
  3. 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从握手到推流的完整流程串起来了:

RTMP握手与连接流程 RTMP握手与连接流程 客户端 服务器 阶段1:握手 C0 (版本) S0 (版本) C1 (随机) S1 (随机) C2 (回声) S2 (回声) 阶段2:连接与推流 connect() _result createStream() _result (ID=1) publish() onStatus(开始) 说明:握手阶段共4次交互(C0/S0/C1/S1/C2/S2),连接阶段共3次交互(connect/createStream/publish) 实际项目中,C0+C1可合并发送,减少一次RTT

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,一眼就能看出问题出在哪个阶段。你想想看,如果握手都没过,后面分析音视频数据就是浪费时间。