W3docs

Java ServerSocket

TCP-Verbindungen in Java mit ServerSocket annehmen und einen einfachen Server aufbauen.

Die Client-Seite einer TCP-Verbindung ist Socket. Die Server-Seite ist java.net.ServerSocket: Sie bindet sich an einen Port, lauscht auf eingehende Verbindungen und übergibt für jeden eingehenden Client einen normalen Socket. Ein ServerSocket akzeptiert viele Clients; jeder accept()-Aufruf gibt eine separate Verbindung zurück.

Dieses Kapitel behandelt, wie man Verbindungen bindet und akzeptiert, wie man viele Clients gleichzeitig bedient, welche Rolle der Verbindungs-Backlog spielt und welche Lifecycle-Disziplin ein dauerhaft laufender Server benötigt.

Binden, akzeptieren, bedienen

ServerSocket server = new ServerSocket(8080);   // bind to port 8080
while (true) {
    Socket client = server.accept();            // blocks until a client connects
    handle(client);                             // read/write the client's streams
}
  • new ServerSocket(port) bindet und beginnt zu lauschen. Verwende 0, damit das Betriebssystem einen freien Port wählt (dann mit getLocalPort() auslesen).
  • accept() blockiert, bis ein Client eine Verbindung aufbaut, und gibt anschließend einen Socket zurück, der diese Verbindung repräsentiert. Der Server-Socket selbst lauscht weiterhin auf die nächste Verbindung.

Die von accept() zurückgegebene Verbindung ist ein gewöhnlicher Socket — identisch mit dem, den ein Client erstellt — das Lesen und Schreiben der Streams funktioniert daher auf genau dieselbe Weise.

Standardmäßig blockiert accept() unbegrenzt. Rufe zuerst server.setSoTimeout(ms) auf, wenn es nach einer Wartezeit eine SocketTimeoutException werfen soll — damit kann ein Server-Thread ein „Soll ich weiterlaufen?"-Flag prüfen, anstatt an einem stillen Port zu hängen.

Clients gleichzeitig bearbeiten

accept() blockiert und jeder Client kann seine Verbindung offen halten, daher kann eine Single-Thread-Schleife nur jeweils einen Client bedienen. Die klassische Lösung ist ein Thread (oder eine gepoolte Aufgabe) pro Verbindung:

ExecutorService pool = Executors.newFixedThreadPool(8);
while (running) {
    Socket client = server.accept();
    pool.submit(() -> handle(client));   // serve this client on a worker thread
}

Die Accept-Schleife bleibt frei, die nächste Verbindung entgegenzunehmen, während Worker-Threads bestehende Clients bedienen. Informationen zur Größenbestimmung und Verwaltung des Pools findest du im Executor-Framework und bei Thread-Pools. Mit virtuellen Threads (Java 21+) bleibt das Muster „ein Thread pro Verbindung" auch bei Zehntausenden von Clients günstig, sodass du den Pool oft weglassen und jede Verbindung einfach einem eigenen virtuellen Thread übergeben kannst.

Der Backlog

new ServerSocket(port, backlog) legt den Backlog fest — wie viele Verbindungen das Betriebssystem in der Warteschlange halten darf, während dein Code zwischen accept()-Aufrufen beschäftigt ist. Darüber hinaus werden neue Verbindungen abgelehnt. Der Standardwert beträgt typischerweise 50.

Ein vierargumentiger Konstruktor fügt die Bind-Adresse hinzu: new ServerSocket(port, backlog, address). Die Übergabe von InetAddress.getLoopbackAddress() macht den Server nur von der gleichen Maschine erreichbar (127.0.0.1) — nützlich für rein lokale Dienste und für das folgende Beispiel.

Den Port freigeben: SO_REUSEADDR

Wenn ein Server stoppt, kann sein Listening-Port im Betriebssystem im Zustand TIME_WAIT verbleiben, und ein Neustart kann mit „Address already in use" fehlschlagen. Das Setzen von SO_REUSEADDR erlaubt einem neuen ServerSocket, sich an einen noch im TIME_WAIT befindlichen Port zu binden:

ServerSocket server = new ServerSocket();      // unbound
server.setReuseAddress(true);
server.bind(new InetSocketAddress(8080));      // now bind explicitly

Deshalb erstellen Produktions-Server meist einen ungebundenen ServerSocket, setzen Optionen und rufen dann bind() auf — anstatt den Port an den Konstruktor zu übergeben.

Ein praktisches Beispiel: ein gleichzeitiger Loopback-Server

Dieses Programm bindet einen ServerSocket an das Loopback-Interface, akzeptiert drei Clients in einem Hintergrund-Thread und bedient jeden in einem Thread-Pool — dann startet es drei Clients. Jeder Worker begrüßt seinen Client und nennt den Thread, der ihn bedient hat, sodass die Parallelität sichtbar wird.

java— editable, runs on the server

Was aus dem Ausführen hervorgeht:

  • Die gesamte Aufgabe des Servers ist Binden → accept() → Bedienen, wiederholt. accept() blockierte, bis sich jeder Client verbunden hatte, und gab dann einen gewöhnlichen Socket für genau diesen Client zurück, während der ServerSocket offen blieb, um den nächsten zu akzeptieren — ein Listener, viele Verbindungen.
  • Das Binden an Port 0 ließ das Betriebssystem einen freien Port wählen, der via getLocalPort() ausgelesen und an die Clients weitergegeben wurde. Das dritte Konstruktorargument hat den Server außerdem auf getLoopbackAddress() fixiert, sodass er nur auf 127.0.0.1 lauschte — dasselbe Adress-und-Port-Paar, das die Clients angewählt haben.
  • Jede akzeptierte Verbindung wurde an einen Thread-Pool übergeben, sodass die Accept-Schleife nie durch die Bedienung eines langsamen Clients blockiert wurde. Die Antworten nannten verschiedene Worker-Threads (pool-1-thread-1, -2, -3), was die Pro-Verbindung-Parallelität konkret macht: Drei Clients wurden parallel bedient, nicht nacheinander.
  • handle() verwendete try-with-resources auf dem Client-Socket (try (client; …)), was sicherstellt, dass jede Verbindung nach ihrem Austausch geschlossen wird. Ein Server, der akzeptierte Sockets vergisst zu schließen, verliert schnell Dateideskriptoren, da er pro Client einen öffnet.
  • Das Herunterfahren war explizit und geordnet: Aufhören zu akzeptieren, pool.shutdown(), auf Beendigung warten, dann server.close(). Ein dauerhaft laufender Server muss seinen Listening-Port bewusst freigeben, und ausstehende Worker-Tasks müssen abschließen dürfen — dieselbe Disziplin skaliert von dieser Drei-Client-Demo bis zu einem echten Server.

Wann ServerSocket verwenden

ServerSocket ist das richtige Werkzeug, wenn du einen verbindungsorientierten (TCP)-Server benötigst, den du auf Byte-/Stream-Ebene steuerst: ein benutzerdefiniertes Protokoll, ein Chat- oder Spiel-Backend, einen Proxy oder eine Lernübung. Für Request/Response über HTTP würde man normalerweise ein höherstufiges Server-Framework verwenden, anstatt die Accept-Schleife von Hand zu schreiben. Wenn du verbindungsloses (UDP)-Messaging benötigst — bei dem es kein accept() gibt und jedes Paket für sich steht — verwende Datagram-Sockets. Hintergrundinformationen zu Adressen, Ports und dem Protokoll-Stack findest du in der Networking-Einführung.

Übungen

Übung
Ein Server verwendet eine Single-Thread-Schleife: 'while (true) { Socket c = server.accept(); handle(c); }', wobei 'handle' die Verbindung für die gesamte Sitzung des Clients offen hält. Unter Last verbinden sich neue Clients, erhalten aber keine Antwort, bis frühere trennen. Was ist die Ursache und die Standardlösung?
Ein Server verwendet eine Single-Thread-Schleife: 'while (true) { Socket c = server.accept(); handle(c); }', wobei 'handle' die Verbindung für die gesamte Sitzung des Clients offen hält. Unter Last verbinden sich neue Clients, erhalten aber keine Antwort, bis frühere trennen. Was ist die Ursache und die Standardlösung?
Was this page helpful?