构建信令服务器
了解清楚信令之后,接下来是如何构建 WebRTC
一对一信令服务器。我们将从以下几个方面来介绍如何构造信令服务器:一是信令服务器的实现方案;二是信令服务器的业务逻辑;三是信令服务器的实现;四是信令服务器的安装与部署。
信令服务器的实现方案
要实现一个一对一的 WebRTC
信令服务器可以有很多种方案,这里介绍两种最常见的实现方案。
方案一,使用 C/C++
、Java
等语言从零开始开发一个信令服务器。这种方案的实现成本非常高,要写很多代码,还要对编写的代码进行大量的测试。使用这种方案,即使开发一个最简单的 HTTPS
服务器,至少也要花两周以上的时间。
方案二,利用现成的 Web
服务器做应用开发,如以 Apache
、Nginx
、NodeJS
为服务,在其上做应用开发是非常不错的选择。
建议采用第二种方案,它有以下几方面优势:
-
一般信令系统都需要使用
HTTP/HTTPS
、WS/WSS
等传输协议,而Apache
、Nginx
、NodeJS
等服务器显然在这方面有天然的优势。 -
实时通信的信令服务器一般负载都不是特别高。举个例子,假设有 10000 个房间同时在线,我们可以评估出大部分房间只需要处理几个信令,那么总的消息量也不过是几万个,这个量级对于
Nginx
和NodeJS
来说,单台服务器就可以应付了。 -
通过
Nginx
或NodeJS
实现信令服务器特别简单,只要几行代码就可以实现。 -
稳定性高。像
Apache
、Nginx
、NodeJS
这类服务器都经过了长时间的验证,所以它们的稳定性是可以得到保障的。
基于以上几点原因,建议你采用第二种方案实现自己的信令系统。
在第二种方案中,尤其推荐使用 Node.js
来实现信令服务器。虽然 NodeJS
在性能上不如 Nginx
,但对于我们这种学习项目来说使用它已经足够了,而且它使用起来也特别简单,还有非常好的生态链,很多逻辑关系不需要我们自己写,大大减少了开发信令服务器的工作量。
信令服务器的业务逻辑
关于 WebRTC
一对一信令服务器的业务逻辑前面已做了一些介绍,其中最重要的是房间的概念。当两个用户要进行通信时,他们首先要创建一个房间,成功加入房间之后,双方才能交换必要的信息,如 Offer/Answer
、Candidate
等。当通信的双方结束通话后,用户需要发送离开房间的消息给信令服务器,此时信令服务器需要将房间内的所有人清除;如果房间里已经没有人了,还需要将空房间销毁掉。
对于这样一套机制,如果我们自己实现的话,需要花不少时间。好消息是,著名的 socket.io
库已经实现了这套逻辑,只要我们在 NodeJS
中引入它即可。
信令服务器的实现
接下来看一下信令服务器的实现。要实现信令服务器,我们需要思考以下几个问题:如何通过 NodeJS
实现一个 HTTP
服务?如何使用 socket.io
库?如何进行信令的转发?下面我就来回答上面的问题,当这几个问题解答完了,信令服务器也就实现了。
1)如何通过 NodeJS
实现一个 HTTP
服务?在 NodeJS
上开发一个 HTTP
应用只要几行代码即可,如代码 4.1 所示。
…
const http = require('http');//引入http库
const express = require('express'); //引入express库
// 创建HTTP服务,并侦听8980端口
const app = express();
const http_server = http.createServer(app);
http_server.listen(8080, '0.0.0.0');
…
上面的代码中引入了两个库:一个是 http
库,用于创建 HTTP
服务;另一个是 express
库,是一套开发 Web
应用的框架,它提供了很多开发 Web
应用的工具。
通过上面引入的两个库,你很容易写出一个 HTTP
服务来。首先,通过 express
创建一个 Web
应用,如第 6 行代码所示;之后调用 HTTP
库的 createServer()
方法创建 HTTP
对象,即 http_server
;最后调用 http_server
对象的 listen()
方法侦听 8080
端口。通过上面的步骤就实现了一个 HTTP
服务。
2)如何使用 socket.io
库?在使用 socket.io
库之前,也需要像开发 HTTP
服务一样,先通过 require
将它引入程序中,然后利用 socket.io
的 on
方法接收消息,用 emit
方法发送数据。下面是 socket.io
的几个常见方法:
-
给本次连接发送消息,如代码 4.2 所示。
代码4.2 发送消息socket.emit('cmd')
-
给本次连接发送带参数的消息,如代码 4.3 所示。
代码4.3 发送带参数消息socket.emit('cmd',arg1); // 多个参数往后排
-
给除本次连接外房间内的所有人发消息,如代码 4.4 所示。
代码4.4 给房间内的所有人发消息socket.to(room).emit('cmd')
-
接收消息,如代码 4.5 所示。
代码4.5 接收消息socket.on('cmd', function(){ … })
-
接收带参数的消息,如代码 4.6 所示。
代码4.6 接收带参数的消息socket.on('cmd', function(arg1){ … })
3)如何转发信令?你需要根据收到的客户端不同的信令,给它返回不同的结果,如代码 4.7 所示。
io.sockets.on('connection', (socket) => {
// 收到 message 时,进行转发
socket.on('message', (message) => {
// 给另一端转发消息
socket.to(room).emit('message', message);
});
// 收到 join 消息
socket.on('join', (room) => {
var o = io.sockets.adapter.rooms[room];
// 得到房间里的人数
var nc = o ? Object.keys(o.sockets).length : 0;
if (nc < 2) { // 如果房间中不超过 2 人
socket.join(room);
// 发送 joined 消息
socket.emit('joined', room);
...
} else { // 最多两个客户端
socket.emit('full', room); // 发送 full 消息
}
});
...
});
从上面的代码片段中可以看到,所有消息的处理都是在客户端与服务器建立连接之后进行的。因此,需要提前将 connection
消息的处理函数注册到 socket.io
中(即第 2 行代码的含义);然后,再分别注册各消息(joined
、message
……)的处理函数。这样,当服务器收到客户端发来的消息时,socket.io
就会根据消息类型调用注册的处理函数,从而完成对应的业务处理。
经过上面的讲解,现在再看代码 4.7 是不是觉得很简单了?其具体过程如下:如果服务端收到 message
消息,它不做任何处理,直接进行转发(第 7 行代码);如果是 join
消息,则首先将用户加入服务端管理的房间中,之后向客户端返回 joined
消息(第17、第19行代码);如果用户加入时房间里已经有两个用户了,则拒绝该用户的加入,并返回 full
消息,以告之目前房间里人已经满了(第 22 行代码);对于其他消息的处理以此类推。
经过上面的步骤后,信令服务器开发完成。接下来介绍如何将开发好的代码部署到服务器上。
信令服务器的安装与部署
在服务器上部署信令服务器需要三个步骤:
-
安装
NodeJS
。 -
安装
NPM
,并安装信令服务器的依赖库。 -
启动服务。
下面我们就按照上面的步骤实际操作一下:
-
安装
NodeJS
。在不同的环境中安装NodeJS
的方法略有不同,但都十分方便,下面是在不同系统下安装NodeJS
的方法:-
Ubuntu系统
apt install nodejs
-
CentOS
yum install nodejs
-
MacOS
brew install nodejs
-
-
安装
NPM
。NPM
起什么作用呢?实际上,它与apt
、yum
、brew
工具类似,也是一个包管理器,只不过是专门用来管理、安装NodeJS
需要的依赖库的。安装NPM
与安装NodeJS
类似,在Ubuntu
下安装命令如下:apt install npm
其他系统中的安装方法不再赘述。
NPM
安装好后,我们就可以用它来安装NodeJS
的依赖库express
和socket.io
库(http
库是NodeJS
自带的,不需要安装)。其安装方法如下:npm install socket.io@2 .0.3 npm install express
-
现在代码编写好了,运行环境也搭建好了,接下来就可以启动服务了。假如你将上面编写的信令服务器程序命名为
signserver.js
,那么只要在安装依赖库的目录下执行下面的命令,就可以启动信令服务器。node signserver.js
此时,你可以在控制终端上执行以下命令,来观察信令服务器是否正常启动。
netstat -ntpl |grep 8080
如果在控制终端上能查看到 8080 端口已经就绪,则说明信令服务器已开始工作,可以随时接收客户端向该端口发送的信令消息。至此,
WebRTC
一对一信令服务器就完成了。
信令服务器的完整代码
通过上面的讲解,相信大多数读者完全能自己实现一个 WebRTC
一对一信令服务系统。不过对于新手来说,即使讲解得再详细,也不如有一个完整的例子做参考来得实际。为此我特意将信令服务器的完整代码放在本章的最后作为参考,具体参见代码 4.8。
'use strict';
// 依赖库
var log4js = require('log4js'); // 用于输出日志
var http = require('http'); // 提供HTTP 服务
var https = require('https'); // 提供HTTPS 服务
var fs = require('fs'); // 用于读取文件内容
var socketIo = require('socket.io');
var express = require('express');
var serveIndex = require('serve-index');
// 一个房间里可以同时在线的最大用户数
var USERCOUNT = 3;
// 日志的配置项
log4js.configure({
appenders: {
file: {
type: 'file',
filename: 'app.log',
layout: {
type: 'pattern',
pattern: '%r %p - %m',
},
},
},
categories: {
default: {
appenders: ['file'],
level: 'debug',
},
},
});
var logger = log4js.getLogger();
var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));
// 设置跨域访问
app.all('*', function (req, res, next) {
// 设置允许跨域的域名,* 代表允许任意域名跨域
res.header('Access-Control-Allow-Origin', '*');
// 允许的header类型
res.header('Access-Control-Allow-Headers', 'content-type');
// 跨域允许的请求方式
res.header('Access-Control-Allow-Methods', 'DELETE, PUT, POST, GET, OPTIONS');
if (req.method.toLowerCase() == 'options') {
res.send(200); // 让options尝试请求快速结束
} else {
next();
}
});
// HTTP 服务
var http_server = http.createServer(app);
http_server.listen(80, '0.0.0.0');
// 你的网站证书
var options = {
key: fs.readFileSync('./cert/cert.key'),
cert: fs.readFileSync('./cert/cert.pem'),
};
// HTTPS 服务
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);
// 处理连接事件
io.sockets.on('connection', (socket) => {
// 中转消息
socket.on('message', (room, data) => {
logger.debug('message, room: ' + room + ', data, type:' + data.type);
socket.to(room).emit('message', room, data);
});
// 用户加入房间
socket.on('join', (room) => {
socket.join(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = myRoom ? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room (' + room + ') is: ' + users);
// 如果房间里人未满
if (users < USERCOUNT) {
// 发给除自己之外房间内的所有人
socket.emit('joined', room, socket.id);
// 通知另一个用户,有人来了
if (users > 1) {
socket.to(room).emit('otherjoin', room, socket.id);
}
} else {
// 如果房间里人满了
socket.leave(room);
socket.emit('full', room, socket.id);
}
});
// 用户离开房间
socket.on('leave', (room) => {
// 从管理列表中将用户删除
socket.leave(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = myRoom ? Object.keys(myRoom.sockets).length : 0;
logger.debug('the user number of room is: ' + users);
// 通知其他用户有人离开了
socket.to(room).emit('bye', room, socket.id);
// 通知用户服务器已处理
socket.emit('leaved', room, socket.id);
});
});
https_server.listen(443, '0.0.0.0');