Das WebSocket
-Protokoll, beschrieben in der Spezifikation RFC 6455, bietet eine Möglichkeit, Daten zwischen Browser und Server über eine persistente Verbindung auszutauschen. Die Daten können in beide Richtungen als „Pakete“ weitergegeben werden, ohne die Verbindung zu unterbrechen und zusätzliche HTTP-Requests auszulösen.
WebSocket eignet sich besonders für Dienste, die einen kontinuierlichen Datenaustausch erfordern, z.B. Online-Spiele, Echtzeit-Handelssysteme und so weiter.
Ein einfaches Beispiel
Um eine Websocket-Verbindung zu öffnen, müssen wir ein new WebSocket
mit dem speziellen Protokoll ws
in der Url erstellen:
let socket = new WebSocket("ws://javascript.info");
Es gibt auch ein verschlüsseltes wss://
Protokoll. Es ist so etwas wie HTTPS für Websockets.
wss://
Das wss://
Protokoll ist nicht nur verschlüsselt, sondern auch zuverlässiger.
Das liegt daran, dass die ws://
Daten nicht verschlüsselt und für jeden Zwischenhändler sichtbar sind. Alte Proxy-Server kennen WebSocket nicht, sie sehen möglicherweise „seltsame“ Header und brechen die Verbindung ab.
Auf der anderen Seite, wss://
ist WebSocket über TLS, (genauso wie HTTPS HTTP über TLS ist), verschlüsselt die Transport-Sicherheitsschicht die Daten beim Sender und entschlüsselt sie beim Empfänger. Die Datenpakete werden also verschlüsselt durch Proxys geleitet. Sie können nicht sehen, was drin ist und sie durchlassen.
Wenn der Socket erstellt ist, sollten wir auf Ereignisse an ihm hören. Es gibt insgesamt 4 Ereignisse:
-
open
– Verbindung aufgebaut, -
message
– Daten empfangen, -
error
– Websocket-Fehler, -
close
– Verbindung geschlossen.
…Und wenn wir etwas senden wollen, dann tut das socket.send(data)
.
Hier ein Beispiel:
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};
Zu Demonstrationszwecken läuft ein kleiner Server server.js, der in Node.js geschrieben wurde, für das obige Beispiel. Er antwortet mit „Hallo vom Server, John“, wartet dann 5 Sekunden und schließt die Verbindung.
So sehen Sie die Ereignisse open
message
close
.
Das war’s eigentlich schon, wir können WebSocket sprechen. Ganz einfach, nicht wahr?
Nun gehen wir etwas mehr in die Tiefe.
Öffnen eines Websockets
new WebSocket(url)Wenn new WebSocket(url)
erstellt wird, beginnt es sofort mit der Verbindung.
Während der Verbindung fragt der Browser (mittels Header) den Server: „Unterstützt du Websocket?“ Und wenn der Server mit „Ja“ antwortet, dann geht das Gespräch im WebSocket-Protokoll weiter, das gar kein HTTP ist.
Hier ist ein Beispiel für Browser-Header für die Anfrage von new WebSocket("wss://javascript.info/chat")
.
GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13
-
Origin
– der Ursprung der Client-Seite, z.B.https://javascript.info
. WebSocket-Objekte sind von Natur aus herkunftsübergreifend. Es gibt keine speziellen Header oder andere Beschränkungen. Alte Server können ohnehin nicht mit WebSocket umgehen, so dass es keine Kompatibilitätsprobleme gibt. Aber derOrigin
-Header ist wichtig, da er dem Server erlaubt, zu entscheiden, ob er mit dieser Website über WebSocket sprechen möchte oder nicht. -
Connection: Upgrade
– signalisiert, dass der Client das Protokoll ändern möchte. -
Upgrade: websocket
– das angeforderte Protokoll ist „websocket“. -
Sec-WebSocket-Key
– ein zufälliger, vom Browser erzeugter Schlüssel zur Sicherheit. -
Sec-WebSocket-Version
– WebSocket-Protokollversion, 13 ist die aktuelle.
Wir können XMLHttpRequest
oder fetch
nicht verwenden, um diese Art von HTTP-Anfrage zu stellen, da JavaScript diese Header nicht setzen darf.
Wenn der Server zustimmt, auf WebSocket umzuschalten, sollte er als Antwort den Code 101 senden:
101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Hier Sec-WebSocket-Accept
steht Sec-WebSocket-Key
, umcodiert mit einem speziellen Algorithmus. Damit stellt der Browser sicher, dass die Antwort mit der Anfrage übereinstimmt.
Danach werden die Daten per WebSocket-Protokoll übertragen, dessen Aufbau („Frames“) wir gleich sehen werden. Und das ist gar nicht HTTP.
Erweiterungen und Unterprotokolle
Es kann zusätzliche Header Sec-WebSocket-Extensions
und Sec-WebSocket-Protocol
geben, die Erweiterungen und Unterprotokolle beschreiben.
Zum Beispiel:
-
Sec-WebSocket-Extensions: deflate-frame
bedeutet, dass der Browser Datenkompression unterstützt. Eine Erweiterung ist etwas, das sich auf die Übertragung der Daten bezieht, eine Funktionalität, die das WebSocket-Protokoll erweitert. Der HeaderSec-WebSocket-Extensions
wird automatisch vom Browser gesendet, mit der Liste aller Erweiterungen, die er unterstützt. -
Sec-WebSocket-Protocol: soap, wamp
bedeutet, dass wir nicht irgendwelche Daten übertragen möchten, sondern die Daten in SOAP oder WAMP („The WebSocket Application Messaging Protocol“) Protokollen. WebSocket-Unterprotokolle sind im IANA-Katalog registriert. Dieser Header beschreibt also Datenformate, die wir verwenden werden.Dieser optionale Header wird über den zweiten Parameter von
new WebSocket
gesetzt. Das ist das Array der Unterprotokolle, z.B. wenn wir SOAP oder WAMP verwenden möchten:let socket = new WebSocket("wss://javascript.info/chat", );
Der Server sollte mit einer Liste von Protokollen und Erweiterungen antworten, die er zu verwenden bereit ist.
Zum Beispiel die Anfrage:
GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp
Antwort:
101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap
Hier antwortet der Server, dass er die Erweiterung „deflate-frame“ und nur SOAP der angeforderten Unterprotokolle unterstützt.
Datenübertragung
Die WebSocket-Kommunikation besteht aus „Frames“ – Datenfragmenten, die von beiden Seiten gesendet werden können und von verschiedenen Arten sein können:
- „Text-Frames“ – enthalten Textdaten, die sich die Parteien gegenseitig senden.
- „Binärdaten-Frames“ – enthalten Binärdaten, die sich die Parteien gegenseitig schicken.
- „Ping/Pong-Frames“ werden zur Überprüfung der Verbindung verwendet, sie werden vom Server gesendet, der Browser antwortet darauf automatisch.
- Es gibt auch einen „Connection-Close-Frame“ und ein paar andere Service-Frames.
Im Browser arbeiten wir direkt nur mit Text- oder Binär-Frames.
Die Methode WebSocket .send()
kann entweder Text- oder Binärdaten senden.
Ein Aufruf socket.send(body)
erlaubt body
im String- oder einem Binärformat, einschließlich Blob
ArrayBuffer
, usw. Keine Einstellungen erforderlich: Senden Sie es einfach in einem beliebigen Format.
Wenn wir die Daten empfangen, kommt der Text immer als String. Und bei binären Daten können wir zwischen den Formaten Blob
und ArrayBuffer
wählen.
Das wird über die socket.binaryType
-Eigenschaft eingestellt, standardmäßig ist es "blob"
, also kommen binäre Daten als Blob
-Objekte.
Blob ist ein binäres Objekt auf hoher Ebene, es integriert sich direkt mit <a>
<img>
und anderen Tags, also ist das ein vernünftiger Standard. Aber für die binäre Verarbeitung, um auf einzelne Datenbytes zuzugreifen, können wir es in "arraybuffer"
ändern:
socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};
Ratenbegrenzung
Stellen Sie sich vor, unsere App generiert eine große Menge an Daten, die gesendet werden. Aber der Benutzer hat eine langsame Netzwerkverbindung, vielleicht auf einem mobilen Internet, außerhalb einer Stadt.
Wir können socket.send(data)
immer wieder aufrufen. Aber die Daten werden im Speicher gepuffert (gespeichert) und nur so schnell gesendet, wie es die Netzwerkgeschwindigkeit erlaubt.
Die socket.bufferedAmount
-Eigenschaft speichert, wie viele Bytes in diesem Moment gepuffert bleiben und darauf warten, über das Netzwerk gesendet zu werden.
Wir können sie untersuchen, um zu sehen, ob der Socket tatsächlich für die Übertragung verfügbar ist.
// every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);
Verbindung schließen
Normalerweise, wenn eine Partei die Verbindung schließen möchte (sowohl Browser als auch Server haben die gleichen Rechte), sendet sie einen „connection close frame“ mit einem numerischen Code und einem textuellen Grund.
Die Methode dafür ist:
socket.close(, );
-
code
ist ein spezieller WebSocket-Schließcode (optional) -
reason
ist eine Zeichenkette, die den Grund des Schließens beschreibt (optional)
Dann erhält die Gegenstelle im close
-Eventhandler den Code und den Grund, e.g.:
// closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};
Die häufigsten Codewerte:
-
1000
– der standardmäßige, normale Abschluss (wird verwendet, wenn keincode
mitgeliefert wird), -
1006
– keine Möglichkeit, einen solchen Code manuell zu setzen, zeigt an, dass die Verbindung verloren wurde (kein Abschlussrahmen).
Es gibt andere Codes wie:
-
1001
– die Partei geht weg, z.B. Server wird heruntergefahren, oder ein Browser verlässt die Seite, -
1009
– die Nachricht ist zu groß, um sie zu verarbeiten, -
1011
– unerwarteter Fehler auf dem Server, - …und so weiter.
Die vollständige Liste ist in RFC6455, §7.4.1 zu finden.
WebSocket-Codes sind ähnlich wie HTTP-Codes, aber anders. Insbesondere sind alle Codes kleiner als 1000
reserviert, es gibt einen Fehler, wenn wir versuchen, einen solchen Code zu setzen.
// in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};
Verbindungsstatus
Um den Verbindungsstatus zu erhalten, gibt es zusätzlich die socket.readyState
Eigenschaft mit Werten:
-
0
– „CONNECTING“: Die Verbindung ist noch nicht aufgebaut, -
1
– „OPEN“: es wird kommuniziert, -
2
– „CLOSING“: die Verbindung wird geschlossen, -
3
– „CLOSED“: die Verbindung wird geschlossen.
Chat-Beispiel
Schauen wir uns ein Chat-Beispiel mit der Browser-WebSocket-API und dem Node.js-WebSocket-Modul https://github.com/websockets/ws an. Wir werden das Hauptaugenmerk auf die Client-Seite legen, aber der Server ist auch einfach.
HTML: Wir brauchen ein <form>
zum Senden von Nachrichten und ein <div>
für eingehende Nachrichten:
<!-- message form --><form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"></form><!-- div with messages --><div></div>
Aus JavaScript wollen wir drei Dinge:
- Öffnen Sie die Verbindung.
- Bei Formularabgabe –
socket.send(message)
für die Nachricht. - Bei eingehender Nachricht – anhängen an
div#messages
.
Hier ist der Code:
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}
Der serverseitige Code sprengt ein wenig unseren Rahmen. Hier werden wir Node.js verwenden, aber das müssen Sie nicht. Andere Plattformen haben auch ihre Mittel, um mit WebSocket zu arbeiten.
Der serverseitige Algorithmus wird sein:
- Erstelle
clients = new Set()
– einen Satz von Sockets. - Für jeden akzeptierten Websocket, fügen Sie ihn der Menge
clients.add(socket)
hinzu und richten Sie einenmessage
-Ereignis-Listener ein, um seine Nachrichten zu erhalten. - Wenn eine Nachricht empfangen wird: iterieren Sie über die Clients und senden Sie sie an alle.
- Wenn eine Verbindung geschlossen wird:
clients.delete(socket)
.
const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);});function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}
Hier ist das Arbeitsbeispiel:
Sie können es auch herunterladen (Button oben rechts im iframe) und lokal ausführen. Vergessen Sie nur nicht, Node.js und npm install ws
vor dem Ausführen zu installieren.
Zusammenfassung
WebSocket ist eine moderne Art, persistente Browser-Server-Verbindungen zu haben.
- WebSockets haben keine herkunftsübergreifenden Beschränkungen.
- Sie werden in Browsern gut unterstützt.
- Sie können Strings und Binärdaten senden/empfangen.
Die API ist einfach.
Methoden:
-
socket.send(data)
, -
socket.close(, )
.
Events:
-
open
, -
message
, -
error
, -
close
.
WebSocket selbst beinhaltet keine Wiederverbindung, Authentifizierung und viele andere High-Level-Mechanismen. Dafür gibt es Client/Server-Bibliotheken, und es ist auch möglich, diese Fähigkeiten manuell zu implementieren.
Manchmal, um WebSocket in ein bestehendes Projekt zu integrieren, wird der WebSocket-Server parallel zum Haupt-HTTP-Server betrieben, und sie teilen sich eine einzige Datenbank. Anfragen an WebSocket verwenden wss://ws.site.com
, eine Subdomain, die zum WebSocket-Server führt, während https://site.com
zum Haupt-HTTP-Server geht.
Sicherlich sind auch andere Wege der Integration möglich.