W3docs

Java Sockets

TCP-Client-Verbindungen in Java mit der Socket-Klasse öffnen und Daten senden und empfangen.

Unterhalb von HTTP und jedem anderen Anwendungsprotokoll liegt der Socket: eine rohe, bidirektionale TCP-Verbindung zwischen zwei Endpunkten. Javas java.net.Socket ist die Client-Seite — Sie erstellen einen Socket, verbinden ihn mit einem Host und Port und lesen und schreiben dann Bytes über gewöhnliche InputStream/OutputStreams. Dies ist die Netzwerkprogrammierung auf niedrigster Ebene, die Sie verwenden, wenn Sie ein benutzerdefiniertes Protokoll implementieren oder mit einem Dienst kommunizieren, der kein HTTP ist.

Dieses Kapitel behandelt, was ein verbundener Socket Ihnen bietet, die zwei Möglichkeiten zur Verbindung (und warum eine sicherer ist), wie man Text und rohe Bytes liest und schreibt, ein vollständiges ausführbares Beispiel für eine Client-Server-Kommunikation, die Timeouts und Fallstricke, die realen Code treffen, und wo Sockets im Verhältnis zum übergeordneten HttpClient stehen, den Sie bereits kennen.

Was ein Socket bietet

Ein verbundener Socket ist eine Pipe mit zwei Streams:

  • socket.getOutputStream() — Bytes, die Sie schreiben, gelangen zum anderen Ende.
  • socket.getInputStream() — Bytes, die das andere Ende schreibt, kommen hier an.

TCP garantiert, dass die Bytes zuverlässig und in der richtigen Reihenfolge ankommen. Es erzwingt keine Nachrichtenstruktur — ein Socket ist ein Bytestrom, kein Nachrichtenstrom. Das Framing (wo eine Nachricht endet und die nächste beginnt) ist Ihre Aufgabe: Verwenden Sie Zeilenumbrüche, Längenpräfixe oder ein übergeordnetes Protokoll. Wenn Sie stattdessen Nachrichtengrenzen benötigen, ist das die Aufgabe von Datagram- (UDP-)Sockets, die Reihenfolge und Zuverlässigkeit gegen diskrete Pakete tauschen.

Die Streams kommen direkt aus Javas I/O-System: getInputStream() gibt einen einfachen InputStream zurück und getOutputStream() einen einfachen OutputStream, sodass jeder Wrapper, den Sie kennen — BufferedReader, PrintWriter, DataInputStream — über einen Socket genauso funktioniert wie über eine Datei.

Verbinden

// Style 1: connect in the constructor
Socket socket = new Socket("example.com", 80);

// Style 2: create then connect with a timeout (preferred)
Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000);

Die zweite Form ermöglicht es Ihnen, ein Connect-Timeout festzulegen — ohne dieses kann ein nicht erreichbarer Host den Thread für den OS-Standard hängen lassen (oft eine Minute oder länger). Sobald die Verbindung hergestellt ist, umwickeln Sie die Streams mit gepufferten Readern/Writern und wählen explizit einen Zeichensatz.

Text lesen und schreiben

var out = new PrintWriter(
        new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
var in = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

out.println("ping");                 // autoFlush=true sends it immediately
String reply = in.readLine();        // blocks until a line arrives

readLine() blockiert, bis Daten (oder End-of-Stream) ankommen — das Merkmal der klassischen blockierenden Socket-API. Schließen Sie den Socket immer (try-with-resources), um die Verbindung freizugeben.

Rohe Bytes lesen

Text ist praktisch, aber viele Protokolle sind binär — Bilddaten, längenpräfixierte Frames, ein benutzerdefiniertes Wire-Format. Dort überspringen Sie die Reader und arbeiten direkt mit den Streams:

try (Socket socket = new Socket()) {
    socket.connect(new InetSocketAddress("example.com", 80), 2000);
    OutputStream out = socket.getOutputStream();
    InputStream in = socket.getInputStream();

    out.write("PING".getBytes(StandardCharsets.UTF_8));
    out.flush();                          // streams are not auto-flushed

    byte[] buffer = new byte[4096];
    int n = in.read(buffer);              // bytes read, or -1 at end-of-stream
    if (n > 0) {
        String chunk = new String(buffer, 0, n, StandardCharsets.UTF_8);
        System.out.println(chunk);
    }
}

Zwei Tatsachen prägen jedes byteweise Lesen:

  • read(byte[]) gibt zurück, wie viele Bytes tatsächlich empfangen wurden, was nicht unbedingt das ist, was Sie angefordert haben. Ein Schreibvorgang auf der anderen Seite kann als mehrere Lesevorgänge ankommen, und mehrere Schreibvorgänge können als ein Lesevorgang ankommen — TCP fasst zusammen und teilt nach Belieben auf. Um eine feste Anzahl von Bytes zu erhalten, müssen Sie eine Schleife verwenden oder den Stream in DataInputStream einwickeln und readFully() aufrufen.
  • Ein Rückgabewert von -1 bedeutet, dass der Peer sein Ende geschlossen hat (End-of-Stream), nicht „gerade keine Daten". Das ist Ihr Signal zum Stoppen des Lesens.

Wichtige Timeouts

Ein blockierender Socket kann an zwei verschiedenen Stellen hängen bleiben, und beide benötigen unterschiedliche Timeouts:

Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000); // connect timeout
socket.setSoTimeout(5000);                                       // read timeout
  • Das Connect-Timeout (das zweite Argument für connect) begrenzt, wie lange der TCP-Handshake dauern darf. Ohne dieses hängt ein nicht erreichbarer Host den Thread für den OS-Standard — oft eine Minute oder länger.
  • Das Read-Timeout (setSoTimeout) begrenzt, wie lange ein einzelnes read/readLine auf Daten warten darf. Wenn es abläuft, wirft der Aufruf eine SocketTimeoutException, ohne den Socket zu schließen, sodass Sie entscheiden können, ob Sie es erneut versuchen oder aufgeben. Ohne dieses blockiert ein schweigsamer Peer Sie für immer.

Echter Netzwerkcode sollte beides setzen. Die zwei Fehler, die Sie abfangen müssen, sind UnknownHostException (DNS konnte den Namen nicht auflösen) und die breite IOException-Familie (Verbindung verweigert, zurückgesetzt oder abgelaufen); siehe Java-Exceptions für Behandlungsmuster.

Ein ausgearbeitetes Beispiel: Ein Client kommuniziert mit einem Loopback-Echo-Server

Dieses Programm startet einen einmaligen Echo-Server in einem Hintergrundthread, der an die Loopback-Adresse gebunden ist, und verbindet dann — das eigentliche Thema des Kapitels — einen Client-Socket, sendet eine Zeile und liest die Antwort. Es ist ein vollständiges TCP-Gespräch innerhalb einer JVM, kein externes Netzwerk.

java— editable, runs on the server

Was aus dem Ausführen zu entnehmen ist:

  • Die Client-Seite besteht aus nur drei Schritten: Einen Socket erstellen, ihn mit connect() an eine Adresse und einen Port verbinden, dann seine Streams lesen und schreiben. Alles, was HTTP in früheren Kapiteln für Sie erledigt hat — Request-Zeilen, Header, Statuscodes — ist weg; ein Socket bewegt rohe Bytes und nichts weiter.
  • connect(new InetSocketAddress(...), 2000) hat ein 2-Sekunden-Connect-Timeout gesetzt. Der Konstruktor ohne Timeout new Socket(host, port) würde beim OS-Standard blockieren, wenn der Host nicht erreichbar wäre, sodass die explizite Timeout-Form die sicherere Gewohnheit für jedes echte Netzwerk ist.
  • Das Protokoll war eine Vereinbarung, kein Feature: Der Client schrieb eine Zeile und las eine Zeile, weil beide Seiten vereinbarten, dass Zeilen Nachrichten sind. TCP lieferte einen geordneten Bytestrom; das Zeilenumbruch-Framing, das ihn in „Nachrichten" verwandelte, war vollständig anwendungsdefiniert.
  • readLine() blockierte, bis die Antwort des Servers ankam. Dieses Thread-per-Connection-, Block-until-Data-Modell ist einfach und korrekt, und genau das ist der Kostenfaktor, den virtuelle Threads günstig machen sollen, wenn die Anzahl der Verbindungen wächst.
  • getRemoteSocketAddress() und getLocalSocketAddress() zeigten beide Enden der aktiven Verbindung — den Loopback-Port des Servers und den vom OS zugewiesenen lokalen Port des Clients. Jede TCP-Verbindung wird durch dieses Paar von Endpunkten identifiziert. Der serverseitige ServerSocket baut den Listener, der diese Verbindung akzeptiert hat.

Wann man einen rohen Socket verwenden sollte

Ein Socket ist das richtige Werkzeug, wenn es keine Bibliothek gibt, die Ihr Protokoll bereits spricht:

  • Sie implementieren oder konsumieren ein benutzerdefiniertes TCP-Protokoll (ein Spielserver, ein Message-Broker-Wire-Format, ein zeilenbasierter Admin-Port).
  • Sie müssen mit einem Nicht-HTTP-Dienst kommunizieren — SMTP, ein Redis-ähnliches Textprotokoll, ein Legacy-Zeilenserver.

Für alles, was HTTP ist, sollten Sie keine Anfragen manuell über einen Socket zusammenstellen. Verwenden Sie den modernen HttpClient, der Ihnen Connection Pooling, Weiterleitungen, HTTP/2 und TLS kostenlos bietet. Die Beziehung ist dieselbe wie zwischen rohen Bytestreams und den übergeordneten Readern, die darauf aufgebaut sind: Gehen Sie nur auf die niedrigere Ebene, wenn die höhere nicht ausdrücken kann, was Sie brauchen.

Übungen

Übung
Ein Client liest von einem Server mithilfe von 'socket.getInputStream()', eingebettet in einen 'BufferedReader', sendet einen zeilenumbruch-terminierten Befehl und erwartet eine zeilenumbruch-terminierte Antwort. Gelegentlich wird eine Antwort auf zwei TCP-Segmente aufgeteilt und der Client liest sie falsch. Was ist das richtige Verständnis?
Ein Client liest von einem Server mithilfe von 'socket.getInputStream()', eingebettet in einen 'BufferedReader', sendet einen zeilenumbruch-terminierten Befehl und erwartet eine zeilenumbruch-terminierte Antwort. Gelegentlich wird eine Antwort auf zwei TCP-Segmente aufgeteilt und der Client liest sie falsch. Was ist das richtige Verständnis?
Was this page helpful?