Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 45 - 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 /* Listing4501.java */ 002 003 import java.net.*; 004 005 public class Listing4501 006 { 007 public static void main(String[] args) 008 { 009 if (args.length != 1) { 010 System.err.println("Usage: java Listing4501 <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 } |
Listing4501.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 muß allerdings korrekt installiert sein). Sollen wirkliche Adressen verarbeitet werden, muß 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
204.160.241.19
www.gkrueger.com
194.88.189.5
www.addison-wesley.de
194.77.140.21
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 muß 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 45.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 45.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 /* Listing4502.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4502 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 1) { 011 System.err.println("Usage: java Listing4502 <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 } |
Listing4502.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, daß
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 45.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 //Ausgabe abwarten 038 th.yield(); 039 } 040 //Programm beenden 041 System.out.println("terminating output thread..."); 042 th.requestStop(); 043 th.yield(); 044 try { 045 Thread.sleep(1000); 046 } catch (InterruptedException e) { 047 } 048 in.close(); 049 out.close(); 050 sock.close(); 051 } catch (IOException e) { 052 System.err.println(e.toString()); 053 System.exit(1); 054 } 055 } 056 } 057 058 class OutputThread 059 extends Thread 060 { 061 InputStream in; 062 boolean stoprequested; 063 064 public OutputThread(InputStream in) 065 { 066 super(); 067 this.in = in; 068 stoprequested = false; 069 } 070 071 public synchronized void requestStop() 072 { 073 stoprequested = true; 074 } 075 076 public void run() 077 { 078 int len; 079 byte[] b = new byte[100]; 080 try { 081 while (!stoprequested) { 082 try { 083 if ((len = in.read(b)) == -1) { 084 break; 085 } 086 System.out.write(b, 0, len); 087 } catch (InterruptedIOException e) { 088 //nochmal versuchen 089 } 090 } 091 } catch (IOException e) { 092 System.err.println("OutputThread: " + e.toString()); 093 } 094 } 095 } |
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.
Das Programm wurde auf einer LINUX-Version entwickelt, die noch kein präemptives Multithreading unterstützt. Die verschiedenen Aufrufe von yield dienen dazu, die Kontrolle an den Lesethread zu übergeben. Ohne diesen Aufruf würde der Lesethread gar nicht zum Zuge kommen und das Programm würde keine Daten vom Socket lesen. Auf Systemen, die präemptives Multithreading unterstützen, sind diese Aufrufe nicht notwendig. |
|
Die Klasse OutputThread implementiert den Thread zum Lesen und Ausgeben der Daten. Da die Methode stop der Klasse Thread im JDK 1.2 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 081).
Problematisch bei dieser Technik ist lediglich, daß der Aufruf von read normalerweise so lange blockiert, bis weitere Zeichen verfügbar sind. Steht das Programm also in Zeile 083, so hat ein Aufruf requestStop zunächst keine Wirkung. Da das Hauptprogramm in Zeile 048 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 087 abgefangen, um anschließend vor dem nächsten Schleifendurchlauf die Variable stoprequested erneut abzufragen.
Die Kommunikation mit einem Web-Server erfolgt über das HTTP-Protokoll, wie es in den RFCs 1945 und 2068 beschrieben wurde. Ein Web-Server 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 Web-Servers zeigen, nämlich das Übertragen einer Seite. Ein Web-Server 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.gkrueger.com/index.html
zu laden, wird folgendes Kommando an den Server www.gkrueger.com
gesendet:
GET /index.html
Der erste Teil gibt den Kommandonamen an, dann folgt die gewünschte Datei. Die Zeile muß 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 muß 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 von einem Web-Server 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.
001 /* Listing4504.java */ 002 003 import java.net.*; 004 import java.io.*; 005 006 public class Listing4504 007 { 008 public static void main(String[] args) 009 { 010 if (args.length != 2) { 011 System.err.println( 012 "Usage: java Listing4504 <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[100]; 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 } |
Listing4504.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>
...
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |