相关文章
HTTP-FLV协议介绍
mini-rtmp-server介绍
RTMP官方文档
cloud.tencent.com/developer/a…
当前文章版本 v0.1
1.rtmp协议介绍
rtmp是一个基于tcp的流数据传输协议,基于字节流实现,是一种常用的直播推流协议, 被设计用来对基于底层传输协议(如TCP)的多媒体传输流(如音频、视频和交互数据)进行复用和分包。许多直播平台都可以使用OBS的RTMP来推流,比如bilibili
2.关键字
负载(payload): 包中所承载的数据。 例如音频或视频数据。
包: 一个数据包由固定头部和所承载的数据组成。一些底层协议可能需要定义数据包的封装格式
端口: 在一个给定计算机中用于区分不同目标的抽象定义。 在TCP/IP协议中用一个小的正整数来表示端口。 OSI传输层的传输选择器等同于端口的概念。
传输地址: 用于唯一标识一个传输终端的网络地址和端口的组合, 例如IP地址和TCP端口的组合。 数据包从源地址传输到目的地址。
消息流: 允许消息传播的逻辑通讯通道。
消息流ID: 每个消息都会有一个关联的ID,用于标识其所在的消息流。
块: 消息的一个片段。 消息在网络上传输之前会被分成更小的片段,并交错存取。 分块确保在多个流中安全的按照时间戳顺序端到端传输。
块流: 允许块向某一确定方向传播的逻辑通讯通道。 块流可以是客户端到服务端, 也可以是服务端到客户端。
块流ID: 每个块都会有一个关联ID,用于标识其所在的块流。
复用: 将分开的音频/视频数据整合为统一的音视频流, 以使多个音视频流可以同步传输的过程。
复用分离: 复用的逆向过程。将交错的音视频数据分离为原始的音频和视频数据的过程。
远程过程调用(RPC) : 允许客户端或服务端调用另一端程序过程的请求。
元数据: 数据的描述信息。 一个影片的元数据包括名称、时长、创建时间等信息。
应用实例: 服务器上允许客户端发起连接请求的应用程序实例。
动作消息格式(AMF) : 用于序列化ActionScript对象图的一种紧凑二进制格式。 AMF有两个版本: AMF0和AMF3。
字节序: 顾名思义字节的顺序,既多字节类型的数据在内存中的存放顺序。TCP/IP各层协议将字节序定义为大字节序,因此TCP/IP协议中使用的字节序通常称之为网络字节序。
大字节序: (Big-Endian)高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
小字节序: (Little-Endian)低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
3.块流
RTMP通常将一个流分成多个块进行传输,创建的每个块都有一个唯一 ID 对其进行关联,这个 ID 叫做 chunk stream ID (块流 ID)。这些块通过网络进行传输。传递时,每个块必须被完全发送才可以发送下一块。在接收端,这些块被根据块流 ID 被组装成消息。
Basic Header (基本头,1 到 3 个字节):这个字段对块流 ID 和块类型进行编码。块类型决定了消息头的编码格式。(这一字段的) 长度完全取决于块流 ID,因为块流 ID 是一个可变长度的字段。
Message Header (消息头,0,3,7,或者 11 个字节):这一字段对正在发送的消息 (不管是整个消息,还是只是一小部分) 的信息进行编码。这一字段的长度可以使用块头中定义的块类型进行决定。
Extended Timestamp (扩展 timestamp,0 或 4 字节):这一字段是否出现取决于块消息头中的 timestamp 或者 timestamp delta 字段。更多信息参考 5.3.1.3 节。
Chunk Data (有效大小):当前块的有效负载,相当于定义的最大块大小。
基础头
块基本头,由块fmt与块流ID组成,其中fmt占 2 bit,分别为(0,1,2)用于决定消息头类型
块基本头长度可能有 1,2 或者 3 个字节,取决于块流 ID。
RTMP 协议最多支持 65597 个流,流 ID 范围 3 - 65599。其中 ID 0、1、2被保留
- 当cs id为 0 值表示二字节形式,并且 ID 范围 64 - 319 (第二个字节 + 64)
- 当cs id为 1 值表示三字节形式,并且 ID 范围为 64 - 65599 ((第三个字节) * 256 + 第二个字节 + 64)。3 - 63 范围内的值表示整个流 ID。
- 当cs id为 2 值的块流 ID 被保留,用于下层协议控制消息和命令
块流 ID 2 - 63 可以编进这一字段的一字节版本中。
块流 ID 64 - 319 可以以二字节的形式编码在头中。ID 计算为 (第二个字节 + 64):
块流 ID 64 - 65599 可以编码在这个字段的三字节版本中。ID 计算为 ((第三个字节) * 256 + (第二个字节) + 64)。
抓包展示
消息头
块消息头又四种不同的格式,由块基本头中的fmt字段进行选择。一个 (RTMP) 实现应该为每个块消息头使用最紧凑的表示。
类型 0
类型 0 块头的长度是 11 个字节。这一类型必须用在块流的起始位置,和流 timestamp 重来的时候 (比如,重置)。
抓包展示
timestamp (三个字节):对于 type-0 块,当前消息的绝对 timestamp 在这里发送。如果 timestamp 大于或者等于 16777215 (十六进制 0xFFFFFF),这一字段必须是 16777215,表明存在 Extended Timestamp 字段来补充无法表示的 timestamp。否则的话,这一字段表示完整的 timestamp。
类型 1
类型 1 块头长为 7 个字节。不包含消息流 ID;这一块使用前一块一样的流 ID。可变消息长度的流应该在第一块之后使用这一格式表示之后的每个新消息。
抓包展示
类型 2
类型 2 块头长度为 3 个字节。既不包含流 ID 也不包含消息长度;这一块具有和前一块相同的流 ID 和消息长度。具有不变长度的消息应该在第一块之后使用这一格式表示之后的每个新消息。
类型 3
类型 3 的块没有消息头。流 ID、消息长度以及 timestamp delta 等字段都不存在;这种类型的块使用前面块一样的块流 ID。当单一一个消息被分割为多块时,除了第一块的其他块都应该使用这种类型。组成流的消息具有同样的大小,流 ID 和时间间隔应该在类型 2 之后的所有块都使用这一类型。如果第一个消息和第二个消息之间的 delta 和第一个消息的 timestamp 一样的话,那么在类型 0 的块之后要紧跟一个类型 3 的块,因为无需再来一个类型 2 的块来注册 delta 了。如果一个类型 3 的块跟着一个类型 0 的块,那么这个类型 3 块的 timestamp delta 和类型 0 块的 timestamp 是一样的。
小结
总之,如果后一个块流的消息头缺少那个字段,说明此字段与前一个块流的数值相同,这样可以尽可能的节约header所占的空间
通用头字段
块消息头中各字段的描述如下:
timestamp delta (三个字节):对于一个类型 1 或者类型 2 的块,前一块的 timestamp 和当前块的 timestamp 的区别在这里发送。如果 delta 大于或者等于 16777215 (十六进制 0xFFFFFF),那么这一字段必须是为 16777215,表示具有扩展 timestamp 字段来对整个 32 位 delta 进行编码。否则的话,这一字段应该是为具体 delta。
message length (三个字节):对于一个类型 0 或者类型 1 的块,消息长度在这里进行发送。注意这通常不同于块的有效载荷的长度。块的有效载荷代表所有的除了最后一块的最大块大小,以及剩余的 (也可能是小消息的整个长度) 最后一块。
message type id (消息类型 id,一个字节):对于类型 0 或者类型 1 的块,消息的类型在这里发送。
message stream id (四个字节):对于一个类型为 0 的块,保存消息流 ID。消息流 ID 以小端格式保存。所有同一个块流下的消息都来自同一个消息流。当可以将不同的消息流组合进同一个块流时,这种方法比头压缩的做法要好。但是,当一个消息流被关闭而其他的随后另一个是打开着的,就没有理由将现有块流以发送一个新的类型 0 的块进行复用了。
Extended timestamp
extended timestamp 字段用于对大于 16777215 (0xFFFFFF) 的 timestamp 或者 timestamp delta 进行编码;也就是,对于不适合于在 24 位的类型 0、1 和 2 的块里的 timestamp 和 timestamp delta 编码。这一字段包含了整个 32 位的 timestamp 或者 timestamp delta 编码。可以通过设置类型 0 块的 timestamp 字段、类型 1 或者 2 块的 timestamp delta 字段 16777215 (0xFFFFFF) 来启用这一字段。当最近的具有同一块流的类型 0、1 或 2 块指示扩展 timestamp 字段出现时,这一字段才会在类型为 3 的块中出现。
4. 总抓包展示
推流
拉流
5. RTMP握手
5.2.1. 握手顺序
RTMP实际上是有握手过程的,如下图
握手以客户端发送 C0 和 C1 块开始。
客户端必须等待接收到 S1 才能发送 C2。
客户端必须等待接收到 S2 才能发送任何其他数据。
服务器端必须等待接收到 C0 才能发送 S0 和 S1,也可以等待接收到 C1 再发送 S0 和 S1。服务器端必须等待接收到 C1 才能发送 S2。服务器端必须等待接收到 C2 才能发送任何其他数据。
但事实并非如此,我们可以使用wireshark
进行抓包,服务器使用nginx
,使用OBS
推流
通过抓包发现,nginx的实现并未去遵循客户端必须等待接收到 S1 才能发送 C2的规范;但这没有影响;当然具体是谁的问题,我们无从考证,由于其他文章顺序正常,我们暂且认为是抓包过程出现问题
经过多次反复重试,发现还是有正常的
我们放弃从rtmp层面进行分析,按照tcp层面进行分析,然后发现是S0+S1与S2分开传输的问题
我们发现,nginx使用的是服务器端接收到 C0 发送 S0 和 S1,服务器端等待接收到 C1 发送 S2
|client|server|
| -C0+C1-> |
| <-S0+S1--|
| <-S2--|
| ---C2--> |
但实际上没必要这么复杂,我们在实现时完全可以等待接收到 C1 发送 S0 和 S1。接收到 C1 发送 S2这样发送数据,而没必要将S0+S1与S2分开, 从而减少一次握手次数
|client|server|
| -C0+C1-> |
| <-S0+S1+S2--|
| ---C2--> |
C0与S0的格式
C0 和 S0 包都是一个单一的八位字节
版本号 (八位):在 C0 中,这一字段指示出客户端要求的 RTMP 版本号。在 S0 中,这一字段指示出服务器端选择的 RTMP 版本号。本文档中规范的版本号为3。0、1、2 三个值是由早期其他产品使用的,是废弃值;4 - 31 被保留为 RTMP 协议的未来实现版本使用;32 - 255 不允许使用 (以区分开 RTMP 和其他常以一个可打印字符开始的文本协议)。无法识别客户端所请求版本号的服务器应该以版本 3 响应,(收到响应的) 客户端可以选择降低到版本 3,或者放弃握手。
C1与S1 的格式
C1 和 S1 数据包的长度都是 1536 字节,包含以下字段:
Time (四个字节):这个字段包含一个 timestamp,用于本终端发送的所有后续块的时间起点。这个值可以是 0,或者一些任意值。要同步多个块流,终端可以发送其他块流当前的 timestamp 的值。
Zero (四个字节):这个字段必须都是 0。
Random data (1528 个字节):这个字段可以包含任意值。终端需要区分出响应来自它发起的握手还是对端发起的握手,这个数据应该发送一些足够随机的数。这个不需要对随机数进行加密保护,也不需要动态值。
C2与S2 的格式
C2 和 S2 数据包长度都是 1536 字节,基本就是 S1 和 C1 的副本 (分别),包含有以下字段:
Time (四个字节):这个字段必须包含终端在 S1 (C2用) 或者 C1 (S2用) 发的 timestamp。
Time2 (四个字节):这个字段必须包含终端先前发出数据包 C1 (C2用) 或者 S1 (S2用) timestamp。
Random echo (1528 个字节):这个字段必须包含终端发的 S1 (C2用) 或者 C1 (S2用) 的随机数。两端都可以一起使用 time 和 time2 字段再加当前 timestamp 以快速估算带宽和/或者连接延迟,但这不太可能是有多大用处。
C0+C1
S0+S1+S2
C2
握手过程主要交换版本信息,时间信息
5. 消息类型
控制信息
RTMP 块流使用消息类型 ID 为 1、2、3、5 和 6 用于协议控制消息。这些消息包含有 RTMP 块流协议所需要的信息。
ID 0x01:设置Chunk大小
ID 0x02:终止消息
ID 0x03:确认
ID 0x05:窗口大小确认
客户端或服务端发送该消息来通知对端发送确认消息所使用的视窗大小,并等待接收端发送确认消息。接收端在接收到视窗大小后必须发送确认消息。
ID 0x06:设置带宽
ID 0x09: 视频类型
ID 0x08: 音频类型
ID 0x12: AMF0 Data
ID 0x14:AMF0 Command
6. AMF0
AMF(Action Message Format)是Adobe定义的一个关于数据结构组织的一种格式, 分为AMF0与AMF3,不过使用方面还是以AMF为主, RTMP Body中的结构就是以AMF的形式进行组织, Rtmp Client与Rtmp Sever之间通过约定好的AMF格式,来进行数据交流。
AMF Type
类型16进制值 | 表示类型 |
---|---|
0x00 | number |
0x01 | boolean |
0x02 | string |
0x03 | object |
0x04 | movieclip |
0x05 | null |
0x06 | undefined |
0x07 | reference |
0x08 | ecma-array |
0x09 | object-end |
0x0a | strict-array |
0x0b | date |
0x0c | long string |
0x0d | unsupported |
0x0e | recordset |
0x0f | xml-document |
0x10 | typed-object |
length
可有可无,比如:null,Number或Object不需要length字段;其他字段需要使用长度,length 字段共占用 1 个 byte.
payload
根据AMF Type与 length字段来确定,用于存放数据
5. 建立连接
建立网络连接(NetConnection)
包括以下步骤:
- 客户端设置Chunk大小
- 客户端请求建立连接
- 服务器设置客户端的应答窗口大小
- 服务器设置客户端的发送带宽大小
- 服务器设置Chunk大小
- 服务器响应连接结果
创建流
推流
- releaseStream:客户端通知服务端释放对应的流,以便接下来重新创建流。
- createStream:客户端请求服务端创建一个新的流。
- _result:服务器响应创建结果
- publish:客户端向服务端请求发布流。
- onStatus:服务端对客户端发布请求的反馈。
FCPublish与publish未查到相关资料
@setDataFrame: 客户端到服务器,向服务器告知一些元信息
接下来就是OBS向rtmp服务器推流的过程
拉流
拉流的创建流过程就比较简单
- 客户端创建流
- 服务端响应
- 客户端请求获取StreamLength
- 客户端发送命令消息中的“播放” (play) 命令到服务器。
- 服务器发送用户控制消息中的 “stream begin” ,告知客户端流 ID
- 告知状态
- 告知元信息(见推流的设置元信息)