Zurück zum Blog
Mai 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Eine sanfte Einführung in WebSockets

#websockets#networking#javascript

HTTP funktioniert nach dem Prinzip Anfrage-Antwort: Der Client fragt, der Server antwortet, die Aufgabe der Verbindung ist erledigt. Dieses Modell bricht in dem Moment zusammen, in dem der Server dem Client etwas mitteilen muss, ohne vorher gefragt worden zu sein – eine Chatnachricht trifft ein, ein Aktienkurs ändert sich, ein Build wird fertig. Genau dafür gibt es WebSockets: eine einzige, langlebige Verbindung, über die jede Seite jederzeit eine Nachricht an die andere schicken kann, ohne den Overhead pro HTTP-Anfrage.

Warum nicht einfach pollen?

Vor WebSockets war Polling der übliche Workaround: Der Client fragt alle paar Sekunden „gibt es etwas Neues?".

setInterval(async () => {
  const response = await fetch('/api/messages/latest');
  const messages = await response.json();
  renderNewMessages(messages);
}, 3000);

Das funktioniert, ist aber in beide Richtungen ein schlechter Tausch. Pollt man zu langsam, fühlen sich Updates träge an; pollt man zu schnell, schickt man größtenteils unnötige HTTP-Anfragen – jede mit eigenem TCP-Handshake-Overhead (oder der Komplexität der Wiederverwendung von Verbindungen), Headern und serverseitiger Request-Verarbeitung –, nur um meistens zu hören: „Nichts hat sich geändert." Eine WebSocket-Verbindung wird einmal geöffnet und überträgt dann nur noch die eigentlichen Daten, in beide Richtungen, sobald sie existieren.

Der Handshake: noch HTTP, aber nur kurz

Eine WebSocket-Verbindung beginnt als normale HTTP-Anfrage mit einem Upgrade-Header. Der Server stimmt dem Protokollwechsel zu, und ab diesem Moment transportiert dieselbe TCP-Verbindung das WebSocket-Framing statt HTTP.

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

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

Diesen Handshake baut man nie von Hand – die WebSocket-API des Browsers und serverseitige Bibliotheken übernehmen das. Es ist aber gut zu wissen, dass es ihn gibt: Deshalb verwendet eine WebSocket-URL ws:// oder wss:// (die sichere Variante über TLS) statt http://, und deshalb kann eine WebSocket-Verbindung durch dieselben Dinge blockiert werden wie eine HTTP-Anfrage – CORS-ähnliche Origin-Prüfungen, Proxys, die Upgrade nicht unterstützen, Firmen-Firewalls.

Die Client-Seite

const socket = new WebSocket('wss://example.com/chat');

socket.addEventListener('open', () => {
  console.log('connected');
  socket.send(JSON.stringify({ type: 'join', room: 'general' }));
});

socket.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  console.log('received:', data);
});

socket.addEventListener('close', (event) => {
  console.log(`connection closed: ${event.code} ${event.reason}`);
});

socket.addEventListener('error', (event) => {
  console.error('socket error', event);
});

Vier Events decken den gesamten Lebenszyklus ab: open, sobald der Handshake abgeschlossen ist, message für jeden eingehenden Frame, close, wenn die Verbindung endet (durch eine der beiden Seiten), und error für Fehler auf Transportebene. Alles, was man sendet und empfängt, ist entweder Text oder binär – es gibt keine eingebaute Zuordnung von Anfrage und Antwort. Wenn man also eine Antwort einer bestimmten gesendeten Nachricht zuordnen muss, liegt das beim eigenen Anwendungsprotokoll (üblicherweise: eine ID in die Nutzlast packen).

Ein minimaler Server mit Node's ws-Bibliothek

import { WebSocketServer } from 'ws';

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

wss.on('connection', (socket) => {
  clients.add(socket);
  console.log(`client connected, ${clients.size} total`);

  socket.on('message', (raw) => {
    const message = JSON.parse(raw.toString());
    // broadcast to everyone except the sender
    for (const client of clients) {
      if (client !== socket && client.readyState === client.OPEN) {
        client.send(JSON.stringify(message));
      }
    }
  });

  socket.on('close', () => {
    clients.delete(socket);
    console.log(`client disconnected, ${clients.size} remaining`);
  });
});

Das ist ein vollständiger Broadcast-Chatserver: Jeder verbundene Client bekommt jede Nachricht weitergeleitet, die jemand anderes schickt. Das Set der offenen Sockets ist die gesamte Vorstellung des Servers davon, „wer zuhört" – kein Polling, kein Vergleichen von Zeitstempeln, einfach Push.

Wiederverbinden ist deine Aufgabe, nicht die des Browsers

Reißt die Verbindung ab – Server-Neustart, Netzwerkaussetzer, Laptop im Ruhezustand –, verbindet sich das WebSocket-Objekt nicht von selbst neu. Das muss man explizit behandeln, in der Regel mit exponentiellem Backoff, damit ein Serverausfall nicht von allen Clients, die im Gleichschritt in dem Moment wieder verbinden, in dem er zurückkommt, überrannt wird.

function connectWithRetry(url, { onMessage }) {
  let attempt = 0;

  function connect() {
    const socket = new WebSocket(url);

    socket.addEventListener('open', () => {
      attempt = 0; // reset backoff once we're actually connected
    });

    socket.addEventListener('message', onMessage);

    socket.addEventListener('close', () => {
      const delay = Math.min(1000 * 2 ** attempt, 30000);
      attempt++;
      setTimeout(connect, delay);
    });
  }

  connect();
}

Die Wartezeit verdoppelt sich mit jedem Versuch (1 s, 2 s, 4 s, 8 s ...) bis zu einer Obergrenze von 30 Sekunden und wird auf null zurückgesetzt, sobald eine Verbindung tatsächlich zustande kommt. Produktivsysteme fügen darüber meist zufälligen Jitter hinzu, damit viele Clients, die sich nach demselben Ausfall neu verbinden, nicht alle exakt zur selben Zeit erneut versuchen.

Wann WebSockets das falsche Werkzeug sind

Sie sind kein universelles Upgrade gegenüber HTTP. Braucht der Client nur gelegentlich vom Server ausgelöste Updates und kann ein paar Sekunden Latenz verkraften, sind Server-Sent Events (SSE) einfacher – nur in eine Richtung (Server zu Client), mit eingebautem automatischem Wiederverbinden, und im Grunde eine normale HTTP-Antwort, die der Browser offen zu halten weiß. Besteht die Kommunikation tatsächlich nur aus Anfrage und Antwort, ist einfaches HTTP mit fetch noch simpler und liefert Caching, Retries und die Standard-HTTP-Semantik gratis dazu. Greife gezielt zu einem WebSocket, wenn du latenzarme, bidirektionale Kommunikation brauchst – Chat, Multiplayer-Zustand, kollaboratives Bearbeiten, Live-Dashboards, bei denen der Server so oft pusht, wie der Client fragen würde.

NeuerRetry-Strategien: Exponentielles Backoff und JitterÄlterAsync-Iteratoren und -Generatoren in JavaScript