Networking bezeichnet im Allgemeinen das Arbeiten mit Anwendungen, die über Netzwerke miteinander kommunizieren. Dass diese Terminologie aber keineswegs eindeutig ist, zeigen die folgenden beiden Sichtweisen. Wenn ein Java-fähiger Browser verwendet wird, können Applets ausgeführt werden, die auf der lokalen Maschine des Benutzers oder im Internet gespeichert sind. Die Information, wo ein Applet gespeichert ist, ist vollständig im HTML-Tag <APPLET> enthalten und daher für den Anwender transparent. Der Browser dekodiert diese Information, lokalisiert das Applet, lädt es und führt es aus. So gesehen ist diese Form des Netzwerkzugriffs auf der höchsten Aggregationsstufe anzusiedeln. Der Benutzer muss sich weder um den Aufbau eines Browsers noch um die Lokalisierung oder das Laden des Applets kümmern. In Kapitel 4.4 wurde bereits erläutert, wie Bilder in Benutzeroberflächen eingebunden werden können. Hierzu ist die Klasse URL notwendig, die Teil des Packages java.net ist. Applets und Applications können URLs verwenden, um Adressen von Ressourcen im Netzwerk anzugeben. Das Laden derartiger Daten ist bereits weniger aggregiert als im ersten betrachteten Beispiel. Um ein Bild zu laden, muss ein Java-Programm zuerst eine URL erzeugen, die die Adresse eines Bildes angibt. Es wird erkennbar, dass der Zugriff auf Netzwerke offensichtlich auf verschiedene Weisen möglich ist, die vor allem von den Kenntnissen des Anwenders abhängen. Je komplexer die Programme sind, die ein Benutzer schreibt, desto weitreichendere Netzwerkmöglichkeiten stehen zur Verfügung. Im Rahmen dieses Kapitels werden die Möglichkeiten vorgestellt, mit denen in Java Anwendungen geschrieben werden können, die über Netzwerke miteinander kommunizieren. Diese Sichtweise entspricht eher dem zweiten als dem ersten Szenario, in dem jeglicher Zugriff transparent erfolgt. Zunächst werden im Folgenden einige Begriffe definiert, ohne deren Kenntnis ein Verständnis des Networking-Konzepts von Java unmöglich ist. Grundbegriffe Um die umfassende Funktionalität eines Netzwerks modellieren zu können, teilt man dieses in der Literatur in Module, sog. Schichten, auf. Dies erlaubt eine Verringerung der Funktionskomplexität, da in jeder Schicht genau definierte Teilprobleme behandelt werden. Ein Modell, das international weite Verbreitung gefunden hat, ist das Referenzmodell der International Standards Organization (ISO). Ziel dieses ISO-Modells, das auch als Open Systems Interconnection (OSI) Reference Model bezeichnet wird, ist die Realisierung einer standardisierten Protokollarchitektur, die eine Kommunikation von Rechnern verschiedenster Hersteller ermöglicht. Das Referenzmodell, das auch in Abb. 5-10 dargestellt ist, behandelt die Kommunikation offener Systeme, also solche, die für eine Kommunikation mit anderen offen sind. Kap. 5.10: ISO-OSI-Modell Grundsätzlich sind im ISO-OSI-Modell zwei Arten der Kommunikation denkbar: die direkte Kommunikation, in der eine Schicht mit der über oder unter ihre liegenden über eine Schnittstelle direkt Daten austauscht oder die indirekte Kommunikation, in der eine Schicht mit einer Partnerinstanz auf dem Rechner, zu dem eine Verbindung besteht, kommuniziert. Man bezeichnet dies auch als Peer-to-Peer-Kommunikation. Bis auf die Kommunikation der physikalischen Schichten eines sendenden und eines empfangenden Rechners tauschen aber Peer-to-Peer-Instanzen niemals direkt Daten aus. Die unterste Schicht (physikalische Schicht) behandelt die Hardware-Spezifikation, z. B. Anschlüsse und Kabel. Daran schließt die Sicherheitsschicht an, die garantiert, dass die Daten vom Sender zum Empfänger gelangen. Darauf setzt die Vermittlungsschicht auf, die unter anderem für die Wegewahl der Pakete im Netz (sog. Routing) verantwortlich ist. Über der Vermittlungsschicht ist die Transportschicht angesiedelt, die eine Ende-zu-Ende-Verbindung garantiert. In diesem Bereich ist z. B. TCP anzusiedeln. Die darauf aufsetzende Kommunikationssteuerungsschicht verwaltet z. B. die Resynchronisation von einmal unterbrochenen Verbindungen. Man kann sich vorstellen, dass beim Ausfall einer Verbindung bei einer gerade ausgeführten Banktransaktion Schwierigkeiten auftreten, die durch diese Schicht behoben werden. Da verschiedene Hardware-Hersteller Daten auf lokalen Rechnern in unterschiedlichen Formaten speichern können, muss ein Datenaustauschformat definiert werden, das die Datenkonversion von der lokalen Darstellung in eine Netzwerkdarstellung und zurück gewährleistet. Dies ist Aufgabe der Darstellungsschicht. Die zuoberst angesiedelte Anwendungsschicht behandelt die Realisierung anwendungsbezogener Dienste, wie die elektronische Post oder den Dateitransfer. Computer, die im Internet betrieben werden, verwenden meist das Transport Control Protocol (TCP) oder das User Datagram Protocol (UDP). Diese Protokolle sind auf der Ebene der Transportschicht anzusiedeln. Java-Programme, die über ein Netzwerk miteinander kommunizieren, sind dagegen auf der Ebene der Anwendungsschicht anzusiedeln. Die genaue Verarbeitung im Rahmen von TCP und UDP spielt daher in Java-Anwendungen meist eine untergeordnete Rolle, da deren Funktionalität im Package java.net zur Verfügung steht. Die hierin enthaltenen Klassen ermöglichen es daher, eine systemunabhängige Netzwerkkommunikation einzusetzen. Andererseits können UDP und TCP nur dann sinnvoll verwendet werden, wenn deren Funktionsweise bekannt ist. Transport Control Protocol (TCP) Möchten zwei Anwendungen auf eine zuverlässige Art und Weise Daten austauschen, so müssen sie eine Verbindung aufbauen und im Sinne eines Telefonanrufs Daten beidseitig austauschen. TCP garantiert, dass Daten, die von einer Anwendung gesendet werden, in derselben Reihenfolge wie sie gesendet wurden und fehlerfrei beim Empfänger ankommen. Tritt hierbei ein Fehler auf, so wird sichergestellt, dass dieser bemerkt und gemeldet wird. TCP stellt auf diese Art und Weise eine Punkt-zu-Punkt-Verbindung für Anwendungen bereit, die eine zuverlässige Kommunikation benötigen. Dies ist bspw. für die folgenden Anwendungen der Fall: Das Hypertext Transfer Protocol (HTTP), File Transfer Protocol (FTP) und auch Telnet verwenden zur Datenübertragung TCP. Diese Anwendungen haben gemein, dass die Reihenfolge, in der die Daten gesendet werden, essentiell ist. Im Falle von HTTP würde eine Änderung der Reihenfolge bspw. die Ausgabe einer Webseite bewirken, in der der Titel möglicherweise im unteren Bereich einer Webseite zu finden wäre. Zusammenfassend kann festgehalten werden, dass TCP ein verbindungsorientiertes Protokoll ist, das einen zuverlässigen Datenfluss zwischen zwei Rechnern gewährleistet. User Datagram Protocol (UDP) Das UDP-Protokoll wird im Gegensatz zu TCP dann eingesetzt, wenn die Eigenschaft der Zuverlässigkeit der Daten keine entscheidende Rolle spielt. Im Unterschied zu TCP ist UDP verbindungslos, sendet also Datenpakete, sog. Datagramme, von einer Anwendung zu einer anderen. Dies ist vergleichbar mit dem Senden einer Menge von Briefen, bei denen weder die Ankunftsreihenfolge noch deren Versendeweg vorhergesagt werden können. Weiterhin kann nicht garantiert werden, dass die Nachricht vollständig beim Empfänger eintrifft, da alle Datagramme voneinander unabhängig sind und deren Auslieferung nicht garantiert ist. Unterschiede zwischen TCP und UDP Viele Anwendungen erfordern eine garantierte zuverlässige Datenübertragung vom Sender zum Empfänger. Andere Kommunikationsformen unterliegen dieser Einschränkung allerdings nicht. Es liegt auf der Hand, dass die Verwendung von TCP daher aufwendiger ist als die von UDP, da eine Vielzahl zusätzlicher Kontrollinformationen übertragen werden müssen. Die Folge der zusätzlichen Datenübertragungen ist, dass TCP oftmals langsamer arbeitet als UDP. Wird bspw. eine Sprachübertragung durchgeführt, so ist es nicht sinnvoll, TCP zu verwenden, da in den meisten Fällen ein erneutes Senden eines verlorengegangenen Pakets erfolglos ist, da es zu spät ankommen würde, um noch ausgegeben werden zu können. Weiterhin könnte es geschehen, dass TCP für diese Art der Datenübertragung zu langsam ist. Meist wird zur Übertragung von Echtzeitdaten daher UDP eingesetzt (es sei denn, es steht eine unbegrenzte Bandbreite zur Verfügung). Ports Computer haben in der Regel eine einzige physikalische Schnittstelle zum Netz. Alle Daten, die an einen bestimmten Rechner geschickt werden, kommen über diese Verbindung an. Hierbei tritt aber das Problem auf, dass auf einem Rechner verschiedene Anwendungen über das Netzwerk kommunizieren können, dass also die ankommenden Daten einer Anwendung zugeordnet werden müssen. Hierzu werden sog. Ports eingesetzt. Daten, die über das Internet übertragen werden, werden gemeinsam mit Informationen übertragen, die den Zielrechner, aber auch den Port identifizieren, für den die Daten bestimmt sind. Der Zielrechner wird über eine IP-Adresse bestimmt, die 32 bit groß ist. Ports werden mittels einer 16 bit großen Adresse identifiziert, die TCP und UDP dazu verwenden, Daten an die richtige Anwendung auszuliefern. Abb. 5.11: Client-Server-Kommunikation Sockets Die verbindungsorientierte Kommunikation, bspw. über TCP, verwendet sog. Sockets, um eine wohldefinierte Schnittstelle zum Netzwerk zu schaffen. Hierbei verbindet eine Server-Applikation einen Socket mit einer spezifischen Port-Nummer. Dieser Vorgang wird auch als Binden bezeichnet und bewirkt, dass der Server sich beim System anmeldet, um Daten, die für diesen Port bestimmt sind, zu empfangen. Ein Client kann nun über den Server-Port mit einem Server kommunizieren (siehe Abb. 5-11). In einer Datagramm-basierten Kommunikation (bspw. UDP) enthalten die Datagramm-Pakete die Port-Nummer des Empfängers. UDP leitet die Pakete an die jeweilige Empfängeranwendung weiter (siehe Abb. 5-12). Abb. 5.12: Adressierung von Ports Port-Nummern liegen immer im Bereich von 0 bis 65.535, da sie durch eine 16 bit-Zahl repräsentiert werden. Die Port-Nummern von 0 bis 1023 können allerdings nur eingeschränkt benutzt werden, da sie von sog. Well-Known-Services wie HTTP, FTP oder anderen Systemdiensten verwendet werden. Diese Ports werden daher oftmals auch als Well-Known-Ports bezeichnet und sollten von Anwendungen nicht an einen Socket gebunden werden. Networking in Java Java-Programme können TCP oder UDP zur Kommunikation über das Internet verwenden, wenn die Klassen im Package java.net eingesetzt werden. Die Klassen URL, URLConnection, Socket und ServerSocket benutzen zur Kommunikation TCP, die Klassen DatagramPacket, DatagramSocket und MulticastSocket hingegen UDP. URLs in Java Ruft ein Benutzer einen Verweis in einem HTML-Dokument auf, oder gibt er eine Datei explizit an, so muss der Browser neue Daten laden. Dies ist keine triviale Aufgabe, da das World Wide Web eine Vielzahl von Computern enthält und die Daten theoretisch auf jedem dieser Rechner liegen könnten. Weiterhin kann jeder Rechner eine Vielzahl von Dateien enthalten und drittens kann ein Dokument als Text (z. B. HTML) oder als Binärdatei (z. B. Bild) vorliegen. Da im World Wide Web zusätzlich noch eine Reihe von Diensten (z. B. Zugriff auf HTML-Dokumente, aber auch FTP) zur Verfügung stehen, musste eine Syntax entwickelt werden, die eine spezielle Seite des World Wide Web adressiert. Diese bezeichnet man als Uniform Resource Locator (URL). Die URL hat die Form Protokoll://Rechner-Name:port/Dokument-Name Protokoll gibt hierbei das verwendete Zugriffsprotokoll an (z. B. HTTP oder FTP), port gibt an, über welchen Port das Dokument abgerufen werden kann und Rechner-Name bzw. Dokument-Name spezifizieren die IP-Adresse des Zielrechners bzw. den Namen des gewünschten Dokuments. Ein Beispiel für eine solche URL wäre daher die Adresse http://www.kom.e-technik.tu-darmstadt.de/index.html Zur Verwendung von URLs in Java muss das Package java.net verwendet werden. Die einfachste Art und Weise, ein URL-Objekt zu erzeugen, ist, einen String zu verwenden, der eine URL-Adresse repräsentiert. In einem Java-Programm kann ein URL-Objekt dann folgendermaßen generiert werden: URL urlObjekt = new URL("http://www.kom.e-technik.tu-darmstadt.de/"); Die derart erzeugte URL wird auch als absolute URL bezeichnet. Eine absolute URL enthält die vollständige Information, die notwendig ist, um ein Objekt im Internet zu adressieren. URLs können allerdings auch als relative URL-Adresse angegeben werden. Eine relative URL enthält nur die Information, die notwendig ist, um ein Objekt relativ zu einer anderen URL zu adressieren. Relative URLs werden oftmals in HTML-Dateien verwendet, um Dateien anzugeben, die sich im selben Verzeichnis befinden wie eine weitere Datei, die mittels einer absoluten URL angegeben wird. Zur Spezifikation einer relativen URL im Kontext einer absoluten URL wird dann die folgende Syntax verwendet: // absolute URL URL urlObjekt = new URL("http://www.kom.e-technik.tu-darmstadt.de/"); //relative URL URL urlRelativ = new URL(urlObjekt, "index.html"); Das angegebene Code-Segment verwendet den URL-Konstruktor, um ein URL-Objekt zu erzeugen, das aus einem weiteren URL-Objekt (der Basis) und einer relativen URL-Spezifikation besteht. Die allgemeine Form des Konstruktors lautet daher: URL(URL basisURL, String relativeURL); Das erste Argument des Konstruktors ist ein URL-Objekt, das die Basis der neu anzulegenden URL angibt. Das zweite Argument ist ein String, der den Rest des Namens in Abhängigkeit von der Basis-URL angibt. Wenn die Basis-URL leer ist, so behandelt der Konstruktor die relative URL wie eine absolute URL-Spezifikation. Weiterhin ignoriert der Konstruktor die Basis-URL, wenn bereits die relative URL eine absolute URL-Spezifikation darstellt. Der Konstruktor kann außerdem auch dazu verwendet werden, Namensanker (sog. Referenzen) in einer HTML-Datei anzugeben. Wird in der Datei index.html ein Anker ENDE definiert, so muss die folgende Anweisung verwendet werden, um diesen Anker zu referenzieren: // absolute URL URL urlObjekt = new URL("http://www.kom.e-technik.tu-darmstadt.de/"); //relative URL URL urlRelativ = new URL(urlObjekt, "index.html"); //Anker URL urlAnker = new URL(urlRelativ, "#ENDE"); Die Klasse URL stellt zwei weitere Konstruktoren zur Verfügung, um URL-Objekte zu erzeugen. Diese Konstruktoren sind insbesondere dann nützlich, wenn mit URLs (bspw. mit HTTP-URLs) gearbeitet wird, die aus dem Host-Namen, dem Dateinamen, der Port-Nummer und Referenzen bestehen. Hierbei steht meist kein String, der die vollständige URL enthält, zur Verfügung, jedoch sind die verschiedenen Komponenten der URL bekannt. Eine Anwendung, die häufig vorkommt, ist das Zusammensetzen von URLs aus Komponenten. Hierbei kann zunächst zwischen verschiedenen Protokollen, wie bspw. HTTP oder FTP, gewählt werden, zwischen verschiedenen Host-Namen, Port-Nummern und Dateinamen, wozu Auswahllisten eingesetzt werden. Die URL wird dann aus den verschiedenen ausgewählten Komponenten konstruiert. Das folgende Beispiel demonstriert dieses Vorgehen: URL urlObjekt = new URL("http", "www.kom.e-technik.tu-darmstadt.de", "/index.html"); Hierbei gibt das erste Argument das Protokoll an, das zweite den Host-Namen und das dritte den Pfadnamen der Datei. Der Dateiname muss in diesem Fall mit einem Schrägstrich beginnen, um anzugeben, dass der Pfad relativ zur Wurzel des Hosts zu lesen ist. Ein weiterer Konstruktor ermöglicht die Angabe der Port-Nummer in der Liste der Argumente: URL urlObjekt = new URL("http", "www.kom.e-technik.tu-darmstadt.de", 80, "index.html"); Wird ein derartiges URL-Objekt mit einem der Konstruktoren erzeugt, so kann die Methode toString oder die äquivalente Methode toExternalForm, die Teil der Klasse URL ist, dazu verwendet werden, die String-Darstellung des Objekts zu erzeugen. Ausnahmebehandlung Jeder der vier URL-Konstruktoren generiert eine MalformedURLException-Ausnahme, wenn die Argumente, die dem Konstruktor übergeben werden, entweder Null sind oder ein unbekanntes Protokoll angeben. Typischerweise wird eine derartige Ausnahme allerdings abgefangen, wozu der folgende Code verwendet werden kann: try { URL urlObjekt= new URL(. . .) } catch (MalformedURLException e) { // Exception-Handler wird hier implementiert } Die Klasse URL stellt verschiedene Methoden zur Verfügung, um URL-Objekte abzufragen: getProtocol gibt den Identifikationsteil des Protokolls der URL zurück. getHost gibt den Host-Namensteil der URL zurück. getPort gibt analog den Port-Nummernteil der URL als Integer-Wert zurück. Ist der Port nicht gesetzt, so wird der Wert -1 zurückgegeben. getFile liefert den Dateinamensteil der URL zurück. getRef liefert die Referenzkomponente der URL zurück. Das folgende Beispiel demonstriert die Anwendung dieser Methoden. import java.net.*; import java.io.*; public class urlAusgabe { public static void main(String[] args) throws Exception { URL urlO= new URL("http://www.kom.e-technik.tu-darmstadt.de/in dex.html"); System.out.println("Protokoll: " + urlO.getProtocol()); System.out.println("Host: " + urlO.getHost()); System.out.println("Dateiname: " + urlO.getFile()); System.out.println("Port: " + urlO.getPort()); } } Streams über URLs Nachdem ein URL-Objekt angelegt wurde, kann die Methode openStream(), die Teil der Klasse URL ist, aufgerufen werden, um einen Stream zu generieren, über den die Inhalte einer URL gelesen werden können. Die Methode openStream() erzeugt ein Objekt vom Typ java.io.InputStream, wodurch das Lesen aus einer URL völlig äquivalent zum Lesen eines Eingabe-Streams erfolgen kann. Das folgende Beispiel verdeutlicht dieses Vorgehen. Wird das Programm ausgeführt, so werden die HTML-Kommandos und der Textinhalt der HTML-Datei im Kommandozeilenfenster ausgegeben. import java.net.*; import java.io.*; public class urlLesen { public static void main(String[] args) throws Exception { URL urlO= new URL("http://www.kom.e-technik.tu-darmstadt.de/") ; BufferedReader einlesen = new BufferedReader(new InputStreamReader(urlO.openStream())); String eingabeZeile; while ((eingabeZeile = einlesen.readLine()) != null) System.out.println(eingabeZeile); einlesen.close(); } } Nachdem ein URL-Objekt angelegt wurde, kann die Methode openConnection aufgerufen werden, um eine Verbindung zur angegebenen URL zu erzeugen. Hierdurch wird eine Kommunikationsverbindung zwischen dem Java-Programm und der URL über das Netzwerk geschaffen. Soll eine Verbindung zur oben angegebenen URL erzeugt werden, so muss das folgende Programmstück verwendet werden: try { URL urlO= new URL("http://www.kom.e-technik.tu-darmstadt.de/"); urlO.openConnection(); } catch (MalformedURLException e) { // Fehlschlag } catch (IOException e) { // openConnection() //fehlgeschlagen } Die Methode openConnection erzeugt eine neue URL-Verbindung, falls dies möglich ist (bzw. falls nicht bereits eine derartige Verbindung existiert). Diese Verbindung wird initialisiert, mit der URL verbunden und als URLConnection-Objekt zurückgegeben. Schlägt dieser Ablauf fehl, so löst die Methode openConnection die Ausnahme IOException aus. Nachdem eine Verbindung erstellt wurde, können Lese- und Schreiboperationen erfolgen. Hierbei ist stets zu beachten, dass die URL-Verarbeitung in Java auf das Hypertext-Transfer-Protokoll (HTTP) konzentriert ist. Viele der im Folgenden beschriebenen Methoden sind daher nur dann sinnvoll, wenn mit HTTP-URLs gearbeitet wird. Lesen von URL-Verbindungen Im Folgenden ist ein Beispielprogramm angegeben, das Daten von einer URL liest. Im Gegensatz zum oben angegebenen Beispiel (Erzeugen eines Input-Streams aus einer URL) öffnet dieses Beispiel eine explizite Verbindung mit einer URL und erhält von dieser Verbindung einen Input-Stream. Wie auch im vorangegangenen Beispiel wird dann ein BufferedReader-Objekt erzeugt, mit dem Daten aus dem Stream gelesen werden. import java.net.*; import java.io.*; public class URLVerbindungLesen { public static void main(String[] args) throws Exception { String zeile; URL kom = new URL("http://www.kom.e-technik.tu-darmstadt.de/") ; URLConnection komC = kom.openConnection(); BufferedReader einlesen = new BufferedReader( new InputStreamReader(komC.getInputStream())); while ((zeile = einlesen.readLine()) != null) System.out.println(zeile); einlesen.close(); } } Die Ausgabe dieses Beispiels ist mit der Ausgabe des vorangegangenen Beispiels identisch. Beide Arten können daher dazu verwendet werden, Daten von einer URL zu lesen. Das Auslesen einer URL-Verbindung kann allerdings vorteilhafter sein, da ein URLConnection-Objekt gleichzeitig auch für andere Aufgaben eingesetzt werden kann. Schreiben auf URL-Verbindungen Eine Vielzahl von Webseiten, vor allem die kommerzieller Anbieter, enthalten Formulare oder andere GUI-Objekte, in die Daten eingetragen werden müssen, die anschließend an den Server geschickt werden. Wird Text in derartige Elemente angegeben, so überträgt der Browser die Informationen schreibend zur angegebenen URL. Üblicherweise empfängt der Server die Daten über ein CGI-Skript, verarbeitet sie und sendet eine Antwort in Form einer neuen HTML-Seite. Eine Reihe von CGI-Skripten verwendet die Anweisung POST METHOD zum Lesen der Daten, die von einem Client gesendet werden. Ein Java-Programm kann mit CGI-Skripten auf der Server-Seite interagieren. Hierzu muss das Java-Programm auf eine URL schreiben und damit Daten an den Server senden. Zur Umsetzung sind die folgenden Schritte notwendig: Erzeugen einer URL Öffnen einer Verbindung zur URL Schreibberechtigung der URL-Verbindung herstellen Erzeugen eines Output-Streams für die Verbindung. Dieser Strom wird mit dem Standard-Input-Stream des CGI-Skripts des Servers verbunden. Schreiben in den Output-Strom Schließen des Stroms Die Angabe eines Beispiels, das die Interaktion mit einem CGI-Skript demonstriert, ist problematisch, da der Leser in der Regel nicht in der Lage sein dürfte, ein CGI-Skript auszuführen. Im Folgenden sei daher angenommen, dass ein imaginäres Skript auf einem Server läuft, das eine Textangabe in Form eines Strings als Argument akzeptiert. Das folgende Beispiel demonstriert, wie mit diesem Skript interagiert werden muss. import java.io.*; import java.net.*; public class SchreibeBeispiel { public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Eingabe: java SchreibeBeispiel Parameterstring"); System.exit(1); } //Senden der Daten String stringArgument = URLEncoder.encode(args[0]); URL url = new URL("http://www.kom.e-technik.tu-darmstadt.de/cg i-bin/beispiel"); URLConnection verbindung = url.openConnection(); verbindung.setDoOutput(true); PrintWriter ausgabe = new PrintWriter(verbindung.getOutputStream()); ausgabe.println("string=" + stringArgument); ausgabe.close(); //Einlesen der Antwort BufferedReader einlesen= new BufferedReader(new InputStreamReader(verbindung.getInputStream())); String zeile; while ((zeile = einlesen.readLine()) != null) System.out.println(zeile); einlesen.close(); } } Zuerst werden die Argumente verarbeitet, die auf Kommandozeilenebene eingegeben werden. Es darf lediglich ein Argument angegeben werden, das an das CGI-Skript auf der Server-Seite übertragen wird. Da der Inhalt des Strings auch aus Leer- und Sonderzeichen bestehen kann, muss er vor der Übertragung kodiert werden. Hierzu wird die Klasse URLEncoder verwendet, die die Zeichen des Strings kodiert. Anschließend wird ein URL-Objekt angelegt, das die Adresse des CGI-Skripts auf der Server-Seite bezeichnet. Nachdem dieses Objekt zur Verfügung steht, wird eine Verbindung geöffnet (URLConnection), deren Schreibberechtigung mittels der Methode setDoOutput(true) gesetzt wird. Im nächsten Schritt wird ein Output-Strom erzeugt, auf dem ein PrintWriter-Objekt operiert. Wenn die URL dieses Vorgehen nicht unterstützt, wird an dieser Stelle eine UnknownServiceException-Ausnahme ausgeworfen. Unterstützt die URL allerdings diesen Prozess, so wird ein Output-Strom zurückgegeben, der mit dem Standard-Input der URL auf der Server-Seite verbunden ist. Die Ausgabe des Clients fungiert dann als Eingabe des Servers. Nach dem Schreiben der Information in den Stream wird dieser anschließend geschlossen. Grundsätzlich funktioniert das Schreiben auf eine URL daher genau so wie das Schreiben in einen Stream. Nachdem das CGI-Skript auf der Server-Seite das Argument verarbeitet hat, wird üblicherweise eine Antwort generiert, die der Client dann wieder einlesen muss. Das Lesen von einer URL wurde im vorangegangenen Unterkapitel bereits beschrieben. Sockets URLs und URL-Verbindungen stellen einen Mechanismus dar, mit dem auf abstrakte Art und Weise Daten ausgetauscht werden können. Oftmals ist es aber erforderlich, dass Client und Server auf einer niedrigeren Ebene direkt miteinander kommunizieren. Ein Beispiel hierfür sind Client-Server-Anwendungen, bei denen der Server einen Dienst anbietet (bspw. Datenbankabfragen) und bei denen der Client den Dienst in Anspruch nimmt. Ein weiteres Beispiel, das im Rahmen dieses Buches eine große Rolle spielt, ist das Spiel Schiffe versenken, eine typische Client-Server-Anwendung. Die Kommunikation zwischen Server und Client muss zuverlässig arbeiten. Daten dürfen daher bei der Übertragung weder verloren gehen, noch in einer anderen Reihenfolge eintreffen, als in der, in der der Server die Daten geschickt hat. Zur Umsetzung dieser Aufgabe muss TCP eingesetzt werden. TCP realisiert eine zuverlässige Punkt-zu-Punkt-Kommunikation. Um über TCP miteinander kommunizieren zu können, müssen der Client und der Server eine Verbindung über Sockets aufbauen. Jedes Programm bindet hierbei einen Socket, über den Daten dann geschrieben und gelesen werden können. Betrachtet man in Analogie hierzu bspw. eine Fährverbindung über einen Fluss, so sind Sockets die Anlegestellen auf beiden Seiten des Ufers, an denen Passagiere darauf warten, befördert zu werden. Üblicherweise läuft der Server auf einem bestimmten Rechner. Der Server verwendet einen Socket, der an eine bestimmte Port-Nummer gebunden ist. Ports werden dazu verwendet, Netzwerkverbindungen mit Anwendungen zu assoziieren. Der Server, der einen Dienst anbietet, hört nun den Socket ab und wartet auf die Verbindungsanforderung eines Clients. Um eine Verbindung erstellen zu können, muss der Client den Namen des Hosts kennen, auf der der Server läuft und die Port-Nummer, über die der Server erreichbar ist. Um eine Verbindungsanforderung stellen zu können veranlasst der Client ein sog. Rendezvous mit dem Server. Wenn der Server die Verbindungsanforderung akzeptiert, bindet der Server einen neuen Socket an einen Port. Der neue Socket wird (zusammen mit der Port-Nummer) benötigt, um den Original-Socket weiter abhören zu können. Üblicherweise bietet der Server seinen Dienst einer Vielzahl von Clients an. Während daher die Anforderung eines Clients abgearbeitet wird, muss der Dienst auch potentiellen weiteren Clients angeboten werden können. Akzeptiert ein Client eine Verbindung, so wird auch hier ein Socket angelegt, der zur Kommunikation mit dem Server verwendet wird. Der Socket auf der Client-Seite ist allerdings nicht mit der Port-Nummer assoziiert, die für das Rendezvous mit dem Server verwendet wird. Anstelle dessen wird dem Client eine Port-Nummer zugewiesen, die für die Maschine, auf der der Client läuft, lokale Bedeutung hat. Nachdem dieser Vorgang abgeschlossen ist, können der Client und der Server Daten über die Sockets austauschen. Ein Socket ist demzufolge ein Endpunkt einer Zwei-Wege-Kommunikation zwischen zwei Programmen, die Daten über ein Netzwerk austauschen. Ein Socket ist immer mit einer Port-Nummer assoziiert, mit deren Hilfe TCP die Anwendung identifizieren kann, an die Daten geschickt werden sollen. Das Package java.net beinhaltet die Klasse Socket, die eine Seite einer Zwei-Wege-Kommunikation zwischen einem Java-Programm und einem anderen Programm über das Netz implementiert. Die Klasse Socket setzt auf einer plattformunabhängigen Implementierung auf und verbirgt Details eines bestimmten Systems vor dem Java-Programm. Indem die Klasse java.net.Socket anstelle von native Code verwendet wird, kann eine Kommunikation über ein Netzwerk in einer plattformunabhängigen Art und Weise realisiert werden. Zusätzlich enthält das Package java.net die Klasse ServerSocket, die einen Socket implementiert, den Server benutzen können, um Verbindungsanforderungen eines Clients zu erwarten und um diese zu akzeptieren. Es sei darauf hingewiesen, dass Verbindungen über das World Wide Web mit Hilfe der bereits beschriebenen Klasse URL (bzw. mit den hierzu in Verbindung stehenden Klassen URLConnection und URLEncoder) eventuell einfacher realisiert werden können als mit Sockets. URLs arbeiten aber auf einer höheren Aggregationsstufe. Sockets auf Client-Seite Im Folgenden wird zunächst erläutert, wie ein Programm eine Verbindung mit einem Server aufbauen kann, indem die Klasse Socket verwendet wird. Anschließendwird gezeigt, wie der Client über den Socket Daten senden und empfangen kann. Zur Verdeutlichung der Konzepte wird eine Anwendung erstellt, die das Wort Hallo an den Server schickt. Dieser schickt das Wort Hallo an den Client zurück. Zunächst muss der Hallo-Client einen Socket erzeugen und damit eine Verbindung zum Hallo-Server aufbauen. Im Folgenden ist die Implementierung der Klasse HalloClient angegeben. import java.io.*; import java.net.*; public class HalloClient { public static void main(String[] args) throws IOException { Socket halloSocket = null; PrintWriter senden = null; BufferedReader empfangen= null; try { halloSocket = new Socket("plato", 4444); senden = new PrintWriter(halloSocket.getOutputStream(), true); empfangen = new BufferedReader(new InputStreamReader(halloSocket.getInputStrea m())); } catch (UnknownHostException e) { System.err.println("Host unbekannt: plato."); System.exit(1); } catch (IOException e) { System.err.println("I/O zu Plato abgelehnt."); System.exit(1); } String zeile = "Hallo"; out.println(zeile); System.out.println("Hallo gesendet, empfangen: " + empfangen.readLine()); senden.close(); empfangen.close(); halloSocket.close(); } } Der Hallo-Client nutzt in diesem Beispiel denselben Socket zum Senden und zum Empfangen der Daten. Innerhalb der try-Anweisung wird zunächst ein Socket zur Kommunikation zwischen Client und Server angelegt. Anschließend werden zum Senden und zum Empfangen der Daten ein PrintWriter-Objekt und ein BufferedReader-Objekt angelegt. In der ersten Anweisung des try-Blocks wird ein neues Socket-Objekt (halloSocket) angelegt. Hierzu müssen als Parameter der Name der Maschine und der Port angegeben werden, um die zu erstellende Verbindung zu charakterisieren. Im Beispiel wird der Name plato verwendet, um eine Maschine des lokalen Netzwerks anzugeben. Dieser Name ist so zu ändern, dass eine Maschine bezeichnet wird, die auch tatsächlich existiert. Die im Beispiel verwendete Port-Nummer entspricht der Nummer, an der der Server auf Verbindungen wartet. Um nun Daten zu senden, muss der Hallo-Client lediglich über das PrintWriter-Objekt schreiben. Die Antwort des Servers wird durch Lesen des BufferedReader-Objekts abgefragt. Zum Abschluss des Programms werden die Stream-Objekte und das Socket-Objekt geschlossen. Hierbei ist unbedingt auf die Reihenfolge der Anweisungen zu achten. Vor dem Schließen des Sockets müssen zuerst die Streams geschlossen werden. Verallgemeinert man das hier angegebene Programm, so finden sich meist die folgenden Schritte, die ein Client zur Etablierung der Kommunikation ausführt: Öffnen eines Sockets Öffnen eines Input- und eines Output-Streams zum Empfangen und Senden von Daten über den Socket Lesen des Streams und Schreiben in den Stream gemäß des Protokolls, das der Server vorgibt Schließen der Streams Schließen des Sockets Da der Datenaustausch zwischen Client und Server vom Protokoll abhängt, das der Server vorgibt, kann in diesem Schema der dritte Schritt von Client zu Client variieren. Ein Protokoll ist das Regelwerk, das bei einer Kommunikation einzuhalten ist. Übliche Protokolle beinhalten immer die Phasen Verbindungsaufbau, Datenübertragung und Verbindungsabbau. Sockets auf Server-Seite In diesem Abschnitt wird erläutert, wie die Server-Komponente realisiert werden muss, die das Hallo-Beispiel vervollständigt. import java.net.*; import java.io.*; public class HalloServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(4444); } catch (IOException e) { System.err.println("Port nicht verfügbar: 4444."); System.exit(1); } Socket clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Accept fehlgeschlagen."); System.exit(1); } PrintWriter senden = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader empfangen= new BufferedReader(new InputStreamReader(clientSocket.getInputStream()) ); String zeile = "Hallo"; senden.println(zeile); senden.close(); empfangen.close(); clientSocket.close(); serverSocket.close(); } } Das Server-Programm erzeugt zuerst ein ServerSocket-Objekt, um einen bestimmten Port abzuhören. Hierbei muss ein Port gewählt werden, der nicht bereits von einem anderen Dienst belegt ist. Oftmals sind Port-Nummern größer als 1000 frei und können dementsprechend belegt werden. Die Klasse ServerSocket ist eine Klasse des Packages java.net, die eine systemunabhängige Implementierung der Server-Seite einer Client-Server-Verbindung zur Verfügung stellt. Es ist zu beachten, dass der Konstruktor der Klasse ServerSocket eine Exception auslöst, wenn der angegebene Port nicht abgehört werden kann (bspw. wenn der Port bereits verwendet wird). Wenn der Server einen Port erfolgreich anbindet, wird im nächsten Schritt eine Verbindung eines Clients akzeptiert. Die accept-Methode wartet solange, bis ein Client eine Verbindung zum Host über einen Port des Servers anfordert. Wird eine neue Verbindung erfolgreich aufgebaut, so liefert die accept-Methode ein neues Socket-Objekt zurück, das an einen neuen Port gebunden ist. Der Server kann dann über den neuen Socket mit dem Client kommunizieren, parallel aber auf dem anderen Port auf Anfragen weiterer Clients warten. Im obigen Beispiel wartet der Server nicht auf weitere Anfragen. Zur Umsetzung dieser Funktionalität muss mit Threads gearbeitet werden, wie im folgenden Beispiel dargestellt wird. import java.net.*; import java.io.*; public class MultiServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(4444); } catch (IOException e) { System.err.println("Port-Fehler: 4444."); System.exit(-1); } while (true) new MultiServerThread(serverSocket.accept()).st art(); serverSocket.close(); } } import java.net.*; import java.io.*; public class MultiServerThread extends Thread { private Socket socket = null; public MultiServerThread(Socket socket) { super("MultiServerThread"); this.socket = socket; } public void run() { try { PrintWriter senden = new PrintWriter(socket.getOutputStream(), true); BufferedReader empfangen = new BufferedReader(new InputStreamReader(socket.getInputStream())) ; //Verarbeitung senden.close(); empfangen.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } Datagramme Die Kommunikation mit Sockets realisiert eine zuverlässige Datenverbindung. Der Aspekt der Zuverlässigkeit impliziert aber immer auch eine aufwendige Verarbeitung. Einige Anwendungen, die über ein Netzwerk miteinander kommunizieren, benötigen allerdings keine zuverlässige Punkt-zu-Punkt-Kommunikation, wie sie bspw. durch TCP zur Verfügung gestellt wird. In diesem Fall kann eine Kommunikationsform, die unabhängige Datenpakete liefert (Ankunftszeitpunkt und Reihenfolge der Pakete unbekannt), durchaus ausreichend sein. Das Protokoll UDP realisiert eine derartige Funktionalität. In UDP wird eine Netzwerkkommunikation realisiert, in der Anwendungen Datenpakete als sog. Datagramme versenden. Ein Datagramm ist eine unabhängige und eigenständige Nachricht, deren Ankunft an sich, deren Ankunftszeit und deren Korrektheit des Paketinhalts nicht garantiert werden. Im Package java.net stehen die Klassen DatagramPacket und DatagramSocket zur Verfügung, um mit Datagrammen arbeiten zu können, die über UDP kommunizieren. Client-Server-Kommunikation mit Datagrammen Ziel dieses Abschnitts ist es, eine Client-Server-Kommunikation mit Datagrammen zu implementieren. Hierzu wird nochmals das Beispiel des vorangegangenen Abschnitts aufgegriffen, das für eine Datagramm-Kommunikation umgeschrieben wird. Der Client sendet in dieser Anwendung wiederum eine Hallo-Nachricht an den Server, der wiederum mit einer Hallo-Nachricht antwortet. In Erweiterung zum Socket-Beispiel soll dieses Beispiel aber sofort so angelegt werden, dass der Server eine vorab unbekannte Anzahl von Clients bedienen kann. Zunächst wird die Funktionalität des Clients erläutert. import java.io.*; import java.net.*; import java.util.*; public class HalloDClient { public static void main(String[] args) throws IOException { if (args.length != 1) { System.out.println("Eingabe: java HalloDClient <Hostname>"); return; } // Anlegen eines Datagramm-Sockets DatagramSocket socket = new DatagramSocket(); // Senden der Anforderung String zeile = "Hallo"; byte[] buf = new byte[256]; buf = zeile.getBytes(); InetAddress adresse = InetAddress.getByName(args[0]); DatagramPacket paket = new DatagramPacket(buf, buf.length, adresse, 4444); socket.send(paket); // Empfangen der Antwort paket = new DatagramPacket(buf, buf.length); socket.receive(paket); // Anzeige der Antwort String empfangen = new String(paket.getData()); System.out.println("Empfangen wurde: "+empfangen); socket.close(); } } Zunächst werden die Kommandozeilenargumente verarbeitet, um den Namen der Maschine einzulesen, an die ein Datagramm geschickt werden soll. Anschließend wird ein DatagramSocket-Objekt angelegt. Der Client verwendet hier einen Konstruktor, der keine Angabe der Port-Nummer erfordert. Der Konstruktor bindet daher das DatagramSocket-Objekt an eine beliebige freie lokale Nummer. Diese Nummer darf daher beliebig sein, da Datagramm-Pakete bereits die vollständige Adressierungsinformation beinhalten. Der Server wird daher anschließend die Adresse aus den Paketen auslesen und die dementsprechende Antwort senden. Im nächsten Schritt sendet der Client eine Anforderung an den Server. Das hierzu notwendige Code-Segment stellt zunächst die Internet-Adresse des Hosts fest, der in der Kommandozeile angegeben wurde. Diese sog. Inet-Adresse und die Port-Nummer 4444 werden dann dazu verwendet, ein DatagramPacket-Objekt zu erzeugen, das an die so bestimmte Adresse geschickt wird. Es ist zu beachten, dass die Methode getBytes dazu verwendet wird, den Byte-Array mit den Werten zu füllen, die an den Server geschickt werden sollen. Der Client wartet anschließend auf eine Antwort des Servers und zeigt diese an. Um diese Antwort empfangen zu können, erzeugt der Client ein empfangen-Paket und verwendet die Methode receive der Klasse DatagramSocket, um die Antwort des Servers entgegenzunehmen. Die receive-Methode wartet, bis ein an den Client adressiertes Paket am Socket ankommt. Dies impliziert, dass der Client möglicherweise unendlich wartet, wenn dieses Paket verloren geht. Im Regelfall setzt ein Client daher einen Zähler, bei dessen Ablauf die Anforderung nochmals gesendet wird. Zur Extraktion der Daten aus dem empfangenen Paket verwendet der Client die Methode getData. Die empfangenen Daten werden dann in einen String konvertiert und angezeigt. Im Folgenden wird nun der Server beschrieben, der mit dem Client interagiert. Wie im vorangegangenen Abschnitt besteht die Server-Applikation aus zwei Klassen: Einer Hauptklasse, die Verbindungsanforderungen entgegennimmt und einer weiteren Klasse, die für den Client Aufgaben abarbeitet. Zunächst wird die Funktion der Hauptklasse betrachtet. public class HalloDServer { public static void main(String[] args) throws java.io.IOException { new HalloDServerThread().start(); } } Aufgabe der Hauptklasse ist es, einen neuen Thread zu erzeugen, der die eigentliche Verarbeitung vornimmt. Aus diesem Grund ist die Hauptklasse außerordentlich kurz. Im Folgenden wird die Funktion der Thread-Klasse erläutert. import java.io.*; import java.net.*; import java.util.*; public class HalloDServerThread extends Thread { protected DatagramSocket socket = null; public HalloDServerThread() throws IOException { this("HalloDServerThread"); } public HalloDServerThread(String name) throws IOException { super(name); socket = new DatagramSocket(4444); } public void run() { try { byte[] buf = new byte[256]; // Empfangen der Anforderung DatagramPacket paket = new DatagramPacket(buf, buf.length); socket.receive(paket); // Antwort generieren String zeile = "Hallo"; buf = zeile.getBytes(); // senden der Antwort InetAddress adresse = paket.getAddress(); int port = paket.getPort(); paket = new DatagramPacket(buf, buf.length, adresse, port); socket.send(paket); } catch (IOException e) { e.printStackTrace(); } socket.close(); } } Es ist leicht erkennbar, dass die Server-Klasse bezüglich des Sendens und Empfanges der Daten in entgegengesetzter Weise arbeitet wie der Client. Soll in Abhängigkeit von einer Anforderung eines Clients eine dedizierte Abarbeitung erfolgen, so ist der Code dementsprechend zu modifizieren. Es sei abschließend darauf hingewiesen, dass bei der Ausführung des Servers und des Clients auf derselben Maschine der Client mit der Adresse localhost aufgerufen werden muss. Broadcast und Multicast Zusätzlich zur Klasse DatagramSocket, mit der Programme Pakete versenden können, beinhaltet das Package java.net die Klasse MulticastSocket. Diese spezielle Art von Sockets wird dazu verwendet, auf der Client-Seite Pakete zu empfangen, die der Server an eine Reihe von Clients versendet (Broadcast bzw. im Falle einer Gruppe von Empfängern Multicast). Im Folgenden soll das Beispiel so umgeschrieben werden, dass der Server Datagramme per Broadcast an mehrere Empfänger schicken kann. In dieser Anwendung sendet der Server zu regelmäßigen Zeitpunkten Hallo-Nachrichten an Clients. Der Client muss demzufolge so modifiziert werden, dass er passiv auf derartige Nachrichten wartet. Hierzu muss ein Multicast-Socket verwendet werden. Zunächst wird die Funktionalität des modifizierten Clients betrachtet. import java.io.*; import java.net.*; import java.util.*; public class HalloDMClient { public static void main(String[] args) throws IOException { // Anlegen eines Datagramm-Sockets MulticastSocket socket = new MulticastSocket(4445); InetAddress adresse = InetAddress.getByName("127.0.0.1"); socket.joinGroup(adresse); for (int i=0;i<100;i++) { byte[] buf = new byte[256]; // Empfangen der Antwort DatagramPacket paket = new DatagramPacket(buf, buf.length); socket.receive(paket); // Anzeige der Antwort String empfangen = new String(paket.getData()); System.out.println("Empfangen wurde: "+empfangen); } socket.leaveGroup(adresse); socket.close(); } } Um den Port mit der Nummer 4444 abhören zu können, muss das neue Client-Programm ein MulticastSocket-Objekt mit dieser Port-Nummer erzeugen. Um ein Mitglied der Multicast-Gruppe werden zu können, wird im Anschluss daran die joinGroup-Methode der Klasse MulticastSocket mit dem Parameter InetAddress, der die Gruppe identifiziert, ausgeführt. Der Client ist nun in der Lage, Datagramme zu empfangen, die an diesen Port und an die entsprechende Gruppe gesendet werden. Im Folgenden ist die modifizierte Server-Variante angegeben. Das Hauptprogramm des Servers bleibt hierbei (bis auf den geänderten Klassennamen) vollständig unverändert. public class HalloDMServer { public static void main(String[] args) throws java.io.IOException { new HalloDMServerThread().start(); } } import java.io.*; import java.net.*; import java.util.*; public class HalloDMServerThread extends HalloDServerThread { private long EINE_SEKUNDE = 1000; public HalloDMServerThread() throws IOException { super("HalloDMServerThread"); } public void run() { try { byte[] buf = new byte[256]; //Nachricht aufbauen String zeile = "Hallo"; buf = zeile.getBytes(); // senden InetAddress gruppe = InetAddress.getByName("127.0.0.1"); DatagramPacket paket = new DatagramPacket(buf, buf.length, gruppe, 4445); socket.send(paket); // pausieren try { sleep((long)(Math.random() * EINE_SEKUNDE)); } catch (InterruptedException e) { } } catch (IOException e) { e.printStackTrace(); } socket.close(); } } Interessanterweise kann der Server ein DatagramSocket-Objekt zur Verteilung der Pakete an Multicast-Clients verwenden. Alternativ könnte allerdings auch ein Multicast-Socket eingesetzt werden. Der Socket, den der Server verwendet, spielt daher keine Rolle. Entscheidend ist vielmehr die Adressierungsinformation, die in den Datagrammen enthalten ist, bzw. der Socket, den der Client zum Empfang verwendet. Als Erweiterung zum vorangegangenen Beispiel wurde die neue Klasse als Subklasse der Klasse HalloDServerThread angelegt. Hierdurch kann der Konstruktor (und einige Variablen) wiederverwendet werden. Im Unterschied zum bisher verwendeten Beispiel wird nun das Datagramm anders aufgebaut. Im vorangegangenen Beispiel wurden die Inet-Adresse sowie die Port-Nummer aus dem Paket ausgelesen, das der Client übermittelt. In der neuen Anwendung kennt aber der Server die Clients in der Regel nicht. Aus diesem Grund wurden diese Information hart im Code verdrahtet. Die im Rahmen dieses Kapitels erläuterten Kenntnisse werden nun im Anwendungsbeispiel im praktischen Einsatz demonstriert.