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 |
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ümmert sich Java.
- 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:
static final Map<String, Double> PRICES = Map.of("Kaffee", 7.50, "Tee", 3.20);
static void serve(ServerSocket server) { try (Socket client = server.accept(); BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter out = new PrintWriter(client.getOutputStream(), true)) { String product = in.readLine(); System.out.println("Server erhielt: " + product); Double price = PRICES.get(product); out.println(price == null ? "unbekanntes Produkt" : product + ": " + price + " €"); } catch (IOException e) { System.out.println("Server: " + e.getMessage()); }}ServerSocketlauscht auf einem Port.server.accept()wartet, bis sich ein Client verbindet, und gibt einenSocketfür die Verbindung mit ihm zurück.- Der Socket hat zwei Streams:
getInputStream()für das, was ankommt, undgetOutputStream()für das, was du sendest. Wir verpacken sie wie bei Dateien:BufferedReaderliest Zeilen,PrintWriterschreibt Zeilen. trueinPrintWriterbedeutet „nach jedemprintlnsenden“. Du wirst gleich sehen, warum das wichtig ist.- try-with-resources schließt am Ende die Verbindung und die Streams.
Sieh es dir an
Der Client
new Socket(address, port) und nutzt dieselben zwei Streams. Hier sind Server und Client in einem Programm: Der Server läuft in seinem eigenen Thread, und main ist der Client. So kannst du beide ohne zweiten Computer ausprobieren:ServerSocket server = new ServerSocket(5961);new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5961); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { out.println("Kaffee"); System.out.println("Client erhielt: " + in.readLine());}server.close();Server erhielt: KaffeeClient erhielt: Kaffee: 7.5 €new ServerSocket(5961) in main geöffnet, bevor der Thread startet. So lauscht der Server schon, wenn sich der Client verbindet.Die beiden Seiten wechseln sich ab: Der Client schreibt eine Zeile und wartet mit 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.
ServerSocket server = new ServerSocket(5961);new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5961); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { out.println("Kaffee"); System.out.println("Client erhielt: " + in.readLine());}server.close();Schritt für Schritt, in Worten
- Der Server öffnet Port 5961 und beginnt zu lauschen.
- In seinem eigenen Thread ruft der Server accept() auf und wartet auf einen Client.
- Der Client verbindet sich. accept() gibt einen Socket zurück, und jede Seite hat zwei Streams: einen zum Schreiben und einen zum Lesen.
- Der Client schreibt eine Zeile: „Kaffee“. Sie wandert über die Verbindung zum Eingabe-Stream des Servers.
- Der Client wartet in readLine(). Der Server liest die Zeile und gibt sie aus.
- Der Server findet den Preis und schreibt zurück: „Kaffee: 7.5 €“.
- readLine() gibt dem Client die Antwort zurück, und der Client gibt sie aus.
- try schließt den Socket, und server.close() schließt den Port.
Genauer, für Neugierige
- Das ganze Programm, mit den
imports:
import java.io.*;import java.net.*;import java.util.Map;
public class Main { static final Map<String, Double> PRICES = Map.of("Kaffee", 7.50, "Tee", 3.20);
static void serve(ServerSocket server) { try (Socket client = server.accept(); BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter out = new PrintWriter(client.getOutputStream(), true)) { String product = in.readLine(); System.out.println("Server erhielt: " + product); Double price = PRICES.get(product); out.println(price == null ? "unbekanntes Produkt" : product + ": " + price + " €"); } catch (IOException e) { System.out.println("Server: " + e.getMessage()); } }
public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(5961); new Thread(() -> serve(server)).start();
try (Socket socket = new Socket("localhost", 5961); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { out.println("Kaffee"); System.out.println("Client erhielt: " + in.readLine()); } 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, wirft
new SocketConnectException: Connection refused. Wenn der Port schon von einem anderen Programm belegt ist, wirftnew ServerSocketBindException: Address already in use. - Text reist als Bytes über das Netzwerk. Java 18 und neuer wandelt Text standardmäßig in UTF-8 um, deshalb kommen Umlaute wie ü und das Zeichen € unversehrt an.
Probier es aus
Auf einen Client warten
Welche Methode wartet, bis sich ein Client verbindet?
ServerSocket server = new ServerSocket(5961);
Socket client = server.();accept() blockiert, bis sich jemand verbindet, und gibt einen Socket für die Verbindung mit ihm zurück. Der Server kann es viele Male aufrufen, für viele Clients.Die Idee
Viele Clients gleichzeitig
accept() nicht aufgerufen, und die anderen warten. Deshalb läuft ein echter Server in einer Endlosschleife: Er nimmt einen Client an, gibt ihn an einen neuen Thread und kehrt sofort zu accept() zurück:static void acceptAll(ServerSocket server) { int number = 0; while (true) { try { Socket client = server.accept(); number++; int n = number; new Thread(() -> handle(client, n)).start(); } catch (IOException e) { return; } }}
static void handle(Socket client, int number) { try (client; BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter out = new PrintWriter(client.getOutputStream(), true)) { String line; while ((line = in.readLine()) != null) { System.out.println("Client " + number + ": " + line); out.println("Echo: " + line); } } catch (IOException e) { System.out.println("Client " + number + " getrennt: " + e.getMessage()); }}handle beantwortet jede Zeile, bis der Client die Verbindung schließt: Dann gibt readLine() null zurück. Zwei Clients nacheinander:ServerSocket server = new ServerSocket(5962);Thread acceptor = new Thread(() -> acceptAll(server));acceptor.setDaemon(true);acceptor.start();
for (String order : new String[] { "Tisch 4: zwei Kaffee", "Tisch 7: die Rechnung" }) { try (Socket socket = new Socket("localhost", 5962); PrintWriter out = new PrintWriter(socket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { out.println(order); System.out.println(in.readLine()); }}server.close();Client 1: Tisch 4: zwei KaffeeEcho: Tisch 4: zwei KaffeeClient 2: Tisch 7: die RechnungEcho: Tisch 7: die RechnungGenauer, für Neugierige
setDaemon(true)macht den Thread zu einem „Hintergrund“-Thread: Das Programm endet, wennmainendet, ohne auf die Endlosschleife zu warten. Ein echter Server macht ihn nicht zum Hintergrund-Thread: Er muss laufen, bis du ihn stoppst.try (client; ...)nimmt den schon vorhandenen Socket in try-with-resources auf, damit er am Ende geschlossen wird.- Bei Tausenden von Clients ist ein Thread für jeden teuer. Dann gibt die Annahmeschleife die Clients an einen Thread-Pool aus der vorigen Lektion:
pool.execute(() -> handle(client, n)).
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.
PrintWriter ohne true sendet nicht sofort: Er hält den Text in einem Puffer, bis sich mehr davon angesammelt hat. println("Kaffee") bleibt im Speicher des Clients, der Server wartet auf eine Zeile, und der Client wartet auf eine Antwort. Die Korrektur ist new PrintWriter(socket.getOutputStream(), true) oder out.flush() nach println.Die Idee
UDP: Briefe ohne Verbindung
DatagramPacket (Bytes plus Adresse und Port) über einen DatagramSocket:DatagramSocket receiver = new DatagramSocket(5963);
try (DatagramSocket sensor = new DatagramSocket()) { InetAddress address = InetAddress.getByName("localhost"); for (String reading : new String[] { "Küche 21.5", "Lager 4.0", "Kühlschrank 3.2" }) { byte[] data = reading.getBytes(); sensor.send(new DatagramPacket(data, data.length, address, 5963)); }}
byte[] buffer = new byte[256];for (int i = 0; i < 3; i++) { DatagramPacket packet = new DatagramPacket(buffer, buffer.length); receiver.receive(packet); String text = new String(packet.getData(), 0, packet.getLength()); System.out.println("Temperatur: " + text + " °C");}receiver.close();Temperatur: Küche 21.5 °CTemperatur: Lager 4.0 °CTemperatur: Kühlschrank 3.2 °C- Der Empfänger lauscht auf Port 5963 mit
new DatagramSocket(5963). Der Sender nutztnew DatagramSocket()ohne Port: Das System gibt ihm einen freien. receivewartet auf ein Paket und schreibt es in den Puffer.getLength()sagt dir, wie viele Bytes angekommen sind.- 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 Paket selbst, packet.getAddress() und packet.getPort(), und sendet ein neues Paket dorthin.
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 Java macht das HttpClient:
import java.net.URI;import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com")).build();HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());System.out.println("Typ: " + response.headers().firstValue("content-type").orElse("?"));String body = response.body();System.out.println("Titel: " + body.substring(body.indexOf("<title>") + 7, body.indexOf("</title>")));Status: 200Typ: text/html; charset=utf-8Titel: Example DomainHttpRequest.newBuilder(URI)beschreibt die Anfrage, standardmäßigGET.sendsendet die Anfrage und wartet auf die Antwort.BodyHandlers.ofString()sagt „gib mir den Body als Text“.- Der Status sagt dir, wie es gelaufen ist:
200ist Erfolg,404heißt, dass es keine solche Seite gibt,500ist ein Fehler auf dem Server.
Die Codeprüfung der Website läuft ohne Internet, deshalb wird dieses Programm mit der Ausgabe eines Laufs mit Netzwerk gezeigt. Ohne Netzwerk wirft send eine IOException.
Genauer, für Neugierige
com.sun.net.httpserver). So kannst du HttpClient auch ohne Internet ausprobieren, auf localhost:HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 5964), 0);server.createContext("/stock", exchange -> { byte[] answer = "Kaffee: 12 Stk.".getBytes(); exchange.sendResponseHeaders(200, answer.length); exchange.getResponseBody().write(answer); exchange.close();});server.start();
HttpClient client = HttpClient.newHttpClient();for (String path : new String[] { "/stock", "/prices" }) { HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:5964" + path)).build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(path + " -> " + response.statusCode() + " " + response.body());}server.stop(0);/stock -> 200 Kaffee: 12 Stk./prices -> 404 <h1>404 Not Found</h1>No context found for request/prices gibt es keinen Handler, deshalb gibt der Server von selbst 404 zurück. In echten Projekten baut man Webserver mit Bibliotheken wie Spring Boot. Sie bauen auf denselben Sockets und Threads 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 (LocalTime.now() und DateTimeFormatter.ofPattern("HH:mm")), 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: UHRZEITJonas erhielt: Hallo Jonas! Hast du um 18 Uhr Zeit?Anna erhielt: Ja, ich komme.Jonas erhielt: Super, bis gleich.Anna erhielt: Tschüss!Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen · HashMap und HashSet · Lambdas · 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