搭建信令服务器
在创建 WebRTC 应用程序的某个时刻,您将不得不脱离为客户端开发并构建服务器。大多数 WebRTC 应用程序不仅仅依赖于能够通过音频和视频进行通信,而且通常需要许多其他功能才能引起兴趣。在本章中,我们将使用 JavaScript 和 Node.js 深入研究服务器编程。我们将为本书的其余部分创建基本信令服务器的基础。
这一章,主要分为以下部分:
- 用 nodeJs 搭建开发环境
- websocket 与客户端连接
- 识别用户
- 启动并回答 WebRTC 呼叫
- 处理 ICE 候选人传输
- 挂断
在本章中,我们将仅关注应用程序的服务器部分。在下一章中,我们将构建此示例的客户端部分。我们的示例服务器本质上是简单的,这足以让我们建立一个 WebRTC 对等连接。
搭建信令服务器
我们将在本章中构建的服务器帮助我们将不在同一台计算机上的两个用户连接在一起。服务器的目标是用通过网络传输的信息机制替换信令机制。服务器简单明了,仅支持最基本的 WebRTC 连接。
我们的实施必须响应并回答来自多个用户的请求。它将通过在客户端之间使用简单的双向消息传递给系统来实现此目的。它将允许一个用户呼叫另一个用户并在它们之间建立 WebRTC
连接。一旦用户呼叫另一个用户,服务器将在两个用户之间传递 offer,answer 和 ICE 候选者。这将允许他们成功设置 WebRTC 连接。
上图显示了使用信令服务器建立连接时客户端之间的消息流。每一方都将通过向服务器注册自己开始。我们的登录将只是向服务器发送一个基于字符串能唯一标识用户的 ID。一旦两个用户都注册了服务器,他们就可以呼叫另一个用户。使用他们希望调用的用户标识符进行回应即可,其他用户也是依次回答。最后,候选人在客户端之间发送,直到他们能够成功建立连接。在任何时候,用户都可以通过发送离开消息来终止连接。实现很简单,主要用作用户向对方发送消息的传递。
设置环境
我们将利用 Node.js 的强大功能来构建我们的服务器。如果您以前从未在 Node.js 中编程,请不要担心!该技术利用 JavaScript 引擎完成所有工作。这意味着所有编程都将使用 JavaScript,因此不需要学习新语言。现在,让我们执行以下步骤来设置 Node.js 环境:
- 运行 node.js 服务器的第一步是安装 node.js.
-
现在,您可以打开终端应用程序并使用 node 命令启动 Node.js VM。Node.js 基于 Google Chrome 附带的 V8 JavaScript 引擎。这意味着它与浏览器解释 JavaScript 的方式非常接近。键入一些命令以熟悉它的工作原理:
> 1 + 1 2 > var hello = "world"; undefined > "Hello" + hello; 'Helloworld'
- 从这里开始,我们可以开始创建服务器程序。幸运的是,Node.js 运行 JavaScript 文件和终端输入命令是一样的。用下列内容创建
index.js
,并且用node index.js
运行:console.log("Hello from node!");
当你执行
node index.js
命令后,将会在 Node.js 控制台看到如下信息:Hello from node!
这是我们将在本书中介绍的 Node.js 概念的结束。我们对信号服务器的实现并不是最先进的,而深入研究服务器工程需要整整一本书的内容。随着我们继续前进,花些时间了解更多有关 Node.js 的信息,甚至我们将用自己喜欢的语言构建信令服务器!
获取连接
创建 WebRTC
连接所需的步骤必须是实时的。这意味着客户端必须能够在不使用 WebRTC
对等连接的情况下实时地在彼此之间传输消息。这是我们将利用 HTML5 的另一个强大功能WebSockets
。
WebSocket
正是它听起来的样子 – 两个端点之间的开放双向套接字连接 – Web 浏览器和 Web 服务器。您可以使用字符串和二进制信息在套接字上来回发送消息。它旨在 Web 浏览器和 Web 服务器中实现,以便在 AJAX 请求范围之外实现它们之间的通信。
WebSocket
协议自 2010 年左右开始出现,是当今大多数浏览器都可以使用的定义明确的标准。它对 Web 客户端提供广泛的支持,许多服务器技术都有专门用于它们的框架。甚至整个框架都依赖于 WebSocket
技术,例如 Meteor JavaScript 框架。
WebSocket
协议和 WebRTC
协议之间的最大区别在于使用 TCP 堆栈。WebSockets
本质上是客户端到服务器,并利用 TCP 传输实现可靠的连接。这意味着它有许多 WebRTC
没有的瓶颈,我们在第 3 章创建基本 WebRTC 应用程序中的 ” 理解 UDP 传输和实时传输 ” 一节中对此进行了描述。这也是它作为信令传输协议很好地工作的原因。由于它是可靠的,我们的信号不太可能在用户之间丢失,从而为我们提供更成功的连接。它也内置在浏览器中,使用 Node.js
可以轻松设置,这使我们的信令服务器的实现更容易理解。
要在我们的项目中利用 WebSockets
的强大功能,我们必须首先为 Node.js 安装支持的 WebSockets 库。我们将使用 npm 注册表中的 ws 项目。要安装库,请进入到服务器的目录并运行以下命令:
npm install ws
你会看到如下输出:
现在我们安装了 websocket
库,我们可以在服务器中开始使用,您可以在 index.js
文件中插入以下代码:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8888});
wss.on('connection', function (connection) {console.log("User connected");
connection.on('message', function (message) {console.log("Got message:", message);
});
connection.send('Hello World');
});
首先,我们需要引入我们在命令行安装的 ws 包。之后,我们创建一个 websocket
服务,告诉客户端连接的端口,如果你想更改设置,你可以填写任何你喜欢的端口。
接下来,我们监听来自服务器的连接事件。只要用户与服务器建立 WebSocket
连接,就会调用此代码。它将为您提供一个连接对象,其中包含有关刚刚连接的用户的各种信息。
然后,我们收听用户发送的任何消息。现在,我们只是将这些消息记录到控制台。
最后,当服务器完成与客户端的 WebSocket
连接时,服务器向客户发送回复Hello World
。
请注意,连接事件发生在连接到服务器的任何用户。这意味着您可以让多个用户连接到同一服务器,每个用户将单独触发连接事件。这种基于异步的代码通常被视为 Node.js
编程的优势之一。
现在我们可以通过运行 node index.js
来运行我们的服务器。该过程开始并等待处理 WebSocket
连接。它会无限期地执行此操作,直到您停止运行该进程。
测试服务
测试我们的代码是否正常运行,我们可以使用 ws 库附带的 wscat
命令。关于 npm 的好处是,您不仅可以安装要在应用程序中使用的库,还可以全局安装库以用作命令行工具。运行npm install -g ws
,运行此命令时可能需要使用管理员权限。
这应该给我们一个名为 wscat
的新命令。此工具允许我们从命令行直接连接到 WebSocket
服务器,并针对它们测试命令。为此,我们在一个终端窗口中运行我们的服务器,然后打开一个新服务器并运行 wscat -c ws:// localhost:8888
命令。您会注意到 ws://
,它是WebSocket
协议的自定义指令,而不是 HTTP。
您的输出应该类似于:
服务器端打印 log 如下:
如果其中任何一步都不起作用,那么请根据列表检查代码并阅读 ws 库以及 Node.js 和 npm 的文档。这些工具在不同环境中的工作方式可能不同,在某些情况下需要额外设置。如果一切正常,请在 Node.js 中编写一个包含 12 行代码的 WebSocket 服务器。
识别用户
在典型的 Web 应用程序中,服务器需要一种方法来识别连接的客户端。今天的大多数应用程序使用唯一身份规则,并让每个用户登录到相应的基于字符串的标识符,称为用户名。我们还将在信令应用程序中使用同样的规则。它不会像今天使用的某些应用那样复杂,因为我们甚至不需要用户输入密码。我们只需要为每个连接提供一个 ID,这样我们就知道在哪里发送消息。
首先,我们将稍微更改一下连接处理程序,看起来类似于:
connection.on('message', function (message) {
var data;
try {data = JSON.parse(message);
} catch (e) {console.log("Error parsing JSON");
data = {};}
});
这会将我们的 WebSocket
实现更改为仅接受 JSON
消息。
由于 WebSocket
连接仅限于字符串和二进制数据,因此我们需要一种通过线路发送结构化数据的方法。JSON 允许我们定义结构化数据,然后将其序列化为可以通过 WebSocket
连接发送的字符串。它也是在 JavaScript 中使用的最简单的序列化形式。
接下来,我们需要一种方法来存储所有已连接的用户。由于我们的服务器本质上是简单的,我们将使用 JavaScript 中已知的哈希映射作为对象来存储我们的数据。我们可以将文件的顶部更改为与此类似:
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({port: 8888});
users = {};
要登录,我们需要知道用户正在发送登录类型消息。为了支持这一点,我们将为客户端发送的每条消息添加一个类型字段。这将允许我们的服务器知道如何处理它正在接收的数据。
首先,我们将定义用户尝试登录时要执行的操作:
connection.on('message', function (message) {
var data;
try {data = JSON.parse(message);
} catch (e) {console.log("Error parsing JSON");
data = {};}
switch (data.type) {
case "login":
console.log("User logged in as", data.name);
if (users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Unrecognized command:" + data.type
});
break;
}
});
我们使用 switch 语句来相应地处理每种消息类型。如果用户发送带有登录类型的消息,我们首先需要查看是否有人已使用该 ID 登录到服务器。如果有,我们告诉客户他们没有成功登录并需要选择一个新名称。如果没有人使用此 ID,我们将连接添加到用户对象中,ID 为密钥。如果我们遇到任何我们无法识别的命令,我们还会向客户端发送一条消息,说明处理他们的请求时出错。
我还在代码中添加了一个名为 sendTo
的辅助函数,用于处理向连接发送消息。这可以添加到文件中的任何位置:
function sendTo(conn, message) {conn.send(JSON.stringify(message));
}
此函数的作用是确保我们的所有消息始终以 JSON
格式编码。这也有助于减少我们必须编写的代码量。将消息封装成一个方法是好的做法,以便在多个地方同时调用。
我们要做的最后一件事是提供一种在断开连接时清理客户端连接的方法。幸运的是,我们的类库在发生这种情况时会提供一个事件。我们可以通过这种方式收听此活动并删除我们的用户:
connection.on('close', function () {if (connection.name) {delete users[connection.name];
}
});
这应该在连接事件中添加,就像消息处理程序一样。
现在是时候用我们的 login 命令测试我们的服务器了。我们可以像以前一样使用客户端来测试我们的登录命令。要记住的一件事是,我们现在发送的消息必须以 JSON
格式编码,以便服务器接受它们。
{""type"": ""login"", ""name"": ""Foo""}
您收到的输出应该类似于:
发送请求
从现在起,我们的代码不会比登录处理程序复杂得多。
我们将创建一组处理程序,以便为每个步骤正确传递消息。登录后进行的第一个调用是 offer
处理程序,它指定一个用户想要调用另一个用户。
最好不要将这里的发送请求与 WebRTC 的 offer
步骤混淆。
在这个例子中,我们将两者结合起来使我们的 API
更易于使用。在大多数设置中,这些步骤将分开。这可以在诸如 Skype 之类的应用程序中看到,其中另一个用户必须在两个用户之间建立连接之前接受来电。
我们现在可以将 offer 处理程序添加到此代码中:
case "offer":
console.log("Sending offer to", data.name);
var conn = users[data.name];
if (conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
我们要做的第一件事是获取我们试图呼叫的用户连接。这很容易做到,因为其他用户的 ID 始终是我们的连接存储在用户查找对象中的位置。然后我们检查其他用户是否存在,如果存在,则向他们发送要约的详细信息。我们还在用户的连接对象中添加了一个 otherName
属性,以便我们稍后可以在代码中轻松查找。您可能还注意到,此代码都不是特定于 WebRTC
的。这可能涉及两个用户之间的任何类型的呼叫技术。我们将在本章后面详细介绍这一点。
您可能还注意到这里缺少错误处理。这可能是 WebRTC
最繁琐的部分之一。由于呼叫在进程的任何一点都可能失败,因此我们有很多地方有可能使连接失败。它也可能由于各种原因而失败,例如网络可用性,防火墙等。在本书中,我们将其留给用户以他们想要的方式单独处理每个错误情况。
回应请求
回应请求就像 offer
一样容易。我们遵循类似的模式,让客户完成大部分工作。我们的服务器会让任何消息通过,作为对其他用户的回答。我们可以在 offer
处理案例之后添加:
case "answer":
console.log("Sending answer to", data.name);
var conn = users[data.name];
if (conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
您可以看到代码在前面的列表中看起来有很多相似。注意,我们也依赖于来自其他用户的答案。如果用户首先发送答案而不是提议,则可能会破坏我们的服务器实施。有许多用例,这个服务器不够用,但在下一章中它将很好地用于集成。
这应该是 WebRTC
中offer
和 answer
机制的良好开端。
您应该看到它遵循 RTCPeerConnection
上的 createOffer
和createAnswer
函数。这正是我们开始插入服务器连接以处理远程客户端的地方。
我们甚至可以使用之前使用的 WebSocket
客户端测试,同时连接两个客户端允许我们在两者之间发送请求和响应。这可以让您更深入地了解这最终将如何运作。您可以在终端窗口中看到同时运行两个客户端的结果,如以下屏幕截图所示:
就我而言,我的 offer
和answer
都是简单的字符串消息。如果您还记得第 3 章,创建基本 WebRTC
应用程序,请参阅 WebRTC API
部分,我们详细介绍了会话描述协议(SDP
)。这是在进行 WebRTC
调用时实际应用的 offer
和answer
字符串。如果您不记得 SDP 是什么,请参阅第 3 章,回忆一下,创建基本 WebRTC
应用程序中的 WebRTC API
部分下的会话描述协议部分。