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

## 45.3 Server-Sockets

- 45.3 Server-Sockets
  - 45.3.1 Die Klasse ServerSocket
  - 45.3.2 Verbindungen zu mehreren Clients
  - 45.3.3 Entwicklung eines einfachen Web-Servers

### 45.3.1 Die Klasse ServerSocket

In den bisherigen Abschnitten hatten wir uns mit dem Entwurf von Netzwerk-Clients beschäftigt. Nun wollen wir uns das passende Gegenstück ansehen, uns also mit der Entwicklung von Servern beschäftigen. Glücklicherweise ist auch das in Java recht einfach. Der wesentliche Unterschied liegt in der Art des Verbindungsaufbaus, für den es eine spezielle Klasse `ServerSocket` gibt. Diese Klasse stellt Methoden zur Verfügung, um auf einen eingehenden Verbindungswunsch zu warten und nach erfolgtem Verbindungsaufbau einen `Socket` zur Kommunikation mit dem Client zurückzugeben. Bei der Klasse `ServerSocket` sind im wesentlichen der Konstruktor und die Methode `accept` von Interesse:

    public ServerSocket(int port)
      throws IOException

    public Socket accept()
      throws IOException
| java.net.ServerSocket

Der Konstruktor erzeugt einen `ServerSocket` für einen bestimmten Port, also einen bestimmten Typ von Serveranwendung (siehe Abschnitt 45.1.4). Anschließend wird die Methode `accept` aufgerufen, um auf einen eingehenden Verbindungswunsch zu warten. `accept` blockiert so lange, bis sich ein Client bei der Serveranwendung anmeldet (also einen Verbindungsaufbau zu unserem Host unter der Portnummer, die im Konstruktor angegeben wurde, initiiert). Ist der Verbindungsaufbau erfolgreich, liefert `accept` ein `Socket`-Objekt, das wie bei einer Client-Anwendung zur Kommunikation mit der Gegenseite verwendet werden kann. Anschließend steht der `ServerSocket` für einen weiteren Verbindungsaufbau zur Verfügung oder kann mit `close` geschlossen werden.

Wir wollen uns die Konstruktion von Servern an einem Beispiel ansehen. Dazu soll ein einfacher ECHO-Server geschrieben werden, der auf Port 7 auf Verbindungswünsche wartet. Alle eingehenden Daten sollen unverändert an den Client zurückgeschickt werden. Zur Kontrolle sollen sie ebenfalls auf die Konsole ausgegeben werden:

    001 /* SimpleEchoServer.java */
    002
    003 import java.net.*;
    004 import java.io.*;
    005
    006 public class SimpleEchoServer
    007 {
    008   public static void main(String[] args)
    009   {
    010     try {
    011       System.out.println("Warte auf Verbindung auf Port 7...");
    012       ServerSocket echod = new ServerSocket(7);
    013       Socket socket = echod.accept();
    014       System.out.println("Verbindung hergestellt");
    015       InputStream in = socket.getInputStream();
    016       OutputStream out = socket.getOutputStream();
    017       int c;
    018       while ((c = in.read()) != -1) {
    019         out.write((char)c);
    020         System.out.print((char)c);
    021       }
    022       System.out.println("Verbindung beenden");
    023       socket.close();
    024       echod.close();
    025     } catch (IOException e) {
    026       System.err.println(e.toString());
    027       System.exit(1);
    028     }
    029   }
    030 }
| SimpleEchoServer.java

Listing 45.5: Ein ECHO-Server für Port 7

Wird der Server gestartet, kann via Telnet oder mit dem `EchoClient` aus Listing 45.3 auf den Server zugegriffen werden:

    telnet localhost 7

Wenn der Server läuft, werden alle eingegebenen Zeichen direkt vom Server zurückgesendet und als Echo in Telnet angezeigt. Läuft er nicht, gibt es beim Verbindungsaufbau eine Fehlermeldung.

|  | Wird das Programm unter UNIX gestartet, kann es möglicherweise Probleme geben. Einerseits kann es sein, daß bereits ein ECHO-Server auf Port 7 läuft. Er könnte nötigenfalls per Eintrag in inetd.conf oder ähnlichen Konfigurationsdateien vorübergehend deaktiviert werden. Andererseits dürfen Server auf Ports kleiner 1024 nur mit Root-Berechtigung gestartet werden. Ein normaler Anwender darf dagegen nur Server-Ports größer 1023 verwenden. |  |
Hinweis

### 45.3.2 Verbindungen zu mehreren Clients

Wir wollen das im vorigen Abschnitt vorgestellte Programm nun in mehrfacher Hinsicht erweitern:

- Der Server soll mehr als einen Client gleichzeitig bedienen können. Die Clients sollen zur besseren Unterscheidung durchnumeriert werden.
- Beim Verbindungsaufbau soll der Client eine Begrüßungsmeldung erhalten.
- Für jeden Client soll ein eigener Thread angelegt werden.

Um diese Anforderungen zu erfüllen, verändern wir das obige Programm ein wenig. Im Hauptprogramm wird nun nur noch der `ServerSocket` erzeugt und in einer Schleife jeweils mit `accept` auf einen Verbindungswunsch gewartet. Nach dem Verbindungsaufbau erfolgt die weitere Bearbeitung nicht mehr im Hauptprogramm, sondern es wird ein neuer Thread mit dem Verbindungs-Socket als Argument erzeugt. Dann wird der Thread gestartet und erledigt die gesamte Kommunikation mit dem Client. Beendet der Client die Verbindung, wird auch der zugehörige Thread beendet. Das Hauptprogramm braucht sich nur noch um den Verbindungsaufbau zu kümmern und ist von der eigentlichen Client-Kommunikation vollständig befreit.

    001 /* EchoServer.java */
    002
    003 import java.net.*;
    004 import java.io.*;
    005
    006 public class EchoServer
    007 {
    008   public static void main(String[] args)
    009   {
    010     int cnt = 0;
    011     try {
    012       System.out.println("Warte auf Verbindungen auf Port 7...");
    013       ServerSocket echod = new ServerSocket(7);
    014       while (true) {
    015         Socket socket = echod.accept();
    016         (new EchoClientThread(++cnt, socket)).start();
    017       }
    018     } catch (IOException e) {
    019       System.err.println(e.toString());
    020       System.exit(1);
    021     }
    022   }
    023 }
    024
    025 class EchoClientThread
    026 extends Thread
    027 {
    028   private int    name;
    029   private Socket socket;
    030
    031   public EchoClientThread(int name, Socket socket)
    032   {
    033     this.name   = name;
    034     this.socket = socket;
    035   }
    036
    037   public void run()
    038   {
    039     String msg = "EchoServer: Verbindung " + name;
    040     System.out.println(msg + " hergestellt");
    041     try {
    042       InputStream in = socket.getInputStream();
    043       OutputStream out = socket.getOutputStream();
    044       out.write((msg + "\r\n").getBytes());
    045       int c;
    046       while ((c = in.read()) != -1) {
    047         out.write((char)c);
    048         System.out.print((char)c);
    049       }
    050       System.out.println("Verbindung " + name + " wird beendet");
    051       socket.close();
    052     } catch (IOException e) {
    053       System.err.println(e.toString());
    054     }
    055   }
    056 }
| EchoServer.java

Listing 45.6: Eine verbesserte Version des Echo-Servers

Zur besseren Übersicht werden alle Client-Verbindungen durchnumeriert und als erstes Argument an den Thread übergeben. Unmittelbar nach dem Verbindungsaufbau wird diese Meldung auf der Server-Konsole ausgegeben und an den Client geschickt. Anschließend wird in einer Schleife jedes vom Client empfangene Zeichen an diesen zurückgeschickt, bis er von sich aus die Verbindung unterbricht. Man kann den Server leicht testen, indem man mehrere Telnet-Sessions zu ihm aufbaut. Jeder einzelne Client sollte eine Begrüßungsmeldung mit einer eindeutigen Nummer erhalten und autonom mit dem Server kommunizieren können. Der Server sendet alle Daten zusätzlich an die Konsole und gibt sowohl beim Starten als auch beim Beenden eine entsprechende Meldung auf der Konsole aus.

### 45.3.3 Entwicklung eines einfachen Web-Servers

In Abschnitt 45.2.4 war schon angeklungen, daß ein Web-Server in seinen Grundfunktionen so einfach aufgebaut ist, daß wir uns hier eine experimentelle Implementierung ansehen können. Diese ist nicht nur zu Übungszwecken nützlich, sondern wird uns in Kapitel 46 bei der RMI-Programmierung behilflich sein, Bytecode "on demand" zwischen Client und Server zu übertragen.

Die Kommunikation zwischen einem Browser und einem Web-Server entspricht etwa folgendem Schema:

- Der Web-Browser baut eine Verbindung zum Server auf.
- Er schickt eine Seitenanforderung und ein paar zusätzliche Informationen in Form eines Requests gemäß HTTP-Spezifikation.
- Der Server analysiert den Request und schickt die gewünschte Datei (bzw. eine Fehlermeldung) an den Browser.
- Der Server beendet die Verbindung.

Hat der Browser auf diese Weise eine HTML-Seite erhalten, interpretiert er den HTML-Code und zeigt die Seite formatiert auf dem Bildschirm an. Enthält die Datei IMG-, APPLET- oder ähnliche Elemente, werden diese in derselben Weise vom Server angefordert und in die Seite eingebaut. Die wichtigste Aufgabe des Servers besteht also darin, eine Datei an den Client zu übertragen. Wir wollen uns zunächst das Listing ansehen und dann auf Details der Implementierung eingehen:

    001 /* ExperimentalWebServer.java */
    002
    003 import java.io.*;
    004 import java.util.*;
    005 import java.net.*;
    006
    007 /**
    008  * Ein ganz einfacher Web-Server auf TCP und einem
    009  * beliebigen Port. Der Server ist in der Lage,
    010  * Seitenanforderungen lokal zu dem Verzeichnis,
    011  * aus dem er gestartet wurde, zu bearbeiten. Wurde
    012  * der Server z.B. im Verzeichnis c:\tmp gestartet, so
    013  * würde eine Seitenanforderung
    014  * http://localhost:80/test/index.html die Datei
    015  * c:\tmp\test\index.html laden. CGIs, SSIs, Servlets
    016  * oder ähnliches wird nicht unterstützt.
    017  * <p>
    018  * Die Dateitypen .htm, .html, .gif, .jpg und .jpeg werden
    019  * erkannt und mit korrekten MIME-Headern übertragen, alle
    020  * anderen Dateien werden als "application/octet-stream"
    021  * übertragen. Jeder Request wird durch einen eigenen
    022  * Client-Thread bearbeitet, nach Übertragung der Antwort
    023  * schließt der Server den Socket. Antworten werden mit
    024  * HTTP/1.0-Header gesendet.
    025  */
    026 public class ExperimentalWebServer
    027 {
    028   public static void main(String[] args)
    029   {
    030     if (args.length != 1) {
    031       System.err.println(
    032         "Usage: java ExperimentalWebServer <port>"
    033       );
    034       System.exit(1);
    035     }
    036     try {
    037       int port = Integer.parseInt(args[0]);
    038       System.out.println("Listening to port " + port);
    039       int calls = 0;
    040       ServerSocket httpd = new ServerSocket(port);
    041       while (true) {
    042         Socket socket = httpd.accept();
    043         (new BrowserClientThread(++calls, socket)).start();
    044       }
    045     } catch (IOException e) {
    046       System.err.println(e.toString());
    047       System.exit(1);
    048     }
    049   }
    050 }
    051
    052 /**
    053  * Die Thread-Klasse für die Client-Verbindung.
    054  */
    055 class BrowserClientThread
    056 extends Thread
    057 {
    058   static final String[][] mimetypes = {
    059     {"html", "text/html"},
    060     {"htm",  "text/html"},
    061     {"txt",  "text/plain"},
    062     {"gif",  "image/gif"},
    063     {"jpg",  "image/jpeg"},
    064     {"jpeg", "image/jpeg"},
    065     {"jnlp", "application/x-java-jnlp-file"}
    066   };
    067
    068   private Socket       socket;
    069   private int          id;
    070   private PrintStream  out;
    071   private InputStream  in;
    072   private String       cmd;
    073   private String       url;
    074   private String       httpversion;
    075
    076   /**
    077    * Erzeugt einen neuen Client-Thread mit der angegebenen
    078    * id und dem angegebenen Socket.
    079    */
    080   public BrowserClientThread(int id, Socket socket)
    081   {
    082     this.id     = id;
    083     this.socket = socket;
    084   }
    085
    086   /**
    087    * Hauptschleife für den Thread.
    088    */
    089   public void run()
    090   {
    091     try {
    092       System.out.println(id + ": Incoming call...");
    093       out = new PrintStream(socket.getOutputStream());
    094       in = socket.getInputStream();
    095       readRequest();
    096       createResponse();
    097       socket.close();
    098       System.out.println(id + ": Closed.");
    099     } catch (IOException e) {
    100       System.out.println(id + ": " + e.toString());
    101       System.out.println(id + ": Aborted.");
    102     }
    103   }
    104
    105   /**
    106    * Liest den nächsten HTTP-Request vom Browser ein.
    107    */
    108   private void readRequest()
    109   throws IOException
    110   {
    111     //Request-Zeilen lesen
    112     Vector request = new Vector(10);
    113     StringBuffer sb = new StringBuffer(100);
    114     int c;
    115     while ((c = in.read()) != -1) {
    116       if (c == '\r') {
    117         //ignore
    118       } else if (c == '\n') { //line terminator
    119         if (sb.length() <= 0) {
    120           break;
    121         } else {
    122           request.addElement(sb);
    123           sb = new StringBuffer(100);
    124         }
    125       } else {
    126         sb.append((char)c);
    127       }
    128     }
    129     //Request-Zeilen auf der Konsole ausgeben
    130     Enumeration e = request.elements();
    131     while (e.hasMoreElements()) {
    132       sb = (StringBuffer)e.nextElement();
    133       System.out.println("< " + sb.toString());
    134     }
    135     //Kommando, URL und HTTP-Version extrahieren
    136     String s = ((StringBuffer)request.elementAt(0)).toString();
    137     cmd = "";
    138     url = "";
    139     httpversion = "";
    140     int pos = s.indexOf(' ');
    141     if (pos != -1) {
    142       cmd = s.substring(0, pos).toUpperCase();
    143       s = s.substring(pos + 1);
    144       //URL
    145       pos = s.indexOf(' ');
    146       if (pos != -1) {
    147         url = s.substring(0, pos);
    148         s = s.substring(pos + 1);
    149         //HTTP-Version
    150         pos = s.indexOf('\r');
    151         if (pos != -1) {
    152           httpversion = s.substring(0, pos);
    153         } else {
    154           httpversion = s;
    155         }
    156       } else {
    157         url = s;
    158       }
    159     }
    160   }
    161
    162   /**
    163    * Request bearbeiten und Antwort erzeugen.
    164    */
    165   private void createResponse()
    166   {
    167     if (cmd.equals("GET") || cmd.equals("HEAD")) {
    168       if (!url.startsWith("/")) {
    169         httpError(400, "Bad Request");
    170       } else {
    171         //MIME-Typ aus Dateierweiterung bestimmen
    172         String mimestring = "application/octet-stream";
    173         for (int i = 0; i < mimetypes.length; ++i) {
    174           if (url.endsWith(mimetypes[i][0])) {
    175             mimestring = mimetypes[i][1];
    176             break;
    177           }
    178         }
    179         //URL in lokalen Dateinamen konvertieren
    180         String fsep = System.getProperty("file.separator", "/");
    181         StringBuffer sb = new StringBuffer(url.length());
    182         for (int i = 1; i < url.length(); ++i) {
    183           char c = url.charAt(i);
    184           if (c == '/') {
    185             sb.append(fsep);
    186           } else {
    187             sb.append(c);
    188           }
    189         }
    190         try {
    191           FileInputStream is = new FileInputStream(sb.toString());
    192           //HTTP-Header senden
    193           out.print("HTTP/1.0 200 OK\r\n");
    194           System.out.println("> HTTP/1.0 200 OK");
    195           out.print("Server: ExperimentalWebServer 0.5\r\n");
    196           System.out.println(
    197             "> Server: ExperimentalWebServer 0.5"
    198           );
    199           out.print("Content-type: " + mimestring + "\r\n\r\n");
    200           System.out.println("> Content-type: " + mimestring);
    201           if (cmd.equals("GET")) {
    202             //Dateiinhalt senden
    203             byte[] buf = new byte[256];
    204             int len;
    205             while ((len = is.read(buf)) != -1) {
    206               out.write(buf, 0, len);
    207             }
    208           }
    209           is.close();
    210         } catch (FileNotFoundException e) {
    211           httpError(404, "Error Reading File");
    212         } catch (IOException e) {
    213           httpError(404, "Not Found");
    214         } catch (Exception e) {
    215           httpError(404, "Unknown exception");
    216         }
    217       }
    218     } else {
    219       httpError(501, "Not implemented");
    220     }
    221   }
    222
    223   /**
    224    * Eine Fehlerseite an den Browser senden.
    225    */
    226   private void httpError(int code, String description)
    227   {
    228     System.out.println("> ***" + code + ": " + description + "***");
    229     out.print("HTTP/1.0 " + code + " " + description + "\r\n");
    230     out.print("Content-type: text/html\r\n\r\n");
    231     out.println("<html>");
    232     out.println("<head>");
    233     out.println("<title>ExperimentalWebServer-Error</title>");
    234     out.println("</head>");
    235     out.println("<body>");
    236     out.println("<h1>HTTP/1.0 " + code + "</h1>");
    237     out.println("<h3>" + description + "</h3>");
    238     out.println("</body>");
    239     out.println("</html>");
    240   }
    241 }
| ExperimentalWebServer.java

Listing 45.7: Ein experimenteller Web-Server

Der Web-Server besteht aus den beiden Klassen `ExperimentalWebServer` und `BrowserClientThread`, die nach dem in Abschnitt 45.3.2 vorgestellten Muster aufgebaut sind. Nachdem in `ExperimentalWebServer` eine Verbindung aufgebaut wurde, wird ein neuer Thread erzeugt und die weitere Bearbeitung des Requests an ein Objekt der Klasse `BrowserClientThread` delegiert. Der in `run` liegende Code beschafft zunächst die Ein- und Ausgabestreams zur Kommunikation mit dem Socket und ruft dann die beiden Methoden `readRequest` und `createResponse` auf. Anschließend wird der Socket geschlossen und der Thread beendet.

In `readRequest` wird der HTTP-Request des Browsers gelesen, der aus mehreren Zeilen besteht. In der ersten wird die eigentliche Dateianforderung angegeben, die übrigen liefern Zusatzinformationen wie den Typ des Browsers, akzeptierte Dateiformate und ähnliches. Alle Zeilen werden mit CRLF abgeschlossen, nach der letzten Zeile des Requests wird eine Leerzeile gesendet. Entsprechend der Empfehlung in RFC1945 ignoriert unser Parser die '\r'-Zeichen und erkennt das Zeilenende anhand eines '\n'. So arbeitet er auch dann noch korrekt, wenn ein Client die Headerzeilen versehentlich mit einem einfachen LF abschließt.

Ein typischer Request könnte etwa so aussehen (in diesem Beispiel wurde er von Netscape 4.04 unter Windows 95 generiert):

    GET /ansisys.html HTTP/1.0
    Connection: Keep-Alive
    User-Agent: Mozilla/4.04 [en] (Win95; I)
    Host: localhost:80
    Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
    Accept-Language: en
    Accept-Charset: iso-8859-1,*,utf-8
    HTTP/1.0 200 OK
    Server: ExperimentalWebServer 0.5
    Content-type: text/html

Unser Web-Server liest den Request zeilenweise in den `Vector` `request` ein und gibt alle Zeilen zur Kontrolle auf der Konsole aus. Anschließend wird das erste Element extrahiert und in die Bestandteile Kommando, URL (Dateiname) und HTTP-Version zerlegt. Diese Informationen werden zur weiteren Verarbeitung in den Membervariablen `cmd`, `url` und `httpversion` gespeichert.

Nachdem der Request gelesen wurde, wird in `createResponse` die Antwort erzeugt. Zunächst prüft die Methode, ob es sich um ein `GET`- oder `HEAD`-Kommando handelt (HTTP kennt noch mehr). Ist das nicht der Fall, wird durch Aufruf von `httpError` eine Fehlerseite an den Browser gesendet. Andernfalls fährt die Methode mit der Bestimmung des Dateityps fort. Der Dateityp wird mit Hilfe der Arraykonstante `mimetypes` anhand der Dateierweiterung bestimmt und in einen passenden MIME-Typ konvertiert, der im Antwortheader an den Browser übertragen wird. Der Browser entscheidet anhand dieser Information, was mit der nachfolgend übertragenen Datei zu tun ist (Anzeige als Text, Anzeige als Grafik, Speichern in einer Datei usw.). Wird eine Datei angefordert, deren Erweiterung nicht bekannt ist, sendet der Server sie als `application/octet-stream` an den Browser, damit dieser dem Anwender die Möglichkeit geben kann, die Datei auf der Festplatte zu speichern.

|  | Der Mime-Typ `application/x-java-jnlp-file` wird für den Betrieb von Java Web Start benötigt. Dieses seit dem JDK 1.4 verfügbare Werkzeug zum Laden, Aktualisieren und Starten von Java-Programmen über Internet-Verbindungen wird ausführlich in Abschnitt 13.5 erläutert. |  |
Hinweis

Nun wandelt der Server den angegebenen Dateinamen gemäß den Konventionen seines eigenen Betriebssystems um. Dazu wird das erste "/" aus dem Dateinamen entfernt (alle Dateien werden lokal zu dem Verzeichnis geladen, aus dem der Server gestartet wurde) und alle "/" innerhalb des Pfadnamens werden in den lokalen Pfadseparator konvertiert (unter MS-DOS ist das beispielsweise der Backslash). Dann wird die Datei mit einem `FileInputStream` geöffnet und der HTTP-Header und der Dateiinhalt an den Client gesendet. Konnte die Datei nicht geöffnet werden, wird eine Ausnahme ausgelöst und der Server sendet eine Fehlerseite.

Der vom Server gesendete Header ist ähnlich aufgebaut wie der Request-Header des Clients. Er enthält mehrere Zeilen, die durch CRLF-Sequenzen voneinander getrennt sind. Nach der letzten Headerzeile folgt eine Leerzeile, also zwei aufeinanderfolgende CRLF-Sequenzen. HTTP 1.0 und 1.1 spezifizieren eine ganze Reihe von (optionalen) Headerelementen, von denen wir lediglich die Versionskennung, unseren Servernamen und den MIME-Bezeichner mit der Typkennung der gesendeten Datei an den Browser übertragen. Unmittelbar nach dem Ende des Headers wird der Dateiinhalt übertragen. Eine Umkodierung erfolgt dabei normalerweise nicht, alle Bytes werden unverändert übertragen.

Unser Server kann sehr leicht getestet werden. Am einfachsten legt man ein neues Unterverzeichnis an und kopiert die übersetzten Klassendateien und einige HTML-Dateien in dieses Verzeichnis. Nun kann der Server wie jedes andere Java-Programm gestartet werden. Beim Aufruf ist zusätzlich die Portnummer als Argument anzugeben:

    java ExperimentalWebServer 80

Nun kann ein normaler Web-Browser verwendet werden, um Dateien vom Server zu laden. Befindet sich beispielsweise eine Datei index.html im Server-Verzeichnis und läuft der Server auf derselben Maschine wie der Browser, kann die Datei über die Adresse `http://localhost/index.html` im Browser geladen werden. Auch über das lokale Netz des Unternehmens oder das Internet können leicht Dateien geladen werden. Hat der Host, auf dem der Server läuft, keinen Nameserver-Eintrag, kann statt dessen auch direkt seine IP-Adresse im Browser angegeben werden.

|  |  |  | Auf einem UNIX-System darf ein Server die Portnummer 80 nur verwenden, wenn er Root-Berechtigung hat. Ist das nicht der Fall, kann der Server alternativ auf einem Port größer 1023 gestartet werden:
    java ExperimentalWebServer 7777
Im Browser muß die Adresse dann ebenfalls um die Portnummer ergänzt werden: `http://localhost:7777/index.html`. |  |
Warnung

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