WebRTC - 信令

  • 简述

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

    要连接到另一个用户,您应该知道他在 Web 上的位置。您设备的 IP 地址允许支持 Internet 的设备直接在彼此之间发送数据。RTCPeerConnection对象负责这个。一旦设备知道如何通过 Internet 找到彼此,它们就会开始交换有关每个设备支持哪些协议和编解码器的数据。
    要与其他用户交流,您只需交换联系信息,其余的将由 WebRTC 完成。连接到其他用户的过程也称为信令和协商。它包括几个步骤 -
    • 创建对等连接的潜在候选者列表。
    • 用户或应用程序选择要与之建立连接的用户。
    • 信令层通知另一个用户有人要连接到他。他可以接受也可以拒绝。
    • 第一个用户收到接受报价的通知。
    • 第一个用户与另一个用户启动RTCPeerConnection 。
    • 双方用户通过信令服务器交换软件和硬件信息。
    • 两个用户交换位置信息。
    • 连接成功或失败。
    WebRTC 规范不包含任何有关交换信息的标准。所以请记住,以上只是信号如何发生的一个例子。您可以使用您喜欢的任何协议或技术。
  • 构建服务器

    我们要构建的服务器将能够将两个不在同一台计算机上的用户连接在一起。我们将创建自己的信号机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦用户呼叫另一个用户,服务器就会在他们之间传递提议、答案、ICE 候选者并设置 WebRTC 连接。
    构建服务器
    上图是使用信令服务器时用户之间的消息流。首先,每个用户向服务器注册。在我们的例子中,这将是一个简单的字符串用户名。用户注册后,他们就可以互相呼叫。用户 1 使用他希望呼叫的用户标识符进行报价。其他用户应该回答。最后,ICE 候选者在用户之间发送,直到他们可以建立连接。
    要创建 WebRTC 连接,客户端必须能够在不使用 WebRTC 对等连接的情况下传输消息。这是我们将使用 HTML5 WebSockets 的地方——两个端点之间的双向套接字连接——网络服务器和网络浏览器。现在让我们开始使用 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 连接时,将执行此代码。然后我们收听用户发送的任何消息。最后,我们向连接的用户发送“Hello from server”的响应。
    现在运行节点服务器,服务器应该开始监听套接字连接。
    为了测试我们的服务器,我们将使用我们也已经安装的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]; 
       } 
    });
    
    现在让我们用登录命令测试我们的服务器。请记住,所有消息都必须以 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" 
                }); 
             }  
          } 
       } 
    });
    
    现在,如果连接终止,我们的用户将断开连接。当我们仍处于offeranswercandidate状态时,用户关闭浏览器窗口时将触发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 连接时乱序操作可能会导致问题。
  • 概括

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