16.7 Mit dem Socket zum Server
 
Die URL-Verbindungen sind schon High-level-Verbindungen, und wir müssen uns nicht erst um Übertragungsprotokolle wie HTTP oder – noch tiefer – TCP/IP kümmern. Aber alle höheren Verbindungen bauen auf Sockets auf, und auch die Verbindung zu einem Rechner über URL ist mit Sockets realisiert. Beschäftigen wir uns also nun etwas mit dem Hintergrund.
16.7.1 Das Netzwerk ist der Computer
 
Die Rechner, die im Internet verbunden sind, kommunizieren über Protokolle, wobei TCP/IP das wichtigste geworden ist. Die Entwicklung des Protokolls geht in die Achtzigerjahre zurück. Die ARPA (Advanced Research Projects Agency) gab der Universität von Berkeley (Kalifornien) den Auftrag, unter Unix das TCP/IP-Protokoll zu implementieren, um dort im Netzwerk zu kommunizieren.1
Was sich die Kalifornier ausgedacht hatten, fand auch in der Berkeley Software Distribution (BSD), einer Unix-Variante, Verwendung: die Berkeley-Sockets. Mittlerweile hat sich das Berkeley-Socket-Interface über alle Betriebssystemgrenzen hinweg entwickelt und ist der De-facto-Standard für TCP/IP-Kommunikation, so auch unter Windows.
16.7.2 Sockets
 
Ein Socket dient zur Abstraktion und ist ein Verbindungspunkt in einem TCP/IP-Netzwerk. Werden mehrere Computer verbunden, so implementiert jeder Rechner einen Socket: Derjenige, der Daten empfängt (Client), öffnet eine Socket-Verbindung zum Horchen, und derjenige, der sendet, öffnet eine Verbindung zum Senden (Server). Es lässt sich in der Realität nicht immer ganz trennen, wer Client und wer Server ist, da Server zum Datenaustausch ebenfalls Verbindungen aufbauen können. Doch für den Betrachter von außen ist der Server der Wartende und der Client derjenige, der die Verbindung initiiert.
Serveradresse und Port
Damit der Empfänger den Sender auch hören kann, muss dieser durch eine eindeutige Adresse als Server ausgemacht werden. Er bekommt also eine IP-Adresse im Netz und eine ebenso eindeutige Port-Adresse. Der Port ist so etwas wie eine Zimmernummer im Hotel. Die Adresse bleibt dieselbe, aber in jedem Zimmer sitzt jemand und macht seine Aufgaben.
Jeder Dienst (Service), den ein Server zur Verfügung stellt, läuft auf einem anderen Port. Eine Port-Nummer ist eine 16-Bit-Zahl und in die Gruppen »System« und »Benutzer« eingeteilt. Die so genannten Well-Known-System-Ports (auch Contact Ports genannt) liegen im Bereich von 0–1023. (Noch vor einigen Jahren haben 255 definierte Nummern ausgereicht.) Die User-Ports umfassen den restlichen Bereich von 1024–65535. Server vergeben die Ports aber nicht willkürlich. Die IANA2
(Internet Assigned Numbers Authority) ist der Koordinator für IP-Adressen, Domain-Namen, MIME-Typen und für viele andere Parameter, unter anderem auch für die Port-Nummern – Näheres unter http://www.iana.org/.
Die folgende Tabelle zeigt einige wenige dieser Port-Nummern. Die vollständige Liste ist unter http://www.iana.org/assignments/port-numbers verfügbar.
Tabelle 16.2
Einige ausgewählte System-Ports
Service
|
Port
|
Beschreibung
|
echo
|
7
|
Echo
|
daytime
|
13
|
Daytime
|
qotd
|
17
|
Quote of the Day
|
ftp-data
|
20
|
File Transfer [Default Data]
|
ftp
|
21
|
File Transfer [Control]
|
ssh
|
22
|
SSH Remote Login Protocol
|
telnet
|
23
|
Telnet
|
smtp
|
25
|
Simple Mail Transfer
|
time
|
37
|
Time
|
nicname
|
43
|
Who Is
|
domain
|
53
|
Domain Name Server
|
whois++
|
63
|
whois++
|
gopher
|
70
|
Gopher
|
finger
|
79
|
Finger
|
www
|
80
|
World Wide Web HTTP
|
pop2
|
109
|
Post Office Protocol – Version 2
|
pop3
|
110
|
Post Office Protocol – Version 3
|
Stream-Sockets/Datagram-Sockets
Ein Stream-Socket baut eine feste Verbindung zu einem Rechner auf. Das Besondere daran: Die Verbindung bleibt für die Dauer der Übertragung bestehen. Dies ist bei der anderen Form der Sockets, den Datagram-Sockets, nicht der Fall. Wir behandeln die Stream-Sockets zuerst.
16.7.3 Standarddienste unter Windows nachinstallieren
 
Wir wollen nun mit der Netzwerkprogrammierung bei den Clients beginnen. Diese greifen auf einen Server zu und nutzen dessen Dienste. Ein solcher Dienst wäre etwa ein Zeit-Server oder ein Echo-Server. Der Zeit-Server liefert die aktuelle Tageszeit, und ein Echo-Server würde alle gesendeten Zeichenketten wieder zurückschicken. Wenn wir allerdings einen Dienst auf einem Rechner nutzen wollen, muss dieser auch angeboten werden. Unter Unix gibt es zahlreiche Standarddienste. Allerdings haben die Windows-Leser überhaupt keine Dienste (ein Portscann zeigt dies leicht). Um Unix- und Windows-Leser auf den gleichen Stand zu bringen, wollen wir unter Windows zwei Standarddienste installieren. Diese sind unter Windows abhängig von WinSock (http://www.sockets.com/). WinSock ist die Abkürzung für Windows Socket und wird immer dann benötigt, wenn ein Computer in einem lokalen Netzwerk und über Modem an das Internet soll. Eine manuelle Installation von WinSock ist nicht mehr nötig; TCP/IP sollte aber auf dem Rechner installiert sein. Da sich viele Rechner jedoch im Internet oder im Netz befinden, ist dies vermutlich die Standardkonfiguration. Uns interessieren aber die Dienste. Sie bauen auf WinSock auf, und unter der Web-Seite »WinSock Development Information« finden wir unter »Sample Applications« einen Verweis (ftp://ftp.sockets.com/sockets/wsa_all.zip) auf das Paket mit den Quellcodes. Dieses Paket ist etwas über ein Megabyte groß und enthält mehr, als wir benötigen.3
Daher habe ich nur die ausführbaren Programme unter http://java-tutor.com/download/internet/WindowsSocketsBinaries.zip abgelegt. Das Archiv weist eine Größe von 196 KB auf. In ihm befindet sich zum Beispiel ASECHO32.EXE, das den Echo-Dienst anbietet.
 Hier klicken, um das Bild zu Vergrößern
Abbildung 16.1
Der Echo-Server bei der Arbeit
16.7.4 Eine Verbindung zum Server aufbauen
 
Um Daten von einer Stelle zur anderen zu schicken, muss zunächst eine Verbindung zum Server bestehen. Dieser wiederum beantwortet die eingehenden Fragen. Mit den Netzwerkklassen unter Java lassen sich sowohl Client- als auch Server-basierte Programme schreiben. Da die Client-Seite noch einfacher als die Server-Seite ist – in Java ist Netzwerkprogrammierung ein Genuss –, beginnen wir mit dem Client. Dieser muss zu einem horchenden Server verbunden werden – eine Verbindung, die durch die java.net.Socket-Klasse aufgebaut wird.
Socket clientSocket = new Socket( "die.weite.welt", 80 );
Der erste Parameter des Konstruktors erwartet den Namen des Servers (Host-Adresse), mit dem wir uns verbinden wollen. Der zweite Parameter steht für den Port – wir haben hier 80 gewählt, um zu einem Web-Server eine Verbindung aufzubauen.
Hinweis Verbinden wir ein Applet mit dem Server, von dem es geladen wurde, würden wir mit getCodeBase().getHost() arbeiten, etwa so:
Socket server = new Socket( getCodeBase().getHost(), 7 );
|
Es gibt noch eine andere Möglichkeit, um zu einem Host zu gelangen: über die Klasse InetAddress.
secondSocket = new Socket( server.getInetAddress(), 1234 );
Alternativ ermittelt die Funktion getHostByName(String) die InetAddress eines Hosts. Ist der Server nicht erreichbar, so wirft das System bei allen Socket-Konstruktionsversuchen eine UnknownHostException aus; dabei handelt es sich um eine Unterklasse von IOException, sodass grundsätzlich ein Auffangen/Weiterleiten einer IOException ausreicht.
|
Socket( String host, int port ) throws IOException
Erzeugt einen Stream-Socket und verbindet ihn mit der Port-Nummer am angegebenen Host. |
|
Socket( InetAddress address, int port ) throws IOException
Erzeugt einen Stream-Socket und verbindet ihn mit der Port-Nummer am Host mit der angegebenen IP-Nummer. |
|
Socket( String host, int port, InetAddress localAddr, int localPort )
throws IOException
Erzeugt einen Socket für den Host host am Port port und bindet ihn an die lokale Adresse localAddr und an den lokalen Port localPort. |
|
Socket( InetAddress address, int port, InetAddress localAddr, int localPort )
throws IOException
Erzeugt einen Socket für den durch address gegebenen Host am Port port und bindet ihn an die lokale Adresse localAddr und an den lokalen Port localPort. |
|
Socket() throws IOException
Erzeugt einen nicht verbundenen Socket über die Standard-SocketImpl. |
|
protected Socket( SocketImpl impl ) throws IOException
Erzeugt einen unverbundenen Socket mit einer benutzerdefinierten SocketImpl. Nützlich für Unterklassen mit angepassten Verbindungen, etwa gesicherten Verbindungen. |
16.7.5 Server unter Spannung: die Ströme
 
Besteht erst einmal die Verbindung, so wird mit den Daten vom Server genauso verfahren wie mit den Daten aus einer Datei. Die Socket-Klasse liefert uns Streams, mit denen wir lesen und schreiben können. Nun bietet die Klasse Socket die Methoden getInputStream() und getOutputStream(), die einen Zugang zum Datenstrom erlauben. Holen wir uns zunächst einen Ausgabestrom vom Typ OutputSteam:
OutputStream out = server.getOutputStream()
Oft wird dieser einfache OutputStream zu einem DataOutputStream oder PrintWriter beziehungsweise PrintStream aufgewertet, damit die Ausgabemöglichkeiten vielfältiger sind. Genauso wird mit dem Eingabestrom verfahren. Wandeln wir ihn gleich in einen BufferedReader um:
BufferedReader in = new BufferedReader(
new InputStreamReader( server.getInputStream()) );
Wir kennen das Prinzip schon von den URL-Verbindungen und von der Dateieingabe/-ausgabe.
|
InputStream getInputStream() throws IOException
Liefert den Eingabestrom für den Socket. |
|
OutputStream getOutputStream() throws IOException
Liefert den Ausgabestrom für den Socket. |
16.7.6 Die Verbindung wieder abbauen
 
Die Methode close() leitet das Ende einer Verbindung ein und gibt dem Betriebssystem die reservierten Handles zurück. Ohne Freigabe könnte das Betriebssystem unter Umständen nach einer gewissen Zeit keine Handles mehr zurückgeben, und eine Fortsetzung der Arbeit wäre nicht möglich. Dies geht so weit, dass auch der Browser keine HTML-Seite mehr vom Server bekommt. Kommt es jedoch vor, dass sich zwar einige Verbindungen aufbauen lassen, danach aber Schluss ist, sollte diese Lücke untersucht werden.
|
void close() throws IOException
Schließt den Socket. |
16.7.7 Ein kleines Echo – lebt der Rechner noch?
 
Möchten wir überprüfen, ob ein Rechner in der Lage ist, Kommandos über seine Netzwerkschnittstelle entgegenzunehmen, so können wir ein Kommando hinschicken und warten, ob etwas passiert. Am einfachsten ist der Aufbau zum Echo-Server, ein Service, der alle ankommenden Kommandos gleich wieder zurückschickt.
Wenn wir den Echo-Dienst nutzen wollen, dann senden wir ein Testwort zum Server und überprüfen, ob das gleiche Wort wieder zurückkommt. Die Herstellung der Verbindung zum Echo-Server ist mit der Socket-Klasse kein Problem. Da der Echo-Service immer an Port 7 liegt, eröffnet die Anweisung Socket(IPAdress, 7) die Verbindung. Anschließend lassen sich InputStream und OutputStream holen, und die Anfrage lässt sich verarbeiten. Die IP-Adresse lesen wir aus der Kommandozeile oder nutzen localhost. Läuft der Echo-Dienst unter einem Rechner nicht, dann wird eine »java.net.ConnectException: Connection refused: connect«-Ausnahme angezeigt.
Listing 16.10
com/javatutor/insel/net/Echo.java
package com.javatutor.insel.net;
import java.io.*;
import java.net.*;
class Echo
{
public static void main( String[] args )
{
Socket t = null;
try
{
t = new Socket( args.length == 0 ? "localhost" : args[0], 7 );
PrintStream os = new PrintStream( t.getOutputStream() );
String test = "Superkalifragilistischexpialigetisch";
os.println( test );
BufferedReader in = new BufferedReader(
new InputStreamReader( t.getInputStream()) );
String s = in.readLine();
if ( s.equals(test) )
System.out.println( "Hurra, er lebt!" ) ;
}
catch ( /* UnknownHostException is a */ IOException e ) {
e.printStackTrace();
}
finally
{
if ( t != null )
try { t.close(); } catch ( IOException e ) { e.printStackTrace(); }
}
}
}
16.7.8 Blockierendes Lesen
 
Eine Eigenschaft ist bei der Server-Programmierung zu beachten: Erwartet der Client aus dem InputStream Daten, der Server schickt aber keine, blockiert die Funktion. Aus dieser Sackgasse gibt es zwei Auswege: das einfache Schließen des Sockets mit close() und der völlig unterschiedliche Ansatz mit NIO. Wenn der Socket geschlossen wird, werden alle Datenstrom-Operationen abgebrochen und eine IOException ausgelöst.
Damit ist ein gutes Mittel gefunden, um wenigstens blockierte Socket-Verbindungen wieder zu befreien. Dies soll auch das nächste Beispiel demonstrieren. Zuerst wird ein nutzloser ServerSocket aufgebaut, der weder etwas annimmt, noch etwas schickt. Der Client verbindet sich zum Server und versucht zu lesen. Da aber vom Server kein Zeichen gesendet wird, hängt read() und wartet auf ein Byte. All das läuft in einem Thread ab. Nach dem Start wird zwei Sekunden später der Socket geschlossen, was zum Abbruch von read() und in den Anweisungsblock der Exception-Behandlung führt.
Listing 16.11
com/javatutor/insel/net/CloseConnection.java
package com.javatutor.insel.net;
import java.io.IOException;
import java.net.*;
public class CloseConnection
{
public static void main( String[] args ) throws Exception
{
ServerSocket s = new ServerSocket( 12345 );
final Socket t = new Socket( "localhost", 12345 );
new Thread( new Runnable()
{
public void run()
{
try
{
System.out.println( "Gleich hängt er!" );
System.out.println( t.getInputStream().read() );
System.out.println( "Hier hängt er!" );
}
catch ( IOException e )
{
System.out.println( "Blockierung gelöst" );
}
}
}).start();
Thread.sleep( 2000 );
t.close(); // Löst die Blockierung
}
}
Mit der Ausgabe
Gleich hängt er!
Blockierung gelöst
16.7.9 Informationen über den Socket
 
Wie beim URL-Objekt lässt auch die Klasse Socket keine grundsätzlich wichtigen Änderungen zu. Port-Adresse wie auch das Ziel müssen beim Erzeugen bekannt sein, doch lassen sich wie bei einer URL Informationen über das Socket-Objekt einholen.
|
InetAddress getInetAddress()
Liefert die Adresse, mit der der Socket verbunden ist. |
|
InetAddress getLocalAddress()
Liefert die lokale Adresse, an die der Socket gebunden ist. |
|
int getPort()
Gibt den Remote-Port zurück, mit dem der Socket verbunden ist. |
|
int getLocalPort()
Gibt den lokalen Port des Sockets zurück. |
Weitere Funktionen kommen noch hinzu, die allerdings an einem Beispiel demonstriert werden sollen:
Listing 16.12
com/javatutor/insel/net/SocketProperties.java
package com.javatutor.insel.net;
import java.io.IOException;
import java.net.Socket;
import static java.lang.System.out;
public class SocketProperties
{
public static void main( String[] args ) throws IOException
{
Socket s = new Socket( "java-tutor.com", 80 );
out.println( s.getKeepAlive() ); // false
out.println( s.getLocalAddress() ); // /192.168.2.135
out.println( s.getLocalPort() ); // 1456
out.println( s.getLocalSocketAddress() ); // /192.168.2.135:1427
out.println( s.getOOBInline() ); // false
out.println( s.getPort() ); // 80
out.println( s.getRemoteSocketAddress() ); // java-tutor.com/82.96.75.60:80
out.println( s.getReuseAddress() ); // false
out.println( s.getReceiveBufferSize() ); // 8192
out.println( s.getSendBufferSize() ); // 8192
out.println( s.getSoLinger() ); // –1
out.println( s.getTcpNoDelay() ); // false
out.println( s.getTrafficClass() ); // 0
}
}
 Hier klicken, um das Bild zu Vergrößern
16.7.10 Mit telnet an den Ports horchen
 
Wir können zum Server mittels des Kommandozeilenprogramms telnet eine Verbindung aufbauen und die Funktionalität eines Servers einfach prüfen, wenn dieser auf ASCII-Kommandos antwortet. Die Signatur von telnet ist:
$ telnet IP Port
Mit dieser Technik können wir uns zum Beispiel direkt mit dem FTP-Server verbinden, ohne ein Frontend wie ftp zu nutzen, oder auch ein Echo-Kommando absetzen, um damit den Server auf Erreichbarkeit zu testen.
16.7.11 Reine Verbindungsdaten über SocketAddress
 
Die Socket-Klasse bietet neben der Beschreibung der Verbindungsparameter auch Methoden zum Aufbau der Verbindung und Metadaten. Sind nur die Verbindungsdaten Adresse und Port nötig, so lassen sich diese auch durch InetSocketAddress-Objekte beschreiben. Alle InetSocketAddress-Objekte sind von der Klasse SocketAddress abgeleitet, wofür es bisher jedoch nur diese eine Unterklasse gibt. Für den Aufbau von InetSocketAddress-Objekten stehen drei Konstruktoren bereit:
class java.net. InetSocketAddress
extends SocketAddress
|
|
InetSocketAddress( String hostname, int port ) |
|
InetSocketAddress( InetAddress addr, int port ) |
|
InetSocketAddress( int port ) |
Natürlich stellt sich die Frage, warum ein Programm InetSocketAddress-Objekte nutzen sollte, wenn doch auch Socket-Objekte alle Verbindungsdaten enthalten. Ein Grund ist, dass Objekte vom Typ InetSocketAddress serialisierbar sind, und ein anderer, dass über SocketAddress-Objekte bei einer gewünschten Verbindung leicht ein Timeout gesetzt werden kann.
Beispiel Versuche, eine Verbindung zu einem Rechner aufzubauen. Wenn nach 100 Millisekunden kein Kontakt zu Stande kommt, folgt eine SocketTimeoutException.
SocketAddress addr = new InetSocketAddress( host, port );
Socket socket = new Socket();
socket.connect( addr, 100 );
|
|
void connect( SocketAddress endpoint, int timeout ) throws IOException
Baue eine Socket-Verbindung auf. Die Verbindungsparameter kommen aus dem SocketAddress-Objekt. |
|
void connect( SocketAddress endpoint, int timeout ) throws IOException
Baue eine Socket-Verbindung auf. Wenn nach timeout Millisekunden keine Verbindung möglich ist, erfolgt eine SocketTimeoutException. |
1 Die Geschichte, dass das Internet nur deshalb entwickelt wurde, damit bei Rechnerausfällen durch kriegerische Aktivitäten weiter Kommunikation möglich ist, ist so falsch. Larry Roberts sagt dazu, dass die Entwickler dem Ministerium die Vorteile des Internets nur deswegen so verkauften, damit sie mehr Forschungsgelder bekamen.
2 Nicht zu verwechseln mit »Illinois Association of Nurse Anesthetists« beziehungsweise »Intermodal Association of North America«! Die Internet Assigned Numbers Authority ging aus der ISOC (Internet Society) und der FNC (Federal Network Council) hervor.
3 Die einzelnen Programme liegen allerdings auch unter http://www.sockets.com/sample.htm.
|