Netzwerkkommunikation mit Java

In Java gibt es keinen nennenswerten Unterschied wischen I/O mit Daten und I/O mit Netzwerkverbindungen. In beiden Fällen basiert die Ein- und Ausgabe auf InputStream und OutputStream, der Unterschied liegt nur darin, wo diese Datenströme herkommen. Bei der Netzwerkkommunikation mit dem TCP-Protokoll kommen sie aus einem Socket. Bei UDP wird die Klasse DatagramSocket verwendet, welche nicht auf Streams basiert.

Socket hat zwar eine lange Liste von Methoden, aber bei der grundlegenden Verwendung kann man die meisten davon ignorieren.

Client-Seite

String nachricht = in.readLine();
try (Socket verbindung = new Socket("localhost", 23456)){
    BufferedReader reader = new BufferedReader(
     new InputStreamReader(verbindung.getInputStream())); 
    BufferedWriter writer = new BufferedWriter(
     new OutputStreamWriter(verbindung.getOutputStream()));
    writer.write(nachricht);
    writer.newLine();
    writer.flush();
    String antwort = reader.readLine();
}

Im Beispiel wird dem Socket im Konstruktor Adresse (IP oder Hostname) und Port des Servers angegeben, mit dem eine Verbindung hergestellt werden soll. Die Verbindung wird automatisch hergestellt und mit den Methoden getInputStream und getOutputStream kann man Daten vom Server empfangen und zum Server senden.

Einen kleinen Unterschied zwischen Netzwerk I/O und Datei I/O gibt es mit der flush-Methode. Sie sorgt dafür, dass der Schreibpuffer sofort weiterverarbeitet wird, auch wenn er noch nicht voll ist. Dabei wird der Strom aber nicht sofort geschlossen, denn es sollen nicht nur Daten in eine Richtung versendet werden, es soll echte Kommunikation in beide Richtungen stattfinden. Damit der Server eine Antwort schicken kann, die dann mit readLine gelesen werden kann, muss er zunächst die Nachricht vom Client erhalten und dazu muss der Client den Puffer leeren.

Ausserdem wird weder InputStream noch OutputStream geschlossen. Beide sind fest mit dem Socket verbunden, aus dem sie hergestellt wurden und wenn man einen der Ströme schliesst, wird auch der Socket geschlossen. Andersherum werden die Datenströme aber auch geschlossen, wenn man den Socket schliesst, deswegen reicht es, diesen als Ressource für den try-Block anzugeben.

Server-Seite

Ein einfaches Serverprogramm in Java zu schreiben, ist kaum anders als beim Client, nur die Herkunft des Sockets ändert sich:

ServerSocket server = new ServerSocket(23456);
try (Socket verbindung = server.accept()){
    BufferedReader reader = new BufferedReader(
     new InputStreamReader(verbindung.getInputStream())); 
    BufferedWriter writer = new BufferedWriter(
     new OutputStreamWriter(verbindung.getOutputStream()));
    String nachricht = reader.readLine();
    writer.write(antwort);
    writer.flush();
}

Ein ServerSocket dient nicht direkt der Kommunikation, er wartet nur auf eingehende Verbindungen. Der Konstruktor-Parameter gibt den Port an, auf dem Verbindungen akzeptiert werden sollen; die Methode accept wartet, bis auf diesem Port eine Verbindung hergestellt wird. Und warten heisst hier wirklich warten: accept blockiert so lange, bis eine Verbindung zustande kommt. Wenn dies der Fall ist, gibt accept einen Socket zurück, mit dem man genauso verfahren kann, wie mit einem Socket auf der Client-Seite.

Wie demonstriert, wird nur eine Verbindung akzeptiert und verarbeitet. Für ein Beispiel ausreichend, werden für einen echten Serverprozess dagegen üblicherweise Verbindungen in einer Schleife akzeptiert und die Verarbeiten wird in einem neuen Thread durchgeführt, so dass dieser Thread erneut mit accept auf Verbindungen warten kann.

Hier ein Beispiel-Code für ServerSocket mit Threads:

ServerSocket server = new ServerSocket(23456);
while(!beendet){
    try (Socket verbindung = server.accept()){
        new Thread(() -> verarbeiteVerbidnung(verbindung));
    }
}