WebSockets verstehen: Echtzeit-Kommunikation im Web

WebSockets ermöglichen bidirektionale Echtzeit-Kommunikation zwischen Browser und Server mit praktischen Implementierungsbeispielen.

WebSockets verstehen: Echtzeit-Kommunikation im Web

WebSockets sind eines der Tools, die ich überraschend selten einsetze. Wenn jemand "Echtzeit-Web-Funktion" hört, ist WebSocket oft die erste Assoziation — aber in den meisten Fällen reichen Server-Sent Events, Polling mit modernen Techniken, oder gar kein "Echtzeit" sondern nur ein schnelleres Refresh. WebSockets lohnen sich, wenn du wirklich bidirektionalen Traffic mit vielen kleinen Messages hast.

Chat, kollaborative Editoren (Google Docs, Figma), Live-Spiele, Trading-Dashboards — das sind die klassischen Use-Cases. Ein Notification-System, das einmal pro Minute was pusht, braucht keine WebSockets.

Das Protokoll

WebSockets starten als HTTP/1.1-Verbindung und upgraden dann auf ein eigenes Protokoll. Der Handshake sieht so aus:

Client:

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Server:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Das Sec-WebSocket-Accept ist eine Art Challenge-Response-Beweis, dass der Server das Protokoll versteht. Danach wechselt die TCP-Verbindung ins WebSocket-Framing.

In der Praxis schreibt man das nie selbst — jede Server-Library und jeder Browser bietet eine API-Ebene darüber.

Minimal-Beispiel mit Node

Server:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    console.log('Empfangen:', data.toString());
    ws.send(`Echo: ${data}`);
  });
  ws.send('Willkommen');
});

Client (im Browser):

const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => ws.send('Hallo');
ws.onmessage = (e) => console.log('Empfangen:', e.data);

Das ist die komplette Grundfunktionalität. Was in Produktion fehlt: Authentifizierung, Reconnect-Logik, Rate Limiting.

Authentifizierung

WebSockets können keine Custom-Headers im Handshake setzen (außer Cookies). Das macht Authentifizierung fummelig.

Drei gängige Muster:

  1. Cookie-basiert — der User ist vorher per HTTP eingeloggt, der Session-Cookie wird mit dem Upgrade geschickt. Funktioniert auf derselben Origin.
  2. Token in der URLwss://example.com/ws?token=.... Funktioniert überall, aber Tokens erscheinen in Server-Logs.
  3. Erste Message ist Auth — nach dem Connect schickt der Client ein {"type":"auth","token":"..."}, der Server schließt die Verbindung bei ungültigem Token. Mehr Code, aber saubere Separation.

Ich nutze meist Pattern 3, kombiniert mit kurzlebigen Tokens vom HTTP-Endpoint.

Reconnect-Logik

WebSockets brechen irgendwann ab — Netzwerk-Hiccups, Server-Restart, Mobilwechsel. Jeder produktive Client braucht Reconnect mit Backoff:

class ReconnectingWS {
  constructor(url) {
    this.url = url;
    this.retries = 0;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => { this.retries = 0; };
    this.ws.onclose = () => {
      const delay = Math.min(1000 * 2 ** this.retries, 30000);
      this.retries++;
      setTimeout(() => this.connect(), delay);
    };
  }
}

Exponential Backoff, gekappt bei 30 Sekunden. Zusätzlich idealerweise Jitter (kleiner Zufallsanteil), damit bei einem Server-Restart nicht alle Clients gleichzeitig reconnecten.

nginx als Proxy

WebSockets durch nginx proxien braucht explizites Upgrade-Handling:

location /ws {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}

Das proxy_read_timeout ist wichtig. Der Default von 60 Sekunden schließt die Verbindung nach einer Minute Inaktivität — was für lange WebSocket-Sessions nicht passt.

Wann nicht WebSockets

Server-Sent Events (SSE) sind einfacher und oft besser, wenn der Server pusht und der Client nur empfängt. Ein reiner Notifications-Stream, Live-Ticker, Event-Feeds — SSE reicht, und es nutzt HTTP/2 nativ.

Polling mit modernen Mitteln (lange Polls, HTTP/2 Streams) ist weniger verpönt, als es früher war. Für Use-Cases mit weniger als einem Update pro Sekunde und nicht-kritischer Latenz ist Polling einfacher zu betreiben.

Scale-Out-Gotchas

Mehrere WebSocket-Server hinter einem Load-Balancer = Probleme. Wenn Client A auf Server 1 lauscht und Client B auf Server 2 sendet, müssen die Server untereinander reden. Die Standard-Lösung: Redis Pub/Sub als Message-Bus, jeder Server subscribed auf die Topics, die seine verbundenen Clients interessieren.

Das macht aus einer einfachen WebSocket-App schnell eine verteilte Anwendung. Für viele Hobby-Projekte reicht ein einzelner Server — und das ist okay, solange man weiß, dass man damit einen Skalierungs-Horizont gesetzt hat.