rtmp handshake | rtmp握手简单模式和复杂模式
客户端往服务端发送 c0+c1
服务端往客户端发送 s0+s1+s2
客户端往服务端发送 c2
长度
c0和s0都是1字节
c1和c2和s1和s2都是1536字节
两种模式
rtmp握手分两种模式:简单模式和复杂模式
下表是我整理的一些常见rtmp客户端程序、软件使用的握手模式。
名称 | 模式 |
---|---|
obs推流 | 简单 |
vlc播放器拉流 | 复杂 |
mpv播放器拉流 | 复杂 |
ffmpeg推流/拉流 | 复杂 |
nginx-rtmp-module推流/拉流 | 复杂 |
rtmp服务端正常来说应该两种模式都支持。(不然使用其中一种模式的rtmp客户端可以握手,另一种不行,就尴尬了。。)
本文档主要参照nginx-rtmp-module(以下简称 nrm
)的实现以 lal
的实现。
nrm作为rtmp开源服务器,有作为服务端时的握手实现。同时,由于它支持中继的功能,所以也有作为客户端的握手实现。
lal则是我自己使用Go语言写的流媒体服务器,支持rtmp协议。rtmp握手部分的实现是参考nrm写的。内部rtmp客户端握手使用简单模式。rtmp服务端握手两种模式都支持。
lal github地址: https://github.com/q191201771/lal
c0和s0的这个单字节为版本号,简单模式和复杂模式都一样。固定为0x03。
c1和c2和s1和s2在两种模式下格式不一样。
s1可看为c1的回复,c2可看为s2的回复。
nrm中把c0c1和s0s1称为challenge,把c2和s2称为response。
简单模式
可参考 spec-rtmp_specification_1.0.pdf
c0和c1
版本号,固定为0x03
c1和s1
| 4字节时间戳time | 4字节全0二进制串 | 1528字节随机二进制串 |
最前面的4字节时间戳一般以毫秒为单位。
nrm作为客户端时,c1中time使用的是当前unix时间戳的毫秒部分。
nrm作为服务端时,如果判断客户端为简单模式,解析完c1中的时间戳后并没有使用这个时间戳。 发送s1时,是将c1的1536字节原样返回的。
通过4字节二进制串全0,服务端可以判断出是客户端使用的是简单模式。
c2和s2
| 4字节时间戳time | 4字节time2 | 1528字节随机二进制串 |
按文档中的说法:
c2的time应该设置为s1中的time字段。c2的time2应该设置为收到s1的时间点。
s2的time应该设置为c1中的time字段。s2的time2应该设置为收到c1的时间点。
nrm作为服务端时,如果判断客户端为简单模式, 发送s2时,是将c1的1536字节原样返回的。
如果使用obs客户端(obs使用简单握手模式)和nrm服务端握手,你会发现c1、c2、s1、s2的整个1536字节是完全相同的。说明time和time2这些字段,nrm并没有完全按照文档说的来做。
复杂模式
hmac-sha256
介绍复杂模式前,先介绍一个哈希签名算法,即hmac-sha256算法。复杂模式会使用它做一些签名运算和验证。
简单来说,这个算法的输入为一个key(长度可以为任意)和一个input字符串(长度可以为任意),经过hmac-sha256运算后得到一个32字节的签名串。
key和input固定时,hmac-sha256运算结果也是固定唯一的。
c0
固定为0x03
c1
格式如下:
| 4字节时间戳time | 4字节模式串 | 1528字节复杂二进制串 |
time字段参照简单模式下time的说明。
4字节模式串, nrm
使用的是[0x0C, 0x00, 0x0D, 0x0E]。
1528字节复杂二进制串生成规则如下:
步骤一,将1528字节复杂二进制串进行随机化处理。
步骤二,在1528字节随机二进制串中写入32字节的digest签名。
digest的位置
先说明digest的位置如何确定。digest的位置可以在前半部分,也可以在后半部分。
当digest在前半部分时,digest的位置信息(以下简称offset)保存在前半部分的起始位置。
c1格式展开如下:
| 4字节time | 4字节模式串 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
offset = (c1[8] + c1[9] + c1[10] + c1[11]) % 728 + 12
计算出的offset是相对于整个c1的起始位置而言的。
为什么要取余728呢,因为前半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
为什么要加12呢,是因为要跳过4字节time+4字节模式串+4字节offset。
offset的取值范围为[12,740)。
当offset=12时, left
部分就不存在,当offset=739时, right
部分就不存在。
当digest在后半部分时,offset保存在后半部分的起始位置。
c1格式展开如下:
| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
offset = (c1[8+764] + c1[8+764+1] + c1[8+764+2] + c1[8+764+3]) % 728 + 8 + 764 + 4
计算出的offset依赖是相对于c1的其实位置而言的。
为什么要取余728呢,因为后半部分的764字节要减去offset字段的4字节,再减去digest的32字节。
为什么加8加764加4呢,是因为要跳过4字节time+4字节模式串+前半部分764字节+4字节offset。
offset的取值范围为[776,1504)。
当offset=776时, left
部分就不存在,当offset=1503时, right
部分就不存在。
nrm
作为客户端构造c1时,使用的是第一种格式,即digest放在前半部分。
digest如何生成
说完digest的位置,再说digest如何生成。
即将c1 digest左边部分拼接上c1 digest右边部分(如果右边部分存在的话)作为hmac-sha256的input(整个大小是1536-32),以下大小为30字节固定key作为hmac-sha256的key,进过hmac-sha256计算得出32字节的digest填入c1中digest字段中。
'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',
服务端在收到c1后,首先通过c1中的模式串,初步判断是否为复杂模式,如果是复杂模式,则通过c1重新digest,看计算得出的digest和c1中的包含的digest字段是否相同来确定握手是否为复杂模式。
注意,由于服务端无法直接得知客户端是将digest放在前半部分还是后半部分,所以服务端只能先验证其中一种,如果验证失败,再验证另外一种,如果都失败了,就考虑回退使用简单模式和客户端继续握手。
s0
固定为0x03
s1
s1的构造方法和c1相同。
只不过将模式串换成了 [0x0D, 0x0E, 0x0A, 0x0D]。
并且将hmac-sha256的key换成了如下36字节固定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',
s2
格式如下:
| 4字节时间戳time | 4字节time2 | 1528字节随机二进制串 |
其中time和time2字段参考简单模式下s2的说明。
1528字节随机二进制串中也需要填入digest。
nrm的做法是 将32字节digest直接填入s2的尾部,也即没有设置相应的offset
,digest的计算方法是,使用digest的左边部分作为hmac-sha256的input(大小是1536-32), 使用c1中的digest作为hmac-sha256的key
,通过hmac-sha256计算得出digest。
c2
c2的构造方法和s2相同。
只不过它是用s2中的digest作为hmac-sha256的key。