搭建信令服务器
在创建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
部分下的会话描述协议部分。