上一篇文章简单阐述了,在 H5 中,做直播需要哪些技术知识点,有哪些直播流协议和技术。通过对比,本篇主要聚焦于 RTMP 直播协议的相关内容,也就是说,本篇将会直接进行实际操作 Buffer 的练习和相关的学习。
RTMP 是什么
RTMP 全称即是 Real-Time Messaging Protocol。顾名思义就是用来作为实时通信的一种协议。该协议是 Adobe 搞出来的。主要是用来传递音视频流的。它通过一种自定义的协议,来完成对指定直播流的播放和相关的操作。和现行的直播流相比,RTMP 主要的特点就是高效,这里,我就不多费口舌了。我们先来了解一下 RTMP 是如何进行握手的。
RTMP 握手
RTMP 是基于 TCP 三次握手之后的,所以,RTMP 不是和 TCP 一个 level 的。它本身是基于 TCP 的可靠性连接。RTMP 握手的方式如图:
(C 代表 Client,S 代表 Server)
它主要是通过两端的字段内容协商,来完成可信度认证的。基本过程如下:
client: 客户端需要发 3 个包。C0,C1,C2
server: 服务端也需要发同样 3 个包。 S0,S1,S2。
整个过程如上图所述,但实际上有些细节需要注意。
握手开始:
【1】 客户端发送 C0,C1 包
此时,客户端处于等待状态。客户端有两个限制:
客户端在未接受到 S1 之前不能发送 C2 包
客户端在未接收到 S2 之前不能发送任何实际数据包
【2】 服务端在接受到 C0,发送 S0,S1 包。也可以等到接受到 C1 之后再一起发送,C1 包的等待不是必须的。
此时,服务端处于等待状态。服务端有两个限制:
服务端在未接受到 C1 之前不能发送 S2.
服务端在未接收到 C2 之前不能发送任何实际数据包
【3】客户端接受到 S1/S0 包后,发送 C2 包。
【4】服务端接受到 C2 包后,返回 S2 包,并且此时握手已经完成。
不过,在实际应用中,并不是严格按照上面的来。因为 RTMP 并不是强安全性的协议,所以,S2/C2 包只需要 C1/S1 中的内容,就可以完成内容的拼接。
这么多限制,说白了,其实就是一种通用模式:
C0+C1
S0+S1+S2
C2
接下来,我们来具体看看 C/S 012 包分别代表什么。
C0 && S0
C0 和 S0 其实区别不大,我这里主要讲解一下 C0,就差不多了。首先,C0 的长度为 1B。它的主要工作是确定 RTMP 的版本号。
C0:客户端发送其所支持的 RTMP 版本号:3~31。一般都是写 3。
S1:服务端返回其所支持的版本号。如果没有客户端的版本号,默认返回 3。
C1 && S1
C1/S1 长度为 1536B。主要目的是确保握手的唯一性。格式为:
time: 发送时间戳,这个其实不是很重要,不过需要记住,不要超出 4B 的范围即可。
zero: 保留值 0.
random: 该字段长尾 1528B。主要内容就是随机值,不管你用什么产生都可以。它主要是为了保证此次握手的唯一性,和确定握手的对象。
C2 && S2
C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值。上图也简单说明了就是,对应 C1/S1 的 Copy 值,不过第二个字段有区别。基本格式为:
time: 时间戳,同上,也不是很重要
time2: C1/S1 发送的时间戳。
random: S1/C1 发送的随机数。长度为 1528B。
这里需要提及的是,RTMP 默认都是使用 Big-Endian 进行写入和读取,除非强调对某个字段使用 Little-Endian 字节序。
上面握手协议的顺序也是根据其中相关的字段来进行制定的。这样,看起来很容易啊哈,但是,我们并不仅仅停留在了解,而是要真正的了解,接下来,我们来实现一下,如果通过 Buffer 来进行 3 次握手。这里,我们作为 Client 端来进行请求的发起,假设 Server 端是按照标准进行发送即可。
Buffer 实操握手
我们使用 Buffer 实操主要涉及两块,一个块是 request server 的搭建,还有一块是 Buffer 的拼接。
Request Server 搭建
这里的 Server 是直接使用底层的 TCP 连接。
如下,一个简易的模板:
const client = new net.Socket();
client.connect({
port: 1935,
host: "6721.myqcloud.com"},
()=>{
console.log("connected");
});
client.on('data',(data)=>{
client.write('hello');
});
不过,为了更好的进行实际演练,我们通过 EventEmitter 的方式,来做一个筛选器。这里,我们使用 mitt 模块来做代理。
const Emitter = require('mitt')();
然后,我们只要分析的就是将要接受到的 S0/1/2 包。根据上面的字节包图,可以清楚的知道包里面的详细内容。这里,为了简单起见,我们排除其他协议的包头,只是针对 RTMP 里面的包。而且,我们针对的只有 3 种包,S0/1/2。为了达到这种目的,我们需要在 data 时间中,加上相应的钩子才行。
这里,我们借用 Now 直播的 RTMP 流来进行相关的 RTMP 直播讲解。
Buffer 操作
Server 的搭建其实上网搜一搜,应该都可以搜索出来。关键点在于,如何针对 RTMP 的实操握手进行 encode/decode。所以,这里,我们针对上述操作,来主要讲解一下。
我们主要的工作量在于如何构造出 C0/1/2。根据上面格式的描述,大家应该可以清楚的知道 C0/1/2 里面的格式分别有啥。
比如,C1 中的 time 和 random,其实并不是必须字段,所以,为了简单起见,我们可以默认设为 0。具体代码如下:
class C {
constructor() {
this.time;
this.random;
}
C0() {
let buf = Buffer.alloc(1);
buf[0] = 3;
return buf;
}
C1() {
let buf = Buffer.alloc(1536);
return buf;
}
/**
* write C2 package
* @param {Number} time the 4B Number of time
* @param {Buffer} random 1528 byte
*/
produceC2(){
let buf = Buffer.alloc(1536);
// leave empty value as origin time
buf.writeUInt32BE(this.time, 4);
this.random.cop