rtmp handshake | rtmp握手简单模式和复杂模式

文章前放个广告,我个人采用Go语言编写的一个开源流媒体服务器,欢迎关注: lal (https://github.com/q191201771/lal)

交互顺序

客户端往服务端发送 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,地址: https://github.com/q191201771/doc/blob/master/spec-rtmp_specification_1.0.pdf

c0和c1

版本号,固定为0x03

c1和s1

1
| 4字节时间戳time | 4字节全0二进制串 | 1528字节随机二进制串 |

最前面的4字节时间戳一般以毫秒为单位。

nrm作为客户端时,c1中time使用的是当前unix时间戳的毫秒部分。
nrm作为服务端时,如果判断客户端为简单模式,解析完c1中的时间戳后并没有使用这个时间戳。发送s1时,是将c1的1536字节原样返回的。

通过4字节二进制串全0,服务端可以判断出是客户端使用的是简单模式。

c2和s2

1
| 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

格式如下:

1
| 4字节时间戳time | 4字节模式串 | 1528字节复杂二进制串 |

time字段参照简单模式下time的说明。

4字节模式串,nrm使用的是[0x0C, 0x00, 0x0D, 0x0E]。

1528字节复杂二进制串生成规则如下:

步骤一,将1528字节复杂二进制串进行随机化处理。

步骤二,在1528字节随机二进制串中写入32字节的digest签名。

digest的位置

先说明digest的位置如何确定。digest的位置可以在前半部分,也可以在后半部分。

当digest在前半部分时,digest的位置信息(以下简称offset)保存在前半部分的起始位置。

c1格式展开如下:

1
| 4字节time | 4字节模式串 | 4字节offset | left[...] | 32字节digest | right[...] | 后半部分764字节 |
1
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格式展开如下:

1
| 4字节time | 4字节模式串 | 前半部分764字节 | 4字节offset | left[...] | 32字节digest | right[...] |
1
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字段中。

1
2
3
'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

1
2
3
4
'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

格式如下:

1
| 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。

原文链接: https://pengrl.com/p/20027/
原文出处: yoko blog (https://pengrl.com)
原文作者: yoko
版权声明: 本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。

0%