WebSocket in Node.js: Server, Client, Heartbeat und Reconnect
WebSocket in Node.js: ws-Server, eingebauter Client, Heartbeat | We.Inc
Baue einen WebSocket-Server in Node.js mit dem Paket ws, verbinde dich mit dem eingebauten WebSocket-Client von Node, erkenne tote Sockets mit ping und pong und verbinde dich mit Backoff neu.
Node.js hat keinen eingebauten WebSocket-Server, deshalb installieren die meisten Projekte das Paket ws und erstellen einen WebSocketServer. Für die Client-Seite liefert Node ein browserkompatibles globales WebSocket mit, eingeführt in v21.0.0 und v20.10.0, ohne Flag ab v22.0.0 und als stabil markiert ab v22.4.0. Ergänze vor dem Livegang einen Heartbeat mit ping und pong sowie Reconnect mit Backoff.
Welcher Baustein was erledigt
Aufgabe
Verwende
Woher es kommt
Verbindungen annehmen
new WebSocketServer({ port })
Paket ws (npm install ws)
Aus Node verbinden
new WebSocket(url)
Global ab Node 22
Aus einem Browser verbinden
new WebSocket(url)
In jeden modernen Browser eingebaut
Tote Sockets finden
ws.ping() plus ein pong-Listener
Paket ws
Neustarts überstehen
Reconnect mit exponentiellem Backoff
Dein eigener Client-Code
Diese Anleitung baut der Reihe nach einen kleinen Server im Chat-Stil und die Clients, die mit ihm sprechen: einen Server, der Nachrichten zurückschickt und an alle verteilt, ein Node-Skript, das sich ohne jede Abhängigkeit verbindet, einen Heartbeat, der still gestorbene Verbindungen verwirft, und einen Client, der nach einem Server-Neustart zurückkommt. Der Rechner unten zeigt, wie deine Wartezeiten zwischen den Versuchen wachsen, bevor du sie in Code gießt.
Führe npm install ws in einem Projekt aus, dessen package.json "type": "module" enthält, damit die import-Syntax der Beispiele funktioniert.
Starte die Server-Datei mit node server.js. Sie lauscht auf Port 8080 und protokolliert jede Nachricht.
Führe die Client-Datei in einem zweiten Terminal mit Node 22 oder neuer aus oder öffne die Browser-Konsole und füge denselben Code ein.
Stoppe den Server mit Ctrl+C und starte ihn erneut, um zu beobachten, wie der Client wartet, es erneut versucht und sich erholt.
Hänge an jeden serverseitigen Socket immer einen error-Listener. ws löst error-Events aus, und ein unbehandeltes Event kann den Prozess zum Absturz bringen.
Prüfe req.headers.origin im connection-Handler und schließe Sockets von Origins, die du nicht bedienst. Browser senden diesen Header, auch die Seiten von Angreifern.
Reverse Proxies kappen inaktive Sockets. nginx schließt einen weitergeleiteten WebSocket standardmäßig, wenn das Backend 60 Sekunden lang nichts sendet, halte dein ping-Intervall also darunter.
Füge den Reconnect-Wartezeiten zufälligen Jitter hinzu. Ohne ihn versuchen es nach einem Neustart alle Clients im selben Moment und treffen gleichzeitig auf den Server.
Broadcast-Schleifen sollten Clients überspringen, deren readyState nicht OPEN ist, denn das Senden an einen sich schließenden Socket bringt nichts.
Häufige Fragen
Hat Node.js WebSocket eingebaut?
Nur den Client. Die globale WebSocket-Klasse erschien in v21.0.0 und v20.10.0 hinter dem Flag --experimental-websocket, verlor das Flag in v22.0.0 und war ab v22.4.0 nicht mehr experimentell. Einen eingebauten Server gibt es weiterhin nicht, deshalb bleibt ws die übliche Wahl.
Sollte ich ws oder Socket.IO verwenden?
ws spricht das reine WebSocket-Protokoll, daher kann sich jeder Standard-Client verbinden, auch der Browser und das globale WebSocket von Node. Socket.IO setzt ein eigenes Protokoll mit Rooms und automatischem Reconnect obendrauf, braucht aber auf der Gegenseite eine eigene Client-Bibliothek.
Warum sterben Verbindungen ohne close-Event?
Verschwindet ein Netzwerkpfad, etwa weil ein Laptop zugeklappt wird, wird nie ein close-Frame gesendet, und der Server behält einen halb offenen Socket. Der Heartbeat findet diese: Ein Socket, der auf den letzten ping nicht geantwortet hat, wird in der nächsten Runde beendet.
Kann ein Browser-Client pings senden?
Nein. Die Browser-API kennt nur send() und close(). Browser beantworten Server-pings aber von sich aus mit pongs, deshalb funktioniert der serverseitige Heartbeat auch mit Browser-Clients.
Wie teile ich einen Port mit einem HTTP-Server?
Erstelle den HTTP-Server selbst, zum Beispiel mit Express oder node:http, und übergib ihn als new WebSocketServer({ server }). Upgrade-Anfragen gehen an ws, normale Anfragen an deine App.
Wie eine Verbindung beginnt
Ein WebSocket beginnt sein Leben als HTTP-GET mit Upgrade: websocket. Der ws-Server prüft die Anfrage, antwortet mit 101 Switching Protocols und löst sein connection-Event mit dem neuen Socket und der ursprünglichen Anfrage aus. Alles, was du aus dem Handshake brauchst, etwa ein Token im Query-String oder den Origin-Header, liest du aus dieser Anfrage.
Heartbeats einfach erklärt
Laut Protokoll muss ein Endpunkt, der einen ping empfängt, mit einem pong mit denselben Daten antworten. Der Server markiert jeden Socket als lebendig, sobald ein pong eintrifft, löscht die Markierung dann per Timer und sendet erneut einen ping. Jeder Socket, der beim nächsten Durchlauf noch unmarkiert ist, hat nie geantwortet, also beendet der Server ihn.
Die README von ws verwendet dafür einen Timer von 30 Sekunden. Wähle einen Wert, der deutlich unter dem Idle-Timeout jedes vorgeschalteten Proxys liegt.
Neu verbinden, ohne den Server zu überrennen
Ein guter Client wartet nach dem ersten Abbruch kurz, nach jedem weiteren Fehlschlag länger, bis zu einer Obergrenze, und setzt den Zähler zurück, sobald eine Verbindung steht. Zufällig verteilte Wartezeiten verhindern, dass eine Menge Clients im Gleichschritt zurückkehrt.
Schritt 1: ein Server mit ws
server.js: Echo und Broadcast
Jede Nachricht wird an alle offenen Clients zurückgesendet. Der error-Listener verhindert, dass ein fehlerhafter Socket den Prozess abstürzen lässt.
Schritt 2: ein Client ohne Abhängigkeiten
client.js: das globale WebSocket von Node
Läuft unverändert ab Node 22 und in einer Browser-Konsole, weil beide dieselbe WebSocket-Klasse bereitstellen.
Schritt 3: tote Verbindungen mit ping und pong entfernen
Unsere Version des Musters aus der README von ws: bei pong als lebendig markieren, alles beenden, was die letzte Runde verpasst hat.
Schritt 4: ein Client, der sich neu verbindet
reconnect.js: exponentieller Backoff mit Jitter
Die Wartezeit verdoppelt sich nach jedem Fehlversuch bis zu einer Obergrenze, bekommt eine zufällige Streuung und wird zurückgesetzt, sobald eine Verbindung steht.
Hinter nginx
nginx reicht das Upgrade nur durch, wenn du die Header Upgrade und Connection selbst weiterleitest. Vor nginx 1.29.7 brauchst du außerdem proxy_http_version 1.1.
nginx-location für den Socket
proxy_read_timeout hebt das Idle-Limit von 60 Sekunden an, der Heartbeat oben hält den Datenverkehr ohnehin am Laufen.
Rechner für Reconnect-Wartezeiten
Lege eine erste Wartezeit, einen Multiplikator und eine Obergrenze fest, um zu sehen, wie lange ein Client vor jedem Versuch wartet, und die kürzeste und längste Wartezeit mit Jitter.
Erste Wartezeit (ms)
Multiplikator
Obergrenze (ms)
Angezeigte Versuche
Jitter hinzufügen (zwischen halber und voller Wartezeit warten)