WebRTC - Signalisierung

Die meisten WebRTC-Anwendungen können nicht nur über Video und Audio kommunizieren. Sie benötigen viele andere Funktionen. In diesem Kapitel werden wir einen grundlegenden Signalisierungsserver erstellen.

Signalisierung und Verhandlung

Um eine Verbindung zu einem anderen Benutzer herzustellen, sollten Sie wissen, wo er sich im Web befindet. Die IP-Adresse Ihres Geräts ermöglicht es internetfähigen Geräten, Daten direkt untereinander zu senden. Das RTCPeerConnection- Objekt ist dafür verantwortlich. Sobald Geräte wissen, wie sie sich über das Internet finden, tauschen sie Daten darüber aus, welche Protokolle und Codecs jedes Gerät unterstützt.

Um mit einem anderen Benutzer zu kommunizieren, müssen Sie lediglich die Kontaktinformationen austauschen. Der Rest wird von WebRTC erledigt. Das Herstellen einer Verbindung mit dem anderen Benutzer wird auch als Signalisieren und Aushandeln bezeichnet. Es besteht aus ein paar Schritten -

  • Erstellen Sie eine Liste potenzieller Kandidaten für eine Peer-Verbindung.

  • Der Benutzer oder eine Anwendung wählt einen Benutzer aus, mit dem eine Verbindung hergestellt werden soll.

  • Die Signalisierungsschicht benachrichtigt einen anderen Benutzer, dass jemand eine Verbindung zu ihm herstellen möchte. Er kann akzeptieren oder ablehnen.

  • Der erste Nutzer wird über die Annahme des Angebots informiert.

  • Der erste Benutzer initiiert RTCPeerConnection mit einem anderen Benutzer.

  • Beide Benutzer tauschen Software- und Hardwareinformationen über den Signalisierungsserver aus.

  • Beide Benutzer tauschen Standortinformationen aus.

  • Die Verbindung ist erfolgreich oder schlägt fehl.

Die WebRTC-Spezifikation enthält keine Standards für den Informationsaustausch. Denken Sie also daran, dass das oben Genannte nur ein Beispiel dafür ist, wie die Signalisierung erfolgen kann. Sie können jedes beliebige Protokoll oder jede Technologie verwenden.

Server erstellen

Der Server, den wir erstellen werden, kann zwei Benutzer miteinander verbinden, die sich nicht auf demselben Computer befinden. Wir werden unseren eigenen Signalmechanismus erstellen. Unser Signalisierungsserver ermöglicht es einem Benutzer, einen anderen anzurufen. Sobald ein Benutzer einen anderen angerufen hat, leitet der Server das Angebot, die Antwort und die ICE-Kandidaten zwischen ihnen weiter und richtet eine WebRTC-Verbindung ein.

Das obige Diagramm zeigt den Nachrichtenfluss zwischen Benutzern bei Verwendung des Signalisierungsservers. Zunächst registriert sich jeder Benutzer beim Server. In unserem Fall ist dies ein einfacher String-Benutzername. Sobald sich Benutzer registriert haben, können sie sich gegenseitig anrufen. Benutzer 1 macht ein Angebot mit der Benutzer-ID, die er anrufen möchte. Der andere Benutzer sollte antworten. Schließlich werden ICE-Kandidaten zwischen Benutzern gesendet, bis sie eine Verbindung herstellen können.

Um eine WebRTC-Verbindung herzustellen, müssen Clients in der Lage sein, Nachrichten ohne Verwendung einer WebRTC-Peer-Verbindung zu übertragen. Hier verwenden wir HTML5 WebSockets - eine bidirektionale Socket-Verbindung zwischen zwei Endpunkten - einem Webserver und einem Webbrowser. Beginnen wir nun mit der Verwendung der WebSocket-Bibliothek. Erstellen Sie die Datei server.js und geben Sie den folgenden Code ein:

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

Die erste Zeile erfordert die WebSocket-Bibliothek, die wir bereits installiert haben. Dann erstellen wir einen Socket-Server am Port 9090. Als nächstes lauschen wir dem Verbindungsereignis . Dieser Code wird ausgeführt, wenn ein Benutzer eine WebSocket-Verbindung zum Server herstellt. Wir hören dann alle vom Benutzer gesendeten Nachrichten ab. Schließlich senden wir eine Antwort an den verbundenen Benutzer mit der Aufschrift "Hallo vom Server".

Führen Sie nun den Knotenserver aus, und der Server sollte auf Socket-Verbindungen warten.

Zum Testen unseres Servers verwenden wir das Dienstprogramm wscat, das wir ebenfalls bereits installiert haben. Dieses Tool hilft beim Herstellen einer direkten Verbindung zum WebSocket-Server und beim Testen von Befehlen. Führen Sie unseren Server in einem Terminalfenster aus, öffnen Sie ein anderes und führen Sie den Befehl wscat -c ws: // localhost: 9090 aus . Auf der Client-Seite sollte Folgendes angezeigt werden:

Der Server sollte auch den verbundenen Benutzer protokollieren -

Benutzer Registration

In unserem Signalisierungsserver verwenden wir für jede Verbindung einen auf Zeichenfolgen basierenden Benutzernamen, damit wir wissen, wohin Nachrichten gesendet werden sollen. Lassen Sie uns unseren Verbindungshandler ein wenig ändern -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
});

Auf diese Weise akzeptieren wir nur JSON-Nachrichten. Als nächstes müssen wir alle verbundenen Benutzer irgendwo speichern. Wir werden dafür ein einfaches Javascript-Objekt verwenden. Ändern Sie den Anfang unserer Datei -

//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 = {};

Wir werden ein hinzuzufügen Typen Feld für jede Nachricht von dem Client kommen. Zum Beispiel , wenn ein Benutzer anmelden möchte, sendet er die Login - Typ - Nachricht. Definieren wir es -

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

Wenn der Benutzer eine Nachricht mit dem Anmeldetyp sendet , werden wir -

  • Überprüfen Sie, ob sich bereits jemand mit diesem Benutzernamen angemeldet hat

  • Wenn ja, teilen Sie dem Benutzer mit, dass er sich nicht erfolgreich angemeldet hat

  • Wenn niemand diesen Benutzernamen verwendet, fügen wir dem Verbindungsobjekt einen Benutzernamen als Schlüssel hinzu.

  • Wenn ein Befehl nicht erkannt wird, senden wir einen Fehler.

Der folgende Code ist eine Hilfsfunktion zum Senden von Nachrichten an eine Verbindung. Fügen Sie es der Datei server.js hinzu -

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

Die obige Funktion stellt sicher, dass alle unsere Nachrichten im JSON-Format gesendet werden.

Wenn der Benutzer die Verbindung trennt, sollten wir die Verbindung bereinigen. Wir können den Benutzer löschen, wenn das Abschlussereignis ausgelöst wird. Fügen Sie dem Verbindungshandler den folgenden Code hinzu :

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

Testen wir nun unseren Server mit dem Login-Befehl. Beachten Sie, dass alle Nachrichten im JSON-Format codiert sein müssen. Führen Sie unseren Server aus und versuchen Sie sich anzumelden. Sie sollten so etwas sehen -

Einen Anruf tätigen

Nach erfolgreicher Anmeldung möchte der Benutzer einen anderen anrufen. Er sollte einem anderen Benutzer ein Angebot machen , um dies zu erreichen. Fügen Sie den Angebotshandler hinzu -

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;

Erstens erhalten wir die Verbindung des Benutzers, den wir anrufen möchten. Wenn es existiert, senden wir ihm Angebotsdetails . Wir fügen dem Verbindungsobjekt auch otherName hinzu . Dies dient der Einfachheit, es später zu finden.

Antworten

Die Beantwortung der Antwort hat ein ähnliches Muster, das wir im Angebotshandler verwendet haben . Unser Server leitet einfach alle Nachrichten als Antwort an einen anderen Benutzer weiter. Fügen Sie nach dem Angebotshander den folgenden Code hinzu :

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;

Sie können sehen, wie ähnlich dies dem Angebotshandler ist . Beachten Sie, dass dieser Code den Funktionen createOffer und createAnswer für das RTCPeerConnection- Objekt folgt .

Jetzt können wir unseren Angebots- / Antwortmechanismus testen. Verbinden Sie zwei Clients gleichzeitig und versuchen Sie, ein Angebot abzugeben und zu antworten. Sie sollten Folgendes sehen -

In diesem Beispiel offer und answer sind einfache Zeichenfolgen, aber in einer realen Anwendung werden sie mit den SDP-Daten gefüllt.

ICE-Kandidaten

Der letzte Teil ist die Behandlung von ICE-Kandidaten zwischen Benutzern. Wir verwenden dieselbe Technik, um nur Nachrichten zwischen Benutzern zu übertragen. Der Hauptunterschied besteht darin, dass Kandidatennachrichten in jeder Reihenfolge mehrmals pro Benutzer auftreten können. Fügen Sie den Kandidaten- Handler hinzu -

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;

Es sollte ähnlich wie die Arbeit bieten und Antwort - Handler.

Verbindung verlassen

Damit sich unsere Benutzer von einem anderen Benutzer trennen können, sollten wir die Funktion zum Auflegen implementieren. Außerdem wird der Server angewiesen, alle Benutzerreferenzen zu löschen. Ergänzen Sie dieleave Handler -

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;

Dadurch wird dem anderen Benutzer auch das Urlaubsereignis gesendet, damit er seine Peer-Verbindung entsprechend trennen kann. Wir sollten auch den Fall behandeln, wenn ein Benutzer seine Verbindung vom Signalisierungsserver trennt. Lassen Sie uns unseren engen Handler modifizieren -

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

Wenn nun die Verbindung beendet wird, werden unsere Benutzer getrennt. Das Abschlussereignis wird ausgelöst, wenn ein Benutzer sein Browserfenster schließt, während wir uns noch im Angebot , in der Antwort oder im Kandidatenstatus befinden .

Vollständiger Signalisierungsserver

Hier ist der gesamte Code unseres Signalisierungsservers -

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

Damit ist die Arbeit erledigt und unser Signalisierungsserver ist bereit. Denken Sie daran, dass es zu Problemen kommen kann, wenn beim Herstellen einer WebRTC-Verbindung Probleme auftreten.

Zusammenfassung

In diesem Kapitel haben wir einen einfachen und unkomplizierten Signalisierungsserver erstellt. Wir haben den Signalisierungsprozess, die Benutzerregistrierung und den Angebots- / Antwortmechanismus durchlaufen. Wir haben auch das Senden von Kandidaten zwischen Benutzern implementiert.