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

## 45.2 Client-Sockets

- 45.2 Client-Sockets
  - 45.2.1 Adressierung
  - 45.2.2 Aufbau einer einfachen Socket-Verbindung
  - 45.2.3 Lesen und Schreiben von Daten
  - 45.2.4 Zugriff auf einen Web-Server

### 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:

- Zunächst wird eine Verbindung aufgebaut.
- Dann werden Daten gelesen und/oder geschrieben.
- Schließlich wird die Verbindung wieder abgebaut.

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
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de