Zum Inhalt springen

WebSocket-Grundlagen für Backend-Entwickler

Wenn HTTP-Polling nicht reicht: ein praktischer Leitfaden zu WebSocket-Verbindungen, Nachrichtenmustern, Reconnection und Skalierung.

3 Min. Lesezeit
Diagramm des WebSocket-Verbindungslebenszyklus mit Handshake, Nachrichtenaustausch und Schließen

HTTP ist Request-Response. Der Client fragt, der Server antwortet. Für Echtzeit-Funktionen — Live-Benachrichtigungen, kollaboratives Editieren, Chat, Datenstreaming — erzwingt dieses Modell umständliche Workarounds wie Long Polling. WebSockets lösen das mit einer persistenten, bidirektionalen Verbindung, bei der jede Seite jederzeit Daten senden kann.

Wann WebSockets sinnvoll sind

Nicht jede Echtzeit-Funktion braucht WebSockets. Nutze sie, wenn der Server Daten an Clients pushen muss, ohne dass diese fragen.

MusterHTTP funktioniertWebSockets besser
Dashboard, das alle 30s aktualisiert✅ Polling reichtÜbertrieben
Live-Chat-NachrichtenPolling verschwendet Bandbreite✅ Sofortige Zustellung
Kollaborative DokumentenbearbeitungNicht praktikabel✅ Erforderlich
Aktienkurs-Ticker✅ SSE ist einfacher✅ Wenn bidirektional nötig
Benachrichtigungsglocke mit Zähler✅ Polling oder SSE✅ Wenn WS bereits genutzt wird

Server-Sent Events (SSE) behandeln das Muster "Server pusht, Client hört zu" einfacher als WebSockets. Greife nur dann zu WebSockets, wenn du bidirektionale Kommunikation brauchst.

Grundlegendes Server-Setup

tstypescript
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "http";
 
const server = createServer();
const wss = new WebSocketServer({ server });
 
wss.on("connection", (ws: WebSocket, req) => {
  const userId = authenticateFromRequest(req);
  if (!userId) {
    ws.close(4001, "Unauthorized");
    return;
  }
 
  console.log(`Client connected: ${userId}`);
 
  ws.on("message", (data) => {
    const message = JSON.parse(data.toString());
    handleMessage(ws, userId, message);
  });
 
  ws.on("close", (code, reason) => {
    console.log(`Client disconnected: ${userId}, code: ${code}`);
    cleanupConnection(userId);
  });
 
  ws.on("error", (error) => {
    console.error(`WebSocket error for ${userId}:`, error.message);
  });
 
  // Send initial state
  ws.send(JSON.stringify({ type: "connected", userId }));
});
 
server.listen(3001);

Authentifiziere immer während des Verbindungs-Handshakes, nicht danach. Eine unauthentifizierte WebSocket-Verbindung ist eine offene Tür.

Design des Nachrichtenprotokolls

Definiere ein typisiertes Nachrichtenprotokoll im Voraus. Ohne das landest du beim Parsen unstrukturierter Blobs und hoffst auf das Beste.

tstypescript
// ❌ Untyped messages — no contract, no validation
ws.send("hello");
ws.send(JSON.stringify({ action: "send", text: "hi" }));
ws.send(JSON.stringify({ type: "msg", body: "hello" }));
 
// ✅ Typed message protocol — both sides know the contract
type ClientMessage =
  | { type: "chat:send"; roomId: string; content: string }
  | { type: "chat:typing"; roomId: string }
  | { type: "presence:update"; status: "online" | "away" };
 
type ServerMessage =
  | { type: "chat:new"; roomId: string; message: ChatMessage }
  | { type: "chat:typing"; roomId: string; userId: string }
  | { type: "presence:changed"; userId: string; status: string }
  | { type: "error"; code: string; message: string };
 
function handleMessage(ws: WebSocket, userId: string, msg: ClientMessage) {
  switch (msg.type) {
    case "chat:send":
      broadcastToRoom(msg.roomId, {
        type: "chat:new",
        roomId: msg.roomId,
        message: { userId, content: msg.content, timestamp: Date.now() },
      });
      break;
    case "chat:typing":
      broadcastToRoom(msg.roomId, {
        type: "chat:typing",
        roomId: msg.roomId,
        userId,
      });
      break;
  }
}

Namespace die Nachrichtentypen mit Doppelpunkten (chat:send, presence:update). Das hält das Protokoll organisiert, während es wächst.

Verbindungsverwaltung

Verfolge verbundene Clients in einer Map für gezieltes Messaging und Aufräumen.

tstypescript
const connections = new Map<string, WebSocket>();
 
function registerConnection(userId: string, ws: WebSocket) {
  // Close existing connection if user reconnects
  const existing = connections.get(userId);
  if (existing?.readyState === WebSocket.OPEN) {
    existing.close(4000, "Replaced by new connection");
  }
 
  connections.set(userId, ws);
}
 
function sendToUser(userId: string, message: ServerMessage) {
  const ws = connections.get(userId);
  if (ws?.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(message));
  }
}
 
function broadcastToRoom(roomId: string, message: ServerMessage) {
  const members = getRoomMembers(roomId);
  const payload = JSON.stringify(message);
 
  for (const userId of members) {
    const ws = connections.get(userId);
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(payload);
    }
  }
}

Reconnection auf Client-Seite

Verbindungen brechen ab. Netzwerke wechseln. Server starten neu. Der Client muss Reconnection sauber handhaben.

tstypescript
// ✅ Reconnecting WebSocket client with exponential backoff
function createReliableSocket(url: string) {
  let ws: WebSocket | null = null;
  let retryCount = 0;
  const maxRetryDelay = 30000;
 
  function connect() {
    ws = new WebSocket(url);
 
    ws.onopen = () => {
      retryCount = 0; // Reset backoff on successful connection
      console.log("WebSocket connected");
    };
 
    ws.onclose = (event) => {
      if (event.code === 4001) return; // Auth failure — don't retry
 
      const delay = Math.min(1000 * 2 ** retryCount, maxRetryDelay);
      retryCount++;
      console.log(`Reconnecting in ${delay}ms...`);
      setTimeout(connect, delay);
    };
 
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      handleServerMessage(message);
    };
  }
 
  connect();
 
  return {
    send: (msg: ClientMessage) => {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(msg));
      }
    },
  };
}

Exponentieller Backoff verhindert Thundering Herds, wenn der Server neu startet und alle Clients gleichzeitig versuchen, sich neu zu verbinden.

Heartbeats und tote Verbindungen

TCP-Verbindungen können still sterben (Netzwerkwechsel, Laptop-Standby). Heartbeats erkennen tote Verbindungen, damit der Server Ressourcen aufräumen kann.

tstypescript
const HEARTBEAT_INTERVAL = 30000;
const HEARTBEAT_TIMEOUT = 10000;
 
wss.on("connection", (ws) => {
  let isAlive = true;
 
  ws.on("pong", () => {
    isAlive = true;
  });
 
  const heartbeat = setInterval(() => {
    if (!isAlive) {
      ws.terminate();
      clearInterval(heartbeat);
      return;
    }
 
    isAlive = false;
    ws.ping();
  }, HEARTBEAT_INTERVAL);
 
  ws.on("close", () => clearInterval(heartbeat));
});

Ohne Heartbeats sammeln sich tote Verbindungen an und verschwenden Server-Speicher. Eine Verbindung, die still stirbt, löst nie das close-Event aus.

Die wichtigsten Punkte

  1. Nutze WebSockets für bidirektionale Echtzeitkommunikation — SSE ist einfacher für reine Server-Push-Muster
  2. Authentifiziere während des Handshakes — lehne unauthentifizierte Verbindungen sofort ab
  3. Definiere ein typisiertes Nachrichtenprotokoll — namespace die Typen mit Doppelpunkten zur Organisation
  4. Implementiere Reconnection mit exponentiellem Backoff auf dem Client, um Thundering Herds zu vermeiden
  5. Heartbeat-Pings erkennen tote Verbindungen — ohne sie verlieren veraltete Verbindungen Speicher
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX