CategoryResourceRepost/极客时间专栏/geek/从0打造音视频直播系统/WebRTC 1对1通话/11 | 如何通过Node.js实现一套最简单的信令系统?.md
louzefeng bf99793fd0 del
2024-07-09 18:38:56 +00:00

18 KiB
Raw Blame History

通过前面几篇文章的讲解,我想现在你应该已经对 WebRTC 有了一个清楚的认知了。接下来的章节咱们就使用 WebRTC 逐步实现一套真实可用的 1 对 1 实时直播系统吧。

WebRTC 1.0 规范对 WebRTC 要实现的功能、API 等相关信息做了大量的约束,比如规范中定义了如何采集音视频数据、如何录制以及如何传输等。甚至更细的,还定义了都有哪些 API以及这些API 的作用是什么。但这些约束只针对于客户端,并没有对服务端做任何限制。

那WebRTC规范中为什么不对服务器也做约束呢其实这样做有以下三点好处。

  • 第一点,可以集中精力将 WebRTC 库做好。WebRTC的愿景是使浏览器能够方便地处理音视频相关的应用规范中不限制服务端的事儿可以使它更聚焦。
  • 第二点,让用户更好地对接业务。你也清楚,信令服务器一般都与公司的业务有着密切的关系,每家公司的业务都各有特色,让它们按照自已的业务去实现信令服务器会更符合它们的要求。
  • 第三点,能得到更多公司的支持。WebRTC扩展了浏览器的基础设施及能力而不涉及到具体的业务或产品这样会更容易得到像苹果、微软这种大公司的支持否则这些大公司之间就会产生抗衡。

当然,这样做也带来了一些坏处,最明显的一个就是增加了学习 WebRTC 的成本因为你在学习WebRTC的时候必须自己去实现信令服务器,否则你就没办法让 WebRTC 运转起来,这确实增加了不少学习成本。

不过有了本专栏,你就不用再担心这个问题了。接下来,我就向你讲解一下,如何实现一套最简单的 WebRTC 信令服务系统。有了这套信令服务系统WebRTC就能运转起来这样你就能真正地体验到 WebRTC 的强大之处了。

在WebRTC 处理过程中的位置

在开始讲解 WebRTC 信令服务器之前,我们先来看一下本文在 WebRTC 处理过程中的位置。

通过上面这幅图,你可以清楚地知道本文所讲的主要内容就是红色方框中的信令服务器部分。

WebRTC信令服务器的作用

你若想要实现 WebRTC 信令服务器,首先就要知道它在 WebRTC 1 对 1 通信中所起的作用。实际上它的功能是蛮简单的,就是进行信令的交换,但作用却十分关键。在通信双方彼此连接、传输媒体数据之前,它们要通过信令服务器交换一些信息,如媒体协商

举个例子,假设 A 与 B 要进行音视频通信,那么 A 要知道 B 已经上线了同样B 也要知道 A 在等着与它通信呢。也就是说,只有双方都知道彼此存在,才能由一方向另一方发起音视频通信请求,并最终实现音视频通话。比如我们在《08 | 有话好商量,论媒体协商》一文中讲的媒体信息协商的过程就是这样一个非常典型的案例,双方的 SDP 信息生成后,要通过信令服务器进行交换,从而达到媒体协商的目的。

那在 WebRTC 信令服务器上要实现哪些功能,才能实现上述结果呢?我想至少要实现下面两个功能:

  1. 房间管理。即每个用户都要加入到一个具体的房间里,比如两个用户 A 与 B 要进行通话,那么它们必须加入到同一个房间里。
  2. 信令的交换。即在同一个房间里的用户之间可以相互发送信令。

信令服务器的实现

了解了 WebRTC 信令服务器的作用并且还知道了信令服务器要实现的功能接下来我们就操练起来看看如何实现信令服务器吧我将从下面5个方面来向你逐步讲解如何实现一个信令服务器。

1. 为什么选择 Node.js

要实现信令服务器,你可以使用 C/C++、Java 等语言一行一行从头开始编写代码,也可以以现有的、成熟的服务器为基础,做二次开发。具体使用哪种方式来实现,关键看你的服务器要实现什么功能,以及使用什么传输协议等信息来决策。

以我们要实现的信令服务器为例,因它只需要传输几个简单的信令,而这些信令既可以使用 TCP、 HTTP/HTTPS传输也可以用 WebSocket/WSS 协议传输,所以根据它使用的传输协议,你就可以很容易地想到,通过 Web 服务器如Nginx、Node.js来构建我们的信令服务器是最理想、最省时的、且是最优的方案。

你可以根据自己的喜好选择不同的 Web 服务器(如 Apache、Nginx 或 Node.js来实现而今天我们选择的是Node.js所以接下来我们将要讲解的是如何使用 Node.js 来搭建信令服务器

实际上Apache、Nginx和Node.js都是非常优秀、且成熟的Web服务器其中 Nginx 可以说是性能最好的Web服务器了从未来的发展角度来说Node.js则会更有优势

Node.js 的最大优点是可以使用 JavaScript 语言开发服务器程序。这样使得大量的前端同学可以无缝转到服务器开发,甚至有可能前后端使用同一套代码实现。对于使用 JavaScript 语言实现全栈开发这一点来说,我想无论是对于个人还是对于企业都是极大的诱惑。更可贵的是 Node.js 的生态链非常完整,有各种各样的功能库,你可以根据自己的需要通过安装工具(如 NPM快速将它们引入到你的项目中这极大地提高了 JavaScript 研发同学的开发效率。

Node.js 的核心是V8JavaScript引擎Node.js通过它解析JavaScript 脚本来达到控制服务器的目的。对于 JavaScript 同学来说Node.js的出现是革命性的它不仅让 JavaScript 同学成为了全栈开发工程师而且还让JavaScript开发同学的幸福指数飙升真正地感受到了 JavaScript 无所不能的能力。对于我这样的开发“老鸟”来说10年前还不敢想象通过 JavaScript 来写服务器程序呢,现在它却已成为现实!

当然, Node.js 不仅可以让你用 JavaScript 控制服务器,它还为你留了拓展接口,这些拓展接口甚至可以让你使用 C/C++ 为它编写模块,这样 Node.js 的功能就更加强大了。

2. Node.js的基本工作原理

Node.js的工作原理如上图所示其核心是 V8 引擎。通过该引擎,可以让 JavaScript 调用 C/C++方法或对象。反过来讲,通过它也可以让 C/C++ 访问JavaScript方法和变量。

Node.js 首先将 JavaScript 写好的应用程序交给 V8 引擎进行解析V8理解应用程序的语义后再调用 Node.js 底层的 C/C++ API将服务启动起来。所以 Node.js 的强大就在于JavaScript 与 C/C++ 可以相互调用,从而达到使其能力可以无限扩展的效果

我们以Node.js开发一个 HTTP 服务为例Node.js 打开侦听的服务端口后,底层会调用 libuv 处理该端口的所有 HTTP 请求。其网络事件处理的过程就如下图所示:

当有网络请求过来时首先会被插入到一个事件处理队列中。libuv会监控该事件队列当发现有事件时先对请求做判断如果是简单的请求就直接返回响应了如果是复杂请求则从线程池中取一个线程进行异步处理。

线程处理完后,有两种可能:一种是已经处理完成,则向用户发送响应;另一种情况是还需要进一步处理,则再生成一个事件插入到事件队列中等待处理。事件处理就这样循环往复下去,永不停歇。

3. 安装与使用 Node.js

了解了 Node.js的基本原理后接下来我们还是要脚踏实地来看看具体如何安装、使用Node.js。

1安装Node.js

不同环境下安装 Node.js的方法也不一样不过都很简单。

在Ubuntu系统下执行

apt install nodejs

或在Mac 系统下执行:

brew install nodejs

通过以上步骤Node.js 很快就安装好了(我这里安装的 Node.js版本为v8.10.0)。

2安装 NPM

除了安装 Node.js 之外还要安装NPMNode Package Manager也就是 Node.js 的包管理器,或叫包安装工具。它与 Ubuntu 下的 APTAdvanced Package Tool命令或 Mac 系统下的 BREW 命令类似,是专门用来管理各种依赖库的。

以Linux为例在 APT 没有出现之前在Linux上安装软件是件特别麻烦的事儿比如要安装一个编辑器其基本步骤有如下:

  • 先将这个工具(编辑器)的源码下载下来;
  • 执行./configure 生成Makefile 文件;
  • 执行 make 命令对源码进行编译;
  • 如果编译成功,执行 make install 将其安装到指定目录下;
  • 如果编译过程中发现还需要其他库则要对依赖库执行前面的4步也就是先将依赖库安装好然后再来安装该工具。

由这你可以看出以前在Linux下安装个程序或工具是多么麻烦。

不过 Linux 有了 APT工具后一切都变得简单了。你只要执行apt install xxx一条命令就好了,它会帮你完成上面的一堆操作。

对于 Node.js的安装包也是如此NPM就是相当于 Linux 下的 APT 工具它的出现大大提高了JavaScript 开发人员的工作效率

下面我们就来看一下如何安装 NPM 工具实际上NPM 的安装就像前面安装 Node.js 一样简单。

在Ubuntu下执行

apt install npm

或在Mac下执行

brew install npm

4. Socket.io的使用

除了 Node.js 外我们最终还要借助Socket.io来实现 WebRTC 信令服务器。Socket.io特别适合用来开发WebRTC的信令服务器通过它来构建信令服务器大大简化了信令服务器的实现复杂度这主要是因为它内置了房间的概念。

上图是 Socket.io 与 Node.js配合使用的逻辑关系图其逻辑非常简单。Socket.io 分为服务端和客户端两部分。服务端由 Node.js加载后侦听某个服务端口客户端要想与服务端相连首先要加载 Socket.io 的客户端库,然后调用 io.connect();即可与服务端连接上。

这里需要特别强调的是 Socket.io 消息的发送与接收。Socket.io 有很多种发送消息的方式,其中最常见的有下面几种,也是你必须要掌握的。

  • 给本次连接发消息
	socket.emit()

  • 给某个房间内所有人发消息
	io.in(room).emit()

  • 除本连接外,给某个房间内所有人发消息
	socket.to(room).emit()

  • 除本连接外,给所有人发消息
	socket.broadcast.emit()

你也可以看看下面的例子其中S 表示服务器C 表示客户端,它们是发送消息与接收消息的比对。

  • 发送 command 命令
	S: socket.emit('cmd);
	C: socket.on('cmd',function(){...});

  • 发送了一个 command 命令,带 data 数据
	S: socket.emit('action', data);
	C: socket.on('action',function(data){...});

  • 发送了command命令还有两个数据
	S: socket.emit(action,arg1,arg2);
	C: socket.on('action',function(arg1,arg2){...});

有了以上这些知识,你就可以实现信令数据通讯了。

5. 实现信令服务器

接下来我们来看一下,如何通过 Node.js下的 Socket.io 来构建一个服务器。

首先是客户端代码,也就是在浏览器里执行的代码。以下是index.html代码

<!DOCTYPE html>
<html>
  <head>
    <title>WebRTC client</title>
  </head>
  <body>
    <script src='/socket.io/socket.io.js'></script>
    <script src='js/client.js'></script>
  </body>
</html>

该代码十分简单就是在body里引入了两段 JavaScript 代码。其中socket.io.js 是用来与服务端建立 Socket 连接的client.js 的作用是做一些业务逻辑,并最终通过 Socket 与服务端通讯。

下面是client.js的代码

var isInitiator;

room = prompt('Enter room name:'); //弹出一个输入窗口

const socket = io.connect(); //与服务端建立socket连接

if (room !== '') { //如果房间不空,则发送 "create or join" 消息
  console.log('Joining room ' + room);
  socket.emit('create or join', room);
}

socket.on('full', (room) => { //如果从服务端收到 "full" 消息
  console.log('Room ' + room + ' is full');
});

socket.on('empty', (room) => { //如果从服务端收到 "empty" 消息
  isInitiator = true;
  console.log('Room ' + room + ' is empty');
});

socket.on('join', (room) => { //如果从服务端收到 “join" 消息
  console.log('Making request to join room ' + room);
  console.log('You are the initiator!');
});

socket.on('log', (array) => {
  console.log.apply(console, array);
});

在该代码中,首先弹出一个输入框,要求用户写入要加入的房间;然后,通过 io.connect() 建立与服务端的连接最后再根据socket返回的消息做不同的处理比如收到房间满或空的消息等。

以上是客户端(也就是在浏览器)中执行的代码。下面我们来看一下服务端的处理逻辑。

服务器端代码server.js是这样的

const static = require('node-static');
const http = require('http');
const file = new(static.Server)();
const app = http.createServer(function (req, res) {
  file.serve(req, res);
}).listen(2013);

const io = require('socket.io').listen(app); //侦听 2013

io.sockets.on('connection', (socket) => {

  // convenience function to log server messages to the client
  function log(){ 
    const array = ['>>> Message from server: ']; 
    for (var i = 0; i < arguments.length; i++) {
      array.push(arguments[i]);
    } 
      socket.emit('log', array);
  }

  socket.on('message', (message) => { //收到message时进行广播
    log('Got message:', message);
    // for a real app, would be room only (not broadcast)
    socket.broadcast.emit('message', message); //在真实的应用中,应该只在房间内广播
  });

  socket.on('create or join', (room) => { //收到 “create or join” 消息

	var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; //房间里的人数

    log('Room ' + room + ' has ' + numClients + ' client(s)');
    log('Request to create or join room ' + room);

    if (numClients === 0){ //如果房间里没人
      socket.join(room);
      socket.emit('created', room); //发送 "created" 消息
    } else if (numClients === 1) { //如果房间里有一个人
	  io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room); //发送 “joined”消息
    } else { // max two clients
      socket.emit('full', room); //发送 "full" 消息
    }
    socket.emit('emit(): client ' + socket.id +
      ' joined room ' + room);
    socket.broadcast.emit('broadcast(): client ' + socket.id +
      ' joined room ' + room);

  });

});

该段代码中,在服务端引入了 node-static 库,使服务器具有发布静态文件的功能。服务器具有此功能后,当客户端(浏览器)向服务端发起请求时,服务器通过该模块获得客户端(浏览器)运行的代码,也就是上面我们讲到的 index.html 和 client.js下发给客户端浏览器

服务端侦听 2013 这个端口,对不同的消息做相应的处理:

  • 服务器收到 message 消息时,它会直接进行广播,这样所有连接到该服务器的客户端都会收到广播的消息。
  • 服务端收到“create or join”消息时它会对房间里的人数进行统计如果房间里没有人则发送“created”消息如果房间里有一个人发送“join”消息和“joined”消息如果超过两个人则发送“full”消息。

要运行该程序,需要使用 NPM 安装 socket.io 和node-static,安装方法如下:

npm install socket.io
npm install node-static

启动服务器并测试

通过上面的步骤你就使用“Socket.io + Node.js”实现了一个信令服务器。现在你还可以通过下面的命令将服务启动起来了

node server.js

如果你是在本机上搭建的服务则可以在浏览器中输入“localhost:2013”然后在浏览器中新建一个tab 在里边再次输入“localhost:2013”。这时你就可以通过浏览器的控制台去看看发生了什么吧

最后再说一个快捷键小技巧吧在Chrome下你可以使用Command-Option-J或Ctrl-Shift-J的DevTools快速访问控制台。

小结

在本文中,我们介绍了 Node.js 的工作原理、Node.js的安装与布署以及如何使用“Sokcet.io + Node.js”实现 WebRTC 信令消息服务器。Socket.io 由于有房间的概念所以与WebRTC非常匹配因此用它开发WebRTC信令服务器就会大大地减少工作量。

另外本文中的例子虽说很简单但在后面的文章中我会以这个例子为基础在其上面不断增加一些功能这样最终你就会看到一个完整的Demo程序。所以你现在还是要学习好这每一个知识点打好基础积跬步才能至千里。

思考时间

文中所讲的JavaScript代码需要运行在两个不同的 V8 引擎上,你知道它们的对应关系吗?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

所做Demo的GitHub链接有需要可以点这里