3. Netzwerk-Programmierung

Java unterstützt mehrere Möglichkeiten der Datenübermittlung über das Netz. Zum einen bietet es die Möglichkeit, den Inhalt einer URL herunterzuladen. Der Inhalt kann zum Beispiel im Browser angezeigt werden. Die zweite Art der Datenübermittlung geschieht über Sockets. Hierbei wird nochmal zwischen Streamsockets und Datagrammsockets unterschieden. Alle Netzwerkverbindungen basieren prinzipiell auf dem im Internet eingesetzten TCP/IP-Protokoll.

Über Streamsockets lassen sich prinzipiell unbegrenzte Datenströme übers Netz schicken. Datagrammsockets verschicken/empfangen hingegen Packete bestimmter Größe. Für eine Client/Server-Umgebung lassen sich beide Sockets verwenden. In meinen Beispielen verwende ich jedoch nur die Streamsockets.

3.1 Funktionsweise des Servers

In der Klasse java.net werden u.a. die zwei Klassen Socket und ServerSocket definiert.

Einen ServerSocket findet in Serverprozessen Anwendung und dient dazu, auf Verbindungen von Clients zu warten. Um den Server nicht von seiner Aufgabe - z.B. dem Austauschen von Chat-Nachrichten, wenn es sich um einen Chat-Server handelt - abzuhalten, wird der ServerSocket in einem eigenen Thread implementiert. So kann der ServerSocket ungestört lauschen und der Server chatten. Das Lauschen realisiert die im ServerSocket definierte accept-Methode, die man beim Starten des Threads mit aufgerufen wird.

Meldet sich ein Client beim Server an, so wird dieser - genauer gesagt der ServerSocket - eine neue Instanz der Socket-Klasse erzeugen, über die dann mit dem neuen Client kommuniziert werden kann. Von der Programmlogik ist es sinnvoll, für jeden Client wieder einen neuen Thread zu erzeugen, der dann seine Aufmerksamkeit dem Client spendet.

3.2 Einen Server implementieren

Damit der ServerSocket mit seiner Arbeit beginnen kann, muß ihm eine Portnummer mitgeteilt werden. Das geschieht am geschicktesten mit dem Konstruktor - also beim Instantiieren der Klasse.

import java.net.*;
import java.io.*;
    ...
    ServerSocket myServer;                          // Verweis: der Server-Socket    
    try {
        myServer = new ServerSocket(33333);
    }
    catch(IOException e) { ... }                    // Ausnahme abfangen!

Das ist auch schon alles! Jetzt kann mittels accept() auf dem Port gelauscht werden. Die Methode accept() unterbricht die Ausführung des Threads, bis auf dem Port eine ankommende Verbindung festgestellt wird. Kommt eine neue Verbindung zustande, so wird ein Socket erzeugt und zurückgegeben. Im Beispielt wird es in der lokalen Variablen client_socket gespeichert.

    ...
    try {
        while(true) {                               // endlos lauschen
            Socket client_socket = myServer.accept();
            ...                                     // client_socket verarbeiten     
        }
    } catch(IOException e) { ... }                  // Ausnahme abfangen!
    ...

Will man auf weiteres Lauschen verzichten, so kann man jetzt mit den neuen Client kommunizieren. Im anderen Fall ist es sinnvoll, möglichst schnell wieder zu accept zurückzukehren. Vorher übergibt man den client_socket an einen neuen Thread, der dann mit dem Client kommuniziert.

3.3 Funktionsweise des Clients

Für die Arbeit eines Clients ist es nötig, eine Instanz der Socket-Klasse zu erzeugen. Beim Konstruieren werden ihm die IP-Adresse und die Portnumer mitgeteilt, mit denen die Verbindung aufgebaut werden soll. Bereits beim Konstruieren wird die Verbindung aufgebaut.

Nach erfolgreichem Verbindungsaufbau kann abhängig von der Funktion des Clients weiter verfahren werden. Soll z.B. ein Chat-Client implementiert werden, so müssen Tastatureingaben und Ausgaben vom Server gleichzeitig verarbeitet werden. Hier ist es wieder sinnvoll, verschiedene Threads für verschiedene Funktionen zu erzeugen, damit die Aufgaben nicht kollidieren.

3.4 Einen Client implementieren

Da bereits beim Instantiieren der Socket-Klasse der Verbindungsaufbau startet, müssen hier schon verschiedene Fehlerfälle betrachtet werden. Fehler - besser: Ausnahmen - treten auf, wenn der Server down ist, die Adresse falsch ist oder andere exotische Dinge passieren. In allen diesen Fällen wirft der Konstruktor eine IOException, um die wir uns kümmern müssen.

import java.net.Socket;
import java.io.*;
    ...
    Socket mySocket;                                  // Verweis auf Socket-Instanz  
    try {
        mySocket = new Socket("localhost", 33333);    // Erzeuge den Socket
    }
    catch(IOException e) { ... }                      // Ausnahme abfangen!
    ...

3.5 Mit einem Socket arbeiten

Nachdem die Verbindung zustandegekommen ist, kann mit der Kommunikation über den Socket begonnen werden. Hierzu müssen zwei Streams zum Lesen und zum Schreiben geöffnet werden. Dies tut auch der Socket bereits beim Konstruieren. Die Streams können über die Methoden getInputStream und getOutputStream abgerufen werden.

Die Socket-Klasse ist das Gegenstück zum ServerSocket. Während die ServerSocket-Klasse auf Verbindungen wartet und die Verbindungen entgegennimmt, dient die Socket-Klasse ausschließlich zur Kommunikation.

    ...
    Inputstream in;                                   // ein Eingabestream
    Outputstream out;                                 //     Ausgabestream
    try {
        in = mySocket.getInputStream();               // zum Empfangen von Daten     
        out = mySocket.getOutputStream();             // zum Senden von Daten
    }
    catch(IOException e) { ... }                      // Ausnahmen abfangen!
    ...


Zurück zur Übersicht; Beispiele: Class Hierarchy Index