16.8 Client/Server-Kommunikation
 
Bevor wir nun weitere Dienste untersuchen, wollen wir einen kleinen Server programmieren. Server bauen keine eigene Verbindung auf, sondern horchen an ihrem zugewiesenen Port auf Eingaben und Anfragen. Ein Server wird durch die Klasse ServerSocket repräsentiert. Der Konstruktor bekommt einfach die Port-Nummer, zu der sich Clients verbinden können, als Argument übergeben.
Beispiel Wir richten einen Server ein, der am Port 1234 horcht.
ServerSocket serverSocket = new ServerSocket( 1234 );
|
Natürlich müssen wir unserem Client eine noch nicht zugewiesene Port-Adresse zuteilen, andernfalls ist uns eine IOException sicher. Das häufig verwendete 1234 ist zwar schon vom Infoseek Search Agent (search-agent) zugewiesen, sollte aber dennoch nicht zu Problemen führen, da er auf dem eigenen Rechner gewöhnlich nicht installiert ist. Damit sich der Java-Server nicht mit einem anderen Server kneift, sollte ein Blick auf die aktuell laufenden Dienste gezogen werden. Unter Windows listet auf der Kommandozeile netstat -an die laufenden Serverdienste und die belegten Ports auf. Bei Unix-Systemen können nur Root-Besitzer Ports unter 1024 nutzen. Unter dem herkömmlichen Windows ist das egal. Läuft ein Server unendlich, so muss darauf geachtet werden, eine alte Instanz erst zu beenden, damit der Server neu gestartet werden kann.
 Hier klicken, um das Bild zu Vergrößern
16.8.1 Warten auf Verbindungen
 
Nachdem der Socket eingerichtet ist, kann er auf hereinkommende Meldungen reagieren. Mit der blockierenden Methode accept() der ServerSocket-Klasse nehmen wir genau eine wartende Verbindung an:
Socket server = serverSocket.accept();
Nun können wir mit dem zurückgegebenen Client-Socket genau so verfahren wie mit dem schon programmierten Client. Das heißt, wir öffnen Ein- und Ausgabekanäle und kommunizieren. In der Regel wird ein Thread den Client-Socket annehmen, damit der Server schnell wieder verfügbar ist und neue Verbindungen annehmen und verarbeiten kann.
Wichtig bleibt zu bemerken, dass die Konversation nicht über den Server-Socket selbst läuft. Dieser ist immer noch aktiv und horcht auf eingehende Anfragen. Die accept()-Methode sitzt daher oft in einer Endlosschleife und erzeugt für jeden Hörer einen Thread. Die Schritte, die also jeder Server vollzieht, sind folgende:
1. |
Einen Server-Socket erzeugen, der horcht |
|
|
2. |
Mit der accept()-Methode auf neue Verbindungen warten |
|
|
3. |
Ein- und Ausgabestrom vom zurückgegebenen Socket erzeugen |
|
|
4. |
Mit einem definierten Protokoll die Konversation unterhalten |
|
|
5. |
Stream von Client und Socket schließen |
|
|
6. |
Bei Schritt 2 weitermachen oder Server-Socket schließen |
|
|
Der Server wartet auch nicht ewig
Soll der Server nur eine gewisse Zeit auf einkommende Nachrichten warten, so lässt sich ein Timeout einstellen. Dazu ist der Methode setSoTimeout() die Anzahl der Millisekunden zu übergeben. Nimmt der Server dann keine Fragen entgegen, bricht die Verarbeitung mit einer InterruptedIOException ab. Diese Exception gilt für alle Ein- und Ausgabe-Operationen und ist daher auch eine Ausnahme, die nicht im Net-Paket, sondern im IO-Paket deklariert ist.
Beispiel Der Server soll höchstens eine Minute auf eingehende Verbindungen warten.
ServerSocket server = new ServerSocket( port );
// Timeout nach 1 Minute
server.setSoTimeout( 60000 );
try {
Socket socket = server.accept();
} catch ( InterruptedIOException e ) {
System.err.println( "Timeout after one minute" );
}
|
16.8.2 Ein Multiplikations-Server
 
Der erste Server, den wir programmieren wollen, soll zwei Zahlen multiplizieren. Dazu reichen wir ihm im Eingabestrom zwei Zahlen, die er dann multipliziert und zurückschreibt.
Listing 16.14
MulServer.java
import java.net.*;
import java.io.*;
public class MulServer
{
public static void main( String args[] ) throws IOException
{
ServerSocket server = new ServerSocket( 3141 );
while ( true )
{
Socket client = server.accept();
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
int start = in.read();
int end = in.read();
int result = start * end;
out.write( result );
client.close();
}
}
}
Nach dem Start des Programms horcht der Server auf Anfragen auf Port 3141. Kommt es zu einem Verbindungsaufbau, erfragt der Server den Client und die Kommunikationsströme. Über den Eingabestrom werden zwei byte erwartet; die blockierende read()-Methode übernimmt diese Aufgabe. Kommen die Bytes nicht an, wartet der Server ewig auf seine Daten und ist derweil blockiert, da er in diese Implementierung nur einen Client bedient. Bekommt er die beiden Bytes, multipliziert er sie und sendet das Ergebnis zurück. Nach dem Senden ist das Protokoll beendet und die Verbindung zum Client kann beendet werden. Durch die Endlosschleife ist der Server bereit für neue Anfragen.
Auf der anderen Seite ist der Client, der aktiv eine Verbindung zum Server aufbaut. Er nutzt ein mit Internet-Adresse und Port initialisiertes Socket-Objekt, um ein- und ausgehenden Datenstrom zu erfragen und zwei Bytes zu übertragen. Das Ergebnis sammelt er ein und gibt es auf dem Bildschirm aus. Nach der Kommunikation wird die Verbindung geschlossen, um die nötigen Ressourcen wieder freizugeben.
Listing 16.15
MulClient.java
import java.net.*;
import java.io.*;
class MulClient
{
public static void main( String args[] ) throws IOException
{
Socket server = new Socket ( "localhost", 3141 );
InputStream in = server.getInputStream();
OutputStream out = server.getOutputStream();
out.write( 4 );
out.write( 9 );
int result = in.read();
System.out.println( result );
server.close();
}
}
Natürlich ist der Server in der Funktionalität beschränkt, da nur Bytes übertragen werden. So kann das Ergebnis nicht größer als 127 werden, denn ansonsten würde es falsch übermittelt. Dennoch lässt sich das Programm leicht als Ausgangspunkt für einige Server erweitern. Erster Schritt wäre zum Beispiel die Aufwertung der einfachen Byte-orientierten Ströme zum Beispiel in DataInputStream oder DataOutputStream.
Ein anderer Punkt ist, dass Server im Allgemeinen multithreaded ausgelegt sind, damit sie mehrere Anfragen gleichzeitig ausführen können. Noch besser ist, die Threads in einen Thread-Pool zu legen, denn ein neuer Thread pro Anfrager ist eine teure Tat. Mit Java 5 gibt es hierfür die Thread-Pool-Klasse, die diese Arbeit vorzüglich übernimmt.
Hinweis In einer realistischen Client/Server-Anwendung mit Sockets würden immer gepufferte Ströme eingesetzt, um nicht laufend kleine Datenpakete zu senden. Werden jedoch Ströme wie BufferedInputStream oder BufferedOutputStream eingesetzt, so sollte bedacht werden, dass die Informationen im Puffer zwischengespeichert werden und dadurch nicht direkt zum anderen Rechner übertragen werden. In einem Frage-Antwort-Szenario muss die Anfrage direkt übertragen werden und darf nicht im Puffer verweilen. Zu passenden Zeitpunkten müssen somit mit der flush()-Methode der Puffer-Klasse die aufgenommenen Daten übertragen werden, damit die Kommunikation weitergeht.
|
16.8.3 Von außen erreichbar sein
 
Ein Server kann nur auf unserem Rechner gestartet werden. Ist der Rechner vom Internet aus erreichbar, können externe auf ihn zugreifen. Anders sieht das aus, wenn der Rechner eine Internet-Adresse hat, die von außen nicht sichtbar ist, weil der Rechner zum Beispiel über einen Router ins Internet geht. Dann vergibt dieser Router eine eigene Adresse – die oft mit 192.168 oder 10 beginnt – und setzt diese per NAT um, so dass von außen unsere private Adresse verborgen bleibt. Die Frage ist nun, ob wir trotzdem einen Serverdienst anbieten können.
Diese Möglichkeit gibt es tatsächlich, wenn ein paar Randbedingungen gegeben sind. Die erste Bedingung ist, dass unsere interne IP-Adresse relativ stabil ist und unsere äußere IP-Adresse vom Router ins Internet ebenso. Dann muss auf dem Router eine Einstellung vorgenommen werden, dass wir auf bestimmten Ports von außen angesprochen werden können. Diese Einstellung sieht bei jedem Router anders aus und in größeren Unternehmen wird der Sicherheitsverantwortliche dies nicht mit sich machen lassen. Nach dieser Einstellung benötigen wir eine globale Adresse, die wir weitergeben können. Das wird keine IP-Adresse sein, sondern ein Name, der über DNS aufgelöst wird. Das ist schon der Trick, denn der konstant bleibende Name kann mit immer unterschiedlichen IP-Adressen verbunden werden, was die Tatsache abbildet, dass wir zum Beispiel mit einem Einwahl-Router immer unterschiedliche IP-Adressen bekommen. Daher nennt sich diese Technik auch dynamisches DNS. Eine feste Internet-Adresse gibt es bei unterschiedlichen Anbietern, oft auch frei, zum Beispiel bei http://www.orgdns.org/. Nach dieser Anmeldung lässt sich ein Subname registrieren, dass etwa unter meinserver.orgdns.org die IP-Adresse des Einwahl-Routers steht. Dieser leitet dann nach der entsprechenden Einstellung eine Anfrage an unseren Rechner mit unserem Java-Server weiter.
|