WebRTC - 信令

大多数WebRTC应用程序不仅能够通过视频和音频进行通信.他们需要许多其他功能.在本章中,我们将构建一个基本的信令服务器.

信令和协商

要连接到另一个用户,你应该知道他在哪里位于网站上.设备的IP地址允许启用Internet的设备直接在彼此之间发送数据. RTCPeerConnection 对象负责此事.一旦设备知道如何通过Internet找到对方,他们就会开始交换有关每个设备支持的协议和编解码器的数据.

要与其他用户通信,您只需交换联系信息其余的将由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上创建一个套接字服务器.接下来,我们监听 connection 事件.当用户与服务器建立WebSocket连接时,将执行此代码.然后我们收听用户发送的任何消息.最后,我们向连接的用户发送一个回复,说"来自服务器的Hello".

现在运行节点服务器,服务器应该开始监听套接字连接.

要测试我们的服务器,我们将使用我们已经安装的 wscat 实用程序.此工具有助于直接连接到WebSocket服务器并测试命令.在一个终端窗口中运行我们的服务器,然后打开另一个窗口并运行 wscat -c ws://localhost:9090 命令.您应该在客户端看到以下内容 :

使用wscat实用程序

服务器还应该记录连接的用户 :

记录已连接的用户

用户注册

在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道将消息发送到何处.让我们稍微改变我们的连接处理程序;

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]; 
   } 
});

现在让我们用login命令测试我们的服务器.请记住,所有消息都必须以JSON格式进行编码.运行我们的服务器并尝试登录.您应该看到类似这样的内容;

使用登录命令测试

拨打电话

成功登录后,用户想要拨打另一个电话.他应该向另一个用户提出 offer 来实现它.添加 offer 处理程序 :

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;

首先,我们得到我们试图呼叫的用户的连接.如果它存在,我们发送 offer 详细信息.我们还将 otherName 添加到 connection 对象.这是为了以后找到它的简单性.

回答

回复答案的方式类似于我们在中使用的模式提供处理程序.我们的服务器只是将回答的所有消息传递给另一个用户.在 offer hander :

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("发送候选人"到:",data.name); 
 var conn = users [data.name]; 
 if(conn!= null){
 sendTo(conn,{
 type:"candidate",
 candidate:data.candidate 
}) ; 
} 
休息;

它应该与 offer answer 处理程序类似.

离开连接

要允许我们的用户与其他用户断开连接,我们应该实现挂断功能.它还会告诉服务器删除所有用户引用.添加离开处理程序 :

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;

现在,如果连接终止,我们的用户将被断开连接.当我们仍在 offer 回答候选人关闭事件>状态.

完整信令服务器

以下是我们信令服务器的完整代码 :

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" 
            }); 
         }  
      } 
   } 
});

所以工作已经完成,我们的信令服务器已准备就绪.请记住,在进行WebRTC连接时不按顺序执行操作可能会导致问题.

Complete Signaling Server

这是我们的信令服务器的完整代码:

//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连接时无序处理可能会引起问题。

摘要

在本章中,我们构建了简单直接的信令服务器.我们走过了信令流程,用户注册和提供/回答机制.我们还实施了在用户之间发送候选人.