📜  WebRTC-信令

📅  最后修改于: 2020-10-17 05:42:13             🧑  作者: Mango


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

信令与协商

要连接到另一个用户,您应该知道他在Web上的位置。设备的IP地址允许启用Internet的设备相互之间直接发送数据。 RTCPeerConnection对象对此负责。一旦设备知道如何在Internet上找到对方,它们就会开始交换有关每个设备支持哪些协议和编解码器的数据。

要与其他用户交流,您只需交换联系信息,其余的将由WebRTC完成。连接到另一个用户的过程也称为信令和协商。它包含几个步骤-

  • 创建对等连接的潜在候选者列表。

  • 用户或应用程序选择要与其建立连接的用户。

  • 信令层通知另一个用户某人想要连接到他。他可以接受或拒绝。

  • 通知第一用户要约的接受。

  • 第一个用户与另一个用户启动RTCPeerConnection

  • 两个用户都通过信令服务器交换软件和硬件信息。

  • 两个用户都交换位置信息。

  • 连接成功或失败。

WebRTC规范不包含任何有关交换信息的标准。因此请记住,以上仅是信令如何发生的示例。您可以使用任何喜欢的协议或技术。

构建服务器

我们将要构建的服务器将能够将不在同一台计算机上的两个用户连接在一起。我们将创建自己的信号机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦用户呼叫了另一个用户,服务器就会在他们之间传递要约,应答,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上创建一个套接字服务器。接下来,我们监听连接事件。当用户与服务器建立WebSocket连接时,将执行此代码。然后,我们收听用户发送的任何消息。最后,我们向连接的用户发送响应,说“来自服务器的问候”。

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

为了测试我们的服务器,我们将使用已经安装的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格式编码。运行我们的服务器,然后尝试登录。您应该看到类似这样的内容-

使用登录命令进行测试

拨打电话

成功登录后,用户要呼叫另一个。他应该向其他用户提出要约。添加报价处理程序-

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对象上的createOffercreateAnswer功能。

现在我们可以测试我们的报价/答案机制。同时连接两个客户并尝试提供报价和答案。您应该看到以下内容-

连接两个客户端

在此示例中, offerAnswer是简单的字符串,但是在实际应用中,它们将用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" 
            }); 
         }  
      } 
   } 
});

现在,如果连接终止,我们的用户将被断开连接。当我们仍处于要约答案候选状态时,当用户关闭其浏览器窗口时,将触发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连接时无序操作可能会引起问题。

概要

在本章中,我们构建了简单明了的信令服务器。我们介绍了信令过程,用户注册和提议/回答机制。我们还实现了用户之间的发送候选者。