From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001
From: Sven Eisenhauer
+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:
+
+
+Der Konstruktor erzeugt einen ServerSocket
+für einen bestimmten Port, also einen bestimmten Typ von Serveranwendung
+(siehe Abschnitt 46.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:
+
+
+
+
+
+
+ Titel
+ Inhalt
+ Suchen
+ Index
+ DOC
+ Handbuch der Java-Programmierung, 5. Auflage
+
+ <<
+ <
+ >
+ >>
+ API
+ Kapitel 46 - Netzwerkprogrammierung
+
+
+
+
+
+46.3 Server-Sockets
+
+
+
+
+
+
+
+
+46.3.1 Die Klasse ServerSocket
+
+
+
+
+
+
+
+
+
+
+public ServerSocket(int port)
+ throws IOException
+
+public Socket accept()
+ throws IOException
+
+
+
+java.net.ServerSocket
+
+
+
+Listing 46.5: Ein ECHO-Server für Port 7
+
+
+
+
+
+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 46.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 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 46.2.4 war +schon angeklungen, dass ein Web-Server 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 47 +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 {"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 | +
+Der Web-Server besteht aus den beiden Klassen ExperimentalWebServer +und BrowserClientThread, die +nach dem in Abschnitt 46.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. |
+
+
|
+![]() |
+
+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 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, 5. Auflage, Addison +Wesley, Version 5.0.1 + |
| << + | < + | > + | >> + | API + | © 1998, 2007 Guido Krüger & Thomas +Stark, http://www.javabuch.de + |