Wie WebSockets funktionieren: Ein vollständiger Leitfaden für Echtzeitverbindungen

In den Anfangstagen des Webs war der Browser ein einfacher Dokumentenbetrachter. Sie forderten eine Seite an, der Server stellte sie dar und die Verbindung wurde geschlossen. Dieser Anfrage-Antwort-Zyklus ist der Kern von HTTP (Hypertext Transfer Protocol).

Als sich Webanwendungen jedoch zu interaktiven Anwendungen entwickelten – wie Echtzeit-Chats, Live-Finanz-Tickern, kollaborativer Bearbeitung und Multiplayer-Spielen –, stieß das traditionelle HTTP-Modell an seine Grenzen.

Um Echtzeit-Updates zu erhalten, verließen sich Entwickler zunächst auf Behelfslösungen:

  • Short Polling: Der Browser sendet alle paar Sekunden HTTP-Anfragen an den Server, um nach neuen Daten zu fragen. Dies erzeugt einen massiven Header-Overhead und verschwendet Serverressourcen.
  • Long Polling (Comet): Der Browser sendet eine Anfrage, und der Server hält sie offen, bis neue Daten verfügbar sind. Sobald Daten gesendet wurden, wird die Verbindung geschlossen, und der Browser öffnet sofort eine neue Anfrage. Dies ist komplex zu verwalten und verursacht dennoch erheblichen Overhead beim Verbindungsaufbau.

WebSockets lösten diese Einschränkungen durch die Einführung eines standardisierten Protokolls für die dauerhafte, bidirektionale Vollduplex-Kommunikation über eine einzige TCP-Verbindung.


Was ist ein WebSocket?

WebSockets (definiert in RFC 6455) arbeiten parallel zu HTTP. Während HTTP ein zustandsloses Protokoll ist, bei dem nur der Client Anfragen initiieren kann, bleibt eine WebSocket-Verbindung unbegrenzt offen. Dies ermöglicht es sowohl dem Client als auch dem Server, jederzeit und mit minimaler Latenz Daten aneinander zu senden.

Hier ist die Grundregel von WebSockets:

Sobald die Verbindung hergestellt ist, kann jede Seite jederzeit Nachrichten senden, ohne eine neue Verbindungsanfrage zu initiieren.


Schritt-für-Schritt-Walkthrough: Der Verbindungslebenszyklus

Eine WebSocket-Verbindung durchläuft drei verschiedene Phasen: den Handshake, den Datentransfer und das Schließen der Verbindung.

WebSocket Connection Lifecycle Diagram

1. Der HTTP-Handshake (Protokoll-Upgrade)

Da Firewalls und Router so konfiguriert sind, dass sie standardmäßigen Webverkehr auf den Ports 80 (HTTP) und 443 (HTTPS) zulassen, beginnen WebSockets als Standard-HTTP/1.1-Anfrage. Dies wird als Upgrade-Handshake bezeichnet.

Die Client-Anfrage

Der Client sendet eine HTTP-GET-Anfrage mit spezifischen Headern, die einen Protokollwechsel anfordern:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
  • Upgrade: websocket und Connection: Upgrade: Teilen dem Server mit, dass der Client die Protokolle wechseln möchte.
  • Sec-WebSocket-Key: Ein zufälliger, in Base64 codierter 16-Byte-Wert. Er dient dem Nachweis, dass der Server den Handshake empfangen hat und das WebSocket-Protokoll versteht.
  • Sec-WebSocket-Version: Gibt die Version des WebSocket-Protokolls an (normalerweise 13).
  • Origin: Wird vom Server verwendet, um zu entscheiden, ob die Verbindung zugelassen werden soll (Sicherheitsprüfung gegen unbefugte Websites).

Die Server-Antwort

Wenn der Server WebSockets unterstützt, validiert er die Anfrage und antwortet mit dem HTTP-Statuscode 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • Wie der Server Sec-WebSocket-Accept berechnet:
    1. Der Server nimmt den Sec-WebSocket-Key des Clients (dGhlIHNhbXBsZSBub25jZQ==).
    2. Er verknüpft ihn mit einer standardmäßigen magischen GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
    3. Er berechnet den SHA-1-Hash der kombinierten Zeichenfolge.
    4. Er codiert den resultierenden Hash in Base64.
    5. Wenn der Client überprüft, dass dieser Wert seinen Erwartungen entspricht, ist der Handshake erfolgreich. Die HTTP-Verbindung wechselt zu einem rohen TCP-Socket, und beide Seiten nutzen fortan das WebSocket-Protokoll.

2. Daten-Framing und Transfer

Im Gegensatz zu HTTP, das Klartext-Header gefolgt von einem Body sendet, übertragen WebSockets Daten in strukturierten Binärpaketen, den sogenannten Frames.

Ein WebSocket-Frame hat einen sehr leichten Header (zwischen 2 und 14 Bytes), gefolgt von den Nutzdaten (Payload). Dieser Header enthält:

  • FIN-Bit (1 Bit): Zeigt an, ob dies das letzte Frame einer Nachricht ist.
  • Opcode (4 Bit): Definiert den Typ des Frames:
    • 0x1: Text-Frame (UTF-8 codiert)
    • 0x2: Binär-Frame
    • 0x8: Anfrage zum Schließen der Verbindung
    • 0x9: Ping
    • 0xA: Pong
  • Mask-Bit (1 Bit): Gibt an, ob die Nutzdaten maskiert sind.
  • Payload-Länge: Die Größe der Daten.
  • Masking Key (4 Bytes): Kritische Sicherheitsanforderung: Alle vom Client an den Server gesendeten Frames müssen mit einem zufälligen 4-Byte-Schlüssel maskiert (XOR-obfuscated) werden. Dies verhindert, dass Proxy-Caches den Datenverkehr lesen oder Cache-Poisoning-Angriffe ausführen. Server-zu-Client-Frames dürfen nicht maskiert werden.

Heartbeats (Ping/Pong)

Um zu verhindern, dass Router und Load Balancer inaktive Verbindungen schließen, kann jede Seite ein Ping-Frame senden. Die empfangende Seite muss sofort mit einem Pong-Frame antworten, das dieselben Nutzdaten enthält.

3. Schließen der Verbindung

So wird eine Verbindung sauber geschlossen:

  1. Ein Peer sendet ein Close-Frame mit einem Statuscode (z. B. 1000 für normales Schließen, 1006 für abnormales Schließen) und optional einem Textgrund.
  2. Der andere Peer antwortet mit einem eigenen Close-Frame.
  3. Der zugrunde liegende TCP-Socket wird geschlossen.

Code-Beispiel: Node.js WebSocket-Implementierung

Um WebSockets in Aktion zu sehen, schreiben wir eine einfache Node.js-Anwendung. Wir erstellen einen lokalen WebSocket-Server, der jede empfangene Nachricht zurücksendet, sowie ein Client-Skript, um sich damit zu verbinden.

Der WebSocket-Server (server.js)

const { WebSocketServer } = require('ws');
const http = require('http');

// 1. Standard-HTTP-Server erstellen
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('HTTP Server running. Use WebSocket to connect.\n');
});

// 2. WebSocket-Server an den HTTP-Server anbinden
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`[Server] Neuer Client verbunden von ${clientIp}`);

    // Willkommensnachricht an den Client senden
    ws.send(JSON.stringify({ type: 'welcome', message: 'Verbunden mit dem Ghaznix WebSocket-Server!' }));

    // Auf eingehende Nachrichten von diesem Client hören
    ws.on('message', (message) => {
        console.log(`[Server] Empfangen: ${message}`);
        
        try {
            const data = JSON.parse(message);
            ws.send(JSON.stringify({
                type: 'echo',
                message: `Server-Echo: ${data.text.toUpperCase()}`,
                timestamp: new Date().toISOString()
            }));
        } catch (e) {
            ws.send(JSON.stringify({ type: 'error', message: 'Ungültiges JSON-Format' }));
        }
    });

    // Client-Verbindungsabbruch behandeln
    ws.on('close', (code, reason) => {
        console.log(`[Server] Client getrennt (Code: ${code}, Grund: ${reason.toString() || 'Keiner'})`);
    });

    ws.on('error', (error) => {
        console.error(`[Server] Socket-Fehler: ${error.message}`);
    });
});

server.listen(8080, () => {
    console.log('WebSocket-Server lauscht auf ws://localhost:8080');
});

Der Browser-Client (Clientseitiges JavaScript)

Sie können diesen Client direkt in der Konsole Ihres Browsers ausführen:

// 1. Verbindung zum Server herstellen
const socket = new WebSocket('ws://localhost:8080');

// 2. Handler für geöffnete Verbindung
socket.addEventListener('open', (event) => {
    console.log('[Client] Mit Server verbunden.');
    
    const payload = JSON.stringify({ text: 'hallo, server!' });
    socket.send(payload);
    console.log(`[Client] Gesendet: ${payload}`);
});

// 3. Auf Nachrichten vom Server hören
socket.addEventListener('message', (event) => {
    const response = JSON.parse(event.data);
    console.log('[Client] Nachricht vom Server empfangen:', response);
});

// 4. Auf das Schließen der Verbindung hören
socket.addEventListener('close', (event) => {
    console.log(`[Client] Verbindung geschlossen (Code: ${event.code})`);
});

// 5. Auf Fehler hören
socket.addEventListener('error', (error) => {
    console.error('[Client] WebSocket-Fehler:', error);
});

HTTP vs. WebSockets: Ein detaillierter Vergleich

Feature HTTP/1.1 WebSockets
Kommunikation Unidirektional (vom Client initiiert) Bidirektional (Client oder Server)
Verbindungsmodell Anfrage-Antwort (kurzlebig) Persistent (langlebig)
Overhead Hoch (Header bei jeder Anfrage gesendet) Sehr niedrig (minimaler Framing-Overhead)
Zustand Zustandslos Zustandsbehaftet (Verbindungskontext bleibt erhalten)
Protokoll http:// oder https:// ws:// or wss://
Bestens geeignet für Abrufen von Dokumenten, REST-APIs Echtzeit-Chats, Dashboards, Live-Feeds

Sicherheitsaspekte für WebSockets

Da WebSockets nach dem Handshake das standardmäßige HTTP-Routing umgehen, führen sie einzigartige Sicherheitsvektoren ein:

  1. Verwenden Sie WebSocket Secure (wss://): Betreiben Sie WebSockets immer über TLS/SSL (Port 443). WSS verschlüsselt die Nutzdaten und verhindert Lauschangriffe sowie Manipulationen durch Dritte.
  2. Origin-Validierung: WebSockets unterliegen nicht der Same-Origin-Policy (SOP). Überprüfen Sie während des Handshakes auf dem Server immer den Origin-Header, um unbefugten Zugriff zu verhindern.
  3. Authentifizierung beim Handshake: Authentifizieren Sie Benutzer, bevor die Verbindung hergestellt wird, z. B. durch Übergabe eines Tokens (wie JWT) in Abfrageparametern oder durch Überprüfung von Sitzungscookies.
  4. Eingabebereinigung: Behandeln Sie jede über WebSockets empfangene Nachricht als nicht vertrauenswürdig. Validieren und bereinigen Sie die Payloads, um Cross-Site-Scripting (XSS) zu verhindern.

Zusammenfassung

WebSockets haben Echtzeit-Webanwendungen revolutioniert, indem sie den Overhead des traditionellen HTTP-Pollings eliminiert haben. Durch die Aufrechterhaltung einer einzigen dauerhaften TCP-Verbindung ermöglichen sie einen sofortigen bidirektionalen Nachrichtenaustausch, der moderne Live-Dashboards, Multiplayer-Spiele und Chat-Apps antreibt. Das Verständnis des HTTP-Upgrades, der Framing-Architektur und der wichtigsten Sicherheitspraktiken stellt sicher, dass Sie schnelle und sichere Echtzeitdienste entwickeln.


Entdecken Sie weitere Entwickler-Tutorials und Anleitungen im Ghaznix-Blog →