Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 45 - Netzwerkprogrammierung |
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 |
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.
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 |
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.
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 |
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:
Im Browser muß die Adresse dann ebenfalls um die Portnummer ergänzt werden: http://localhost:7777/index.html. |
|
Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |