Java-Einsatz im Netzwerk

18.08.2005 von PROF. DR. Stephan Euler
In den Bibliotheken von Java findet sich eine ganze Reihe von Klassen zur Unterstützung von Anwendungen über Netzwerke und insbesondere im Internet. Die Spanne reicht dabei von einfachen Ebenen wie Sockets bis zu multifunktionalen Klassen wie URL.

Die Netzwerk-Klassen in Java bieten ein unterschiedliches Einstiegsniveau. Einerseits besteht die Möglichkeit, mittels Sockets auf einer relativ niedrigen Ebene zu arbeiten. Andererseits stehen auch Klassen wie URL zur Verfügung, die bereits einen Großteil der Netzwerkfunktionalität beinhalten. Im Folgenden werden exemplarisch einige Möglichkeiten der beiden Klassen Socket und URL vorgestellt.

Java Client-Sockets

In der Klassenbibliothek von Java stehen mehrere nach Funktionalitäten getrennte Klassen für Sockets zur Verfügung. Die Klasse Socket realisiert einen Client-Socket. Verschiedene Konstruktoren ermöglichen die Angabe des Servers über den Namen oder die IP-Adresse. Folgender Code erzeugt auf einfache Weise einen Socket:

Socket s = new Socket( "www.fh-friedberg.de", 80 );

An den so erzeugten Socket können mit folgenden Methoden Ein- und Ausgangsströme angehängt werden

getInputStream()
getOutputStream()

Eine Anwendung kann dann diese Ströme in der gleichen Art und Weise wie Ströme von und zu Dateien benutzen. Der Socket übernimmt die Details der Netzwerkkommunikation.

Beispiel: E-Mail via Socket

Als Beispiel folgt ein Programm, das über eine Socket-Verbindung eine E-Mail mit einer Klausurnote verschickt. Zunächst öffnet das Programm eine Socket-Verbindung zu einem Mail-Server an dessen Port 25. Anschließend sendet es die Befehle gemäß SMTP. Zur besseren Übersicht verzichtet das Beispiel auf eine Erfolgskontrolle. Dazu müssten über einen InputStream die zurückgeschickten SMTP-Statuscodes gelesen und ausgewertet werden.

import java.net.*;
import java.io.*;

public class TestEmail {

public static void main( String args[]) {
String host = "mailto.t-online.de";
String from = "stephan.euler@t-online.de";
String to = "stephan.euler@t-online.de";
String betreff= "Note der Informatikklausur III";
String inhalt = "Sie haben die Note 3 \\n";
try
{
Socket mailSocket = new Socket(host, 25 );
PrintWriter out = new PrintWriter( mailSocket.getOutputStream(), true );

out.println( "HELO " + host);
out.println( "MAIL FROM: <" + from + ">");
out.println( "RCPT TO: <" + to + ">");
out.println( "DATA" );
out.println( "SUBJECT: " + betreff );
out.println( inhalt );
out.println( ".");
out.println( "QUIT" );

out.close();
mailSocket.close();
}
catch( IOException e ) { System.err.println( e ); }
}
}

Java Server-Sockets

Einen Server für verbindungsorientierte Sockets realisiert die Klasse ServerSocket. Die Klasse enthält eine Reihe von Konstruktoren und Methoden, um einen Server-Socket zu erzeugen und anzumelden. Eine kompakte Form ist:

ServerSocket socket = new ServerSocket(port, backlog);

Die Parameter bestimmen die Portnummer port und die Größe backlog der Warteschlange für anstehende Clients. Nach dem Aufruf der Methode accept() wartet der Socket auf einen Client. Meldet sich ein Client an, so kehrt die Methode mit einem normalen Socket als Rückgabewert zurück.

Mit diesen Elementen ist in der folgenden Klasse SocketServer ein einfacher Server realisiert. Der Server wartet in einer Endlosschleife auf Anfragen. Ein Client erhält bei der Kontaktaufnahme eine kurze Begrüßung. Anschließend wird die Verbindung wieder abgebaut.

import java.net.*;
import java.io.*;

public class SocketServer {
static int backlog = 10; // Laenge der Warteschlange
static int port = 1234;

public static void main( String args[]) {

try
{
ServerSocket socket = new ServerSocket(port, backlog);
for( ;; ) {
System.out.println( "Warte auf Verbindung... " );
Socket sockConnected = socket.accept();
System.out.println( "Verbunden mit " + sockConnected);
PrintStream ps = new PrintStream( sockConnected.getOutputStream() );
ps.println( "Hallo" );
sockConnected.close();
}

}
catch( IOException e ) { System.err.println( e ); }
}
}

Java UDP-Sockets

Der beschriebene Client-Socket verwendet eine stromorientierte Verbindung (TCP). Paketorientierte Verbindungen (UDP) realisiert die Klasse DatagramSocket. So erzeugt folgende Zeile einen neuen Socket für die Datagramm-Kommunikation über Port 8888.

DatagramSocket s = new DatagramSocket(8888);

Der Datenaustausch erfolgt dann mit Objekten der Klasse DatagramPacket. Zum Versenden werden in diese Pakete die Daten und die Zieladressen eingetragen.

Das Zusammenspiel der Klassen soll das Beispiel eines Chat-Servers darstellen. Der Server hat folgende Funktionalitäten:

An- und Abmeldung erfolgen über Pakete, die eine entsprechende Kennung enthalten. Der Server führt eine Liste mit allen angemeldeten Clients. Erhält er von einem der Clients eine Nachricht, so schickt er eine Kopie davon an alle anderen Clients.

Der folgende Code realisiert einen solchen Server. Die angemeldeten Clients werden in einem Objekt der Klasse Vector gespeichert. Die Fehlerprüfung beschränkt sich auf die Ausgabe von eventuell auftretenden Ausnahmefehlern (Exceptions). Einige der benutzten Methoden sind erst ab Java 1.4 vorhanden, können aber durch ältere, weniger elegante Methoden ersetzt werden.

Beispiel: Chat-Server

import java.net.*;
import java.io.*;
import java.util.*;

public class DatagrammServer {
// Kennungen für An- und Abmeldung
static final String ANMELDUNG ="ANMELDUNG";
static final String ENDE ="ENDE";
static int port = 1234;
static int length = 256; // Länge eines Pakets

public static void main( String args[]) {

DatagramPacket paket =
new DatagramPacket( new byte[length], length);
Vector clients = new Vector(); // Liste der Clients

try {
DatagramSocket socket = new DatagramSocket(port);
for( ;; ) {

// Warten auf nächstes Paket
socket.receive( paket );
InetSocketAddress add = (InetSocketAddress)paket.getSocketAddress();

// Text aus Paket extrahieren
String text = new String(paket.getData(), 0, paket.getLength());
System.out.println( add +">" + text);

// Paket auswerten
if( text.equals( ANMELDUNG ) ) {
clients.add( add );
System.out.println( "Anzahl Clients: " + clients.size() );
} else if( text.equals( ENDE ) ){
clients.remove( add );
System.out.println( "Anzahl Clients: " + clients.size() );
} else {
// Versenden von Kopien an alle anderen Clients
for( int i=0; i<clients.size(); i++ ) {
InetSocketAddress dest = (InetSocketAddress) clients.get(i);
if( ! dest.equals( add ) ) {
paket.setSocketAddress( dest );
socket.send( paket );
System.out.println( "Kopie an " + dest );
}
}
}
}
}
catch( IOException e ) {
System.err.println( "Ausnahmefehler: " + e );
}
}
}

Chat-Client

Als passendes Gegenstück dient der folgende Client. Bei seinem Aufruf kann der Name des Servers als Argument übergeben werden. Fehlt diese Angabe, sucht der Client auf dem eigenen Rechner nach dem Server. Der Client meldet sich dann mit einem entsprechenden Paket beim Server an. In einer Schleife wartet er auf Eingaben. Jede Eingabe liest er als Zeichenkette (String) ein, wandelt sie in ein Feld von Bytes und verschickt sie dann als Paket. Besteht die Eingabe aus dem Text ENDE, so sendet er eine Abmeldenachricht an den Server. Anschließend meldet sich der Client ab.

Anders als bei dem Server erfolgt der Nachrichtenaustausch asynchron. Der Server wartet auf eine Nachricht und reagiert darauf. Demgegenüber kann beim Client zu jedem Zeitpunkt vom Server eine neue Nachricht eintreffen. Es gibt keine zeitliche Koppelung zwischen Eingaben und Ausgaben.

In der Programmiersprache Java und in der virtuellen Maschine zur Ausführung von Java-Anwendungen sind Möglichkeiten für parallele Verarbeitung integriert. Eine Anwendung kann in mehrere eigenständige Programmfragmente - so genannte Threads - aufgeteilt werden. Im vorliegenden Fall ist die Aufgabe in zwei solchen Threads implementiert. Die Hauptanwendung mit der Schleife für Benutzereingaben und dem Senden an den Server läuft in einem Thread. Die einkommenden Nachrichten behandelt ein eigener Verarbeitungsstrang in einem zweiten Thread. Beide Threads laufen dabei unabhängig voneinander parallel ab.

Programmtechnisch ist der zweite Thread in einer eigenen Klasse realisiert. Diese Klasse implementiert das Interface Runnable. Damit verpflichtet sie sich, eine Methode run zu realisieren. Bei der Instanzierung eines Objektes der Klasse ruft die Virtual Machine dann automatisch diese Methode auf und führt sie in einem eigenen Thread aus. Die Hauptanwendung braucht daher nur ein Objekt der Klasse anzulegen, um den zweiten Thread zu starten. Beim Anlegen übergibt sie im Konstruktor gleich den zur Kommunikation nötigen Socket.

Beispiel: Chat-Client

import java.net.*;
import java.io.*;

public class DatagrammClient {
static final String ANMELDUNG = "ANMELDUNG";
static final String ENDE = "ENDE";
static int port = 1234;
static int length = 256; // Länge eines Pakets

public static void main( String args[]) {
String servername = "localhost";
String text = null;
DatagramPacket packet;
byte[] ba = ANMELDUNG.getBytes();

// Namen des Servers von Kommandozeile übernehmen
if( args.length > 0 ) servername = args[0];

try {
DatagramSocket socket = new DatagramSocket();
InetAddress ia = InetAddress.getByName( servername );
packet = new DatagramPacket( ba, ba.length, ia, port);
// sende Anmeldung
socket.send( packet );

// Lesen der empfangenen Pakete erfolgt in eigenem Thread
LeseThread lt = new LeseThread( socket );

// Eingaben von Tastatur an Server schicken
BufferedReader br = new BufferedReader(new InputStreamReader( System.in ) );
do {
text = br.readLine();
ba = text.getBytes();
packet.setData( ba, 0, ba.length );
socket.send( packet );
} while( ! text.equals("ENDE") );

// alles beenden
System.exit(0);

}
catch( IOException e ) {
System.err.println("Ausnahmefehler: " + e );
}
}
}

Dazu gehört die Klasse LeseThread zum Empfang von Nachrichten.

class LeseThread implements Runnable {
static int length = 256;
DatagramSocket socket;

LeseThread(DatagramSocket socket ) {
this.socket = socket;
Thread t = new Thread(this,"Lesen");
t.start();
}

public void run() {
DatagramPacket packet =
new DatagramPacket( new byte[length], length);
while( true ) {
try {
socket.receive( packet );
InetSocketAddress add = (InetSocketAddress)packet.getSocketAddress();
String text = new String(packet.getData(), 0, packet.getLength());
System.out.println( add +">" + text);
//System.out.println( ">" + text);
}
catch( IOException e ) {
System.err.println("Ausnahmefehler: " + e );
}
}
}
}

Die Klasse URL

Die Klassen rund um Sockets behandeln die Netzwerkkommunikation auf einer relativ niedrigen Ebene. In Java existieren jedoch auch zahlreiche Möglichkeiten für eine Kommunikation auf höherer Ebene. Eine solche Möglichkeit besteht über die Klasse URL. Ein URL-Objekt erzeugt man im einfachsten Fall durch den Konstruktor mit dem Namen des Links als Parameter.

URL url = new URL("http://www.fh-friedberg.de");

Das URL-Objekt beinhaltet zunächst nur die Information über den Link. In einem weiteren Schritt kann man darüber eine Verbindung aufbauen. Eine Möglichkeit dazu bietet die Methode openConnection(). Sie liefert ein Objekt der Klasse URLConnection, über das die weitere Kommunikation läuft.

Im folgenden Beispiel wird dieses Schema benutzt, um Informationen über einen Link abzufragen. Insbesondere verwendet die Anwendung die Methode getContentType(), um den Typ des Dokuments zu erfragen. Der Rückgabewert ist eine Zeichenkette mit der MIME-Angabe des Typs. Die Methode getContent() liefert ein dazu passendes Objekt, um den Inhalt des Links zu lesen. Bei HTML-Dateien sind dies Varianten von InputStream. Zeigt der Link aber beispielsweise auf eine Bilddatei im GIF-Format, erhält man ein Objekt der Klasse URLImageSource zurück.

Beispiel: URL-Informationen

import java.net.*;
import java.io.*;

public class ExamineURL {
static int keyWidth = 20;

public static void main( String args[]) {
// Kopieren des Argumentes beim Aufruf
String link = args[0];

try {
System.out.println("Trying URL " + link );
URL url = new URL( link );
URLConnection conn = url.openConnection();
int n = 1;

// Ausgabe der Elemente des http-Headers
String key;
System.out.println("Header fields:");
while((key=conn.getHeaderFieldKey(n) ) != null ) { key += ": ";
while( key.length() < keyWidth ) key += " ";
System.out.println( n + " " + key + conn.getHeaderField(n) );
++n;
}

// Objekt für Zugriff auf Inhalt
Object o = conn.getContent();
System.out.println( "Content: " + o );
System.out.println( "ContentType: " + conn.getContentType() );
System.out.println( "Permission: " + conn.getPermission() );

} catch( Exception ex ) {
System.out.println("Cannot create URL " + link + " Excption:" + ex);
}
}
}

Ruft man diese Anwendung mit der Homepage der FH Friedberg als Parameter auf, erhält man folgende Ausgabe:

java ExamineURL http://www.fh-friedberg.de
Trying URL http://www.fh-friedberg.de
Header fields:
1 Date: Mon, 16 Dec 2002 08:56:52 GMT
2 MIME-Version: 1.0
3 Content-Type: text/html
Content: java.io.PushbackInputStream@3169f8
ContentType: text/html
Permission: (java.net.SocketPermission
www.fh-friedberg.de:80 connect,resolve)

Mailto-Links

Ein URL-Objekt kann nicht nur auf Dateien verweisen, sondern auch andere Dienste ansprechen. Ein Beispiel sind Verweise mit dem Protokoll mailto. Damit kann ein URL-Objekt auch E-Mails verschicken. Die Klasse URLemail im Beispiel verwendet diesen Mechanismus, um eine Nachricht mit einer Klausurnote zu verschicken. Wichtig dabei ist, dass vor dem Versand der Java-VM noch der zu verwendende Mail-Server bekannt gemacht werden muss. Dies erfolgt durch Setzen der entsprechenden Systemeigenschaft:

System.setProperty("mail.host", "mailto.t-online.de");

In analoger Art und Weise kann man bei Bedarf auch andere Eigenschaften wie etwa den Name eines Proxy-Servers festlegen. (ala)

import java.io.*;
import java.net.*;

public class URLemail
{
public static void main(String args[])throws Exception {
String from = "stephan.euler@t-online.de";
String to = "stephan.euler@t-online.de";
String subject = "Klausurergebnis IN3";
String nachricht = "Sie haben die Note 2.";

System.setProperty("mail.host", "mailto.t-online.de");
URL u = new URL("mailto:" + to);
URLConnection c = u.openConnection();
c.setDoOutput(true);
System.out.println("Connecting...");
c.connect();

PrintWriter out = new PrintWriter( new OutputStreamWriter(c.getOutputStream()));
out.println("Subject: " + subject);
out.println(nachricht);
out.close();
System.out.println("Nachricht versendet.");
}
}