WebRTC 信令服务器
WebRTC 信令
大多数的WebRTC应用不仅能够通过视频和音频通信,他们还需要更多其他功能。在本章中,我们将构建一个基本的信令服务器。
信令与协商
要连接到另一个用户,您应该知道他在web上的位置。设备的IP地址允许internet设备之间直接发送数据.rtcpeerconnection对象对此负责。一旦设备何通过互联网找到彼此,他们就开始交换彼此支持的协议和编解码器数据。
要与另一个用户通信,您只需交换联系人信息,其余将由WebRTC完成。连接到另一个用户的过程也称为信令和协商。它包括几个步骤:
为对等连接创建的潜在候选列表。
用户或应用程序选择用户创建连接。
信令层通知另一个用户有人想与其建立链接,他可以接受或拒绝。
发起人接到通知对方接受提议。
发起人启动与远程用户的RTCPeerConnection。
双方通过信令服务器交换软件和硬件信息.
双方交换位置信息
链接成功或失败
WebRTC规范没有关于交换信息的任何标准,所以您可以使用任何喜欢的协议或技术。
创建服务器
我们要构建的服务器可以将位于不同计算机上的两个用户连接在一起。我们会创建自己的信令机制,信令服务器允许一个用户呼叫另一个用户,一旦用户呼叫了另一个用户,服务器将通过它们之间的提议、应答、ICE并设置一个webrtc连接。
上图是在使用信令服务器时用户之间的消息传递。首先,每个用户都向服务器注册。在我们的例子里,是一个字符串代表用户名,一旦用户注册,他们就可以相互调用。用户1向提供了他希望调用的用户标识符。另一个用户应该回答。最后,在用户之间发送ice候选人,直到他们可以建立连接。
要创建WebRTC连接客户端,必须能够在不使用WebRTC对等连接的情况下传输消息,我们将使用HTML5的WebSockets。先创建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"。
现在运行服务器,服务器应该开始侦听连接了。
为了测试服务器,我们将使用已经安装的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; } });
如果用户使用登录类型发送消息,我们将:
检查这个用户是否登录过
如果已登录则通知用户登录未成功
如果没有人使用此用户名,我们将此用户名作为连接对象的键。
如果没有识别命令,我们会发送错误。
以下代码是将消息发送到客户端的helper函数。将其添加到server.js文件中:
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
上述函数确保所有消息都以JSON格式发送。
当用户断开连接时,我们应该清理其连接。我们可以在关闭事件时删除用户。将以下代码添加到连接处理程序中-
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
现在让我们用login命令来测试我们的服务器。请记住,所有消息必须以JSON格式编码。运行我们的服务器并尝试登录。你应该看到这样的东西-
建立呼叫
成功登录后,用户希望呼叫另一个用户。他应该向另一个用户发送提议。添加提议处理程序(在connection.on('message')这个方法中添加):
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函数。
现在我们可以测试我们的提议/应答机制。同时连接两个客户端,并尝试提供和应答。你应该看到以下内容:
在这个示例中,提供和回答都是简单的字符串,但是在一个真正的应用程序中,它们将使用SDP数据。
ICE候选
最后一部分是在用户之间处理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;
它应该类似于提供和应答处理程序。
离线
让用户跟另一个用户断开连接,我们应该实现离线功能。它还将通知服务器删除所有用户引用。添加离线处理程序:
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" }); } } } });
现在,如果连接终止,我们的用户将被断开。当用户关闭他的浏览器窗口而我们仍然在提议、回答或候选状态时,将触发关闭事件。
完成
以下是我们的信令服务器的全部代码−
//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)); }
现在我们完成了我们的信令服务器.
总结
本章中,我们建立了简单的信令服务器。我们通过信令服务器实现了用户注册和提议/应答机制。我们还实现了用户之间发送了候选。