Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 48 - 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 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 |
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.
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 |
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.
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 |
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. |
|
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:
Im Browser muss die Adresse dann ebenfalls um die Portnummer ergänzt werden: http://localhost:7777/index.html. |
|
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 |