Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 45 - Netzwerkprogrammierung

45.3 Server-Sockets



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 gegen. 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:

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:

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   };
066 
067   private Socket       socket;
068   private int          id;
069   private PrintStream  out;
070   private InputStream  in;
071   private String       cmd;
072   private String       url;
073   private String       httpversion;
074 
075   /**
076    * Erzeugt einen neuen Client-Thread mit der angegebenen 
077    * id und dem angegebenen Socket.
078    */
079   public BrowserClientThread(int id, Socket socket)
080   {
081     this.id     = id;
082     this.socket = socket;
083   }
084 
085   /**
086    * Hauptschleife für den Thread.
087    */
088   public void run()
089   {
090     try {
091       System.out.println(id + ": Incoming call...");
092       out = new PrintStream(socket.getOutputStream());
093       in = socket.getInputStream();
094       readRequest();
095       createResponse();
096       socket.close();
097       System.out.println(id + ": Closed.");
098     } catch (IOException e) {
099       System.out.println(id + ": " + e.toString());
100       System.out.println(id + ": Aborted.");
101     }
102   }
103 
104   /**
105    * Liest den nächsten HTTP-Request vom Browser ein.
106    */
107   private void readRequest()
108   throws IOException
109   {
110     //Request-Zeilen lesen
111     Vector request = new Vector(10);
112     StringBuffer sb = new StringBuffer(100);
113     int c;
114     while ((c = in.read()) != -1) {
115       if (c == '\r') {
116         //ignore
117       } else if (c == '\n') { //line terminator
118         if (sb.length() <= 0) {
119           break;
120         } else {
121           request.addElement(sb);
122           sb = new StringBuffer(100);
123         }
124       } else {
125         sb.append((char)c);
126       }
127     }
128     //Request-Zeilen auf der Konsole ausgeben
129     Enumeration e = request.elements();
130     while (e.hasMoreElements()) {
131       sb = (StringBuffer)e.nextElement();
132       System.out.println("< " + sb.toString());
133     }
134     //Kommando, URL und HTTP-Version extrahieren
135     String s = ((StringBuffer)request.elementAt(0)).toString();
136     cmd = "";
137     url = "";
138     httpversion = "";
139     int pos = s.indexOf(' ');
140     if (pos != -1) {
141       cmd = s.substring(0, pos).toUpperCase();
142       s = s.substring(pos + 1);
143       //URL
144       pos = s.indexOf(' ');
145       if (pos != -1) {
146         url = s.substring(0, pos);
147         s = s.substring(pos + 1);
148         //HTTP-Version
149         pos = s.indexOf('\r');
150         if (pos != -1) {
151           httpversion = s.substring(0, pos);
152         } else {
153           httpversion = s;
154         }
155       } else {
156         url = s;
157       }
158     }
159   }
160 
161   /**
162    * Request bearbeiten und Antwort erzeugen.
163    */
164   private void createResponse()
165   {
166     if (cmd.equals("GET")) {
167       if (!url.startsWith("/")) {
168         httpError(400, "Bad Request");
169       } else {
170         //MIME-Typ aus Dateierweiterung bestimmen
171         String mimestring = "application/octet-stream";
172         for (int i = 0; i < mimetypes.length; ++i) {
173           if (url.endsWith(mimetypes[i][0])) {
174             mimestring = mimetypes[i][1];
175             break;
176           }
177         }
178         //URL in lokalen Dateinamen konvertieren
179         String fsep = System.getProperty("file.separator", "/");
180         StringBuffer sb = new StringBuffer(url.length());
181         for (int i = 1; i < url.length(); ++i) {
182           char c = url.charAt(i);
183           if (c == '/') {
184             sb.append(fsep);
185           } else {
186             sb.append(c);
187           }
188         }
189         try {
190           FileInputStream is = new FileInputStream(sb.toString());
191           //HTTP-Header senden
192           out.print("HTTP/1.0 200 OK\r\n");
193           System.out.println("> HTTP/1.0 200 OK");
194           out.print("Server: ExperimentalWebServer 0.5\r\n");
195           System.out.println(
196             "> Server: ExperimentalWebServer 0.5"
197           );
198           out.print("Content-type: " + mimestring + "\r\n\r\n");
199           System.out.println("> Content-type: " + mimestring);
200           //Dateiinhalt senden
201           byte[] buf = new byte[256];
202           int len;
203           while ((len = is.read(buf)) != -1) {
204             out.write(buf, 0, len);
205           }
206           is.close();
207         } catch (FileNotFoundException e) {
208           httpError(404, "Error Reading File");
209         } catch (IOException e) {
210           httpError(404, "Not Found");
211         } catch (Exception e) {
212           httpError(404, "Unknown exception");
213         }
214       }
215     } else {
216       httpError(501, "Not implemented");
217     }
218   }
219     
220   /**
221    * Eine Fehlerseite an den Browser senden.
222    */
223   private void httpError(int code, String description)
224   {
225     out.print("HTTP/1.0 " + code + " " + description + "\r\n");
226     out.print("Content-type: text/html\r\n\r\n");
227     out.println("<html>");
228     out.println("<head>");
229     out.println("<title>ExperimentalWebServer-Error</title>");
230     out.println("</head>");
231     out.println("<body>");
232     out.println("<h1>HTTP/1.0 " + code + "</h1>");
233     out.println("<h3>" + description + "</h3>");
234     out.println("</body>");
235     out.println("</html>");
236   }
237 }
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-Kommando handelt (HTTP kennt noch andere Kommandos). 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.

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   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com