Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 3. Auflage
 <<    <     >    >>   API  Kapitel 45 - Netzwerkprogrammierung

45.2 Client-Sockets



45.2.1 Adressierung

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
Listing 45.1: IP-Adressenauflösung

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.

 Hinweis 

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

45.2.2 Aufbau einer einfachen Socket-Verbindung

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
Listing 45.2: Abfrage des DayTime-Services

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:

InetAddress addr = InetAddress.getByName(args[0]);
Socket sock = new Socket(addr, 13);
 Tip 

45.2.3 Lesen und Schreiben von Daten

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
Listing 45.3: Lesender und schreibender Zugriff auf einen Socket

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.

 Warnung 

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.

45.2.4 Zugriff auf einen Web-Server

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.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 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
Listing 45.4: Laden einer Seite von einem Web-Server

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   DOC  Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0.1
 <<    <     >    >>   API  © 1998-2003 Guido Krüger, http://www.javabuch.de