Netzwerkprogrammierung
Kurz gesagt: Netzwerkprogrammierung heißt, dass zwei Programme über das Netzwerk miteinander sprechen: Der Server wartet auf einer IP-Adresse und einem Port, der Client verbindet sich, und Sockets senden die Daten über TCP oder UDP.
Problem
Die Kasse und das Lager
Die Kasse im Shop muss das Lager fragen, wie viel Kaffee noch da ist, und das Lagerprogramm läuft auf einem anderen Computer. Eine Datei hilft nicht: Die beiden Programme sehen die Festplatten des anderen nicht. Eins muss fragen und das andere antworten, sofort, über das Netzwerk.
So funktioniert fast alles um dich herum: Der Browser fragt den Server nach einer Seite, die Banking-App fragt die Bank nach deinem Kontostand, ein Spiel schickt deine Züge an die anderen Spieler.
Die Idee
Adresse, Port und zwei Arten zu senden
Damit Daten ein Programm auf einem anderen Computer erreichen, brauchst du zwei Dinge:
- Eine IP-Adresse: die Nummer des Computers im Netzwerk, zum Beispiel
192.168.1.20oder142.250.185.78. Dein eigener Computer ist immer127.0.0.1, mit dem Namenlocalhost. - Einen Port: eine Zahl von 0 bis 65 535 für das Programm auf diesem Computer. Die Adresse ist das Gebäude, der Port ist die Wohnung. HTTPS nutzt 443, und wir nutzen 5961 und höher.
Das Programm, das darauf wartet, dass man es erreicht, ist der Server. Das Programm, das ihn erreicht, ist der Client. Wie die Daten reisen, entscheidet das Protokoll:
| TCP | UDP | |
|---|---|---|
| Verbindung | erst verbinden sie sich, dann sprechen sie | keine Verbindung, jedes Paket ist ein eigener Brief |
| Zustellung | alles kommt an, in derselben Reihenfolge, oder du erfährst, dass es nicht geklappt hat | ein Paket kann verloren gehen oder in anderer Reihenfolge ankommen |
| Geschwindigkeit | langsamer | schneller |
| Wofür | Web, E-Mail, Zahlungen, Dateien | Sprache und Video live, Spiele, Sensoren |
node:net.Genauer, für Neugierige
- Das Netzwerk wird in Schichten beschrieben. Das OSI-Modell hat sieben: Bitübertragung (Bits über das Kabel oder die Luft), Sicherung (die Verbindung zum nächsten Gerät), Vermittlung (IP-Adressen und Routen), Transport (TCP, UDP), Sitzung, Darstellung und Anwendung (HTTP, E-Mail). In der Praxis nutzt das Internet das einfachere TCP/IP-Modell mit vier Schichten. Dein Programm arbeitet auf der obersten: Um alles darunter kümmern sich Node und das Betriebssystem.
- IP liefert einzelne Pakete an eine Adresse, verspricht aber nicht, dass sie ankommen. TCP baut auf IP auf und nummeriert die Pakete, damit es sie ordnen und die verlorenen noch einmal anfordern kann.
- Protokolle, denen du begegnen wirst: HTTP und HTTPS (das Web; HTTPS ist HTTP, verschlüsselt mit TLS, dessen älterer Name SSL ist), WebSocket (eine Verbindung in beide Richtungen für Chat und Benachrichtigungen im Browser), FTP (Dateien), SSH (sicherer Zugriff auf einen entfernten Computer).
Sieh es dir an
Der Server
Der Lagerserver bekommt den Namen eines Produkts und antwortet mit seinem Preis:
// await readLine() wartet auf die nächste Zeile vom Socket (undefined, wenn die Verbindung geschlossen ist).function lineReader(socket) { const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator](); return async () => (await lines.next()).value;}const PRICES = new Map([["Kaffee", 7.50], ["Tee", 3.20]]);
async function serve(client) { const readLine = lineReader(client); const product = await readLine(); console.log(`Server erhielt: ${product}`); const price = PRICES.get(product); client.end(price === undefined ? "unbekanntes Produkt\n" : `${product}: ${price} €\n`);}- Es gibt keinen Thread und kein
accept(). Node ruftservefür jeden Client auf, der sich verbindet, und gibt ihm einen Socket für die Verbindung (client). - Der Socket ist ein Stream: Die Bytes kommen in Stücken, so wie sie eintreffen.
lineReader(oben) sammelt sie mitnode:readlinezu Zeilen, undawait readLine()wartet auf die nächste Zeile. client.end(text)sendet den letzten Text und schließt die Verbindung. Der Text reist in UTF-8 über das Netzwerk. Das"\n"am Ende sagt der anderen Seite, dass die Zeile zu Ende ist. Du wirst gleich sehen, warum das wichtig ist.
Sieh es dir an
Der Client
net.connect(port, address) und nutzt dieselben lineReader und write. Hier sind Server und Client in einem Programm: Node bedient beide Seiten in einem Thread und wechselt zwischen ihnen, während jede wartet. So kannst du beide ohne zweiten Computer ausprobieren:const server = net.createServer(serve);server.listen(5961, "localhost");await once(server, "listening");
const socket = net.connect(5961, "localhost");const readLine = lineReader(socket);socket.write("Kaffee\n");console.log(`Client erhielt: ${await readLine()}`);socket.end();server.close();Server erhielt: KaffeeClient erhielt: Kaffee: 7.5 €server.listen(5961, "localhost") öffnet den Port, und await once(server, "listening") wartet, bis er offen ist. So lauscht der Server schon, wenn sich der Client verbindet.Die beiden Seiten wechseln sich ab: Der Client schreibt eine Zeile und wartet mit await readLine() auf eine Antwort, und der Server liest die Zeile und antwortet. Wenn sie die Rollen tauschen und beide darauf warten, etwas zu lesen, warten sie ewig.
const server = net.createServer(serve);server.listen(5961, "localhost");await once(server, "listening");
const socket = net.connect(5961, "localhost");const readLine = lineReader(socket);socket.write("Kaffee\n");console.log(`Client erhielt: ${await readLine()}`);socket.end();server.close();Schritt für Schritt, in Worten
- Der Server öffnet Port 5961 und beginnt zu lauschen.
- Es gibt keinen Thread: Node ruft serve auf, wenn ein Client kommt. Bis dahin läuft das Programm weiter.
- Der Client verbindet sich. Node ruft serve mit einem Socket für den Client auf, und beide Seiten können schreiben und lesen.
- Der Client schreibt eine Zeile: „Kaffee“. Sie wandert über die Verbindung zum Server.
- Der Client wartet mit await readLine(). Der Server liest die Zeile und gibt sie aus.
- Der Server findet den Preis und schreibt zurück: „Kaffee: 7.5 €“.
- await readLine() gibt dem Client die Antwort zurück, und der Client gibt sie aus.
- socket.end() schließt die Verbindung, und server.close() schließt den Port.
Genauer, für Neugierige
- Das ganze Programm, mit den
imports:
import net from "node:net";import readline from "node:readline";import { once } from "node:events";
// await readLine() wartet auf die nächste Zeile vom Socket (undefined, wenn die Verbindung geschlossen ist).function lineReader(socket) { const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator](); return async () => (await lines.next()).value;}
const PRICES = new Map([["Kaffee", 7.50], ["Tee", 3.20]]);
async function serve(client) { const readLine = lineReader(client); const product = await readLine(); console.log(`Server erhielt: ${product}`); const price = PRICES.get(product); client.end(price === undefined ? "unbekanntes Produkt\n" : `${product}: ${price} €\n`);}
const server = net.createServer(serve);server.listen(5961, "localhost");await once(server, "listening");
const socket = net.connect(5961, "localhost");const readLine = lineReader(socket);socket.write("Kaffee\n");console.log(`Client erhielt: ${await readLine()}`);socket.end();server.close();- In zwei getrennten Dateien und zwei Terminals ist es dasselbe: Du startest zuerst den Server, dann den Client. Wenn auf dem Port kein Server ist, meldet der Socket einen Fehler
ECONNREFUSED. Wenn der Port schon von einem anderen Programm belegt ist, meldet der Server einen FehlerEADDRINUSE. Die Fehler kommen als Ereignis"error": Ohnesocket.on("error", …)bricht das Programm mit ihnen ab. - Text reist als Bytes über das Netzwerk.
writeundreadlinenutzen standardmäßig UTF-8, deshalb kommen Umlaute wie ü und das Zeichen € unversehrt an.
Probier es aus
Den Port öffnen
Welche Methode öffnet den Port, damit der Server auf Clients warten kann?
const server = net.createServer(serve);
server.(5961, "localhost");listen(port, address) öffnet den Port. Danach ruft Node serve für jeden Client auf, der sich verbindet: JavaScript hat kein accept().Die Idee
Viele Clients gleichzeitig
handle für jeden neuen Client auf. Während ein Client wartet, bedient Node die anderen, alles in einem Thread:let number = 0;
async function handle(client) { number++; const n = number; for await (const line of readline.createInterface({ input: client })) { console.log(`Client ${n}: ${line}`); client.write(`Echo: ${line}\n`); }}handle beantwortet jede Zeile, bis der Client die Verbindung schließt: Dann endet for await. Zwei Clients nacheinander:const server = net.createServer(handle);server.listen(5962, "localhost");await once(server, "listening");
for (const order of ["Tisch 4: zwei Kaffee", "Tisch 7: die Rechnung"]) { const socket = net.connect(5962, "localhost"); const readLine = lineReader(socket); socket.write(`${order}\n`); console.log(await readLine()); socket.end();}server.close();Client 1: Tisch 4: zwei KaffeeEcho: Tisch 4: zwei KaffeeClient 2: Tisch 7: die RechnungEcho: Tisch 7: die RechnungGenauer, für Neugierige
numberwird von allen Clients geteilt, aber es gibt keine Race Condition: Es läuft immer nur eine Funktion, und zwischennumber++undconst n = numbersteht keinawait.server.close()stoppt neue Verbindungen. Node beendet das Programm, wenn auch die alten geschlossen sind.- Dieser eine Thread reicht für Tausende von Clients, weil sie die meiste Zeit auf das Netzwerk warten. Wenn der Server viel rechnen muss, geht die schwere Arbeit an einen Worker aus der vorigen Lektion.
Probier es aus
Beide warten
Der Client sendet „Kaffee“ und wartet auf eine Antwort. Der Server ist derselbe, aber das Programm hängt und gibt nichts aus. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
"\n". Der Server liest Zeilen, und eine Zeile gibt es erst, wenn ein "\n" kommt. Der Server wartet auf das Ende der Zeile, und der Client wartet auf eine Antwort. Die Korrektur ist socket.write("Kaffee\n").Die Idee
UDP: Briefe ohne Verbindung
node:dgram (Datagramm, ein einzelnes Paket):const receiver = dgram.createSocket("udp4");receiver.bind(5963, "localhost");await once(receiver, "listening");
const sensor = dgram.createSocket("udp4");sensor.connect(5963, "localhost");await once(sensor, "connect");for (const reading of ["Küche 21.5", "Lager 4.0", "Kühlschrank 3.2"]) { sensor.send(reading);}
let count = 0;receiver.on("message", (data) => { console.log(`Temperatur: ${data} °C`); count++; if (count === 3) { receiver.close(); sensor.close(); }});Temperatur: Küche 21.5 °CTemperatur: Lager 4.0 °CTemperatur: Kühlschrank 3.2 °CcreateSocket("udp4")erstellt einen UDP-Socket. Der Empfänger lauscht auf Port 5963 mitbind(5963, "localhost").- Der Sender ruft
connect(5963, "localhost")auf: Bei UDP baut das keine Verbindung auf, es merkt sich nur, wohin die Pakete gehen. Dann sendetsend(text)ein Paket. - Jedes Paket kommt als Ereignis
"message". Nach dem dritten schließt das Programm beide Sockets, sonst wartet Node ewig auf weitere Pakete. - Niemand bestätigt, dass das Paket angekommen ist. Auf
localhostgehen keine verloren, in einem echten Netzwerk kann das passieren.
Genauer, für Neugierige
Wenn der Empfänger antworten muss, nimmt er die Adresse und den Port des Senders aus dem zweiten Parameter von "message": (data, rinfo) => receiver.send(answer, rinfo.port, rinfo.address).
Ein Paket hat eine Größengrenze (unter 64 KB, und über das Internet bleibt man sinnvollerweise bei etwa 1 400 Bytes), deshalb taugt UDP nicht für Dateien.
Die Idee
HTTP: die Sprache des Webs
Wenn du eine Seite öffnest, baut der Browser eine TCP-Verbindung zum Server auf und sendet eine textbasierte HTTP-Anfrage (Request): „gib mir /“. Der Server schickt eine Antwort (Response) zurück: einen Status, Header und einen Body (HTML, JSON, ein Bild). In Node (und im Browser) macht das fetch, ohne import:
const response = await fetch("https://example.com");console.log(`Status: ${response.status}`);console.log(`Typ: ${response.headers.get("content-type")}`);const body = await response.text();console.log(`Titel: ${body.slice(body.indexOf("<title>") + 7, body.indexOf("</title>"))}`);Status: 200Typ: text/html; charset=utf-8Titel: Example Domainfetch(address)sendet eineGET-Anfrage und gibt dir ein Promise für die Antwort.awaitwartet darauf.response.text()liest den Body als Text. Das ist auch ein Promise, also noch einmalawait.- Der Status sagt dir, wie es gelaufen ist:
200ist Erfolg,404heißt, dass es keine solche Seite gibt,500ist ein Fehler auf dem Server.fetchwirft bei 404 nichts: Prüferesponse.statusoderresponse.ok.
Die Codeprüfung der Website läuft ohne Internet, deshalb wird dieses Programm mit der Ausgabe eines Laufs mit Netzwerk gezeigt. Ohne Netzwerk wirft fetch TypeError: fetch failed.
Genauer, für Neugierige
node:http). So kannst du fetch auch ohne Internet ausprobieren, auf localhost:const server = http.createServer((request, response) => { if (request.url !== "/stock") { response.writeHead(404).end("unbekannte Adresse"); return; } response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("Kaffee: 12 Stk.");});server.listen(5964, "localhost");await once(server, "listening");
for (const path of ["/stock", "/prices"]) { const response = await fetch(`http://localhost:5964${path}`); console.log(`${path} -> ${response.status} ${await response.text()}`);}server.close();/stock -> 200 Kaffee: 12 Stk./prices -> 404 unbekannte Adresse/prices gibt sie 404 zurück, und fetch liefert dir das als gewöhnliche Antwort. In echten Projekten baut man Webserver mit Bibliotheken wie Express. Sie bauen auf demselben node:http auf.Übung
Server auf localhost
1. Echo-Server. Der Server schickt jede Zeile mit „Echo: “ davor zurück, bis er „ende“ bekommt. Dann antwortet er „Auf Wiedersehen“ und schließt die Verbindung. Der Client sendet „hallo“, „wie geht es dir“ und „ende“ und gibt die Antworten aus:
Echo: halloEcho: wie geht es dirAuf Wiedersehen2. Ein Server mit Befehlen. GROSS Text gibt den Text in Großbuchstaben zurück, ZEIT gibt die aktuelle Uhrzeit als 14:05 zurück (new Date().toTimeString().slice(0, 5)), und alles andere gibt „Unbekannter Befehl: “ und den Befehl zurück. Die Uhrzeit ändert sich, deshalb prüft der Client nur ihre Form mit einem regulären Ausdruck:
BESTELLUNG 42 IST FERTIGDie Uhrzeit hat die Form HH:MM: trueUnbekannter Befehl: UHRZEITasync-Funktion, beide werden mit Promise.all zusammen gestartet, und jeder sendet seine vorbereiteten Zeilen, wobei er nach jeder auf eine Antwort wartet (await once(server, "connection") wartet auf den ersten Client):Jonas erhielt: Hallo Jonas! Hast du um 18 Uhr Zeit?Anna erhielt: Ja, ich komme.Jonas erhielt: Super, bis gleich.Anna erhielt: Tschüss!readline mit input: process.stdin) und sendet sie, und gibt die Zeilen vom Socket aus, sobald sie kommen. Einen zweiten Thread brauchst du nicht: Beides kommt als Ereignis in der Event Loop. So kannst du tippen, während du auf eine Antwort wartest.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Arrays: push, splice und sort · Map und Set · Pfeilfunktionen · Async, await und Threads
- Geht weiter in: Nicht vertrauenswürdige Eingaben · Sitzungen, Cookies und CSRF · JWT und seine Fallen · API-Design: Vertrag, Versionen, wiederholte Anfragen
- Aufgaben zum Thema: Programme in der echten Welt