Ein einfaches RTCDataChannel-Beispiel
Das RTCDataChannel
-Interface ist eine Funktion der WebRTC API, die es Ihnen ermöglicht, einen Kanal zwischen zwei Peers zu öffnen, über den Sie beliebige Daten senden und empfangen können. Die API ist absichtlich ähnlich der WebSocket API gestaltet, sodass dasselbe Programmiermodell für beide verwendet werden kann.
In diesem Beispiel öffnen wir eine RTCDataChannel
-Verbindung, die zwei Elemente auf derselben Seite verbindet. Während dies offensichtlich ein konstruiertes Szenario ist, ist es nützlich, um den Ablauf der Verbindung zweier Peers zu zeigen. Wir werden die Mechanik der Verbindung und der Datenübertragung sowie des Empfangs behandeln, aber wir sparen uns die Details zur Suche und Verbindung mit einem entfernten Computer für ein anderes Beispiel auf.
Das HTML
Zuerst werfen wir einen kurzen Blick auf das benötigte HTML. Hier gibt es nichts besonders Kompliziertes. Zuerst haben wir ein paar Schaltflächen zum Herstellen und Beenden der Verbindung:
<button id="connectButton" name="connectButton" class="buttonleft">
Connect
</button>
<button
id="disconnectButton"
name="disconnectButton"
class="buttonright"
disabled>
Disconnect
</button>
Dann gibt es ein Feld, das das Texteingabefeld enthält, in das der Benutzer eine Nachricht eingeben kann, mit einer Schaltfläche zum Senden des eingegebenen Textes. Dieses <div>
wird der erste Peer im Kanal sein.
<div class="messagebox">
<label for="message"
>Enter a message:
<input
type="text"
name="message"
id="message"
placeholder="Message text"
inputmode="latin"
size="60"
maxlength="120"
disabled />
</label>
<button id="sendButton" name="sendButton" class="buttonright" disabled>
Send
</button>
</div>
Schließlich gibt es das kleine Feld, in das wir die Nachrichten einfügen werden. Dieser <div>
-Block wird der zweite Peer sein.
<div class="messagebox" id="receive-box">
<p>Messages received:</p>
</div>
Der JavaScript-Code
Während Sie sich einfach den Code selbst auf GitHub ansehen können, werden wir unten die Teile des Codes überprüfen, die die Hauptarbeit leisten.
Start
Wenn das Skript ausgeführt wird, richten wir einen load
-Event-Listener ein, sodass unsere startup()
-Funktion aufgerufen wird, wenn die Seite vollständig geladen ist.
let connectButton = null;
let disconnectButton = null;
let sendButton = null;
let messageInputBox = null;
let receiveBox = null;
let localConnection = null; // RTCPeerConnection for our "local" connection
let remoteConnection = null; // RTCPeerConnection for the "remote"
let sendChannel = null; // RTCDataChannel for the local (sender)
let receiveChannel = null; // RTCDataChannel for the remote (receiver)
function startup() {
connectButton = document.getElementById("connectButton");
disconnectButton = document.getElementById("disconnectButton");
sendButton = document.getElementById("sendButton");
messageInputBox = document.getElementById("message");
receiveBox = document.getElementById("receive-box");
// Set event listeners for user interface widgets
connectButton.addEventListener("click", connectPeers, false);
disconnectButton.addEventListener("click", disconnectPeers, false);
sendButton.addEventListener("click", sendMessage, false);
}
Dies ist ziemlich einfach. Wir deklarieren Variablen und holen Referenzen zu allen Seitenelementen, auf die wir zugreifen müssen, und setzen dann Ereignis-Listener auf die drei Schaltflächen.
Eine Verbindung herstellen
Wenn der Benutzer auf die Schaltfläche "Connect" klickt, wird die Methode connectPeers()
aufgerufen. Wir werden dies aufteilen und uns Stück für Stück ansehen, um Klarheit zu schaffen.
Hinweis: Auch wenn beide Enden unserer Verbindung auf derselben Seite sind, werden wir dasjenige, das die Verbindung startet, als "lokales" Ende bezeichnen und das andere als "entferntes" Ende.
Das lokale Peer einrichten
localConnection = new RTCPeerConnection();
sendChannel = localConnection.createDataChannel("sendChannel");
sendChannel.onopen = handleSendChannelStatusChange;
sendChannel.onclose = handleSendChannelStatusChange;
Der erste Schritt besteht darin, das "lokale" Ende der Verbindung zu erstellen. Dies ist der Peer, der die Verbindungsanfrage sendet. Der nächste Schritt besteht darin, das RTCDataChannel
zu erstellen, indem RTCPeerConnection.createDataChannel()
aufgerufen wird, und Ereignis-Listener einzurichten, um den Kanal zu überwachen, damit wir wissen, wann er geöffnet und geschlossen ist (das heißt, wann der Kanal innerhalb dieser Peer-Verbindung verbunden oder getrennt ist).
Es ist wichtig, sich daran zu erinnern, dass jedes Ende des Kanals sein eigenes RTCDataChannel
-Objekt hat.
Das entfernte Peer einrichten
remoteConnection = new RTCPeerConnection();
remoteConnection.ondatachannel = receiveChannelCallback;
Das entfernte Ende wird ähnlich eingerichtet, außer dass wir hier kein eigenes RTCDataChannel
explizit erstellen müssen, da wir über den zuvor eingerichteten Kanal verbunden werden. Stattdessen richten wir einen datachannel
-Ereignis-Handler ein; dieser wird aufgerufen, wenn der Datenkanal geöffnet wird; dieser Handler erhält ein RTCDataChannel
-Objekt; Sie werden dies unten sehen.
Die ICE-Kandidaten einrichten
Der nächste Schritt besteht darin, jede Verbindung mit ICE-Kandidaten-Listenern einzurichten; diese werden aufgerufen, wenn es einen neuen ICE-Kandidaten gibt, um der anderen Seite davon zu berichten.
Hinweis: In einem realen Szenario, in dem die zwei Peers nicht im gleichen Kontext laufen, ist der Prozess ein wenig komplizierter; jede Seite bietet, eine nach der anderen, eine vorgeschlagene Verbindungsweise (zum Beispiel UDP, UDP mit einem Relais, TCP usw.) an, indem sie RTCPeerConnection.addIceCandidate()
aufruft, und sie gehen hin und her, bis eine Einigung erzielt wird. Aber hier akzeptieren wir einfach das erste Angebot auf jeder Seite, da kein echtes Networking beteiligt ist.
localConnection.onicecandidate = (e) =>
!e.candidate ||
remoteConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
remoteConnection.onicecandidate = (e) =>
!e.candidate ||
localConnection.addIceCandidate(e.candidate).catch(handleAddCandidateError);
Wir konfigurieren jede RTCPeerConnection
, um einen Ereignis-Handler für das icecandidate
-Ereignis zu haben.
Beginnen des Verbindungsversuchs
Das Letzte, was wir tun müssen, um unseren Peers zu verbinden, ist, ein Verbindungsangebot zu erstellen.
localConnection
.createOffer()
.then((offer) => localConnection.setLocalDescription(offer))
.then(() =>
remoteConnection.setRemoteDescription(localConnection.localDescription),
)
.then(() => remoteConnection.createAnswer())
.then((answer) => remoteConnection.setLocalDescription(answer))
.then(() =>
localConnection.setRemoteDescription(remoteConnection.localDescription),
)
.catch(handleCreateDescriptionError);
Lassen Sie uns dies Zeile für Zeile durchgehen und entschlüsseln, was es bedeutet.
- Zuerst rufen wir die Methode
RTCPeerConnection.createOffer()
auf, um ein SDP (Session Description Protocol) Blob zu erstellen, das die Verbindung beschreibt, die wir herstellen möchten. Diese Methode akzeptiert optional ein Objekt mit Einschränkungen, die erfüllt sein müssen, damit die Verbindung Ihren Bedürfnissen entspricht, wie z.B., ob die Verbindung Audio, Video oder beides unterstützen soll. In unserem einfachen Beispiel haben wir keine Einschränkungen. - Wenn das Angebot erfolgreich erstellt wurde, übergeben wir das Blob an die Methode
RTCPeerConnection.setLocalDescription()
der lokalen Verbindung. Dies konfiguriert das lokale Ende der Verbindung. - Der nächste Schritt besteht darin, den lokalen Peer mit dem entfernten zu verbinden, indem dem entfernten Peer davon berichtet wird. Dies geschieht durch Aufrufen von
remoteConnection.setRemoteDescription()
. Jetzt kenntremoteConnection
die Verbindung, die erstellt wird. In einer echten Anwendung wäre hierzu ein Signalisierungsserver erforderlich, um das Beschreibungsobjekt auszutauschen. - Das bedeutet, dass es Zeit für den entfernten Peer ist zu antworten. Dies erfolgt durch Aufrufen seiner Methode
createAnswer()
. Dies erzeugt einen SDP-Blob, der die Verbindung beschreibt, die der entfernte Peer bereit und in der Lage ist, zu erstellen. Diese Konfiguration liegt irgendwo in der Schnittmenge der Optionen, die beide Peers unterstützen können. - Sobald die Antwort erstellt wurde, wird sie vom Aufrufen von
RTCPeerConnection.setLocalDescription()
in dieremoteConnection
gegeben. Das stellt das Ende der Verbindung des entfernten Peers her (das, für den entfernten Peer, sein lokales Ende ist. Diese Dinge können verwirrend sein, aber Sie gewöhnen sich daran). Auch dies würde normalerweise über einen Signalisierungsserver ausgetauscht. - Schließlich wird die Remote-Beschreibung der lokalen Verbindung festgelegt, um auf den entfernten Peer zu verweisen, indem die
localConnection
's MethodeRTCPeerConnection.setRemoteDescription()
aufgerufen wird. - Die
catch()
-Aufrufe behandeln alle Fehler, die auftreten können.
Hinweis: Auch hier ist dieser Prozess keine Implementierung für die reale Welt; bei der normalen Nutzung gibt es zwei Codeblöcke, die auf zwei Maschinen laufen, die miteinander interagieren und die Verbindung aushandeln. Ein Nebenkanal, der üblicherweise als "Signalisierungsserver" bezeichnet wird, wird normalerweise verwendet, um die Beschreibung (die im application/sdp-Format vorliegt) zwischen den beiden Peers auszutauschen.
Handhabung der erfolgreichen Peer-Verbindung
Wenn jede Seite der Peer-to-Peer-Verbindung erfolgreich verknüpft ist, wird das entsprechende RTCPeerConnection
's icecandidate
-Ereignis ausgelöst. Diese Handler können tun, was nötig ist, aber in diesem Beispiel müssen wir nur die Benutzeroberfläche aktualisieren:
function handleCreateDescriptionError(error) {
console.log(`Unable to create an offer: ${error.toString()}`);
}
function handleLocalAddCandidateSuccess() {
connectButton.disabled = true;
}
function handleRemoteAddCandidateSuccess() {
disconnectButton.disabled = false;
}
function handleAddCandidateError() {
console.log("Oh noes! addICECandidate failed!");
}
Alles, was wir hier tun, ist, die "Connect"-Schaltfläche zu deaktivieren, wenn der lokale Peer verbunden ist, und die "Disconnect"-Schaltfläche zu aktivieren, wenn der entfernte Peer sich verbindet.
Verbinden des Datenkanals
Sobald das RTCPeerConnection
geöffnet ist, wird das datachannel
-Ereignis an den Remote gesendet, um den Prozess des Öffnens des Datenkanals abzuschließen; dies ruft unsere receiveChannelCallback()
-Methode auf, die folgendermaßen aussieht:
function receiveChannelCallback(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = handleReceiveMessage;
receiveChannel.onopen = handleReceiveChannelStatusChange;
receiveChannel.onclose = handleReceiveChannelStatusChange;
}
Das datachannel
-Ereignis enthält in seiner Channel-Eigenschaft eine Referenz zu einem RTCDataChannel
, das das Ende des entfernten Peers im Kanal darstellt. Dies wird gespeichert, und wir richten auf dem Kanal Ereignis-Listener für die Ereignisse ein, die wir behandeln möchten. Sobald dies erledigt ist, wird unsere handleReceiveMessage()
-Methode jedes Mal aufgerufen, wenn Daten vom entfernten Peer empfangen werden, und die handleReceiveChannelStatusChange()
-Methode wird jedes Mal aufgerufen, wenn sich der Verbindungsstatus des Kanals ändert, damit wir reagieren können, wenn der Kanal vollständig geöffnet oder geschlossen wird.
Umgang mit Statusänderungen des Kanals
Sowohl unsere lokalen als auch entfernten Peers verwenden eine einzige Methode, um Ereignisse zu behandeln, die auf eine Änderung des Status der Kanalverbindung hinweisen.
Wenn der lokale Peer ein "open"- oder "close"-Event erfährt, wird die Methode handleSendChannelStatusChange()
aufgerufen:
function handleSendChannelStatusChange(event) {
if (sendChannel) {
const state = sendChannel.readyState;
if (state === "open") {
messageInputBox.disabled = false;
messageInputBox.focus();
sendButton.disabled = false;
disconnectButton.disabled = false;
connectButton.disabled = true;
} else {
messageInputBox.disabled = true;
sendButton.disabled = true;
connectButton.disabled = false;
disconnectButton.disabled = true;
}
}
}
Wenn sich der Status des Kanals in "open" geändert hat, bedeutet das, dass wir die Verbindung zwischen den beiden Peers vollständig hergestellt haben. Die Benutzeroberfläche wird entsprechend aktualisiert, indem das Texteingabefeld für die zu sendende Nachricht aktiviert, das Eingabefeld fokussiert wird, damit der Benutzer sofort mit dem Tippen beginnen kann, die Schaltflächen "Send" und "Disconnect" aktiviert werden, sobald sie nutzbar sind, und die "Connect"-Schaltfläche deaktiviert wird, da sie bei geöffneter Verbindung nicht benötigt wird.
Wenn sich der Status in "closed" geändert hat, tritt das Gegenteil ein: Das Eingabefeld und die "Send"-Schaltfläche werden deaktiviert, die "Connect"-Schaltfläche wird aktiviert, sodass der Benutzer eine neue Verbindung öffnen kann, wenn er dies wünscht, und die "Disconnect"-Schaltfläche deaktiviert, da sie bei fehlender Verbindung nicht nützlich ist.
Der entfernte Peer unseres Beispiels ignoriert andererseits die Statusänderungsereignisse, mit Ausnahme der Protokollierung des Ereignisses in der Konsole:
function handleReceiveChannelStatusChange(event) {
if (receiveChannel) {
console.log(
`Receive channel's status has changed to ${receiveChannel.readyState}`,
);
}
}
Die Methode handleReceiveChannelStatusChange()
erhält als Eingabeparameter das aufgetretene Ereignis; dies wird ein RTCDataChannelEvent
sein.
Nachrichten senden
Wenn der Benutzer die "Send"-Schaltfläche drückt, wird die sendMessage()
-Methode aufgerufen, die wir als Handler für das click
-Ereignis der Schaltfläche festgelegt haben. Diese Methode ist einfach genug:
function sendMessage() {
const message = messageInputBox.value;
sendChannel.send(message);
messageInputBox.value = "";
messageInputBox.focus();
}
Zuerst wird der Text der Nachricht aus dem value
-Attribut des Eingabefeldes geholt. Dieser wird dann an den entfernten Peer gesendet, indem sendChannel.send()
aufgerufen wird. Das ist alles, was dazu gehört! Der Rest dieser Methode ist nur etwas Benutzererlebnis-Süße — das Eingabefeld wird geleert und neu fokussiert, sodass der Benutzer sofort beginnen kann, eine weitere Nachricht zu tippen.
Nachrichten empfangen
Wenn ein "message"-Ereignis im Remote-Kanal auftritt, wird unsere handleReceiveMessage()
-Methode als Event-Handler aufgerufen.
function handleReceiveMessage(event) {
const el = document.createElement("p");
const textNode = document.createTextNode(event.data);
el.appendChild(textNode);
receiveBox.appendChild(el);
}
Diese Methode führt einige grundlegende DOM-Injektionen durch; sie erstellt ein neues <p>
-Element (Absatz), erstellt dann einen neuen Text
-Knoten, der den Nachrichtentext enthält, der in der data
-Eigenschaft des Ereignisses empfangen wird. Dieser Textknoten wird als Kind des neuen Elements hinzugefügt, das dann in den receiveBox
-Block eingefügt wird, wodurch es im Browserfenster gezeichnet wird.
Trennen der Peers
Wenn der Benutzer auf die "Disconnect"-Schaltfläche klickt, wird die Methode disconnectPeers()
aufgerufen, die vorher als Handler dieser Schaltfläche festgelegt wurde.
function disconnectPeers() {
// Close the RTCDataChannels if they're open.
sendChannel.close();
receiveChannel.close();
// Close the RTCPeerConnections
localConnection.close();
remoteConnection.close();
sendChannel = null;
receiveChannel = null;
localConnection = null;
remoteConnection = null;
// Update user interface elements
connectButton.disabled = false;
disconnectButton.disabled = true;
sendButton.disabled = true;
messageInputBox.value = "";
messageInputBox.disabled = true;
}
Dies beginnt damit, dass jeder Peer das RTCDataChannel
schließt, dann ähnlich jede RTCPeerConnection
. Dann werden alle gespeicherten Referenzen zu diesen Objekten auf null
gesetzt, um eine versehentliche Wiederverwendung zu vermeiden, und die Benutzeroberfläche wird aktualisiert, um den Abschluss der Verbindung anzuzeigen.
Nächste Schritte
Werfen Sie einen Blick auf den Quellcode von webrtc-simple-datachannel, der auf GitHub verfügbar ist.
Siehe auch
- Signalisierung und Videoanrufe.
- Das Perfect Negotiation-Muster.