一、RTMP概述
- RTMP(Real Time Messaging Protocol)是Adobe公司提出的
实时消息传输协议
,用于传递带有时间信息的视频
、音频
和数据消息流
。 - 通过
分块传输机制
实现低延迟的音频、视频和数据通信,广泛应用于直播和视频会议等场景,常用于向云端推流 - 工作在
TCP
之上,默认端口为1935
,基于TCP协议进行传输。
RTMP交互过程:
握手协议
:建立TCP连接后,通过RTMP握手协议来完成RTMP的连接,包括客户端发送C0、C1、C2块,服务端发送S0、S1、S2块。RTMP分块(chunk)
:将RTMP消息(Message)拆分为多个块(Chunk)进行发送,每个Chunk包含音视频数据和信令。协议控制消息
(Protocol Control Message):使用特殊值代表控制消息,如SetChunkSize、SetACKWindowSize、CreateStream等。RTMP消息格式
(RTMP Message Format):包括消息头和有效负载,消息头包含消息类型、负载长度、时间戳和消息流ID
二、握手设计
RTMP握手过程
握手之前先建立TCP连接 然后再去握手 握手之后再去进行一些命令请求 消息控制
客户端发送C0和C1 服务端返回S0 S1
客户端发送C2 服务端返回S2
本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点:
客户端要等收到S1之后才能发送C2
客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)
服务端要等到收到C0之后发送S0,S1
服务端必须等到收到C1之后才能发送S2
服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)
理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的。
| client | server |
|-----C0 + C1---->|
|<----S0+S1+S2----|
|-------C2------->|
客户端将C0和C1捆绑在一起发送到服务器
服务器收到后一起返回S0S1S2
客户端收到后发送C2
服务端收到C2后进行发送其他信息(控制信息或者真实音视频数据)
完成握手
✨2.1 C0C1C2/S0S1S2拆解
C0和S0
:1个字节,包含了RTMP版本
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
| version |
+-+-+-+-+-+-+-+-+
C0 and S0 bits
C1和S1
:4字节时间戳,4字节的0,1528字节的随机数,总共加起来是1536字节
C1/S1 长度为 1536B(4+4+1528)。主要目的是确保握手的唯一性。
格式为 time + zero + random
time 发送时间戳,长度 4 byte
zero 保留值 0,长度 4 byte
random 随机值,长度 1528 byte,保证此次握手的唯一性,确定握手的对象
C2和S2
:4字节时间戳,4字节从对端读到的时间戳,1528字节随机数
C2/S2 的长度也是 1536B(4+4+1528)。
相当于就是 S1/C1的响应值,对应 C1/S1的 Copy 值,在于字段有点区别
time ,C2/S2 发送的时间戳,长度 4 byte
time2 ,S1/C1 发送的时间戳,长度 4 byte(C2 中的 time2 字段是 S1 中的 time 字段值,即客户端收到服务器端发送的 S1 后,将其中的 4 字节时间戳拷贝到 C2 的 time2 字段;同理,S2 中的 time2 字段是 C1 中的 time 字段值。他们各自拷贝的time2字段必须得是相等的,这是为了让对端验证时间戳是否一致)
random,S1/C1发送的随机数,长度为1528B
✨2.2 握手总结
总体的交互流程其实就是
| client | server |
|-----C0 + C1---->|
|<----S0+S1+S2----|
|-------C2------->|
客户端发起:发送 C0(1字节版本号)+ C1(1536字节随机数)
服务器响应:发送 S0(1字节版本号)+ S1(1536字节随机数)+ S2(回显C1)
客户端回显:发送 C2(回显S1)
握手完成后,双方可以进行 RTMP 消息通信。
其中C0和S0会包含RTMP的版本号
C1是客户端随机数 S1是服务端随机数
S2是服务端回显 服务器把客户端发来的 C1 原样返回
C2是客户端回显 客户端把服务端发来的S1原样返回
代码示例:
客户端处理服务端发送来的S0S1S2
三、分块传输
RTMP在传输的过程会将大的消息(Message)会被拆分成小的块(Chunk)进行传输
多个消息可以交错传输,通过 chunk stream ID 区分,主要是为了维持稳定连续传递,避免单次传输数据量问题, 避免大块数据占用带宽,实现小块传输减少延迟
例如:
假设我们同时要传输音频和视频数据:
视频消息(大小3000字节, csid=1)
音频消息(大小100字节, csid=2)
在RTMP中,这两个消息会被分成小块(chunk)进行传输,比如每个chunk大小为128字节:
[视频chunk1] (csid=1, 128字节)
[音频chunk1] (csid=2, 100字节)
[视频chunk2] (csid=1, 128字节)
......
然后接收方解析chunk通过csid区分是什么样的数据流 再去重组完整的消息
这样就能避免大视频消息阻塞小的音频消息 保证音频的实时传输 实现更均匀的带宽使用 避免突发的大数据传输。
✨3.1 块格式
块Chunk分为2个部分 一个是Chunk Header 一个是Chunk Data
Chunk Header包含:Basic Header, Message Header, Extended Timestamp
Basic Header(基本头,1~3字节)
:包含chunk stream ID(块流ID)和chunk type(块类型fmt)。Message Header(消息头,0,3,7,11字节)
:包含被发送的消息信息。Extended Timestamp(扩展时间戳,0,4字节)
:扩展时间戳。
✨3.1.1 Basic Header基本头
基本头包含了块流ID(csid)和块类型(fmt)
基本头的作用: (控制格式和流标识)
- 指示块的类型(fmt)
- 标识数据流(csid)
- 决定消息头MessageHeader的格式(由fmt决定)
它有3个版本 一字节二字节三字节版本 它的第一个字节的高2位都是存放fmt字段
一字节版本
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|fmt| cs id |
+-+-+-+-+-+-+-+-+
高2位存放fmt块类型,低6位存放csid字段
csid范围为(2~63):6位能表示的数值范围为:2^6 = 64个数(0-63),不过csid=0和1被保留用于触发2/3字节的扩展格式,以支持更大的csid值,所以csid范围为2~63
二字节版本
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 0 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
第一个字节的高2位存放fmt块类型,剩下的低6位固定0填充。
第二个字节存放csid的值-64
csid的范围为(64~319)
csid的值=第二个字节的值+64
三字节版本
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 1 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
第一个字节的高2位存放fmt块类型,剩下的低6位固定1填充。
第二和第三个字节组成一个 16 位无符号整数(小端序),表示实际的 CSID 值减去 64。
csid的范围为(64~65599(64+65535))
csid的值=第三字节的值*256 + 第二字节的值 + 64 (第二字节和第三字节看成一个整体存放csid-64,第三字节作为高8位需要*256进行抬高)
✨3.1.2 Message Header消息头
消息头的作用:
- 携带消息的具体消息
- 根据BasicHeader的fmt字段决定包含哪些字段
- 处理时间戳
Message Header(块信息头)
用于存放实际信息的描述信息
。
它有四种不同的格式,由 Basic Header 中的chunk type即fmt(块类型)进行区分
timestamp( 原始时间戳):占用3个字节
messagelength(消息数据长度):占用3个字节
,表示实际发送的消息的数据(如音频帧、视频帧等数据)的长度,是整个Chunk的长度message type id(消息的类型id):占用1个字节
,表示实际发送的数据的类型1、2、3、5、6:用于协议控制信息
8:代表音频数据
9:代表视频数据message stream id(信息流id):占用4个字节
表示该chunk所搭载的信息所在的信息流的ID
✨3.1.3 Extended Timestamp扩展时间戳
时间戳的概念
时间戳主要用于音视频同步点 或者 播放控制等
时间戳分类:
相对时间戳 内部时间戳 扩展时间戳
相对时间戳:表示当前数据包与前一个数据包的时间差
内部时间戳: 记录实际的播放时间点 用于内部时间同步和播放控制 通过累加相对时间戳得到
Extended Timestamp扩展时间戳:用于扩展 Message Header 中时间戳的表示。
- 当相对时间戳超过16777215(约4.66小时)时使用
- 提供更大的时间戳范围
代码中的体现:
扩展时间戳实际使用例子:
原有的时间戳数组只能保存3字节的数据 大概是4.6小时
这时候就需要用到扩展时间戳
✨3.2 分块传输核心函数
包含Chunk的创建发送和解析块进行消息重组
✨3.2.1 CreateChunk()
int RtmpChunk::CreateChunk(uint32_t csid, RtmpMessage &in_msg, char *buf, uint32_t buf_size)
负责处理RTMP中数据块传输 将传来的RtmpMessage大消息分割为小块(chunk)进行高效网络传输,支持流式处理和带宽控制
会先去创建第一个Chunk(构造BasicHeader的时候采用fmt0表示后面的MessageHeader将采用11字节的完整头部,整体包含时间戳、消息长度、类型ID和流ID等完整元数据),创建基本头和消息头,以及处理扩展时间戳
后续的块使用fmt=3格式(仅1字节头部),表示自动复用前一个块的头部信息
这样的好处是:
- 减少重复头部信息的传输
- 提高带宽利用效率
- 保持数据的连续性