Was ist TCP/IP?
TCP/IP ist die Protokollfamilie, die das Internet antreibt. IP ist für die Adressierung und das Routing von Paketen zwischen Hosts verantwortlich; TCP setzt darauf auf und verwandelt diese unzuverlässigen Pakete in einen zuverlässigen, geordneten Byte-Stream. Zusammen bilden sie die Transportschicht unter HTTP, Datenbanken, E-Mails, WebSockets und fast allem anderen, was Ihr Server erledigt.
Man schreibt selten direkt TCP-Code, aber das Verständnis davon erklärt vieles: warum Verbindungen kostspielig sind, warum Keep-Alive wichtig ist, warum ein Datenbank-Pool ein Größenlimit hat und warum einige Datenströme stattdessen UDP verwenden.
Das Schichtenmodell
Netzwerke werden üblicherweise in Schichten beschrieben, wobei jede Schicht eine spezifische Aufgabe übernimmt:
| Schicht | Aufgabe | Beispiele |
|---|---|---|
| Anwendung (Application) | Bedeutung/Semantik | HTTP, WebSocket, DNS |
| Transport | Zuverlässigkeit und Ports | TCP, UDP |
| Internet | Adressierung und Routing | IP, ICMP |
| Sicherung (Link) | Lokale Zustellung | Ethernet, Wi-Fi |
Jede Schicht nutzt die darunterliegende Schicht. HTTP weiß nicht, ob die Daten über Ethernet oder Wi-Fi übertragen werden; es verlässt sich auf TCP für die Zustellung der Bytes, welches sich wiederum auf IP für den Transport der Pakete verlässt. Diese Trennung ist der Grund, warum das Internet so viele verschiedene Arten von Traffic bewältigen kann.
IP-Adressen und Ports
Eine IP-Adresse identifiziert einen Host, und ein Port identifiziert einen Dienst auf diesem Host. Zusammen bilden sie eine Socket-Adresse, wie zum Beispiel 203.0.113.10:443.
- IPv4-Adressen sind 32-Bit (
203.0.113.10); IPv6-Adressen sind 128-Bit und werden in Hexadezimal geschrieben. - Ports unter 1024 sind für bekannte Dienste reserviert.
- Clients erhalten für die Dauer einer Verbindung einen sogenannten ephemeral port (temporärer Port).
- Eine Verbindung wird eindeutig durch das vollständige Tupel identifiziert: Quelladresse, Quellport, Zieladresse und Zielport.
Dieses Tupel ist der Grund, warum ein Server gleichzeitig tausende Verbindungen auf Port 443 akzeptieren kann: Der Quellport jedes Clients ist unterschiedlich.
Der Three-Way-Handshake
Bevor Daten fließen können, baut TCP die Verbindung in drei Schritten auf:
- Der Client sendet ein SYN mit seiner initialen Sequenznummer.
- Der Server antwortet mit SYN-ACK, bestätigt den Empfang des Clients und sendet seine eigene Sequenznummer.
- Der Client sendet ACK, und die Verbindung ist hergestellt.
Dies kostet einen Roundtrip, bevor das erste Byte an Anwendungsdaten übertragen werden kann, was eine spürbare Latenz verursacht. TLS fügt einen weiteren Roundtrip (oder zwei) hinzu, und DNS kann davor noch einen weiteren hinzufügen. Connection Reuse existiert gezielt dazu, diese Kosten nicht wiederholt tragen zu müssen.
Zuverlässigkeit und Flusssteuerung
TCP gewährleistet Zuverlässigkeit durch verschiedene Mechanismen:
- Sequenznummern ordnen die Bytes, sodass der Empfänger sie korrekt wieder zusammensetzen kann.
- Bestätigungen (Acknowledgements) bestätigen den Empfang; nicht bestätigte Daten werden erneut übertragen.
- Prüfsummen (Checksums) erkennen Datenkorruption.
- Flusssteuerung (Flow control) nutzt ein Empfangsfenster, damit ein schneller Sender einen langsamen Empfänger nicht überlastet.
- Überlastungssteuerung (Congestion control) passt die Übertragungsrate an das Netzwerk an, um einen Netzwerkkollaps zu vermeiden.
Das Ergebnis ist ein Byte-Stream, der vollständig und in der richtigen Reihenfolge ankommt – auf Kosten einer gewissen Latenz und eines gewissen Overheads.
UDP: die schnelle Alternative
UDP ist verbindungslos. Es sendet Datagramme ohne Handshake, ohne Reihenfolge und ohne erneute Übertragung. Das klingt zunächst schlechter, ist aber für bestimmte Workloads vorteilhafter:
- Latenz ist wichtiger als Perfektion: Live-Video, Voice-Chat und Games bevorzugen einen verlorenen Frame gegenüber einem verzögerten.
- Die Anwendung übernimmt die Zuverlässigkeit: QUIC, die Basis für HTTP/3, implementiert eine eigene Zuverlässigkeitsschicht auf Basis von UDP.
- Kleine, zustandslose Abfragen: DNS nutzt traditionell UDP, da eine einzelne Anfrage und Antwort in ein einziges Datagramm passen.
Wählen Sie TCP, wenn Korrektheit und Reihenfolge entscheidend sind, und UDP, wenn Sie Geschwindigkeit benötigen und Datenverluste tolerieren können.
Sockets in Node.js
Node stellt TCP über node:net und UDP über node:dgram bereit. Ein TCP-Server gibt Ihnen pro Verbindung einen Socket.
// echo.js
import { createServer } from "node:net";
const server = createServer((socket) => {
socket.setKeepAlive(true, 10_000);
socket.setTimeout(30_000);
socket.on("timeout", () => socket.destroy());
socket.on("data", (data) => socket.write(data));
socket.on("error", (err) => console.error(err));
});
server.listen(9000);
Sockets sind Streams: Sie lesen data-Events und schreiben mit socket.write(). Da TCP ein Byte-Stream ohne Nachrichtengrenzen ist, müssen Anwendungsprotokolle ihr eigenes Framing definieren – deshalb hat HTTP Header mit Content-Lengths und WebSockets nutzen Frames.
Verbindungslebenszyklus und Tuning
- Keep-alive verwendet Verbindungen wieder, wodurch der Handshake für jede einzelne Anfrage entfällt.
- Pools begrenzen die Anzahl der gleichzeitigen Verbindungen zu einer Datenbank oder einem Dienst.
- Timeouts schließen inaktive oder hängende Verbindungen, damit Ressourcen wieder freigegeben werden.
- TIME_WAIT ist ein normaler Zustand nach dem Schließen, aber zu viele kurzlebige Verbindungen können die verfügbaren Ports erschöpfen.
- Backpressure:
socket.write()gibtfalsezurück, wenn der Buffer voll ist; in diesem Fall sollte aufdraingewartet werden.
Die meisten Optimierungen in Produktionsumgebungen betreffen die Wiederverwendung von Verbindungen und die Begrenzung der Nebenläufigkeit, nicht die Änderung von Kernel-Parametern.
Best Practices
- Verbindungen mittels keep-alive oder einem Pool wiederverwenden.
- Timeouts und keep-alive bei langlebigen Verbindungen konfigurieren.
- Die Concurrency begrenzen, um zu verhindern, dass Sockets oder File-Deskriptoren erschöpft werden.
- Fehler und Backpressure an jedem Socket behandeln.
- TCP verwenden, sofern es keinen spezifischen Grund für UDP gibt.
- Bedenken, dass TCP ein Byte-Stream ist, und ein eigenes Framing definieren.
- TIME_WAIT und die Anzahl der Verbindungen überwachen, um Anzeichen für Connection Churn zu erkennen.
Häufige Fehler
- Für jede Anfrage eine neue Verbindung öffnen.
- Die Annahme, dass TCP Nachrichtengrenzen beibehält.
socket.write()Backpressure zu ignorieren und Daten unbegrenzt zu puffern.- Langfristige Verbindungen ohne Keep-Alive oder Timeouts zu lassen.
- Pool-Größen festzulegen, die weit über der Kapazität der Datenbank liegen.
- Auf UDP zurückzugreifen, wenn es tatsächlich auf Zuverlässigkeit ankommt.
Wie geht es weiter?
TCP/IP ist die Schicht, auf der alles andere aufbaut. Vertiefe dein Wissen mit dem HTTP-Guide, verstehe die Netzwerk-Grundlagen in Wie das Internet funktioniert und wechsle zu persistenten Verbindungen mit WebSockets. Überprüfe anschließend mit ss oder netstat echte Verbindungen auf deinem Rechner, während dein Server Traffic verarbeitet.