WebRTC 信令
大多数WebRTC应用程序不仅能够通过视频和音频进行通信。它们还需要许多其他功能。在本章中,我们将构建一个基本的信令服务器。
信令和协商
要连接到另一个用户,您应该知道他在Web上的位置。您设备的IP地址允许可互联网的设备直接之间发送数据。RTCPeerConnection对象负责这一过程。一旦设备知道如何在互联网上找到彼此,它们开始交换有关每个设备支持的协议和编解码器的数据。
与另一个用户通信只需要交换联系信息,WebRTC会处理其余的事情。连接到另一个用户的过程也称为信令和协商。它包括以下几个步骤:
- 创建一个用于点对点连接的候选列表。
-
用户或应用程序选择要建立连接的用户。
-
信令层通知另一个用户有人要与他建立连接。他可以接受或拒绝。
-
第一个用户被通知对方接受了请求。
-
第一个用户与另一个用户发起RTCPeerConnection。
-
两个用户通过信令服务器交换软件和硬件信息。
-
两个用户交换位置信息。
-
连接成功或失败。
WebRTC规范中没有包含任何有关交换信息的标准。因此,请记住上面只是信令可能发生的例子。可以使用任何您喜欢的协议或技术。
构建服务器
我们要构建的服务器能够连接两个位于不同计算机上的用户。我们将创建自己的信令机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦一个用户呼叫了另一个用户,服务器会在它们之间传递offer、answer、ICE候选者,并建立一个WebRTC连接。
上图是用户在使用信令服务器时的消息传输流程。首先,每个用户向服务器注册。在我们的情况下,这将是一个简单的字符串用户名。一旦用户注册成功,他们便可以互相拨打电话。用户1使用他想要呼叫的用户标识符发送一个邀请。其他用户应该回答。最后,ICE候选者在用户之间发送,直到他们可以建立连接。
为了创建WebRTC连接,客户端必须能够在不使用WebRTC对等连接的情况下传输消息。这就是我们将使用HTML5 WebSockets的地方 – 这是两个端点之间的双向套接字连接 – 一个Web服务器和一个Web浏览器。现在让我们开始使用WebSocket库。创建 server.js 文件并插入以下代码 –
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message){
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
第一行需要WebSocket库,我们已经安装了。然后我们在端口9090上创建一个socket服务器。接下来,我们监听连接事件。当用户与服务器建立WebSocket连接时,此代码将被执行。然后我们监听用户发送的任何消息。最后,我们向连接的用户发送一个回复,说“Hello from server”。
现在运行node server,服务器应该开始监听socket连接。
为了测试我们的服务器,我们将使用已经安装的wscat实用工具。这个工具帮助我们直接连接到WebSocket服务器并测试命令。在一个终端窗口中运行我们的服务器,然后打开另一个窗口并运行wscat -c ws://localhost:9090命令。你应该在客户端看到以下内容-
服务器还应记录已连接的用户。
用户注册
在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道向何处发送消息。让我们稍微修改我们的连接处理程序。
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
这样我们只接受JSON消息。接下来,我们需要把所有连接的用户存储起来。我们将使用一个简单的Javascript对象来实现。修改我们文件的顶部:
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
我们将为来自客户端的每条消息添加一个类型字段。例如,如果用户想要登录,他会发送登录类型的消息。让我们来定义它:
connection.on('message', function(message){
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
如果用户发送了类型为登录的消息,我们要做以下操作: - 检查是否有其他人使用过此用户名登录 - 如果是,告诉用户他没有成功登录 - 如果没有人使用此用户名,将用户名添加为连接对象的键 - 如果命令无法识别,发送一个错误 下面的代码是一个帮助函数,用于向连接发送消息。将其添加到server.js文件中。
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
上述的函数确保我们所有的消息都以JSON格式发送。
当用户断开连接时,我们应该清理它的连接。当触发关闭事件时,我们可以删除用户。将以下代码添加到连接处理程序中 –
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
现在让我们使用登录命令来测试我们的服务器。请记住,所有的消息必须以JSON格式进行编码。运行我们的服务器并尝试登录。您应该会看到类似于这样的内容 –
进行电话通话
成功登录后,用户想要拨打另一个用户的电话。他应该向另一个用户发出一个提议以实现这个目标。添加提议处理程序 −
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
首先,我们获取正在尝试呼叫的用户的连接。如果它存在,我们向他发送报价的详细信息。我们还将otherName添加到连接对象中。这样做是为了以后更容易找到它。
回答
回答响应的模式与我们在报价处理程序中使用的模式类似。我们的服务器只是将所有消息作为答复传递给另一个用户。在报价处理程序之后添加以下代码:
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
您可以看到这与提供处理程序相似。请注意,此代码遵循RTCPeerConnection对象上的createOffer和createAnswer函数。
现在我们可以测试我们的提供/回答机制。同时连接两个客户端,并尝试进行提供和回答。您应该会看到以下信息−
ICE候选项
处理用户之间的ICE候选项的最后一部分。我们使用相同的技术在用户之间传递消息。主要区别在于每个用户可能以任何顺序多次发生候选项消息。添加 candidate 处理程序 −
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 from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
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 from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
现在,如果连接终止,我们的用户将会断开连接。当用户在我们仍处于 offer 、 answer 或 candidate 状态时关闭浏览器窗口时,将会触发 close 事件。
完整的信令服务器
这是我们信令服务器的全部代码 −
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
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 from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
所以工作已经完成,我们的信令服务器准备好了。记住,在建立WebRTC连接时,按顺序进行操作可能会导致问题。
总结
在本章中,我们构建了一个简单直接的信令服务器。我们详细介绍了信令过程、用户注册和提供/回答机制。我们还实现了用户之间的候选者发送。