Sockets unter Java
Willemers Informatik-Ecke
Die Basisschnittstelle zur Programmierung von TCP/IP-Anwendungen sind die Sockets. Diese Schnittstelle wurde erstmals in C unter UNIX bereitgestellt und inzwischen auf alle Plattformen und die meisten Programmiersprachen portiert.

Kommunikation im TCP/IP-Netzwerk

Das Netzwerk verbindet zwei oder mehr Computer. Heutzutage werden Netzwerke eigentlich nur noch unter dem Protokoll TCP/IP organisiert.

Die Computer im Netzwerk werden als Hosts bezeichnet und mit einer Adresse oder einer IP-Adresse eindeutig bezeichnet.

Tatsächlich kommunizieren aber nicht Computer, sondern Prozesse. Ein Prozess, der über das Netzwerk kommunizieren will, eröffnet einen Port und ist damit von anderen Hosts eindeutig erreichbar.

Ein typischer HTTP-Server fordert den Port 80 an und ist beispielsweise unter dem Namen meinserver unter der IP-Adresse 192.168.1.20 im Netzwerk zu erreichen. Dann würde ein Browser ihn unter der URL 192.168.1.20:80 oder meinserver:80 erreichen können. Da Webserver aber üblicherweise unter 80 erreichbar sind, kann man den Port weglassen.

Auch ein Browser verwendet einen Port, da er vom Server ja eine Antwort erwartet. Dieser Port muss aber bei jedem Aufruf neu sein, ist also nicht festgelegt und wird vom System bei jeder Anforderung neu und mehr oder weniger zufällig vergeben.

Client-Server

Ein Prozess eröffnet die Netzwerkkommunikation und verbindet sich mit einem anderen Prozess, der auf eine Anfrage wartet.

Die Übertragung über ein Netzwerk erfolgt in Paketen.

Ein Client sendet über einen Socket

Der Verbinder zum Serverprozess stellt ein Socket dar. Der Socket erhält vom System einen gerade freien Port im oberen Zahlenbereich zugewiesen, der mehr oder weniger zufällig ist.

Mit dem Aufruf von connect wird der Socket mit einem Socket auf dem Zielrechner verbunden. Entsprechend gibt es zwei Parameter.

Nach der Verbindung setzt der Client mit dem Aufruf von write oder send seine Daten in Richtung Server ab. Der Parameter ist ein Datenblock als Array von byte, der dann als Paket versendet wird. Tatsächlich erfolgt die Sendung in Paketen. Sollte das Paket, das der Client sendet zu groß für die Netzwerk-Hardware sein, wird das Paket im Hintergrund in mehrere kleinere Pakete umgepackt.

Anschließend schließt der Client den Socket durch den Aufruf von close.

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;

public class SocketWriteClient {
    public static void main(String[] args) throws IOException {
        // Socket anlegen und connect
        Socket socket = new Socket();
        SocketAddress adresse = 
                new InetSocketAddress("127.0.0.1", 33034);
        socket.connect(adresse);
        // Daten vorbereiten
        final int SIZE = 20;
        byte[] bytes = new byte[SIZE];
        for (int i=0; i<SIZE; i++) {
            bytes[i] = (byte) (i % SIZE);
        }
        // Daten senden
        OutputStream out = socket.getOutputStream();
        out.write(bytes, 0, SIZE);
        out.close();
        // Socket schließen
        socket.close();
    }
}

Verkürzung eines Client-Sockets

Da ein Client-Server immer den connect zu einem Serverprozess aufnehmen wird, kann die Socketerzeugung und der Aufruf von connect ...

Socket socket = new Socket();
SocketAddress adresse = new InetSocketAddress(host, port);
socket.connect(adresse);

zu einem Aufruf des Konstruktors zusammengefasst werden, der die Adressangaben direkt übergeben bekommt:

Socket socket = new Socket(host, port);

Ein Server empfängt ein Paket über Sockets

Der Socket des Servers geht nicht auf Reisen zu einem anderen Host oder Prozess, sondern hängt sich mit dem Aufruf bind an einen festen Port, auf dem er Anfragen von Clients erwartet. Dieser Vorgang wird unter Java durch die Klasse ServerSocket abgebildet.

Mit dem Aufruf accept wartet der Server auf Clients. Trifft ein Client ein, erstellt der Server einen neuen Socket, der dessen Host und Port enthält und damit die Kommunikation zum Client ermöglicht.

In einer Schleife werden nun so lange Pakete gelesen, wie noch welche gesendet werden. Auch wenn nur ein Paket gesendet wird, können diese im Zuge der Sendung aus technischen Gründen auf mehrere Pakete verteilt werden.

Nach dem Empfang beendet sich der Server. Allerdings schließt er zuvor alle offenen Resourcen.

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketReadServer {
    public static void main(String[] args) throws IOException {
        // Melde einen Socket auf Port 33034 an
        ServerSocket serverSocket = new ServerSocket(33034);
        // warte auf einen Client und erzeuge einen Socket für die Kommunikation
        Socket socket = serverSocket.accept();
        // Bereite den Daten-Cache vor
        final int SIZE = 2048;  // etwas großzügig ist gut.
        byte[] bytes = new byte[SIZE];
        // Lese die gesendeten Pakete
        InputStream in = socket.getInputStream();
        int count;
        // Solange noch Pakete eintreffen,
        while ((count = in.read(bytes)) > 0) {
            // zeige deren Inhalt an!
               System.out.println("count = " + count);
            for (int i=0; i<count; i++) {
                System.out.print(" " + bytes[i]);
            }
        }
        // Aufräumen:
        in.close();
        socket.close();
        serverSocket.close();
    }
}

Der Client sendet einen Datenstrom und erwartet eine Antwort

Der Socket wird bei der Entstehung durch die Konstruktorparameter gleich an sein Ziel gebunden.

Anstatt die Daten als byte-Array zu übergeben, wird auf den Socket ein Datenstrom gelegt. Das sorgt dafür, dass die Ausgaben in den Strom in Pakete zerlegt werden. Der Client kann so mit einen einfachen println-Aufruf Daten über den Socket senden. So sendet der Client eine Anfrage an den Server auf dem Balkon. Eine solche Sendung blockiert den Client nicht.

Nach dem Senden eröffnet der Client einen Dateneingangsstrom. Er erwartet also eine Antwort vom Server. Dahinter steckt der read-Aufruf, den wir vom Server schon kennen. Dieser Lesevorgang stoppt den Client so lange, bis er eine Antwort erhält.

Sobald die Antwort eintrifft, gibt sie der Client auf dem Bildschirm aus.

Zum Abschluss wird aufgeräumt. Alle Sockets und Datenströme werden geschlossen.

import java.io.DataInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;

public class SocketClient {
    public static void main(String args[]) throws IOException {
        Socket socket = new Socket("127.0.0.1", 33033);
        PrintStream output = new PrintStream(socket.getOutputStream());
        output.println("Hab' mich lieb, Julia!");
        DataInputStream input = new DataInputStream(socket.getInputStream());
        String str = input.readLine();
        System.out.println(str);
        input.close();
        output.close();
        socket.close();
    }
}

Ein Socket-Server antwortet wiederholt auf Anfragen von Clients

Der Server eröffnet einen ServerSocket und bindet sich bereits über den Konstruktor an den übergebenen Port.

Da Server üblicherweise nicht nur eine Anfrage erhalten, gehen sie typischerweise in eine Endlosschleife, die gewaltsam unterbrochen werden muss.

Mit dem Aufruf von accept wird ein normaler Socket abgespalten, der dann die eingehenden Daten des Clients empfängt. Solange es keine Anfrage gibt, blockiert der Server an dieser Stelle.

Er blockiert anschließend, bis er alle Daten des Clients gelesen hat. Anschließend sendet der Server seine Antwort.

Anschließend werden alle für diese Verbindung relevanten Resourcen geschlossen. Ansonsten kann der wiederholte Aufruf des Servers dazu führen, dass das System irgendwann keine Sockets mehr zur Verfügung hat.

Anschließend wird die Schleife wiederholt und der Server steht wiederum vor dem accept.

Bei einer Exception endet der Serverprozess und schließt nun auch den ServerSocket. Da hier noch einmal eine IOException auftreten könnte, muss main signalisieren, dass die IOException geworfen werden könnte.

import java.io.DataInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
    public static void main(String args[]) throws IOException {
        ServerSocket server = null;
        try {
            // Ein Socket für die Verbindung zum well known port
            server = new ServerSocket(33033);
            while (true) {
                // Ein neuer Socket für diese Anfrage
                Socket socket = server.accept();
                // Datenstrom über den Socket bilden
                DataInputStream input =
                        new DataInputStream(socket.getInputStream());
                // Auslegen des Datenstroms in einen String
                String str = input.readLine();
                System.out.println(str);
                // Baue einen Ausgabestrom für den Socket
                PrintStream output = new PrintStream(socket.getOutputStream());
                // Gebe die Antwort per println aus
                output.println("Du mich auch, Romeo!");
                // Aufräumen der offenen Verbindungen für diese Anfrage
                input.close();
                output.close();
                socket.close();
            }
        } catch (Exception e) {
            // Beende Server-Socket, wenn Server beendet wird.
            if (server != null)
                server.close();
            e.printStackTrace();
        }
    }
}

UDP-Sockets

Das Protokoll UDP ist ein Streaming-Protokoll, das vor allem im Medienbereich eingesetzt wird. Hier wird schnell gesendet und empfangen. Die Reihenfolge oder der Verlust von Paketen wird in Kauf genommen, sofern es nur keine Aussetzer gibt.

Der Server

Der Server geht in eine typische Endlosschleife. Darin wird über den Datagramm-Socket receive aufgerufen.

Aus dem Paket können nun die Daten und deren Länge über entsprechende Methoden bestimmt werden.

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServer {
    public static void main(String[] args) throws IOException {
        final int SIZE = 2048;
        DatagramSocket socket = new DatagramSocket(33035);
        while (true) {
            byte[] puffer = new byte[SIZE];
            DatagramPacket paket = new DatagramPacket(puffer, SIZE);
            socket.receive(paket);
            int count = paket.getLength();
            byte[] data = paket.getData();
            for(int i=0; i<count; i++) {
                System.out.print(" "+data[i]);
            }
        }
    }
}

Der Client

Der Client verwendet ebenfalls den DatagramSocket, um ein DatagramPacket zu versenden.

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPClient {

    public static void main(String[] args) throws IOException {
        final int SIZE = 20;
        byte[] data = new byte[SIZE];
        for (int i=0; i<SIZE; i++) {
            data[i] = (byte) (i % SIZE);
        }
        InetAddress host = InetAddress.getByName( "localhost" );
        int port = 33035;
        DatagramPacket paket = 
                new DatagramPacket(data, SIZE, host, port );
        DatagramSocket ziel = new DatagramSocket();
        ziel.send(paket);
    }
}