Was sind WebSockets?
Ein WebSocket ist eine persistente, Full-Duplex-Verbindung zwischen einem Client und einem Server. Nach einem initialen HTTP-Handshake können beide Seiten jederzeit Nachrichten über dieselbe TCP-Verbindung senden, wobei der Overhead pro Nachricht sehr gering ist. Der Server kann Daten in dem Moment pushen, in dem sich etwas ändert, anstatt darauf zu warten, dass der Client anfragt.
Das macht WebSockets zum richtigen Werkzeug für Chats, Präsenzanzeigen, Live-Dashboards, kollaboratives Editieren, Multiplayer-Spiele und alles andere, bei dem Updates sofort ankommen müssen. Für einfache Einweg-Updates sind Server-Sent Events oft die einfachere Wahl.
Der Upgrade-Handshake
Eine WebSocket-Verbindung beginnt als HTTP-Request.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Wenn der Server diesen akzeptiert, antwortet er:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Nach 101 Switching Protocols ist die Verbindung kein HTTP mehr. Sie wird zu einem WebSocket, und die Daten fließen in Frames. Da sie als HTTP beginnt, funktioniert sie über dieselben Ports (80 und 443), durch die meisten Proxies und Firewalls und wird mittels wss:// auf TLS upgegraded.
Frames
WebSocket-Nachrichten werden in Frames mit einem kleinen Header unterteilt. Es gibt Text-Frames, Binary-Frames sowie Control-Frames für Ping, Pong und Close. Das Framing ist minimal, weshalb WebSocket-Nachrichten wesentlich ressourcensparender sind als wiederholte HTTP-Requests.
Das Protokoll definiert zudem Close-Codes, wie zum Beispiel 1000 für einen normalen Abschluss und 1001 für „Going Away“, die beiden Seiten helfen zu verstehen, warum eine Verbindung beendet wurde.
Die Browser-API
Die Client-API ist kompakt und ereignisgesteuert.
// client.js
const socket = new WebSocket("wss://api.example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
render(message);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
});
socket.addEventListener("error", () => {
console.error("connection error");
});
Sie senden Daten mit socket.send() und empfangen message-Events. Die Eigenschaft readyState gibt an, ob der Socket gerade eine Verbindung aufbaut, geöffnet, schließend oder geschlossen ist.
Ein Node.js Server
Die ws Library ist der Standardweg, um einen WebSocket Server in Node.js zu betreiben.
// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
socket.on("close", () => console.log("client left"));
});
Der Server verfolgt jede Verbindung und kann Nachrichten an einen einzelnen Client, eine Teilmenge (einen Room) oder an alle Clients senden. Socket.IO ist eine übergeordnete Alternative, die Rooms, Acknowledgements, automatische Reconnection und Fallbacks bietet – allerdings auf Kosten eines eigenen Protokolls und eines spezifischen Clients.
Räume und Präsenz
Echte Anwendungen senden selten Nachrichten an alle Nutzer. Stattdessen trackst du, welche Verbindung zu welchem Nutzer oder welchem Raum gehört.
// rooms.js
const rooms = new Map();
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
}
function broadcast(room, payload) {
for (const socket of rooms.get(room) ?? []) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
Räume, Präsenzstatus (wer ist online) und Tipp-Indikatoren basieren alle auf diesem Muster, ergänzt um Heartbeats, um Verbindungsabbrüche zu erkennen.
Heartbeats und Reconnection
Eine Verbindung kann abbrechen, ohne dass eine der beiden Seiten es bemerkt – besonders in Mobilfunknetzen oder hinter Proxies. Zwei Mechanismen sorgen dafür, dass die Verbindung stabil bleibt:
- Heartbeats. Sende alle 30 Sekunden einen Ping und beende die Verbindung, falls kein Pong zurückkommt. Viele Proxies schließen zudem inaktive Verbindungen; kontinuierischer Traffic hält diese offen.
- Reconnection. Der Client sollte eine Verbindung mit einem Exponential Backoff wiederherstellen und seine Subscriptions erneut aufbauen. Hier spart eine Library wie Socket.IO erheblichen Aufwand.
// heartbeat.js
const interval = setInterval(() => {
for (const socket of wss.clients) {
if (socket.isAlive === false) return socket.terminate();
socket.isAlive = false;
socket.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
Skalierung
Eine WebSocket-Verbindung besteht zu genau einer Server-Instanz, daher benötigt das Broadcasting über mehrere Instanzen hinweg einen gemeinsamen Kanal.
- Pub/sub: Nachrichten an Redis, NATS oder einen anderen Broker senden und jede Instanz diese an ihre lokalen Clients zustellen lassen.
- Sticky sessions: Falls Ihre Infrastruktur dies erfordert, halten Sie einen Client über die gesamte Lebensdauer der Verbindung auf derselben Instanz.
- State in einem shared store: Speichern Sie Präsenz- und Raummitgliedschaften in Redis, damit jede Instanz darauf antworten kann.
- Backpressure und Limits: Begrenzen Sie die Verbindungen pro Instanz und überwachen Sie den Speicher, da jede Verbindung einen Socket und einen gewissen State belegt.
Sicherheit
- Verwenden Sie in der Produktion immer
wss://, damit der Datenverkehr verschlüsselt ist. - Authentifizieren Sie während des Handshakes und lehnen Sie nicht autorisierte Verbindungen ab.
- Validieren Sie den
Origin-Header, um Cross-Site WebSocket Hijacking zu verhindern. - Autorisieren Sie jede Nachricht; vertrauen Sie niemals blind darauf, dass ein verbundener Client berechtigt ist, eine bestimmte Aktion auszuführen.
- Begrenzen Sie die Nachrichtengröße und die Rate, um Missbrauch zu vermeiden.
- Setzen Sie Timeouts, damit nicht mehr genutzte Verbindungen keine Ressourcen verbrauchen (Memory Leaks).
Best Practices
- Verwenden Sie WebSockets für bidirektionale Updates mit geringer Latenz und SSE für einseitige Push-Benachrichtigungen.
- Implementieren Sie Heartbeats und Reconnect-Logik; gehen Sie niemals davon aus, dass eine Verbindung aktiv ist.
- Authentifizieren Sie während des Handshakes und autorisieren Sie jede einzelne Nachricht.
- Halten Sie den Status pro Verbindung gering und verwalten Sie Rooms in einem gemeinsamen Store.
- Nutzen Sie Pub/Sub, um Nachrichten über mehrere Instanzen hinweg zu broadcasten.
- Legen Sie Limits für die Nachrichtengröße und die Übertragungsrate (Rate Limits) fest.
- Beenden Sie inaktive Verbindungen und überwachen Sie die Anzahl der aktiven Verbindungen.
Häufige Fehler
- Verwendung von WebSockets für einfache Request/Response-Zyklen, bei denen HTTP ausreichend wäre.
- Fehlende Heartbeats, was zu Leaks durch tote Verbindungen führt.
- Broadcasting an alle Clients, obwohl nur ein bestimmter Room die Nachricht erhalten sollte.
- Speicherung des maßgeblichen Zustands (authoritative state) ausschließlich im Arbeitsspeicher, wodurch dieser bei einem Neustart verloren geht.
- Verzicht auf Origin-Checks und die Autorisierung von Nachrichten.
- Die Annahme, dass der Load Balancer die Verbindung ohne entsprechende Konfiguration aufrechterhält.
Wie geht es weiter?
WebSockets bilden die Schicht für persistente Verbindungen auf Basis von TCP. Vertiefen Sie Ihr Wissen mit dem HTTP-Guide sowie den Themen TCP/IP & Sockets, setzen Sie WebSockets auf Node.js um und vergleichen Sie das Push-Modell mit GraphQL subscriptions. Bauen Sie anschließend einen kleinen Chat-Room mit verschiedenen Räumen und Heartbeats, um die Muster in der Praxis zu erleben.