vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 18


Kommunikation über das Internet

Eines der bemerkenswerteren Dinge an Java ist seit seiner Einführung, in welchem Maß die Sprache für das Internet geeignet ist. Wie Sie sich bestimmt vom ersten Tag her erinnern werden, wurde Java ursprünglich als Sprache zur Steuerung eines Netzes interaktiver Geräte mit dem Namen Star7 entwickelt. Duke - das animierte Maskottchen von JavaSoft - war der Star dieser Geräte.

Java`s Klassenbibliothek beinhaltet das Paket java.net, das es Ihren Java-Programmen ermöglicht, über ein Netzwerk zu kommunizieren. Das Paket bietet eine plattformübergreifende Abstraktionsebene für einfache Netzwerkoperationen, darunter Verbindung zu Dateien aufzubauen und diese zu übertragen. Dazu werden Standard- Web-Protokolle verwendet und elementare Sockets, wie sie von Unix her bekannt sind, erzeugt.

In Verbindung mit den Eingabe- und Ausgabestreams, die Sie gestern kennengelernt haben, wird das Lesen und Schreiben von Dateien über ein Netzwerk genauso einfach, wie es von einer lokalen Festplatte ist.

Heute werden Sie einige Applikationen schreiben, die in der Lage sind, über ein Netzwerk zu kommunizieren. Außerdem werden Sie lernen, warum es bedeutend schwieriger ist, dasselbe mit einem Applet zu tun. Sie werden ein Programm erstellen, das ein Dokument über das World Wide Web laden kann, und werden untersuchen, wie Client/-Server-Programme erzeugt werden.

Netzwerkprogrammierung in Java

Dieser Abschnitt beschreibt zwei einfache Wege, wie Sie mit Systemen im Netz kommunizieren können:

Öffnen von Web-Verbindungen

Anstatt den Browser lediglich aufzufordern, den Inhalt einer Datei zu laden, möchten Sie vielleicht den Inhalt der Datei in Ihrem Applet benutzen. Ist die betreffende Datei im Web gespeichert und über die üblichen URL-Formen (http, ftp usw.) zugänglich, können Sie die URL-Klasse benutzen, um die Datei in Ihrem Applet zu verwenden.

Beachten Sie, daß Applets aus Sicherheitsgründen nur zurück zu dem gleichen Host, von dem sie ursprünglich geladen wurden, Verbindungen herstellen können. Das bedeutet beispielsweise bei einem Applet, das auf einem System namens www.myhost.com gespeichert ist, daß Ihr Applet nur mit diesem Host (und dem gleichen Hostnamen, deshalb vorsichtig mit Aliasnamen!) eine Verbindung herstellen kann. Befindet sich eine Datei, die das Applet abrufen möchte, auf dem gleichen System, sind URL-Verbindungen die einfachste Möglichkeit, dies zu erreichen.

Diese Sicherheitseinschränkung wird die Art und Weise, in der Sie Applets bis jetzt geschrieben und getestet haben, ändern. Da wir uns noch nicht mit Netzverbindungen beschäftigt haben, war es uns möglich, alle Tests auf der lokalen Platte durch einfaches Öffnen der HTML-Dateien oder mit dem Werkzeug zum Betrachten des Applets durchzuführen. Dies ist mit Applets, die Netzverbindungen öffnen, nicht möglich. Damit diese Applets richtig funktionieren, müssen Sie eines von zwei Dingen tun:

Sie werden schon merken, ob Ihr Applet und die Verbindung, die es öffnet, auf dem gleichen Server sind. Bei dem Versuch, ein Applet oder eine Datei von unterschiedlichen Servern zu laden, erhalten Sie, zusammen mit anderen auf Ihrem Bildschirm oder der Java-Konsole ausgegebenen Fehlermeldungen, eine Sicherheitsausnahme.

Beschäftigen wir uns jetzt mit den Methoden und Klassen zum Laden von Dateien aus dem Web.

openStream()

Die URL-Klasse definiert eine Methode namens openStream(), die eine Netzverbindung mit einem bestimmten URL öffnet (eine HTTP-Verbindung für Web-URLs, eine FTP-Verbindung für FTP-URLs usw.) und eine Instanz der Klasse InputStream (Teil des java.io-Pakets) ausgibt. Wenn Sie diesen Stream in einen DataInputStream (mit einem BufferedInputStream in der Mitte, um die Leistung zu steigern) konvertieren, können Sie Zeichen und Zeilen aus diesem Stream lesen. Die folgenden Zeilen öffnen beispielsweise eine Verbindung zu der URL, die in der Variablen theURL gespeichert ist, und lesen dann den Inhalt jeder Zeile der Datei und geben ihn auf dem Standardausgabegerät aus:

try {
   InputStream in = theURL.openStream();
   DataInputStream data = new DataInputStream(new BufferedInputStream(in);

   String line;
   while ((line = data.readLine()) != null) {
   System.out.println(line);
}
} catch (IOException e) {
   System.out.println("IO Error: " + e.getMessage());
}


Sie müssen alle Zeilen zwischen eine try...catch-Anweisung setzen, um erzeugte IOExceptions zu berücksichtigen. IOExceptions und die try...catch-Anweisung wurden am Tag 16 behandelt.

Das folgende Beispiel eines Applets nutzt die openStream()-Methode, um eine Verbindung zu einem Web-Standort herzustellen. Dann wird über diese Verbindung eine Datei (»Der Rabe« von Edgar Allen Poe) gelesen und in einem Textbereich angezeigt. Listing 18.1 enthält den Code; das Ergebnis nach dem Lesen der Datei sehen Sie in Abbildung 18.1.


Abbildung 18.1:
Das GetRaven-Applet

Hierzu ein äußerst wichtiger Hinweis: Wenn Sie diesen Code wie geschrieben kompilieren, funktioniert er nicht - und Sie erhalten eine Sicherheitsausnahme. Der Grund dafür ist, daß dieses Applet eine Verbindung zu dem Server www.lne.com zum Holen der Datei raven.txt öffnet. Wenn Sie dieses Applet kompilieren und damit arbeiten, läuft dieses Applet nicht auf www.lne.com (es sei denn, Sie sind »Ich« und kennen somit das Problem bereits). Bevor Sie dieses Applet kompilieren können, müssen Sie unbedingt die Zeile 18 so verändern, daß sie auf eine Kopie von raven.txt auf Ihrem Web-Server verweist und Ihr Applet und Ihre HTML-Dateien auf dem gleichen Server installieren (Sie können raven.txt von der CD oder von der oben angegebenen URL holen).

Alternativ dazu können Sie mit Ihrem Browser zu der URL http://www.lne.com/Web/ JavaProf/GetRaven.html gehen. Diese Webseite lädt genau dieses Applet und sorgt für korrektes Runterladen der Datei. Da sich sowohl das Applet als auch die Textdatei auf dem gleichen Server befinden, funktioniert alles bestens.

Listing 18.1: Der komplette Quelltext der GetRaven-Klasse

 1: import java.awt.*;
 2: import java.io.DataInputStream;
 3: import java.io.BufferedInputStream;
 4: import java.io.IOException;
 5: import java.net.URL;
 6: import java.net.URLConnection;
 7: import java.net.MalformedURLException;
 8:
 9: public class GetRaven extends java.applet.Applet implements Runnable {
10:   URL theURL;
11:  Thread runner;
12:   TextArea ta = new TextArea("Getting text...");
13:
14:   public void init() {
15:    setLayout(new GridLayout(1,1));
16:
17:     // DIESEN TEXT VOR DER KOMPILIERUNG ÄNDERN!!!
18:     String url = "http://www.lne.com/Web/JavaProf/raven.txt";
19:     try { this.theURL = new URL(url); }
20:     catch ( MalformedURLException e) {
21:       System.out.println("Bad URL: " + theURL);
22:     }
23:     add(ta);
24:  }
25:
26:   public Insets insets() {
27:     return new Insets(10,10,10,10);
28:  }
29:
30:   public void start() {
31:     if (runner == null) {
32:       runner = new Thread(this);
33:       runner.start();
34:     }
35:  }
36:
37:   public void stop() {
38:     if (runner != null) {
39:       runner.stop();
40:       runner = null;
41:     }
42:  }
43:
44:   public void run() {
45:     URLConnection conn = null;
46:     DataInputStream data = null;
47:     String line;
48:    StringBuffer buf = new StringBuffer();
49:
50:     try {
51:       conn = this.theURL.openConnection();
52:       conn.connect();
53:      ta.setText("Connection opened...");
54:       data = new DataInputStream(new BufferedInputStream(
55:          conn.getInputStream()));
56:       ta.setText("Reading data...");
57:      while ((line = data.readLine()) != null) {
58:         buf.append(line + "\n");
59:       }
60:       ta.setText(buf.toString());
61:     }
62:     catch (IOException e) {
63:       System.out.println("IO Error:" + e.getMessage());
64:     }
65:}
66:}

Die init()-Methode (Zeilen 14 bis 24) setzt die URL und richtet den Textbereich ein, in dem diese Datei angezeigt wird. Die URL könnte leicht über einen HTML-Parameter an das Applet abgegeben werden; hier wurde er der Einfachheit halber hart kodiert. Da es einige Zeit dauern kann, bis die Datei über das Netz geladen wird, stellen Sie diese Routine in einen eigenen Thread und benutzen die Ihnen inzwischen bestens bekannten Methoden start(), stop() und run(), um diesen Thread zu steuern. Innerhalb von run() (Zeilen 44 bis 64) findet die eigentliche Arbeit statt. Hier initialisieren Sie mehrere Variablen und öffnen dann die Verbindung zu der URL (mit der openStream()-Methode in Zeile 50). Ist die Verbindung aufgebaut, richten Sie in den Zeilen 51 bis 55 einen Eingabestream ein, von dem zeilenweise gelesen wird. Das Ergebnis wird in eine Instanz von StringBuffer (das ist eine änderbare Zeichenkette) gestellt. Ich stelle die gesamte Arbeit in einen Thread, da der Verbindungsaufbau und das Lesen der Datei - insbesondere über langsamere Verbindungen, einige Zeit in Anspruch nehmen kann. Parallel zum Laden der Datei sind möglicherweise andere Aktivitäten in dem Applet auszuführen.

Nachdem alle Daten gelesen wurden, konvertiert Zeile 60 das StringBuffer-Objekt in eine echte Zeichenkette und stellt das Ergebnis in den Textbereich.

Bezüglich dieses Beispiels ist noch etwas anderes zu beachten: nämlich daß der Teil des Codes, der eine Netzverbindung geöffnet, aus der Datei gelesen und eine Zeichenkette erstellt hat, zwischen eine try...catch-Anweisung gestellt wird. Tritt während des Versuchs, die Datei zu lesen oder zu verarbeiten, ein Fehler auf, ermöglicht diese Anweisung die Fehlerbehandlung, ohne daß das gesamte Programm abstürzt (in diesem Fall endet das Programm mit einem Fehler, weil ansonsten wenig getan werden kann, wenn das Applet die Datei nicht lesen kann). Mit try...catch können Sie Ihrem Applet die Möglichkeit geben, auf Fehler zu reagieren und diese entsprechend zu behandeln.

Sockets

Für vernetzte Anwendungen, die über das hinausgehen, was die Klassen URL und URLconnection bieten (z.B. für andere Protokolle oder allgemeinere vernetzte Anwendungen), bietet Java die Klassen Socket und ServerSocket als Abstraktion von standardmäßigen TCP-Socket-Programmiertechniken.


Java bietet ebenfalls Möglichkeiten der Verwendung von Datagram-Sockets (UDP, User Datagram Protocol), auf die ich hier allerdings nicht eingehen werde. Wenn Sie daran interessiert sind, mit Datagrammen zu arbeiten, finden Sie entsprechende Informationen in der API-Dokumentation des java.net-Pakets.

Die Socket-Klasse bietet eine clientseitige Socket-Schnittstelle, die mit Unix-Standard- Sockets vergleichbar ist. Um eine Verbindung herzustellen, legen Sie eine neue Instanz von Socket an (wobei der hostname der Host ist, zu dem die Verbindung herzustellen, und portnum die Portnummer ist):

Socket connection = new Socket(hostname, portnum);

Auch wenn Sie Sockets in einem Applet verwenden, unterliegen Ihre Applets nach wie vor den Sicherheitseinschränkungen, die Sie daran hindern, eine Verbindung zu einem anderen als dem System, von dem das Applet geladen wird, herzustellen.

Nachdem Sie den Socket geöffnet haben, können Sie Ein- und Ausgabestreams verwenden, um über diesen Socket zu lesen und zu schreiben:

DataInputStream in = new DataInputStream(
new BufferedInputStream(connection.getInputStream()));
DataOutputStream out= new DataOutputStream(
new BufferedOutputStream(connection.getOutputStream()));

Zum Schluß müssen Sie den Socket schließen (dadurch werden auch alle Ein- und Ausgabestreams geschlossen, die Sie für diesen Socket eingerichtet haben):

connection.close();

Server-seitige Sockets funktionieren auf ähnliche Weise, mit Ausnahme der accept()- Methode. Ein Server-Socket richtet sich nach einem TCP-Port, um eine Client-Verbindung aufzubauen; wenn sich ein Client mit diesem Port verbindet, akzeptiert die accept()-Methode eine Verbindung von diesem Client. Durch Verwendung von Client- und Server-Sockets können Sie Anwendungen entwickeln, die miteinander über das Netz kommunizieren.

Um einen Server-Socket zu erstellen und an einen Port anzubinden, legen Sie eine neue Instanz von ServerSocket mit der Portnummer an:

ServerSocket sconnection = new ServerSocket(8888);

Um diesen Port zu bedienen (und bei Anfrage eine Verbindung von Clients entgegenzunehmen), benutzen Sie die accept()-Methode:

sconnection.accept();

Sobald die Socket-Verbindung aufgebaut ist, können Sie die Ein- und Ausgabestreams verwenden, um vom Client zu lesen und zu schreiben.

Im nächsten Abschnitt, »Trivia: Ein einfacher Socket-Client und -Server«, gehen wir einige Codes durch, um eine einfache Socket-basierte Anwendung zu realisieren.

In der Version 1.02 von Java bieten die Klassen Socket und ServerSocket eine einfache abstrakte Socket-Implementierung. Sie können neue Instanzen dieser Klassen zum Aufbau oder Akzeptieren von Verbindungen anlegen und zur Weiter- oder Rückgabe von Daten von einem Client an einen Server.

Das Problem entsteht bei dem Versuch, das Socket-Verhalten von Java zu erweitern oder zu ändern. Die Klassen Socket und ServerSocket im java.net-Paket sind Final- Klassen, was bedeutet, daß Sie von diesen Klassen keine Subklassen erzeugen können. Um das Verhalten der Socket-Klassen zu erweitern - beispielsweise um es Netzverbindungen zu ermöglichen, über eine Firewall oder einen Proxy zu arbeiten -, können Sie die abstrakten Klassen SocketImpl und die Schnittstelle SocketImplFactory verwenden, um eine neue Transportebene der Socket-Implementierung zu erstellen. Dieses Design stimmt mit dem ursprünglichen Konzept für die Java-Socket-Klassen überein: es diesen Klassen zu ermöglichen, mit unterschiedlichen Transportmechanismen auf andere Systeme portierbar zu sein. Das Problem dieses Mechanismus besteht darin, daß, während er in einfachen Fällen funktioniert, er es Ihnen aber nicht ermöglicht, zusätzlich zu TCP noch andere Protokolle hinzuzufügen (z.B. einen Verschlüsselungsmechanismus wie SSL zu realisieren) oder mehrere Socket-Implementierungen zur Java-Laufzeit zu haben.

Deshalb wurden Sockets nach Java 1.02 so geändert, daß die Klassen Socket und ServerSocket »nicht final« und erweiterbar sind. Sie können jetzt Subklassen dieser Klassen mit Java 1.1 erstellen, die entweder die Standard-Socket-Implementierung benutzen oder eine von Ihnen selbst kreierte. Dies gestaltet die Netzwerkfähigkeiten wesentlich flexibler.

Darüber hinaus wurden dem java.net-Paket verschiedene neue Features hinzugefügt:

Trivia: Ein einfacher Socket-Client und -Server

Den Abschluß des Themas Netzwerkprogrammierung mit Java bildet das Beispiel eines Java-Programms, das die Socket-Klasse zur Realisierung einer einfachen netzbasierten Anwendung namens Trivia benutzt.

Trivia arbeitet folgendermaßen: Das Server-Programm wartet geduldig auf die Herstellung einer Verbindung eines Clients. Wird die Verbindung von einem Client hergestellt, übermittelt der Server eine Frage und wartet auf die Reaktion. Am anderen Ende erhält der Client die Frage und veranlaßt den Benutzer zur Antwort. Der Benutzer gibt eine Antwort ein, die an den Server zurückübermittelt wird. Der Server überprüft dann, ob die Antwort richtig ist, und informiert den Benutzer. Der Server faßt noch einmal nach, indem er den Client fragt, ob er eine andere Frage möchte. Falls ja, wird der Prozeß wiederholt.

Trivia entwerfen

Im allgemeinen erweist es sich als zweckdienlich, bevor Sie damit beginnen, in umfangreichem Maße Code zu produzieren, einen kurzen vorläufigen Entwurf anzufertigen . Schauen wir uns also zuerst einmal an, was wir für den Trivia-Server und -Client benötigen. Server-seitig brauchen Sie ein Programm, das einen spezifischen Port der Hostmaschine hinsichtlich Client-Verbindungen überwacht. Wird ein Client entdeckt, wählt der Server eine Zufallsfrage und übermittelt sie über diesen spezifischen Port an den Client. Der Server gibt dann einen Wartestatus ein, bis er erneut eine Reaktion vom Client verzeichnet. Erhält der Server eine Antwort vom Client, überprüft er sie und gibt dem Client bekannt, ob die Antwort richtig oder falsch ist. Anschließend fragt der Server den Client, ob er eine weitere Frage wünscht, woraufhin er bis zur Antwort des Clients einen weiteren Wartestatus eingibt. Abschließend wiederholt der Server entweder den Prozeß, indem er eine weitere Frage stellt, oder beendet die Verbindung mit dem Client. Zusammenfassend führt der Server die folgenden Aufgaben aus:

1. Warten auf die Verbindungsherstellung eines Clients

2. Akzeptieren der Client-Verbindung

3. Übermittlung einer Zufallsfrage an den Client

4. Warten auf eine Antwort vom Client

5. Überprüfung der Antwort und Information des Clients

6. Anfrage an den Client, ob er eine weitere Frage wünscht

7. Warten auf eine Antwort vom Client

8. Falls erforderlich, erneutes Ansetzen bei Schritt 3.

Client-seitig ist dieses Trivia-Beispiel eine Anwendung, die von einer Befehlszeile aus arbeitet (auf diese Art leichter zu demonstrieren). Der Client ist für die Verbindungsherstellung zum Server zuständig und wartet auf eine Frage. Bei Erhalt einer Frage vom Server zeigt der Client diese dem Benutzer an und gibt dem Benutzer die Möglichkeit zur Eingabe einer Antwort. Diese Antwort wird an den Server zurückübermittelt, und der Client wartet wieder auf die Reaktion des Servers. Der Client zeigt dem Benutzer die Antwort des Servers an und ermöglicht dem Benutzer zu bestätigen, ob er eine weitere Frage wünscht. Der Client sendet dann die Antwort des Benutzers an den Server und beendet die Verbindung, falls der Benutzer keine weiteren Fragen wünscht. Die hauptsächlichen Aufgaben des Clients sind:

1. Herstellen der Verbindung zum Server

2. Warten auf eine zu übermittelnde Frage

3. Anzeige der Frage und Eingabe der Antwort des Benutzers

4. Übermittlung der Antwort an den Server

5. Warten auf Antwort vom Server

6. Anzeige der Antwort des Servers und Veranlassung des Benutzers zur Bestätigung einer weiteren Frage

7. Übermittlung der Antwort des Benutzers an den Server

8. Falls erforderlich, erneutes Ansetzen bei Schritt 2.

Trivia-Server implementieren

Der Server bildet den wesentlichsten Bestandteil bei den Trivia-Beispielen. Das Trivia- Server-Programm heißt TriviaServer. Hier die in der TriviaServer-Klasse definierten Instanzvariablen:

private static final int PORTNUM = 1234;
private static final int WAITFORCLIENT = 0;
private static final int WAITFORANSWER = 1;
private static final int WAITFORCONFIRM = 2;
private String[] questions;
private String[] answers;
private ServerSocket serverSocket;
private int numQuestions;
private int num = 0;
private int state = WAITFORCLIENT;
private Random rand = new Random(System.currentTimeMillis());

Die Konstanten WAITFORCLIENT, WAITFORANSWER und WAITFORCONFIRM sind allesamt Statuskonstanten, die der Definition unterschiedlicher Status, in denen sich der Server befinden kann, dienen. Den Einsatz dieser Konstanten sehen Sie gleich. Die Frage- und Antwortvariablen sind Zeichenketten-Arrays zur Speicherung der Fragen und Antworten. Die serverSocket-Instanzvariable richtet sich nach der Server-Socket-Verbindung. numQuestions wird zur Speicherung der Gesamtanzahl der Fragen benutzt, wobei num die Anzahl der aktuell gestellten Fragen wiedergibt. Die state-Variable verfügt über den aktuellen Status des Servers wie von den drei Statuskonstanten (WAITFORCLIENT, WAITFORANSWER und WAITFORCONFIRM) festgelegt. Und die rand-Variable wird dazu verwendet, zufällig Fragen auszuwählen.

Der TriviaServer-Konstruktor macht nicht viel, mit Ausnahme der Erstellung eines ServerSocket anstatt eines DatagramSocket. Schauen Sie sich`s an:

public TriviaServer() {
super("TriviaServer");
try {
serverSocket = new ServerSocket(PORTNUM);
System.out.println("TriviaServer up and running..."); 
}
catch (IOException e) {
System.err.println("Exception: couldn't create socket");
System.exit(1);
}
}

Der größte Teil der Aktionen spielt sich in der run()-Methode in der TriviaServer- Klasse ab. In Anschluß sehen Sie den Quellcode für die run()-Methode.

public void run() {
   Socket  clientSocket;

   // Initialisieren der Fragen-/Antwort-Arrays 
   if (!initQnA()) {
     System.err.println("Error: couldn't initialize questions and answers");
     return;
   }

   // Nach Clients suchen und Trivia-Fragen stellen 
   while (true) {
     // Auf Client warten 
     if (serverSocket == null)
       return;
     try {
       clientSocket = serverSocket.accept();
     }
     catch (IOException e) {
       System.err.println("Exception: couldn't connect to client socket");
       System.exit(1);
     }

     // Fragen-/Antwortverarbeitung durchführen 
     try {
       DataInputStream is = new DataInputStream(new
       BufferedInputStream(clientSocket.getInputStream()));
       PrintStream os = new PrintStream(new
       BufferedOutputStream(clientSocket.getOutputStream()), false);
       String inLine, outLine;

       // Serveranfrage ausgeben 
       outLine = processInput(null);
       os.println(outLine);
       os.flush();

       // Verarbeitung und Ausgabe der Benutzereingabe 
       while ((inLine = is.readLine()) != null) {
         outLine = processInput(inLine);
         os.println(outLine);
         os.flush();
         if (outLine.equals("Bye."))
           break;
       }

       // Aufräumen
       os.close();
       is.close();
       clientSocket.close();
     }
     catch (Exception e) {
       System.err.println("Exception: " + e);
       e.printStackTrace();
     }
   }
 }

Durch Aufrufen von initQnA() initialisiert die run()-Methode zunächst die Fragen und Antworten. Über die initQnA()-Methode werden Sie gleich mehr erfahren. Danach wird eine Endlos-while-Schleife gestartet, die auf eine Client-Verbindung wartet. Stellt ein Client die Verbindung her, werden die entsprechenden I/O-Streams erzeugt und die Kommunikation durch die processInput()-Methode verarbeitet. processInput() ist unser nächstes Thema. processInput() verarbeitet kontinuierlich Client- Antworten und sorgt für das Stellen neuer Fragen, bis der Client sich entschließt, keine weiteren Fragen mehr zu erhalten. Dies wird vom Server entsprechend durch Übermitteln der Zeichenkette »Bye.« bestätigt. Die run()-Methode sorgt anschließend dafür, daß die Streams und der Client-Socket geschlossen werden.

Die processInput()-Methode richtet sich nach dem Server-Status und stellt die Logik des gesamten Fragen-/Antwortprozesses. Im Anschluß finden Sie den Quellcode für processInput.

String processInput(String inStr) {
    String outStr = null;

    switch (state) {
        case WAITFORCLIENT:
            // Eine Frage stellen
            outStr = questions[num];
            state = WAITFORANSWER;
            break;

        case WAITFORANSWER:
            // Die Antwort prüfen
            if (inStr.equalsIgnoreCase(answers[num]))
                outStr = "That's correct! Want another? (y/n)";
            else
                outStr = "Wrong, the correct answer is " + answers[num] +
                    ". Want another? (y/n)";
            state = WAITFORCONFIRM;
            break;

        case WAITFORCONFIRM:
            // Prüfen, ob eine weitere Frage gewünscht wird
            if (inStr.equalsIgnoreCase("Y")) {
                num = Math.abs(rand.nextInt()) % questions.length;
                outStr = questions[num];
                state = WAITFORANSWER;
            }
            else {
                outStr = "Bye.";
                state = WAITFORCLIENT;
            }
            break;
    }
    return outStr;
}

Als erstes ist bei der processInput()-Methode die lokale Variable outStr zu beachten. Der Wert dieser Zeichenkette wird in der Run-Methode an den Client zurückgesandt, wenn processInput() antwortet. Beachten Sie also, wie processInput() die lokale Variable outStr benutzt, um Informationen an den Client zurückzuführen.

In FortuneServer stellt der Status WAITFORCLIENT den Server im Status leer und auf eine Client-Verbindung wartend dar. Das bedeutet also, daß jedes case-Statement in der processInput()-Methode den Server in dem Status, den er gerade verläßt, darstellt. Das case-Statement WAITFORCLIENT wird beispielsweise eingegeben, wenn der Server den Status WAITFORCLIENT gerade verlassen hat. Anders ausgedrückt hat ein Client gerade eine Verbindung zum Server hergestellt. In diesem Fall setzt der Server die Ausgabezeichenkette auf die aktuelle Frage und den Status auf WAITFORANSWER.

Verläßt der Server den Status WAITFORANSWER, bedeutet dies, daß der Client mit einer Antwort reagiert hat. processInput() vergleicht die Antwort des Clients mit der richtigen Antwort und setzt dementsprechend die Ausgabezeichenkette. Anschließend setzt sie den Status auf WAITFORCONFIRM.

Im WAITFORCONFIRM-Status wartet der Server auf eine Bestätigungsantwort vom Client. In der Methode processInput() zeigt das case-Statement WAITFORCONFIRM an, daß der Server den Status verläßt, da der Client mit einer Bestätigung (ja oder nein) geantwortet hat. Hat der Client die Frage mit y bejaht, wählt processInput() eine neue Frage und setzt den Status wieder auf WAITFORANSWER. Andernfalls gibt der Server Bye. an den Client aus und setzt den Status erneut auf WAITFORCLIENT, um auf eine neue Client-Verbindung zu warten.

Die Trivia-Fragen und -Antworten sind in einer Textdatei namens QnA.txt gespeichert und dort zeilenweise mit Fragen und Antworten im Wechsel aufgebaut. Wechselweise bedeutet, daß auf jede Frage eine entsprechende Antwort auf der nächsten Zeile folgt, woran sich wiederum die nächste Frage anschließt. Hier eine teilweise Auflistung der Datei QnA.txt:

What caused the craters on the moon?
meteorites 
How far away is the moon (in miles)?
239000
How far away is the sun (in millions of miles)?
93
Is the Earth a perfect sphere?
no
What is the internal temperature of the Earth (in degrees)?
9000

Die initQnA()-Methode kümmert sich um das Lesen der Fragen und Antworten aus der Textdatei und deren Speicherung in getrennten String-Arrays. Im folgenden sehen Sie den Quellcode für die initQnA()-Methode.

private boolean initQnA() {
    try {
        File inFile = new File("QnA.txt");
        FileInputStream inStream = new FileInputStream(inFile);
        byte[] data = new byte[(int)inFile.length()];

        // Die Fragen und Antworten in ein Array vom Typ byte einlesen
        if (inStream.read(data) <= 0) {
            System.err.println("Error: couldn't read questions and answers");
            return false;
        }
        // Die Anzahl der Fragen/Antworten-Paare ermitteln
        for (int i = 0; i < data.length; i++)
            if (data[i] == (byte)'\n')
                numQuestions++;
        numQuestions /= 2;
        questions = new String[numQuestions];
        answers = new String[numQuestions];

        // Die Fragen und Antworten in String-Arrays einlesen
        int start = 0, index = 0;
        boolean isQ = true;
        for (int i = 0; i < data.length; i++)
            if (data[i] == (byte)'\n') {
                if (isQ) {
                    questions[index] = new String(data, start, i - start - 1);
                    isQ = false;
                }
                else {
                    answers[index] = new String(data, start, i - start - 1);
                    isQ = true;
                    index++;
                }
            start = i + 1;
        }
    }
    catch (FileNotFoundException e) {
        System.err.println("Exception: couldn't find the question file");
        return false;
    }
    catch (IOException e) {
        System.err.println("Exception: I/O error trying to read questions");
        return false;
    }

    return true;
}

Die initQnA()-Methode setzt zwei Arrays ein und füllt sie abwechselnd mit Zeichenketten aus der QnA.txt-Datei: erst eine Frage, dann eine Antwort, jeweils im Wechsel, bis das Dateiende erreicht ist.

Die einzige in TriviaServer verbleibende Methode ist main(), die lediglich das Server-Objekt erzeugt und es mit einem Aufruf an die start()-Methode startet:

public static void main(String[] args) {
TriviaServer server = new TriviaServer();
server.start();
}

In Listing 18.2 finden Sie den vollständigen Quelltext der Server-Applikation.

Listing 18.2: Der gesamte Quelltext von TriviaServer.java

  1: import java.io.*;
  2: import java.net.*;
  3: import java.util.Random;
  4:
  5: public class TriviaServer extends Thread {
  6:     private static final int PORTNUM = 1234;
  7:     private static final int WAITFORCLIENT = 0;
  8:     private static final int WAITFORANSWER = 1;
  9:     private static final int WAITFORCONFIRM = 2;
 10:     private String[] questions;
 11:     private String[] answers;
 12:     private ServerSocket serverSocket;
 13:     private int numQuestions;
 14:     private int num = 0;
 15:     private int state = WAITFORCLIENT;
 16:     private Random rand = new Random();
 17:
 18:     public TriviaServer() {
 19:         super("TriviaServer");
 20:         try {
 21:             serverSocket = new ServerSocket(PORTNUM);
 22:             System.out.println("TriviaServer up and running ...");
 23:         }
 24:         catch (IOException e) {
 25:             System.err.println("Exception: couldn't create socket");
 26:             System.exit(1);
 27:         }
 28:     }
 29:
 30:     public static void main(String[] arguments) {
 31:         TriviaServer server = new TriviaServer();
 32:         server.start();
 33:     }
 34:
 35:     public void run() {
 36:         Socket clientSocket = null;
 37:
 38:         // Initialisieren der Fragen-/Antwort-Arrays
 39:         if (!initQnA()) {
 40:             System.err.println("Error: couldn't initialize questions
                 å and answers");
 41:             return;
 42:         }
 43:
 44:         // Nach Clients suchen und Trivia-Fragen stellen
 45:         while (true) {
 46:             // Auf Client warten
 47:             if (serverSocket == null)
 48:                 return;
 49:             try {
 50:                 clientSocket = serverSocket.accept();
 51:             }
 52:             catch (IOException e) {
 53:                 System.err.println("Exception: couldn't connect to
                     å client socket");
 54:                 System.exit(1);
 55:             }
 56:
 57:             // Fragen-/Antwortverarbeitung durchführen
 58:             try {
 59:                 InputStreamReader isr = new
                     å InputStreamReader(clientSocket.getInputStream());
 60:                 BufferedReader is = new BufferedReader(isr);
 61:                 PrintWriter os = new PrintWriter(new
 62:                     BufferedOutputStream(clientSocket.getOutputStream()),
                         å false);
 63:                 String outLine;
 64:
 65:                 // Serveranfrage ausgeben
 66:                 outLine = processInput(null);
 67:                 os.println(outLine);
 68:                 os.flush();
 69:
 70:                 // Verarbeitung und Ausgabe der Benutzereingabe
 71:                 while (true) {
 72:                     String inLine = is.readLine();
 73:                     if (inLine.length() > 0) {
 74:                         outLine = processInput(inLine);
 75:                         os.println(outLine);
 76:                         os.flush();
 77:                         if (outLine.equals("Bye."))
 78:                             break;
 79:                     }
 80:                 }
 81:
 82:                 // Aufräumen
 83:                 os.close();
 84:                 is.close();
 85:                 clientSocket.close();
 86:             }
 87:             catch (Exception e) {
 88:                 System.err.println("Exception: " + e);
 89:                 e.printStackTrace();
 90:             }
 91:         }
 92:     }
 93:
 94:     private boolean initQnA() {
 95:         try {
 96:             File inFile = new File("QnA.txt");
 97:             FileInputStream inStream = new FileInputStream(inFile);
 98:             byte[] data = new byte[(int)inFile.length()];
 99:
100:             // Die Fragen und Antworten in ein Array vom Typ byte einlesen
101:             if (inStream.read(data) <= 0) {
102:                 System.err.println("Error: couldn't read questions and
                     å answers");
103:                 return false;
104:             }
105:
106:             // Die Anzahl der Fragen/Antworten-Paare ermitteln
107:             for (int i = 0; i < data.length; i++)
108:                 if (data[i] == (byte)'\n')
109:                     numQuestions++;
110:             numQuestions /= 2;
111:             questions = new String[numQuestions];
112:             answers = new String[numQuestions];
113:
114:             // Die Fragen und Antworten in String-Arrays einlesen
115:             int start = 0, index = 0;
116:             boolean isQ = true;
117:             for (int i = 0; i < data.length; i++)
118:                 if (data[i] == (byte)'\n') {
119:                     if (isQ) {
120:                         questions[index] = new String(data, start, i - start
- 1);
121:                         isQ = false;
122:                     }
123:                     else {
124:                         answers[index] = new String(data, start, i - start - 1);
125:                         isQ = true;
126:                         index++;
127:                     }
128:                 start = i + 1;
129:             }
130:         }
131:         catch (FileNotFoundException e) {
132:             System.err.println("Exception: couldn't find the question file");
133:             return false;
134:         }
135:         catch (IOException e) {
136:             System.err.println("Exception: I/O error trying to read
                 å questions");
137:             return false;
138:         }
139:
140:         return true;
141:     }
142:
143:     String processInput(String inStr) {
144:         String outStr = null;
145:
146:         switch (state) {
147:             case WAITFORCLIENT:
148:                 // Eine Frage stellen
149:                 outStr = questions[num];
150:                 state = WAITFORANSWER;
151:                 break;
152:
153:             case WAITFORANSWER:
154:                 // Die Antwort prüfen
155:                 if (inStr.equalsIgnoreCase(answers[num]))
156:                     outStr = "That's correct! Want another? (y/n)";
157:                 else
158:                     outStr = "Wrong, the correct answer is " + answers[num] +
159:                         ". Want another? (y/n)";
160:                 state = WAITFORCONFIRM;
161:                 break;
162:
163:             case WAITFORCONFIRM:
164:                 // Prüfen, ob eine weitere Frage gewünscht wird
165:                 if (inStr.equalsIgnoreCase("Y")) {
166:                     num = Math.abs(rand.nextInt()) % questions.length;
167:                     outStr = questions[num];
168:                     state = WAITFORANSWER;
169:                 }
170:                 else {
171:                     outStr = "Bye.";
172:                     state = WAITFORCLIENT;
173:                 }
174:                 break;
175:         }
176:         return outStr;
177:     }
178: }

Den Trivia-Client implementieren

Da Client-seitig im Trivia-Beispiel der Benutzer Antworten eingeben und Rückantworten vom Server erhalten muß, stellt sich die Implementierung als Befehlszeilenanwendung unkomplizierter dar. Vielleicht ist das nicht so nett wie ein grafisches Applet, gestaltet es aber sehr einfach, die Entwicklung der Kommunikationsereignisse zu verfolgen. Die Client-Anwendung heißt Trivia.

Die einzige in der Trivia-Klasse definierte Instanzvariable ist PORTNUM, die zur Definition der sowohl vom Client als auch vom Server benutzten Portnummer dient. Es gibt auch nur eine in der Trivia-Klasse definierte Methode: main(). Der Quellcode der main()-Methode ist in Listing 18.3 aufgeführt.

Listing 18.3: Der gesamte Quelltext von Trivia.java

 1: import java.io.*;
 2: import java.net.*;
 3:
 4: public class Trivia {
 5:     private static final int PORTNUM = 1234;
 6:
 7:     public static void main(String[] arguments) {
 8:         Socket socket = null;
 9:         InputStreamReader isr = null;
10:         BufferedReader in = null;
11:         PrintWriter out = null;
12:         String address;
13:
14:         // Befehlszeilenargumente für Host-Adresse prüfen
15:         if (arguments.length != 1) {
16:             System.out.println("Usage: java Trivia <address>");
17:             return;
18:         }
19:         else
20:             address = arguments[0];
21:
22:         // Socket und Streams initialisieren
23:         try {
24:             socket = new Socket(address, PORTNUM);
25:             isr = new InputStreamReader(socket.getInputStream());
26:             in = new BufferedReader(isr);
27:             out = new PrintWriter(socket.getOutputStream(),true);
28:         }
29:         catch (IOException e) {
30:             System.err.println("Exception: couldn't create stream socket "
31:                 + e.getMessage());
32:             System.exit(1);
33:         }
34:
35:         // Benutzereingabe und Server-Reaktion verarbeiten
36:         try {
37:             StringBuffer str = new StringBuffer(128);
38:             String inStr;
39:             int c;
40:
41:             while ((inStr = in.readLine()) != null) {
42:                 System.out.println("Server: " + inStr);
43:                 if (inStr.equals("Bye."))
44:                     break;
45:                 while ((c = System.in.read()) != '\n')
46:                     str.append((char)c);
47:                 System.out.println("Client: " + str);
48:                 out.println(str.toString());
49:                 out.flush();
50:                 str.setLength(0);
51:             }
52:             // Aufräumen
53:             out.close();
54:             in.close();
55:             socket.close();
56:         }
57:         catch (IOException e) {
58:             System.err.println("I/O error: "+ e.toString());
59:         }
60:     }
61: }

Was Ihnen vielleicht als erstes bei der main()-Methode auffällt ist, daß sie ein Befehlszeilenargument sucht. Das erforderliche Befehlszeilenargument des Trivia-Clients ist die Server-Adresse wie z.B. thetribe.com. Da es sich hierbei um eine Java-Anwendung und nicht um ein Applet handelt, reicht es nicht aus, nur die Verbindung zurück zum Server, von dem das Applet stammt, herzustellen - es gibt keinen Standard-Server, Sie können also die Verbindung zu einem beliebigen Server herstellen. In der Client-Anwendung müssen Sie die Server-Adresse entweder hart kodieren oder sie als Befehlszeilenargument anfordern. Ich bin kein großer Freund des Hartkodierens, da jede Änderung erneutes Kompilieren notwendig macht. Also das Befehlszeilenargument!


Die meisten Leser werden wahrscheinlich keinen Zugriff auf einen Web-Server haben, der Server-seitige Java-Programme wie die TriviaServer-Applikation ausführt. Unter manchen Betriebssystemen können Sie Serverprogramme testen, indem Sie den Trivia-Server in einem Fenster und den Trivia-Client in einem anderen Fenster ausführen, und die Domäne »localhost« verwenden. Im Anschluß ein Beispiel hierfür:

java Trivia "localhost"

Dies veranlaßt Java dazu, auf dem lokalen Host - mit anderen Worten dem System, auf dem die Applikation ausgeführt wird - nach einem Server zu suchen, mit dem eine Verbindung aufgebaut werden kann. Abhängig davon, wie Internet-Verbindungen auf Ihrem System eingerichtet wurden, müssen Sie sich eventuell mit dem Internet verbinden, bevor eine erfolgreiche Socket-Verbindung zwischen dem Trivia-Client und dessen Server aufgebaut werden kann.

Ist das Befehlszeilenargument der Server-Adresse gültig (ungleich Null), erstellt die main()-Methode den erforderlichen Socket und I/O-Streams. Anschließend startet sie eine while-Schleife, wobei sie Informationen vom Server verarbeitet und Benutzeranfragen an den Server zurückübermittelt. Wenn der Server die Übermittlung von Informationen beendet (mit Bye.), wird die while-Schleife mit break verlassen, und die main()-Methode sorgt für das Schließen des Sockets und der Streams. Jetzt wissen Sie alles über den Trivia-Client!

Trivia starten

Der Trivia-Server muß laufen, damit der Client arbeiten kann. Dazu müssen Sie zuerst unter Verwendung des Java-Interpreters den Server starten; dies wird folgendermaßen über einen Befehl auf Befehlszeilen-Ebene erreicht:

java TriviaServer

Auch der Trivia-Client wird über die Befehlszeile gestartet, allerdings müssen Sie eine Server-Adresse als einziges Argument spezifizieren. Nachfolgend ein Beispiel zum Starten des Trivia-Clients und Anschluß an den Server localhost:

java Trivia "localhost"

Wenn Trivia-Client läuft und einige Fragen beantwortet hat, sollte die Ausgabe etwa so aussehen:

Server: Is the Galaxy rotating?
yes
Client: yes
Server: That's correct! Want another? (y/n)
y
Client: y
Server: Is the Earth a perfect sphere?
no
Client: no
Server: That's correct! Want another? (y/n)
y
Client: y
Server: What caused the craters on the moon?
asteroids
Client: asteroids
Server: Wrong, the correct answer is meteorites. Want another? (y/n)
n
Client: n
Server: Bye.

Zusammenfassung

Die Netzwerkprogrammierung hat viele Anwendungen, die Sie in Ihren Anwendungen nutzen können. Sie haben es vielleicht nicht gemerkt, aber das GetRaven-Projekt war ein sehr rudimentärer Webbrowser. Hier wurde über das Internet der Inhalt einer Text-Datei zu einem Java-Programm übertragen und dort angezeigt. Natürlich fehlt jegliche Funktionalität für das HTML-Parsing. JavaSoft hat allerdings einen kompletten Webbrowser in Java geschrieben - HotJava.

Heute haben Sie gelernt, wie Sie URLs, URL-Verbindungen und Eingabestreams gemeinsam verwenden, um Daten aus dem World Wide Web in Ihre Programme zu bekommen.

Sie haben auch gelernt, wie Client- und Server-Programme in Java geschrieben werden und wie ein Server-Programm sich an einen Internet-Port hängt, um auf ein Client-Programm zu warten, das Kontakt aufnimmt.

Fragen und Antworten

Frage:
Wie kann ich eine HTML-Übertragung in einem Java-Applet simulieren?

Antwort:
Derzeit ist das in Applets schwierig. Die beste (und einfachste) Möglichkeit ist die Verwendung der GET-Notation, um den Browser zu veranlassen, den Formularinhalt für Sie einzureichen.

HTML-Formulare können auf zwei Arten übermittelt werden: durch Verwendung der GET-Anfrage oder mit POST. Wenn Sie GET verwenden, werden die Informationen Ihres Formulars in der URL kodiert. Das kann etwa so aussehen:

http://www.blah.com/cgi-bin/myscript?foo=1&bar=2&name=Laura

Da das Formular im URL kodiert ist, können Sie ein Java-Applet schreiben, das ein Formular simuliert, Eingaben vom Benutzer anfordern und dann ein neues URL-Objekt mit den Formulardaten erstellen. Dann geben Sie diese URL mit getAppletContext(), showDocument(), an den Browser weiter. Der Browser übermittelt die Formularergebnisse selbst. Bei einfachen Formularen genügt das.

Frage:
Wie kann ich POST für die Formularübermittelung realisieren?

Antwort:
Sie müssen simulieren, was ein Browser macht, um Formulare mit POST übersenden zu können. Öffnen Sie einen Socket zum Server, und übersenden Sie die Daten. Das sieht etwa so aus (das genaue Format wird vom HTTP bestimmt):

POST /cgi-bin/mailto.cgi HTTP/1.0
Content-type: application/x-www-form-urlencoded
Content-length: 36

{hier stehen Ihre codierten Formulardaten }

Wenn Sie alles richtig gemacht haben, erhalten Sie die CGI-Formularausgabe vom Server zurück. Dann liegt es an Ihrem Applet, diese Ausgabe korrekt zu verarbeiten. Im Fall einer Ausgabe in HTML besteht eigentlich keine Möglichkeit, diese Ausgabe an den Browser, in dem Ihr Applet ausgeführt wird, weiterzugeben. Falls Sie eine URL zurückerhalten, können Sie den Browser auf diese URL umleiten.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Ein Imprint des Markt&Technik Buch- und Software-Verlag GmbH.
Elektronische Fassung des Titels: Java 2 in 21 Tagen, ISBN: 3-8272-5578-3