WebRTC - 信令

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

信令和协商

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

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

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

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

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

  • 第一个用户被告知接受要约。

  • 第一个用户与另一个用户发起 RTCPeerConnection

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

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

  • 连接成功或失败。

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

构建服务器

我们要构建的服务器将能够将不在同一台计算机上的两个用户连接在一起。我们将创建自己的信令机制。我们的信令服务器将允许一个用户呼叫另一个用户。一旦用户呼叫了另一个用户,服务器就会在他们之间传递要约、答案、ICE 候选并建立 WebRTC 连接。

构建服务器

上图是使用信令服务器时用户之间的消息流。首先,每个用户在服务器上注册。在我们的例子中,这将是一个简单的字符串用户名。一旦用户注册,他们就可以互相呼叫。用户 1 使用他希望呼叫的用户标识符发出要约。另一个用户应该回答。最后,在用户之间发送 ICE 候选,直到他们能够建立连接。

要创建 WebRTC 连接,客户端必须能够在不使用 WebRTC 对等连接的情况下传输消息。在这里我们将使用 HTML5 WebSockets - 两个端点(Web 服务器和 Web 浏览器)之间的双向套接字连接。现在让我们开始使用 WebSocket 库。创建 server.js 文件并插入以下代码 −

//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;

//在端口 9090 处创建 websocket 服务器
var wss = new WebSocketServer({port: 9090});

//当用户连接到我们的服务器时 
wss.on('connection', function(connection) { 
   console.log("user connected");
	
   //当服务器收到来自已连接用户的消息时
   connection.on('message', function(message){ 
      console.log("Got message from a user:", message); 
   }); 
	
   connection.send("Hello from server"); 
}); 

第一行需要我们已经安装的 WebSocket 库。然后我们在端口 9090 上创建一个套接字服务器。接下来,我们监听 connection 事件。当用户与服务器建立 WebSocket 连接时,将执行此代码。然后,我们监听用户发送的任何消息。最后,我们向已连接的用户发送响应"来自服务器的问候"。

现在运行 node server,服务器应该开始监听套接字连接。

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

使用 wscat 实用程序

服务器还应记录已连接的用户 −

记录已连接的用户

用户注册

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

connection.on('message', function(message) { 
   var data; 
	
   //仅接受 JSON 消息
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
});

这样我们只接受 JSON 消息。接下来,我们需要将所有连接的用户存储在某个地方。我们将使用一个简单的 Javascript 对象。更改文件顶部的 −

//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;

//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({port: 9090});

//所有连接到服务器的用户
var users = {};

我们将为来自客户端的每条消息添加一个 type 字段。例如,如果用户想要登录,他会发送 login 类型的消息。让我们定义它 −

connection.on('message', function(message){
   var data; 
	
   //仅接受 JSON 消息
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   }
	
   //切换用户消息类型
   switch (data.type) { 
      //当用户尝试登录时
      case "login": 
         console.log("User logged:", data.name); 
			
         //如果有人用这个用户名登录则拒绝
         if(users[data.name]) { 
            sendTo(connection, { 
               type: "login", 
               success: false 
            }); 
         } else { 
            //在服务器上保存用户连接 
            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; 
   } 
	
});

如果用户发送了 login 类型的消息,我们 −

  • 检查是否有人已经使用此用户名登录

  • 如果是,则告诉用户他尚未成功登录

  • 如果没有人使用此用户名,我们将用户名作为键添加到连接对象。

  • 如果无法识别命令,我们会发送错误。

以下代码是用于向连接发送消息的辅助函数。将其添加到 server.js 文件中 −

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

The above function ensures that all our messages are sent in the JSON format.

当用户断开连接时,我们应该清理其连接。我们可以在触发 close 事件时删除用户。将以下代码添加到 connection 处理程序 −

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

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

Test with Login Command

Making a Call

成功登录后,用户想要呼叫另一个用户。他应该向另一个用户发出offer来实现它。添加offer处理程序 −

case "offer": 
   //例如,用户 A 想要呼叫用户 B
   console.log("Sending offer to: ", data.name); 
	
   //如果 UserB 存在则向他发送优惠详情
   var conn = users[data.name]; 
	
   if(conn != null){ 
      //设置UserA与UserB连接
      connection.otherName = data.name; 
		
      sendTo(conn, { 
         type: "offer", 
         offer: data.offer, 
         name: connection.name 
      }); 
   }
	
   break;

首先,我们获取我们尝试呼叫的用户的 connection。如果存在,我们会向他发送 offer 详细信息。我们还将 otherName 添加到 connection 对象。这样做是为了以后查找起来更简单。

应答

应答响应的模式与我们在 offer 处理程序中使用的模式类似。我们的服务器只是将所有消息作为 answer 传递给另一个用户。在 offer 处理程序后添加以下代码 −

case "answer": 
   console.log("Sending answer to: ", data.name); 
	
   //例如 UserB 回答 UserA
   var conn = users[data.name]; 
	
   if(conn != null) { 
      connection.otherName = data.name; 
      sendTo(conn, { 
         type: "answer", 
         answer: data.answer 
      }); 
   }
	
   break;

您可以看到这与 offer 处理程序有多么相似。请注意,此代码位于 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;

它的工作方式应与 offeranswer 处理程序类似。

离开连接

为了允许我们的用户断开与另一个用户的连接,我们应该实现挂断功能。它还会告诉服务器删除所有用户引用。添加 leave 处理程序 −

case "leave": 
   console.log("Disconnecting from", data.name); 
   var conn = users[data.name]; 
   conn.otherName = null; 
	
   //通知其他用户,以便他可以断开他的对等连接
   if(conn != null) { 
      sendTo(conn, { 
         type: "leave" 
      }); 
   } 
	
   break;

这还将向其他用户发送 leave 事件,以便他可以相应地断开对等连接。我们还应该处理用户从信令服务器断开连接的情况。让我们修改我们的 close 处理程序 −

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 事件。

完整的信令服务器

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

//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;

//在端口 9090 处创建 websocket 服务器
var wss = new WebSocketServer({port: 9090});

//所有连接到服务器的用户
var users = {};

//当用户连接到我们的服务器时
wss.on('connection', function(connection) {
  
   console.log("User connected");
	
   //当服务器收到来自已连接用户的消息时
   connection.on('message', function(message) { 
	
      var data; 
      //仅接受 JSON 消息
      try {
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      } 
		
      //切换用户消息类型
      switch (data.type) { 
         //当用户尝试登录时
			
         case "login": 
            console.log("User logged", data.name); 
				
            //如果有人用这个用户名登录则拒绝
            if(users[data.name]) { 
               sendTo(connection, { 
                  type: "login", 
                  success: false 
               }); 
            } else { 
               //在服务器上保存用户连接 
               users[data.name] = connection; 
               connection.name = data.name; 
					
               sendTo(connection, { 
                  type: "login", 
                  success: true 
               }); 
            } 
				
            break; 
				
         case "offer": 
            //例如,用户 A 想要呼叫用户 B
            console.log("Sending offer to: ", data.name); 
				
            //如果 UserB 存在则向他发送优惠详情
            var conn = users[data.name];
				
            if(conn != null) { 
               //设置UserA与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); 
            //例如 UserB 回答 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; 
				
            //通知其他用户,以便他可以断开他的对等连接
            if(conn != null) { 
               sendTo(conn, { 
                  type: "leave" 
               }); 
            }  
				
            break;  
				
         default: 
            sendTo(connection, { 
               type: "error", 
               message: "Command not found: " + data.type 
            }); 
				
            break; 
      }  
   });  
	
   //当用户退出时,例如关闭浏览器窗口
   //如果我们仍处于"offer","answer" 或 "candidate" 状态,这可能会有所帮助
   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 连接时,如果操作顺序不正确,可能会导致问题。

总结

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