kopf
brlogo
fensterobenrechts
   
   
fensteruntenblau
   
  Die Vorgaben zum Zentralabitur Informatik NRW geben Klassen für die Programmierung verteilter Systeme vor und stellen Beispielimplementationen für diese Klassen (Java, Delphi) zur Verfügung. Wer hinter die Java-Implementierung blicken will, findet hier erste Hinweise, wie sich Client-Server-Anwendungen in 'Java pur' darstellen.

 

Programmierung verteilter Objekte: Client-Server-Programmierung in Java

Autor: Klaus Killich

 

 

1. Einleitung

 

Die folgenden Programme wurden mit einem Leistungskurs der Jahrgangsstufe 13 erarbeitet. Die Unterrichssequenz ist den Inhalten 'Technische, funktionale und organisatorische Prinzipien von Hard- und Softwaresystemen kennen lernen und einordnen' und 'Typische Einsatzbereiche, Möglichkeiten, Grenzen, Chancen und Risiken der Informations- und Kommunikationssysteme untersuchen und einschätzen'  (Richtlinien S. 31) der Obligatorik des objektorientierten Ansatzes zuzuordnen.

Die Schülerinnen und Schüler haben in diesem Zusammenhang den Aufbau und die Funktionsweise eines Netzwerkes kennen gelernt. Das OSI-Schichtenmodell und die verschiedenen Netzwerktopologien sind den Schülerinnen und Schülern bekannt.

Mit der Erstellung der Programme wurden die Probleme der Kommunikation zwischen Rechnern auf der Softwareebene erarbeitet.

Implementationssprache ist Java, das mit seinem Socket-Konzept eine elegante Möglichkeit der Client-Server-Programmierung bietet.

Die Programme sind alle noch erweiterbar (s. dazu die Anmerkungen zu den einzelnen Realisierungen). 'Schulintern' dienen sie als Basis für die Unterrichtseinheiten 'Kryptologie' und 'Datenbanken' des Leistungskurses der Jahrgangsstufe 12.

 

 

2. Stromklassen als Voraussetzung zum Verständnis der Client-Server-Programmierung

 

Die Stromklassen in Java sind grundsätzlich eigenständige Bausteine. Die Kombinierbarkeit der Stromklassen ist im Wesentlichen durch die Konstruktoren festgelegt. Die Verbindung zwischen den Strömen wird dabei gekapselt.

Beipielsweise kann ein Bytestrom aus einer Datei lesen und die gelesenen Bytes an einen Strom weitergeben, der die Bytes zu Zahlenfolgen zusammenfasst.

Eingabeströme beziehen die bereitgestellte Eingabe aus unterschiedlichenQuellen (entsprechend für Ausgabeströme) : Sie lesen beispielsweise aus Dateien, aus Datenstrukturen (z.B. Feldern) oder aus einem Netzwerk.

Java bietet in seiner Bibliothek von Stromklassen für die verschiedenen Ein- und Ausgabeformate sowie -schnittstellen eine jeweils angepasste Funktionalität. Die meisten dieser Klassen befinden sich im Paket java.io. Im Wesentlichen sind diese Klassen in vier Hierarchien organisiert:

 

     1.  Reader-Klassen sind Eingabeströme auf Basis des Typs char.

     2.  Writer-Klassen sind Ausgabeströme auf Basis des Typs char.

     3.  InputStream-Klassen sind Eingabeströme aufBasis des Typs byte.

     4.  OutputStream-Klassen sind Ausgabeströme auf Basis des Typs byte.

 

Die folgende Grafik gibt einen Überblick über die Reader-Klassen:

 

bild_10

Aus: Goll/Weiß/Rothländer

 

Der InputStream-Reader liest z.B. Bytes aus einem InputStream und konvertiert diese nach char, also in Unicode-Zeichen. Die verwendete Konvertierungsfunktion kann beim Konstruktoraufrufeingestellt werden. Ein FileReader ist ein InputStreamReader, der die Eingabebytes aus einer Datei liest. File-Reader leisten das Gleiche wie InputStreamReader, die einen FileInputStream als Quelle haben.

Ein BufferedReader hat einen anderen Reader als Quelle. Er liest immer mehrere Zeichen auf einmal und speichert diese zwischen. Dadurch wird es vermieden, für jedes einzelne Zeichen immer neu auf eine Quelle zuzugreifen. Durch dieses Zwischenschalten eines Buffered-Readers kann beispielsweise die Effizienz beim Zugriff auf Dateien und Netzwerke erheblich gesteigert werden.

Alles für die Reader Gesagte gilt entsprechend für die Writer-Klassen :

 

bild_20

Aus: Goll/Weiß/Rothländer

 

Die Klasse PrintWriter stellt darüber hinaus Methoden zum Ausdrucken der Basisdatentypen in lesbarer Form zu Verfügung. Mit PrintWriter können Werte der einfachen Datentypen im Klartext in einen anderen Stream ausgegeben werden.

Die Klasse FileWriter stellt einen Stream zu Verfügung, mit dem Unicode-Zeichen in Dateien geschrieben werden können.

 

bild_30

Aus: Goll/Weiß/Rothländer

 

Java unterstützt auch die Ein- und Ausgabe von Objekten bzw. Objektgeflechten mittels Strömen. Die Ein- und Ausgabe von Objekten ist wichtig, um Objekte zwischen mehreren Prozessen austauschen zu können (beispielsweise über eine Netzverbindung) und um Objekte, die in einem Projektlauf erzeugt wurden, späteren Programmen verfügbar zu machen (Abspeichern von Objekten und Objektgeflechten). In Java lösen Objektströme diese Aufgabe.

 

bild_40

Aus : Goll/Weiß/Rothländer

 

Das Paket Stroeme (Download weiter unten) gibt Beispiele für die Verwendung der genannten Stromklassen.

 

 

3. Sockets als Grundlage der Kommunikation zwischen Rechnern

 

Daten werden im Internet in Paketen endlicher Länge übertragen. Deshalb ist es im Allgemeinen notwendig, die Daten, die von einem Prozess zu einem anderen verschickt werden sollen, in mehrere Pakete zu zerteilen und am Zielrechner wieder zusammenzusetzen. Dabei fallen eine Reihe nichttrivialer Tätigkeiten an : Zerteilen der Daten; Einpacken der Daten in Pakete mit Adressinformationen, Verwaltungsinformationen und Paketinhalt; eintreffende Pakete auspacken; Reihenfolge der eintreffenden Pakete sicherstellen; fehlende Pakete nachfordern; Pakete amZielort wieder zusammensetzen, usw.

Sockets bilden in Java eine Abstraktionsschicht zur Kommunikation zwischen verteilten Prozessen, die den Programmierer von dieser Tätigkeit befreit. Sie unterstützen die Kommunikation zwischen Clients und Servern. Die Verbindung wird über sogenannte Ports an demjenigen Rechner geknüpft, auf dem der Serverprozess ausgeführt wird. Ports werden über Nummern identifiziert, Rechner über ihre Internetadresse (oder über 127.1.1.1, dem sog. Localhost).

 

Das typische Szenario sieht wie folgt aus:

Ein Serverprozess schließt sich an einen Port seines Rechners an, dies geschieht mittels des Konstruktors ServerSocket.

Beispiel für eine Definition mit der Portnummer 8189:

   ServerSocket s = new ServerSocket (8189);

Dann wartet erdarauf, dass sich ein Client ebenfalls an diesen Port anschließt und damit die Socket-Verbindung zwischen Client und Server herstellt. Dazu wird im Server die Methode accept aufgerufen, die solange blockiert, bis sich ein Client anmeldet, und dann die Verbindung als Socket-Objekt zurückliefert.

Definition:

   Socket incoming = s.accept ();

Ein Client, der einen Dienst von einem Server inAnspruch nehmen will, muss wissen, auf welchem Rechner der Serverprozess läuft und an welchem Port der Dienst angeboten wird. Er schließt sich an diesen Port an und stellt damit die Socket-Verbindung her. Dies erledigt der Konstruktoraufruf von Socket.

Beispiel für eine Definition:

  Socket t = new Socket (127.1.1.1, 8189);

Dann kann er den Dienst abrufen und anschließend die Socket-Verbindung wieder lösen.

Eine Socket Verbindung stellt also für jede Richtung der Kommunikation zwischen Client und Server einen Strom zu Verfügung, über den Daten ausgetauscht werden können.

 

Über die Sockets können Informationen ausgetauscht werden. Informationen, die vom Server über den Socket an andere Rechner geliefert werden, gehören zur Klasse InputStream. Entsprechend gehören die Informationen, die von einem Rechner zum Server geliefert werden, zur Klasse OutputStream. Da diese Klassen als abstract gekennzeichnet sind, können ihre Konstruktoren nur implizit aufgerufen werden. Dies geschieht über die Socket-Methoden:

   ServerSocket Server = new ServerSocket ();

   Server.getInputStream ();

   Socket Client= new Socket ()

   Client.getOutputStream ();

Wie oben beschrieben, werden diese Streams zwecks besserer Lesbarkeit gekapselt:

   BufferedReader in = new BufferedReader (new InputStreamReader (client.getInputStream ())));

 

 

4. Realisierung der Client-Server-Programmierung

 

Die folgenden Programme sind mit Kommentaren versehen, so dass sich eineausführliche Erläuterung an dieser Stelle erübrigt.

Die Client-Programme werden wie folgt gestartet :

   <Dateiname> <Adresse des Servers> <Portnummer>

Beispiel :

   ThreadClient127.1.1.18189

 

a. Echo-Server und ein Client

Der Server gibt die Meldungen des Clients an diesen als Echo zurück. Grundlage der Kommunikation sind die Streams BufferedReader und PrintWriter, die wiederum auf den InputStream der Objektklasse Socket basieren. Die Verbindung zwischen Server und Client wird Eingabe des Wortes „BYE“ beendet.

Programm EchoServer:  Download weiter unten

Programm EchoClient:  s.u.

 

b. Echo-Server und mehrere Clients

Das veränderte Server-Programm erlaubt die Verbindung des Server mit mehreren Clients. Realisiert wird dies durch Threads, deren Implementierung den Schülerinnen und Schülern bereits bekannt ist. Die Funktionalität des Servers ist die von 3.a, die Implementierung wird lediglich in die Thread-Klasse ‚EchoHandler’ verlegt. Der Server startet nach Anmeldung eines Clients einen neuen Thread der Objektklasse EchoHandler. Als Argument wird dem Konstruktor der neu erzeugte Socket übergeben, der dadurch zum Thread gehört. In der 'run'-Methode des Threads werden dann wie vorher im Programm Server die Kommunikations-Streams zum Client eingerichtet.  Der Server zählt die Objekte der Threadklasse EchoHandler.

Programm ThreadServer:  s. u.

Klasse EchoHandler:  s. u.

 

c. Ein Chat-System

Bei der Realisierung eines Chat-Systems ändert sich die Funktionalität des Servers grundlegend. Er muss nunmehr nicht mehr lediglich die Meldungen an den jeweiligen Client zurückgeben, sondern die eingehenden Meldungen an alle angeschlossenen Clients weiterleiten. Dazu wird auf dem Server ein Array von Sockets für die Chat-Mitglieder verwaltet. Jedes Mal, wenn sich ein neues Chat-Mitglied anmeldet, wird ein neuer Thread 'c_handler' gestartet und der Client-Socket dem Array von Sockets des Servers hinzugefügt.

Der Server enthält u. a. eine Methode, um die eingehende Nachricht eines Chat-Clients an alle Teilnehmer des Chats zu verteilen.

Das vorliegende System ist erlaubt einem Chat-Mitglied erst nach Eingabe seiner eigenen Nachricht, die Nachrichten der anderen Mitglieder des Chats lesen kann.

Es wäre also insofern zu erweitern, als dass parallel zur Eingabe eines Clients evtl. eintreffende Nachrichten auf dem Bildschirm erscheinen. Realisiert werden kann dies über Threads, die vom Client verwaltet werden.

Programm c_server:  s.u.

Klasse c_handler:  s.u.

Programm c_client:  s.u.

 

d. Client-Server-Anwendung zur Übertragung von Objekten

Im Gegensatz zu den Programmen der Gliederungspunkte 4.a – 4.c geschieht die Kommunikation zwischen Server und Client nicht mehr ausschließlich über Strings. Der Server stellt nunmehr Objekte bereit, hier der Klasse Student, die auf Anfrage an den Client gesendet werden.

Die Klasse Student enthält lediglich die Attribute Name, Immatrikulationsjahr und Immatrikulationsnummer und ist lediglich zu Testzwecken eingerichtet worden. Dies gilt auch für die dynamisch-organisierte Liste, die nicht die volle Funktionalität der Datenstruktur Liste besitzt.

In den vorliegenden Programmen lädt der Server grundsätzlich bei Start eine Liste von Objekten der Klasse 'Student' aus einer Datei.

Da der Schwerpunkt des Interesses auf der Kommunikation zwischen den Rechner liegt, wird davon ausgegangen, dass das durch den Client erfragte Objekt auch in der Liste vorhanden ist. Dies erspart einige nicht zur eigentlichen Fragestellung gehörende Programmzeilen bei der Implementierung des Servers.

Auch die Manipulation der Objekte durch die Clients und das Zurücksenden an den Server wurde in den folgenden Programmen nicht realisiert.

Zentral bei der Übertragung von Objekten ist der Begriff des ObjectStreams, der das Versenden von Objekten ermöglicht.

Objektströme können wie folgt definiert werden :

 

Auf der Server-Seite:

   ServerSocket s = new ServerSocket ();

   Socket incoming = s.accept ();

   ObjectOutputStream to_Client =  new ObjectOutputStream (incoming.getOutputStream()))

 

Auf der Client-Seite :

  Socket t = new Socket (arg [0], Integer.parseInt (arg [1]));

  ObjectInputStream from_server  = new ObjectInputStream (t.getInputStream()))

 

In der ersten Implementierung versendet der Server nach Anfrage durch den Client (Angabe des Namens) ein Objekt der Klasse 'Student'.

Programm Stud_Server:  s.u.

Programm Stud_Client:  s.u.

Klasse Student:  s.u.

Klasse Liste:  s.u.

 

Erweitert wird das Programm durch Threads, die mehreren Clients die Anfrage nach Objekten der Klasse Student erlaubt. Die Threads werden in der Klasse Handler verwaltet. Übergeben wird an Objekte dieser Klasse der Socket und der Anfang der Studentenliste.

Programm Stud_Thread:  s.u.

Klasse Handler:  s.u.

 

Download der oben genannten Programm und Klassen: ClientServerJava.zip

 

 

5. Literatur

 

1. Basistext des Kurses 1618 "Einführung in die objektorientierte Programmierung" der Fernuniversität Hagen im SS 2000

2. Goll/Weiß/Rothländer: Java als erste Programmiersprache. B.G. Teubner, Stuttgart-Leipzig, 2000

 

 

 
 
Friday, 24. November 2017 / 22:52:30