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

48.3 Server-Sockets



48.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 48.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 48.7: Ein ECHO-Server für Port 7

Wird der Server gestartet, kann via Telnet oder mit dem EchoClient aus Listing 48.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, dass 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 

48.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 er 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 48.8: Eine verbesserte Version des Echo-Servers

Zur besseren Übersicht werden alle Client-Verbindungen durchnummeriert 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.

48.3.3 Entwicklung eines einfachen Webservers

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

Die Kommunikation zwischen einem Browser und einem Webserver 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 Webserver 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<StringBuffer> request = new Vector<StringBuffer>(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<StringBuffer> e = request.elements();
131     while (e.hasMoreElements()) {
132       sb = e.nextElement();
133       System.out.println("< " + sb.toString());
134     }
135     //Kommando, URL und HTTP-Version extrahieren
136     String s = 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 48.9: Ein experimenteller Webserver

Der Webserver besteht aus den beiden Klassen ExperimentalWebServer und BrowserClientThread, die nach dem in Abschnitt 48.3.2 vorgestellten Muster aufgebaut sind. Nachdem in ExperimentalWebServer eine Verbindung aufgebaut wurde, wird ein neuer Thread erzeugt und die weitere Bearbeitung des Request 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 Request 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 Webserver 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 Internetverbindungen wird ausführlich in Abschnitt 14.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 werden 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 stattdessen 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 muss 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, 7. Auflage, Addison Wesley, Version 7.0
 <<    <     >    >>   API  © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de