Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 48 - Netzwerkprogrammierung |
Zur Adressierung von Rechnern im Netz wird die Klasse InetAddress des Pakets java.net verwendet. Ein InetAddress-Objekt enthält sowohl eine IP-Adresse als auch den symbolischen Namen des jeweiligen Rechners. Die beiden Bestandteile können mit den Methoden getHostName und getHostAddress abgefragt werden. Mit Hilfe von getAddress kann die IP-Adresse auch direkt als byte-Array mit vier Elementen beschafft werden:
String getHostName() String getHostAddress() byte[] getAddress() |
java.net.InetAddress |
Um ein InetAddress-Objekt zu generieren, stehen die beiden statischen Methoden getByName und getLocalHost zur Verfügung:
public static InetAddress getByName(String host) throws UnknownHostException public static InetAddress getLocalHost() throws UnknownHostException |
java.net.InetAddress |
getByName erwartet einen String mit der IP-Adresse oder dem Namen des Hosts als Argument, getLocalHost liefert ein InetAddress-Objekt für den eigenen Rechner. Beide Methoden lösen eine Ausnahme des Typs UnknownHostException aus, wenn die Adresse nicht ermittelt werden kann. Das ist insbesondere dann der Fall, wenn kein DNS-Server zur Verfügung steht, der die gewünschte Namensauflösung erledigen könnte (beispielsweise weil die Dial-In-Verbindung zum Provider gerade nicht besteht).
Das folgende Listing zeigt ein einfaches Programm, das zu einer IP-Adresse den symbolischen Namen des zugehörigen Rechners ermittelt und umgekehrt:
001 /* Listing4801.java */ 002 003 import java.net.*; 004 005 public class Listing4801 006 { 007 public static void main(String[] args) 008 { 009 if (args.length != 1) { 010 System.err.println("Usage: java Listing4801 <host>"); 011 System.exit(1); 012 } 013 try { 014 //Get requested address 015 InetAddress addr = InetAddress.getByName(args[0]); 016 System.out.println(addr.getHostName()); 017 System.out.println(addr.getHostAddress()); 018 } catch (UnknownHostException e) { 019 System.err.println(e.toString()); 020 System.exit(1); 021 } 022 } 023 } |
Listing4801.java |
Wird das Programm mit localhost
als Argument aufgerufen, ist seine Ausgabe:
localhost
127.0.0.1
localhost ist eine Pseudo-Adresse für den eigenen Host. Sie ermöglicht das Testen von Netzwerkanwendungen, auch wenn keine wirkliche Netzwerkverbindung besteht (TCP/IP muss allerdings korrekt installiert sein). Sollen wirkliche Adressen verarbeitet werden, muss natürlich eine Verbindung zum Netz (insbesondere zum DNS-Server) aufgebaut werden können. |
|
Die nachfolgende Ausgabe zeigt die Ausgabe des Beispielprogramms,
wenn es nacheinander mit den Argumenten java.sun.com,
www.gkrueger.com und www.addison-wesley.de
aufgerufen wird:
java.sun.com
192.18.97.71
www.gkrueger.com
213.221.123.45
www.addison-wesley.de
194.163.213.76
Als Socket bezeichnet man eine streambasierte Programmierschnittstelle zur Kommunikation zweier Rechner in einem TCP/IP-Netz. Sockets wurden Anfang der achtziger Jahre für die Programmiersprache C entwickelt und mit Berkeley UNIX 4.1/4.2 allgemein eingeführt. Das Übertragen von Daten über eine Socket-Verbindung ähnelt dem Zugriff auf eine Datei:
Während die Socket-Programmierung in C eine etwas mühsame Angelegenheit war, ist es in Java recht einfach geworden. Im Wesentlichen sind dazu die beiden Klassen Socket und ServerSocket erforderlich. Sie repräsentieren Sockets aus der Sicht einer Client- bzw. Server-Anwendung. Nachfolgend wollen wir uns mit den Client-Sockets beschäftigen, die Klasse ServerSocket wird im nächsten Abschnitt behandelt.
Die Klasse Socket besitzt verschiedene Konstruktoren, mit denen ein neuer Socket erzeugt werden kann. Die wichtigsten von ihnen sind:
public Socket(String host, int port) throws UnknownHostException, IOException public Socket(InetAddress address, int port) throws IOException |
java.net.Socket |
Beide Konstruktoren erwarten als erstes Argument die Übergabe des Hostnamens, zu dem eine Verbindung aufgebaut werden soll. Dieser kann entweder als Domainname in Form eines Strings oder als Objekt des Typs InetAddress übergeben werden. Soll eine Adresse mehrfach verwendet werden, ist es besser, die zweite Variante zu verwenden. In diesem Fall kann das übergebene InetAddress-Objekt wiederverwendet werden und die Adressauflösung muss nur einmal erfolgen. Wenn der Socket nicht geöffnet werden konnte, gibt es eine Ausnahme des Typs IOException bzw. UnknownHostException (wenn das angegebene Zielsystem nicht angesprochen werden konnte).
Der zweite Parameter des Konstruktors ist die Portnummer. Wie in Abschnitt 48.1.4 erwähnt, dient sie dazu, den Typ des Servers zu bestimmen, mit dem eine Verbindung aufgebaut werden soll. Die wichtigsten Standard-Portnummern sind in Tabelle 48.2 aufgelistet.
Nachdem die Socket-Verbindung erfolgreich aufgebaut wurde, kann mit den beiden Methoden getInputStream und getOutputStream je ein Stream zum Empfangen und Versenden von Daten beschafft werden:
public InputStream getInputStream() throws IOException public OutputStream getOutputStream() throws IOException |
java.net.Socket |
Diese Streams können entweder direkt verwendet oder mit Hilfe der Filterstreams in einen bequemer zu verwendenden Streamtyp geschachtelt werden. Nach Ende der Kommunikation sollten sowohl die Eingabe- und Ausgabestreams als auch der Socket selbst mit close geschlossen werden.
Als erstes Beispiel wollen wir uns ein Programm ansehen, das eine Verbindung zum DayTime-Service auf Port 13 herstellt. Dieser Service läuft auf fast allen UNIX-Maschinen und kann gut zu Testzwecken verwendet werden. Nachdem der Client die Verbindung aufgebaut hat, sendet der DayTime-Server einen String mit dem aktuellen Datum und der aktuellen Uhrzeit und beendet dann die Verbindung.
001 /* Listing4802.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4802 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 1) { 011 System.err.println("Usage: java Listing4802 <host>"); 012 System.exit(1); 013 } 014 try { 015 Socket sock = new Socket(args[0], 13); 016 InputStream in = sock.getInputStream(); 017 int len; 018 byte[] b = new byte[100]; 019 while ((len = in.read(b)) != -1) { 020 System.out.write(b, 0, len); 021 } 022 in.close(); 023 sock.close(); 024 } catch (IOException e) { 025 System.err.println(e.toString()); 026 System.exit(1); 027 } 028 } 029 } |
Listing4802.java |
Das Programm erwartet einen Hostnamen als Argument und gibt diesen
an den Konstruktor von Socket
weiter, der eine Verbindung zu diesem Host auf Port 13 erzeugt. Nachdem
der Socket steht, wird der InputStream
beschafft. Das Programm gibt dann so lange die vom Server gesendeten
Daten aus, bis durch den Rückgabewert -1 angezeigt wird, dass
keine weiteren Daten gesendet werden. Nun werden der Eingabestream
und der Socket geschlossen und das Programm beendet. Die Ausgabe des
Programms ist beispielsweise:
Sat Nov 7 22:58:37 1998
Um in Listing 48.2 den Socket
alternativ mit einem InetAddress-Objekt
zu öffnen, wäre Zeile 015
durch den folgenden Code zu ersetzen:
|
|
Nachdem wir jetzt wissen, wie man lesend auf einen Socket zugreift, wollen wir in diesem Abschnitt auch den schreibenden Zugriff vorstellen. Dazu schreiben wir ein Programm, das eine Verbindung zum ECHO-Service auf Port 7 herstellt. Das Programm liest so lange die Eingaben des Anwenders und sendet sie an den Server, bis das Kommando QUIT eingegeben wird. Der Server liest die Daten zeilenweise und sendet sie unverändert an unser Programm zurück, von dem sie auf dem Bildschirm ausgegeben werden. Um Lese- und Schreibzugriffe zu entkoppeln, verwendet das Programm einen separaten Thread, der die eingehenden Daten liest und auf dem Bildschirm ausgibt. Dieser läuft unabhängig vom Vordergrund-Thread, in dem die Benutzereingaben abgefragt und an den Server gesendet werden.
001 /* EchoClient.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class EchoClient 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 1) { 011 System.err.println("Usage: java EchoClient <host>"); 012 System.exit(1); 013 } 014 try { 015 Socket sock = new Socket(args[0], 7); 016 InputStream in = sock.getInputStream(); 017 OutputStream out = sock.getOutputStream(); 018 //Timeout setzen 019 sock.setSoTimeout(300); 020 //Ausgabethread erzeugen 021 OutputThread th = new OutputThread(in); 022 th.start(); 023 //Schleife für Benutzereingaben 024 BufferedReader conin = new BufferedReader( 025 new InputStreamReader(System.in)); 026 String line = ""; 027 while (true) { 028 //Eingabezeile lesen 029 line = conin.readLine(); 030 if (line.equalsIgnoreCase("QUIT")) { 031 break; 032 } 033 //Eingabezeile an ECHO-Server schicken 034 out.write(line.getBytes()); 035 out.write('\r'); 036 out.write('\n'); 037 } 038 //Programm beenden 039 System.out.println("terminating output thread..."); 040 th.requestStop(); 041 try { 042 Thread.sleep(1000); 043 } catch (InterruptedException e) { 044 } 045 in.close(); 046 out.close(); 047 sock.close(); 048 } catch (IOException e) { 049 System.err.println(e.toString()); 050 System.exit(1); 051 } 052 } 053 } 054 055 class OutputThread 056 extends Thread 057 { 058 InputStream in; 059 boolean stoprequested; 060 061 public OutputThread(InputStream in) 062 { 063 super(); 064 this.in = in; 065 stoprequested = false; 066 } 067 068 public synchronized void requestStop() 069 { 070 stoprequested = true; 071 } 072 073 public void run() 074 { 075 int len; 076 byte[] b = new byte[100]; 077 try { 078 while (!stoprequested) { 079 try { 080 if ((len = in.read(b)) == -1) { 081 break; 082 } 083 System.out.write(b, 0, len); 084 } catch (InterruptedIOException e) { 085 //nochmal versuchen 086 } 087 } 088 } catch (IOException e) { 089 System.err.println("OutputThread: " + e.toString()); 090 } 091 } 092 } |
EchoClient.java |
Eine Beispielsession mit dem Programm könnte etwa so aussehen
(Benutzereingaben sind fettgedruckt):
guido_k@pc1:/home/guido_k/nettest > java EchoClient localhost
hello
hello
world
world
12345
12345
quit
closing output thread...
Wie im vorigen Beispiel wird zunächst ein Socket zu dem als Argument angegebenen Host geöffnet. Das Programm beschafft dann Ein- und Ausgabestreams zum Senden und Empfangen von Daten. Der Aufruf von setSoTimeout gibt die maximale Wartezeit bei einem lesenden Zugriff auf den Socket an (300 ms.). Wenn bei einem read auf den InputStream nach Ablauf dieser Zeit noch keine Daten empfangen wurden, terminiert die Methode mit einer InterruptedIOException; wir kommen darauf gleich zurück. Nun erzeugt das Programm den Lesethread und übergibt ihm den Eingabestream. In der nun folgenden Schleife (Zeile 027) werden so lange Eingabezeilen gelesen und an den Server gesendet, bis der Anwender das Programm mit QUIT beendet.
Die Klasse OutputThread implementiert den Thread zum Lesen und Ausgeben der Daten. Da die Methode stop der Klasse Thread als deprecated markiert wurde, müssen wir mit Hilfe der Variable stoprequested etwas mehr Aufwand treiben, um den Thread beenden zu können. stoprequested steht normalerweise auf false und wird beim Beenden des Programms durch Aufruf von requestStop auf true gesetzt. In der Hauptschleife des Threads wird diese Variable periodisch abgefragt, um die Schleife bei Bedarf abbrechen zu können (Zeile 078).
Problematisch bei dieser Technik ist lediglich, dass der Aufruf von read normalerweise so lange blockiert, bis weitere Zeichen verfügbar sind. Steht das Programm also in Zeile 080, so hat ein Aufruf requestStop zunächst keine Wirkung. Da das Hauptprogramm in Zeile 045 die Streams und den Socket schließt, würde es zu einer SocketException kommen. Unser Programm verhindert das durch den Aufruf von setSoTimeout in Zeile 019. Dadurch wird ein Aufruf von read nach spätestens 300 ms. mit einer InterruptedIOException beendet. Diese Ausnahme wird in Zeile 084 abgefangen, um anschließend vor dem nächsten Schleifendurchlauf die Variable stoprequested erneut abzufragen.
Die Kommunikation mit einem Webserver erfolgt über das HTTP-Protokoll, wie es in den RFCs 1945 und 2068 beschrieben wurde. Ein Webserver läuft normalerweise auf TCP-Port 80 (manchmal läuft er zusätzlich auch auf dem UDP-Port 80) und kann wie jeder andere Server über einen Client-Socket angesprochen werden. Wir wollen an dieser Stelle nicht auf Details eingehen, sondern nur die einfachste und wichtigste Anwendung eines Webservers zeigen, nämlich das Übertragen einer Seite. Ein Webserver ist in seinen Grundfunktionen ein recht einfaches Programm, dessen Hauptaufgabe darin besteht, angeforderte Seiten an seine Clients zu versenden. Kompliziert wird er vor allem durch die Vielzahl der mittlerweile eingebauten Zusatzfunktionen, wie beispielsweise Logging, Server-Scripting, Server-Side-Includes, Security- und Tuning-Features usw.
Fordert ein Anwender in seinem Web-Browser eine Seite an, so wird
diese Anfrage vom Browser als GET-Transaktion
an den Server geschickt. Um beispielsweise die Seite http://www.javabuch.de/index.html
zu laden, wird folgendes Kommando an den Server www.javabuch.de
gesendet:
GET /index.html
Der erste Teil gibt den Kommandonamen an, dann folgt die gewünschte Datei. Die Zeile muss mit einer CRLF-Sequenz abgeschlossen werden, ein einfaches '\n' reicht nicht aus. Der Server versucht nun die angegebene Datei zu laden und überträgt sie an den Client. Ist der Client ein Web-Browser, wird er den darin befindlichen HTML-Code interpretieren und auf dem Bildschirm anzeigen. Befinden sich in der Seite Verweise auf Images, Applets oder Frames, so fordert der Browser die fehlenden Seiten in weiteren GET-Transaktionen von deren Servern ab.
Die Struktur des GET-Kommandos wurde mit der Einführung von HTTP
1.0 etwas erweitert. Zusätzlich werden nun am Ende der Zeile
eine Versionskennung und wahlweise in den darauffolgenden Zeilen weitere
Headerzeilen mit Zusatzinformationen mitgeschickt. Nachdem die letzte
Headerzeile gesendet wurde, folgt eine leere Zeile (also ein alleinstehendes
CRLF), um das Kommandoende anzuzeigen. HTTP 1.0 ist weit verbreitet
und das obige Kommando würde von den meisten Browsern in folgender
Form gesendet werden (jede der beiden Zeilen muss mit CRLF abgeschlossen
werden):
GET /index.html HTTP/1.0
Wird HTTP/1.0 verwendet, ist auch die Antwort des Servers etwas komplexer. Anstatt lediglich den Inhalt der Datei zu senden, liefert der Server seinerseits einige Headerzeilen mit Zusatzinformationen, wie beispielsweise den Server-Typ, das Datum der letzten Änderung oder den MIME-Typ der Datei. Auch hier ist jede Headerzeile mit einem CRLF abgeschlossen und nach der letzten Headerzeile folgt eine Leerzeile. Erst dann beginnt der eigentliche Dateiinhalt.
Das folgende Programm kann dazu verwendet werden, eine Datei mit Hilfe des HTTP 1.0-Protokolls von einem Webserver zu laden. Es wird mit einem Host- und einem Dateinamen als Argument aufgerufen und lädt die Seite vom angegebenen Server. Das Ergebnis wird (mit allen Headerzeilen) auf dem Bildschirm angezeigt. Anpassungen, die für das gebräuchliche HTTP 1.1-Protokoll erforderlich sind, werden im nächsten Abschnitt demonstriert, während sich der übernächste Abschnitt schließlich mit dem komfortablen Zugriff über die Klasse java.net.URL beschäftigt.
001 /* Listing4804.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4804 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 2) { 011 System.err.println( 012 "Usage: java Listing4804 <host> <file>" 013 ); 014 System.exit(1); 015 } 016 try { 017 Socket sock = new Socket(args[0], 80); 018 OutputStream out = sock.getOutputStream(); 019 InputStream in = sock.getInputStream(); 020 //GET-Kommando senden 021 String s = "GET " + args[1] + " HTTP/1.0" + "\r\n\r\n"; 022 out.write(s.getBytes()); 023 //Ausgabe lesen und anzeigen 024 int len; 025 byte[] b = new byte[4096]; 026 while ((len = in.read(b)) != -1) { 027 System.out.write(b, 0, len); 028 } 029 //Programm beenden 030 in.close(); 031 out.close(); 032 sock.close(); 033 } catch (IOException e) { 034 System.err.println(e.toString()); 035 System.exit(1); 036 } 037 } 038 } |
Listing4804.java |
Wird das Programm beispielsweise auf einem SUSE-Linux 5.2 mit frisch
installiertem Apache-Server mit localhost
und /index.html als Argument aufgerufen,
so beginnt seine Ausgabe wie folgt:
HTTP/1.1 200 OK
Date: Sun, 08 Nov 1998 18:26:13 GMT
Server: Apache/1.2.5 S.u.S.E./5.1
Last-Modified: Sun, 24 May 1998 00:46:46 GMT
ETag: "e852-45c-35676df6"
Content-Length: 1116
Accept-Ranges: bytes
Connection: close
Content-Type: text/html
<HTML>
<HEAD>
<TITLE>Apache HTTP Server - Beispielseite</TITLE>
</HEAD>
<BODY bgcolor=#ffffff>
<H1> Der Apache WWW Server </H1> <BR>
Diese Seite soll nur als Beispiel dienen.
Die <A HREF="./manual/">Dokumentation zum
Apache-Server</A> finden Sie hier.
<P>
...
Das am CERN in Genf entwickelte Protokoll HTTP 1.0 standardisierte den netzwerkbasierenden Zugriff auf entfernte Dokumente. Filehoster und multiple URLs auf ein und dieselbe IP-Adresse wurden während der Entwurfsphase des Protokolls allerdings noch nicht vorgesehen und erst in der nachfolgenden Version HTTP 1.1 berücksichtigt. Dieser Abschnitt beschreibt die Anpassungen, die vorgenommen werden müssen, um Dokumente über einen Socket von einem HTTP 1.1-Server herunterzuladen, doch zunächst ein wenig Motivation.
Eventuell sind Sie ja bereits stolzer Besitzer einer eigenen Domain und hosten diese bei einem Anbieter wie Strato, 1und1 etc. Dies bedeutet, dass Sie zwar rechtlicher Inhaber Ihrer Domain sind, alle Aufrufe über das Internet jedoch auf die Server Ihres Webhosters umgeleitet und die Dokumente von dort heruntergeladen werden. Abhängig von Ihrem Vertrag stellt Ihnen Ihr Webhoster dabei einen dedizierten (virtuellen) Server zur Verfügung oder lässt die Anfrage von einem Rechner beantworten, der neben Ihrer Domain auch für eine Reihe weiterer Domains verantwortlich ist. Dieses Konzept wird auch als Virtueller Host bezeichnet. Daraus kann sich aber nun folgendes Problem ergeben: Angenommen, Sie sind der Besitzer der Domain abc.de und hosten diese auf dem gleichen Server wie der Inhaber der Domain xyz.de. Außerdem beinhalten beide Internetauftritte das Dokument index.html. In dieser Konstellation wird der Server nun über seine IP-Netzwerkadresse angesprochen und aufgefordert, die Ressource index.html zu übermitteln. Da diese jedoch doppelt vorhanden ist, benötigt der Server zusätzlich die Information, unter welcher URL die betreffende Ressource zu finden sein soll.
Um das oben geschilderte Problem zu lösen, wurde mit HTTP 1.1 der zusätzliche Request-Header Host eingeführt, dessen Wert die angeforderte Domain enthält. Auf diese Weise kann das Konzept der virtuellen Hosts realisiert werden, da der Server die angeforderten Ressourcen nun eindeutig auflösen kann. Das folgende Listing zeigt, wie unter Verwendung des Host-Headers über das HTTP 1.1-Protokoll auf ein Dokument der Domain www.abc.de zugegriffen werden kann.
001 /* Listing4805.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4805 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 2) { 011 System.err.println( 012 "Usage: java Listing4805 <host> <file>" 013 ); 014 System.exit(1); 015 } 016 try { 017 Socket sock = new Socket(args[0], 80); 018 OutputStream out = sock.getOutputStream(); 019 InputStream in = sock.getInputStream(); 020 //GET-Kommando unter Verwendung des Host-Headers senden 021 String s = "GET " + args[1] + " HTTP/1.1" + "\r\n"; 022 s += "Host: www.abc.de\r\n\r\n"; 023 out.write(s.getBytes()); 024 //Ausgabe lesen und anzeigen 025 int len; 026 byte[] b = new byte[4096]; 027 while ((len = in.read(b)) != -1) { 028 System.out.write(b, 0, len); 029 } 030 //Programm beenden 031 in.close(); 032 out.close(); 033 sock.close(); 034 } catch (IOException e) { 035 System.err.println(e.toString()); 036 System.exit(1); 037 } 038 } 039 } |
Listing4805.java |
Die beiden vorangegangenen Beispiele demonstrieren die Arbeitsweise von Sockets am Beispiel des Zugriffs auf eine Internetressource. Dabei wurde, insbesondere beim Zugriff auf einen virtuellen Host, zusätzliches Wissen über das Protokoll HTTP benötigt.
Um den Zugriff auf eine HTTP-Ressource zu vereinfachen, stellt Java die Klasse URL aus dem Paket java.net zur Verfügung. Diese übernimmt per Konstruktor die Informationen über Host, Port, Protokoll und die angeforderte Ressource und öffnet über die Methode openStream() einen InputStream, über den die Daten abgerufen werden können. Das folgende Listing demonstriert den Zugriff auf eine Internetressource mit der Klasse java.net.URL.
001 /* Listing4806.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4806 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 2) { 011 System.err.println( 012 "Usage: java Listing4806 <host> <file>" 013 ); 014 System.exit(1); 015 } 016 017 try { 018 // Aufbau der Connection 019 URL url = new URL("http", args[0], 80, args[1]); 020 021 InputStream in = url.openStream(); 022 int len; 023 byte[] b = new byte[4096]; 024 while ((len = in.read(b)) != -1) { 025 System.out.write(b, 0, len); 026 } 027 028 //Programm beenden 029 in.close(); 030 } catch (IOException e) { 031 System.err.println(e.toString()); 032 System.exit(1); 033 } 034 } 035 } |
Listing4806.java |
Die vorangegangenen drei Abschnitte haben Ihnen gezeigt, wie Sie mit Hilfe eines Socket und unter stückweiser Implementierung des HTTP-Protokolls sowie unter Verwendung der Klasse java.net.URL auf die Dateien eines Webservers zugreifen. Der nächste Abschnitt demonstriert Ihnen nun, wie Sie das Gegenstück, also einen rudimentären Webserver, in Java implementieren können.
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
<< | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |