RTMP协议解析[一]
文章目录
- RTMP协议解析[一]
- RTMP握手
- 简单握手包结构
- 复杂握手的包结构
- RTMP握手的时序图
- 实现
RTMP协议解析[一]
本专栏重点负责介绍RTMP协议的理论部分, 跳过定义,协议与其他协议的优缺点对比,协议的拓展与改进,协议的历史发展等其他废话
RTMP协议的工作流程大致如下:
- 客户端与服务器建立TCP连接。
- 双方通过握手过程确认协议版本及交换随机数等信息。
- 客户端发送连接命令(connect)到服务器。
- 服务器响应连接命令,返回连接结果。
- 客户端与服务器建立流(stream)进行音视频数据传输。
- 在传输过程中,双方可以发送控制命令,如播放(play)、暂停(pause)等。
- 当连接关闭时,双方结束消息传输并断开连接。
本专栏重点介绍三个部分
-
RTMP的握手
-
RTMP消息
-
RTMP推流
RTMP握手
握手分为3个阶段
- C0和S0:客户端和服务器分别发送一个字节的版本号(C0和S0),表示双方的RTMP协议版本。通常情况下,这个版本号是
0x03
。 - C1和S1:接下来,客户端发送一个2048字节的数据包(C1),其中包括4字节的时间戳、4字节的零填充数据和随机填充的剩余字节。服务器接收到C1后,也会发送一个类似的数据包(S1),包括4字节的时间戳、4字节的客户端时间戳回显(即C1中的时间戳)和随机填充的剩余字节。
- C2和S2:最后,客户端和服务器互相发送确认数据包(C2和S2),包括4字节的对方发送的时间戳(服务器发给客户端的是S1中的时间戳,客户端发给服务器的是C1中的时间戳),以及4字节的对方接收到的时间戳(服务器发给客户端的是S1中的客户端时间戳回显,客户端发给服务器的是C1中的时间戳)和随机填充的剩余字节。
当然,协议规定了在握手的过程中存在时序的限制:
-
客户端通过发送 C0 和 C1 消息来启动握手过程
-
客户端必须接收到 S1 消息,然后发送 C2 消息
-
客户端必须接收到 S2 消息,然后发送其他数据
-
服务端必须接收到 C0 或者 C1 消息,然后发送 S0 和 S1 消息
-
服务端必须接收到 C2 消息,然后发送其他数据
在具体的开始讲解RTMP握手包的包格式的时候,还要区分出简单握手和复杂握手,这两种握手方式的包格式有所差异
简单握手包结构
C0和S0:简单握手的C0和S0包仅一个字节,用来表示rtmp协议的版本号,主流的为3
C1和S1:包长1536个字节,结构如下图
time字段:时间戳,取值可以为零或其他任意值,用于本终端发送的所有后续块的时间起点
zero字段:必须为0
random字段:可以随意填充,但是为了彼此区分,因此要足够随机,这个不需要对随机数进行加密保护,也不需要动态值。
C2S2:包长1536个字节,结构如下图
time字段:这个字段必须包含终端在 S1 (给 C2) 或者 C1 (给 S2) 发的 timestamp
time2字段:这个字段必须包含终端先前发出数据包 (s1 或者 c1) timestamp
radom echo字段:这个字段必须包含终端发的 S1 (给 C2) 或者 S2 (给 C1) 的随机数。两端都可以一起使用 time 和 time2 字段再加当前 timestamp 以快速估算带宽和/或者连接延迟,但这不太可能是有多大用处。
复杂握手的包结构
在复杂握手中,C0和S0的包结构并没有变化,但是后续的包全都发生了变化,具体变化如下:
C1和S1: 包长度为 1536 字节,除了4字节的时间戳和4字节版本号外,还有764字节的key和764字节的digest,具体结构有两种,如下:
time字段:同简单握手
version字段:固定取值,客户端为0x0C, 0x00, 0x0D, 0x0E。服务端为0x0D, 0x0E, 0x0A, 0x0D
digest:密文
key:密钥
密钥:
random-data:offset字节的随机数
key-data字段:128字节的密钥
random-data字段:764-offset-128-4个字节的随机数
offset字段:4个字节,用来表示密钥的位置
密文:
offset字段: 4 bytes,digest的位置信息,offset=offset[0]+offset[1]+offset[2]+offset[3] random-data: (offset) bytes,随机数据
digest-data字段: 32 bytes,计算出来的摘要
random-data字段: (764 - 4 - offset - 32) bytes,随机数据
digest的计算方法:digest-data左边部分+digest-data右边部分的内容,以固定的key,做hmac-sha256计算得到digest。
特别的,服务端和客户端的密钥都是公开的:
static const uint8_t rtmp_player_key[] = {'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ','F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ', '0', '0', '1',0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1, 0x02,0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB, 0x93, 0xB8,0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE};static const uint8_t rtmp_server_key[] = {'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ','F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ','S', 'e', 'r', 'v', 'e', 'r', ' ', '0', '0', '1',0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1, 0x02,0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB, 0x93, 0xB8,0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE};
server 端在收到 C1 后,需要依次尝试 key 和 digest 的两种相对顺序进行解析,得到 key-data 和 digest-data。
key-data 没有用处,digest-data 需要按照 client 生成 C1 digest 的方法,也生成一个进行比较,如果比较失败,则尝试使用 simple handshake
C2S2:包长度为 1536 字节。结构如下:
random-data字段 (1504 bytes):随机数据
digest-data (32 bytes):random-data的摘要
digest-data计算方法:
S2:先通过C1的digest,计算出key,再用这个key计算random-data的digest。
C2:先通过S1的digest,计算出key,再用这个key计算random-data的digest。
RTMP握手的时序图
注意握手过程中存在包的时序限制,例如服务端只有收到C0或C1才能发S0S1
协议理想中的握手时序图:
实际的握手情况:
因为要收到C0或者C1才能发S0和S1,并且S2的发送并没有限制,因此一般都是一起发送
实现
后续会有专栏专门用C++对Rtmp协议进行实现
- C1S1的第5~8字节全0,则表示使用简单握手
- 简单握手不需要验证数据,只需要按照协议按顺序发送和接受消息即可
- 复杂握手的C1S1,发送发只需要选择其中一种,而接受方先验证其中的一种结构,失败的话,再验证另一种结构即可
- digest验证失败,则使用简单握手模式