搭建信令服务器
在创建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
部分下的会话描述协议部分。
处理ICE候选人
WebRTC
信令的最后一部分是处理用户之间的ICE候选者。在这里,我们使用与以前相同的方式在用户之间传递消息。候选消息的不同之处在于它可能给每个用户发多次,并且在两个用户之间以任何顺序发生。值得庆幸的是,我们的服务器设计的方式可以轻松应对。您可以将此候选处理程序代码添加到您的文件中:
case "candidate":
console.log("Sending candidate to", data.name);
var conn = users[data.name];
if (conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
由于已经发起了调用,因此我们不需要在此函数中添加其他用户的名称。继续使用WebSocket
客户端测试这个。它应该offer/answer功能类似,在两者之间传递消息。
挂断
我们的最后一点不是WebRTC
规范的一部分,但仍然是一个很好的功能。这将允许我们的用户与其他用户断开连接,以便他们可以呼叫其他人。这也将通知我们的服务器断开我们在代码中与任何用户引用。您可以添加“leave“处理程序,如以下代码中所述:
case "leave":
console.log("Disconnecting user from", data.name);
var conn = users[data.name];
conn.otherName = null;
if (conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
这还将通知其他用户Leave事件,以便他们可以相应地断开其对等连接。我们要做的另一件事是处理用户从信令服务器断开连接的情况。这意味着我们无法再为他们服务,我们需要终止他们的呼叫。我们可以更改之前使用的close处理程序,看起来类似于:
if (connection.otherName) {
console.log("Disconnecting user from",
connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if (conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
如果他们碰巧从服务器意外终止了连接,现在这将断开与我们用户的连接。这可以使我们仍在offer/answer或candidate状态时,另一个用户关闭其浏览器窗口的情况下提供服务。在这种情况下,WebRTC
API不会发送任何此类事件,我们需要另一种方式来知道用户已经离开。在这种情况下信令服务器处理有助于使我们的应用程序整体更可靠和稳定。
完整的信令服务器
这里是完整的信令服务器代码。包括登陆和操作所有的回复。另外,我还在末尾添加了监听程序,以便在服务器接受WebSocket
连接时通知您。
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({ port: 8888 });
users = {};
wss.on('connection', function (connection) {
console.log("User connected");
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;
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;
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;
case "candidate":
console.log("Sending candidate to", data.name);
var conn = users[data.name];
if (conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting user from", data.name);
var conn = users[data.name];
conn.otherName = null;
if (conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
}
});
connection.on('close', function () {
if (connection.name) {
delete users[connection.name];
if (connection.otherName) {
console.log("Disconnecting user from",
connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if (conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
});
function sendTo(conn, message) {
conn.send(JSON.stringify(message));
}
wss.on('listening', function () {
console.log("Server started...");
});
像之前一样,可以使用WebSocket
客户端随意测试我们的服务器应用程序。您甚至可以尝试将三个,四个或更多用户连接到服务器,并查看它如何处理多个连接。您可能会发现我们的服务器不能处理多用户的情况,记录这些情况,接下来,我们改进服务器以便解决这些问题。
在真实环境发信号
上面我们努力实现了可以承载两个用户的信息服务。这时候,你可能想知道现实环境中信令服务器是怎样搭建的。由于webrtc
的信令没有定义规范(抽象概念),所以答案也是乱七八糟。
信令是一个错综复杂有待解决地问题,因为“anyting goes
”意味着一切皆有可能。WebRTC
制造商提供了许多资源,但是它们都没有详细说明如何为用户实现最佳信令。这里有很多问题要解决,并不是每个用例都一样。
一些开发者需要高速的稳定的方案去连接全球百万用户。另一部分用户可能需要集成到Facebook
,其他人可能需要集成到twitter
。这是一个极其棘手地问题,需要大量时间和研究做出最好方案。在这里,我们将详细介绍研究信令服务器时的一些常见陷阱和解决方案。
websockets
的困境
websockets
的伟大是源于它实现了浏览器双向通信。许多人认为WebSockets
可以解决所有问题,从而可以更快地将套接字直接连接到服务器。话虽这么说,websockets
仍然有许多不足之处。
第一个问题是防火墙。在理想情况下,WebSockets
是可靠的连接,但与HTTP不同,它在代理配置下很容易变得不稳定。虚拟专用网(VPN)或复杂防火墙系统的额外开销可能导致连接成功率显着下降。这意味着您将不得不依靠其他技术(例如HTTP流)来完成相同的任务。
现在,介绍一些webrtc领域的竞争条件。管道中的任何延迟都可能导致乱序的消息处理,从而在WebRTC中进行连接时导致较差的结果。请记住,建立WebRTC连接时,顺序很重要,如果操作混乱会导致连接失败。
除了这些,WebSockets是一项了不起的技术。这也说明,当创建一个现实的产品时,使用WebSockets作为信令服务器技术会遇到麻烦。今天,许多公司都在有效地使用它们,但是如果WebSockets在给定的网络条件下不能很好地工作,它们就会有很多后备。
连接到其他服务
webrtc
是一项零人兴奋的技术,它不但可以单独使用,而且可以和其他技术配对使用。WebRTC
出现之前,已经有许多对等连接应用程序,自从引入以来,人们一直在努力使WebRTC
向后兼容。这包括使用即时消息传递系统中常见的框架,甚至包括我们今天的蜂窝电话使用的技术。
XMPP
XMPP
是一种即时通讯协议,其名称为Jabber,可追溯到90年代。该协议旨在定义一种实现即时消息传递,用户状态和联系人列表的通用方法。这是一个开放标准,任何人都可以使用并将其集成到他们的应用程序中。在某些时候,许多大型即时消息传递平台已将XMPP集成到其服务中,包括Google Talk,Facebook Chat和AOL Instant Messenger。
正是这些丰富的历史数据使XMPP
成为易于使用的平台。它为任何典型的WebRTC
应用程序提供了强大的功能,因为许多视频和音频通信平台有时会需要出席和联系人列表数据。最重要的是,它是安全的,具有大量文档,并且其实现极为灵活。有许多完善的JavaScript和WebRTC
接口,以及致力于提供基于WebRTC
的XMPP
服务的公司。如果您能够使它工作,那将比我们在本章中构建的简单信令服务器要好得多。
SIP
会话初始协议(SIP)是可追溯到90年代的另一个标准。它是一种针对蜂窝网络和电话系统而设计的信令协议。它主要是蜂窝网络和网络设备提供商所使用的一种定义明确且得到很好支持的协议。
SIP集成和WebRTC
的目的是提供交流支持,但是基于SIP的电话设备不支持WebRTC
。如果我们通过服务器转发消息,很容易给手机或者其他设备发送消息。如果用SIP,它还可以支持当今手机的一些其他特性。
SIP是另一个大的主题。在Web上将SIP与WebRTC
集成时,可以找到无数的接口和资源。与XMPP
相比,就难度和复杂性而言,这绝对是另一个极端。基于电话的通信具有自己的技术和标准,是一个完全不同的主题。我们不会在本书的范围内介绍这种集成,但是你们可以停下来随时寻找自己的资源。
自测题
Q1. 信令服务器的目标是连接网络上的两个用户,以便可以在它们之间建立对等连接。对或错?
Q2. WebSockets使用什么技术在客户端和服务器之间建立双向连接?
- UDP
- TCP
- ICE
- STUN
Q3. 对客户端到服务器消息使用JSON可为我们带来以下哪些好处?
- 易于传输基于字符串的数据包数据
- 消息可以定义复杂的结构
- 广泛支持的编码和解码方法
- 以上所有
Q4. 在信令中,操作顺序是offer/answer
,然后来来回回地发送候选对象,直到连接建立,对或错?
小结
在本章中,我们介绍了信令过程的每个步骤。我们逐步设置了Node.js应用程序,识别用户,并在用户之间发送了offer/answer
机制。我们还详细介绍了断开连接,保留连接以及在用户之间发送候选对象的过程。
您现在应该对信令服务器的工作原理有深刻的了解。从纯粹的学习角度来看,我们构建的服务器是简单明了的。我们可以在整本书中添加可以添加到服务器中的新功能,例如身份验证,好友列表等。如果您喜欢冒险,可以随时在我们的实现中添加任意数量的功能。
我们还介绍了一些有关现实应用的信息。就有关信令的信息而言,这只是冰山一角。WebRTC
信令有很多用例和实现。我的建议是尽早收集您的需求,并坚持满足您所有需求的最简单的解决方案。从简单的WebSocket
服务器到最复杂的SIP实现,任何东西都可以。
在下一章中,我们将把服务器与实际的WebRTC
客户端集成在一起。这将使我们能够在不同位置的用户之间建立WebRTC
连接。这是功能完善的WebRTC
应用程序的开始,该应用程序可连接世界各地的人们。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。