Skip to content

8. Programmierung TCP-IP

8.1. Allgemeines

8.1.1. Die Internetprotokolle

Hier geben wir eine Einführung in die Kommunikationsprotokolle des Internets, die auch als TCP/IP-Protokollsuite (Transfer Control Protocol / Internet Protocol) bezeichnet werden, benannt nach den beiden wichtigsten Protokollen. Es ist ratsam, dass der Leser ein umfassendes Verständnis der Funktionsweise von Netzwerken und insbesondere der Protokolle TCP/IP hat, bevor er sich mit der Entwicklung verteilter Anwendungen befasst.

Der folgende Text ist eine Teilübersetzung eines Textes aus dem Dokument „Lan Workplace for Dos – Administrator's Guide“ von NOVELL, einem Dokument aus den frühen 90er Jahren.


Das allgemeine Konzept zur Schaffung eines Netzwerks aus heterogenen Computern geht auf Forschungen der DARPA (Defense Advanced Research Projects Agency) in den Vereinigten Staaten zurück. Die DARPA entwickelte die unter dem Namen TCP/IP bekannte Protokollsuite, die es heterogenen Rechnern ermöglicht, miteinander zu kommunizieren. Diese Protokolle wurden in einem Netzwerk namens ARPAnet getestet, aus dem später das Netzwerk INTERNET hervorging. Die Protokolle TCP/IP definieren Formate sowie Regeln für die Übertragung und den Empfang, die unabhängig von der Netzwerkorganisation und der verwendeten Hardware sind.

Das von DARPA konzipierte und durch die Protokolle TCP/IP verwaltete Netzwerk ist ein paketvermitteltes Netzwerk. Ein solches Netzwerk überträgt Informationen über das Netzwerk in kleinen Einheiten, die als Pakete bezeichnet werden. Wenn also ein Computer eine große Datei überträgt, wird diese in kleine Teile zerlegt, die über das Netzwerk gesendet und am Zielort wieder zusammengesetzt werden. TCP/IP definiert das Format dieser Pakete, nämlich:

  • Herkunft des Pakets
  • Ziel
  • Länge
  • Typ

8.1.2. Das Modell OSI

Die Protokolle TCP/IP folgen in etwa dem offenen Netzwerkmodell namens OSI (Open Systems Interconnection Reference Model), das von der ISO (International Standards Organisation) definiert wurde. Dieses Modell beschreibt ein ideales Netzwerk, in dem die Kommunikation zwischen Rechnern durch ein siebenstufiges Modell dargestellt werden kann:

Image

Jede Schicht erhält Dienste von der darunterliegenden Schicht und stellt ihre eigenen Dienste der darüberliegenden Schicht zur Verfügung. Nehmen wir an, zwei Anwendungen auf den verschiedenen Rechnern A und B möchten miteinander kommunizieren: Dies geschieht auf der Ebene der Schicht Application. Sie müssen nicht alle Details der Netzwerkfunktion kennen: Jede Anwendung übergibt die Informationen, die sie übertragen möchte, an die darunterliegende Schicht, nämlich die Schicht Présentation. Die Anwendung muss daher nur die Schnittstellenregeln zur Schicht Présentation kennen.

Sobald sich die Informationen in der Schicht Présentation befinden, werden sie gemäß weiterer Regeln an die Schicht Session weitergeleitet und so weiter, bis die Informationen auf dem physischen Träger ankommen und physisch an den Zielrechner übertragen werden. Dort durchläuft sie den umgekehrten Prozess, den sie auf dem Absenderrechner durchlaufen hat.

Auf jeder Schicht sendet der für den Versand der Informationen zuständige Senderprozess diese an einen Empfängerprozess auf dem anderen Rechner, der derselben Schicht angehört wie er selbst. Dies geschieht nach bestimmten Regeln, die als Protokoll der jeweiligen Schicht bezeichnet werden. Daraus ergibt sich folgendes endgültiges Kommunikationsschema:

Image

Die verschiedenen Schichten haben folgende Aufgaben:

Physique
Gewährleistet die Übertragung von Bits über ein physikalisches Medium. In dieser Schicht finden sich Endgeräte zur Datenverarbeitung (E.T.T.D.), wie z. B. Terminals oder Computer, sowie Geräte zum Abschluss von Datenverbindungen (E.T.C.D.), wie z. B. Modulatoren/Demodulatoren, Multiplexer und Konzentratoren. Die wichtigsten Aspekte auf dieser Ebene sind:
. die Wahl der Informationskodierung (analog oder digital)
. die Wahl des Übertragungsmodus (synchron oder asynchron).
Liaison de données
Versteckt die physikalischen Besonderheiten der physikalischen Schicht. Erkennt und korrigiert Übertragungsfehler.
Réseau
Verwaltet den Weg, den die über das Netzwerk gesendeten Informationen nehmen müssen. Dies wird als routage bezeichnet: die Bestimmung der Route, die eine Information nehmen muss, damit sie ihren Empfänger erreicht.
Transport
Ermöglicht die Kommunikation zwischen zwei Anwendungen, während die vorherigen Schichten nur die Kommunikation zwischen Rechnern zuließen. Ein von dieser Schicht bereitgestellter Dienst kann das Multiplexing sein: Die Transportschicht kann dieselbe Netzwerkverbindung (von Rechner zu Rechner) nutzen, um Informationen mehrerer Anwendungen zu übertragen.
Session
In dieser Schicht finden sich Dienste, die es einer Anwendung ermöglichen, eine Arbeitssitzung auf einem Remote-Rechner zu eröffnen und aufrechtzuerhalten.
Présentation
Sie zielt darauf ab, die Darstellung der Daten auf den verschiedenen Rechnern zu vereinheitlichen. So werden Daten, die von Rechner A stammen, von der Schicht Présentation des Rechners A gemäß einem Standardformat „aufbereitet“, bevor sie über das Netzwerk gesendet werden. Sobald sie die Schicht Présentation des Zielrechners B erreichen, der sie anhand ihres Standardformats erkennt, werden sie auf andere Weise aufbereitet, damit die Anwendung des Rechners B sie erkennen kann.
Application
Auf dieser Ebene befinden sich die Anwendungen, die in der Regel nah am Benutzer angesiedelt sind, wie beispielsweise E-Mail oder Dateiübertragung.

8.1.3. Das Modell TCP/IP

Das Modell OSI ist ein ideales Modell, das bisher noch nicht realisiert wurde. Die Protokollsuite TCP/IP kommt dem in folgender Form nahe:

Image

Physikalische Schicht

In lokalen Netzwerken kommt in der Regel entweder Ethernet- oder Token-Ring-Technologie zum Einsatz. Wir stellen hier nur die Ethernet-Technologie vor.

Ethernet

So wird eine paketvermittelte LAN-Technologie bezeichnet, die Anfang der 1970er Jahre bei PARC Xerox entwickelt und 1978 von Xerox, Intel und Digital Equipment standardisiert wurde. Das Netzwerk besteht physikalisch aus einem Koaxialkabel mit einem Durchmesser von etwa 1,27 cm und einer Länge von maximal 500 m. Es kann mithilfe von répéteurs erweitert werden, wobei zwei Geräte durch nicht mehr als zwei Repeater voneinander getrennt sein dürfen. Das Kabel ist passiv: Alle aktiven Komponenten befinden sich in den an das Kabel angeschlossenen Geräten. Jedes Gerät ist über eine Netzwerkzugangskarte mit dem Kabel verbunden, die Folgendes umfasst:

  • einen Sender (transceiver), der das Vorhandensein von Signalen auf dem Kabel erkennt und analoge Signale in digitale umwandelt und umgekehrt.
  • einen Koppler, der die digitalen Signale vom Sender empfängt und sie zur Verarbeitung an den Computer weiterleitet oder umgekehrt.

Die wichtigsten Merkmale der Ethernet-Technologie sind folgende:

  • Übertragungsrate von 10 Megabit/Sekunde.
  • Bus-Topologie: Alle Geräte sind an dasselbe Kabel angeschlossen

Image

  • Broadcast-Netzwerk – Ein sendender Rechner überträgt Informationen über das Kabel zusammen mit der Adresse des Empfängerrechners. Alle angeschlossenen Rechner empfangen diese Informationen, doch nur derjenige, für den sie bestimmt sind, behält sie.
  • Das Zugriffsverfahren läuft wie folgt ab: Der Sender, der Daten übertragen möchte, überwacht das Kabel – dabei erkennt er, ob eine Trägerwelle vorhanden ist oder nicht; das Vorhandensein einer Trägerwelle würde bedeuten, dass gerade eine Übertragung stattfindet. Dies ist die Technik CSMA (Carrier Sense Multiple Access). Ist kein Träger vorhanden, kann ein Sender beschließen, seinerseits zu senden. Es können mehrere Sender diese Entscheidung treffen. Die gesendeten Signale vermischen sich: Man spricht von einer Kollision. Der Sender erkennt diese Situation: Während er auf das Kabel sendet, hört er gleichzeitig ab, was tatsächlich darauf übertragen wird. Stellt er fest, dass die auf dem Kabel übertragenen Informationen nicht mit den von ihm gesendeten übereinstimmen, schließt er daraus, dass eine Kollision vorliegt, und unterbricht die Übertragung. Die anderen Sender, die ebenfalls gesendet haben, verfahren ebenso. Jeder wird seine Übertragung nach einer zufälligen, vom jeweiligen Sender abhängigen Zeit wieder aufnehmen. Diese Technik wird als CD (Collision Detect) bezeichnet. Die Zugriffsmethode wird daher als CSMA/CD bezeichnet.
  • eine 48-Bit-Adressierung. Jeder Rechner hat eine Adresse – hier als physikalische Adresse bezeichnet –, die auf der Karte vermerkt ist, über die er mit dem Kabel verbunden ist. Diese Adresse wird als Ethernet-Adresse des Rechners bezeichnet.

Netzwerkschicht

Auf dieser Schicht finden wir die Protokolle IP, ICMP, ARP und RARP.

IP (Internetprotokoll)
Übermittelt Pakete zwischen zwei Knoten im Netzwerk
ICMP
(Internet Control Message Protocol)
ICMP stellt die Kommunikation zwischen dem Protokollprogramm IP eines Rechners und dem eines anderen Rechners her. Es handelt sich also um ein Protokoll zum Austausch von Nachrichten innerhalb des Protokolls IP.
ARP
(Address Resolution Protocol)
stellt die Zuordnung von der Internetadresse eines Rechners zur physikalischen Adresse des Rechners her
RARP
(Reverse Address Resolution Protocol)
stellt die Zuordnung von der physischen Adresse eines Rechners zur Internetadresse des Rechners her

Transport-/Sitzungsschicht

In dieser Schicht finden sich folgende Protokolle:

TCP (Transmission Control Protocol)
Gewährleistet eine zuverlässige Übertragung von Informationen zwischen zwei Clients
UDP (User Datagram Protocol)
Gewährleistet eine unzuverlässige Übertragung von Informationen zwischen zwei Clients

Anwendungs-/Präsentations-/Sitzungsschichten

Hier finden sich verschiedene Protokolle:

TELNET
Terminalemulator, der es einem Rechner A ermöglicht, sich als Terminal mit einem Rechner B zu verbinden
FTP (File Transfer Protocol)
ermöglicht Dateiübertragungen
TFTP (Trivial File Transfer Protocol)
ermöglicht Dateiübertragungen
SMTP (Simple Mail Transfer protocol)
ermöglicht den Austausch von Nachrichten zwischen Netzwerkbenutzern
DNS (Domain Name System)
wandelt einen Rechnernamen in die Internetadresse des Rechners um
XDR (eXternal Data Representation)
Erstellt von Sun MicroSystems, legt eine standardisierte, maschinenunabhängige Darstellung der Daten fest
RPC(Remote Procedures Call)
ebenfalls von Sun definiert, ist ein transportschichtunabhängiges Kommunikationsprotokoll zwischen entfernten Anwendungen. Dieses Protokoll ist wichtig: Es entlastet den Programmierer von der Kenntnis der Details der Transportschicht und macht Anwendungen portabel. Dieses Protokoll basiert auf dem Protokoll XDR
NFS (Network File System)
das ebenfalls von Sun definiert wurde. Dieses Protokoll ermöglicht es einem Rechner, das Dateisystem eines anderen Rechners „zu sehen“. Es stützt sich auf das zuvor genannte Protokoll RPC

8.1.4. Funktionsweise der Internetprotokolle

Anwendungen, die in der Umgebung TCP/IP entwickelt wurden, nutzen in der Regel mehrere Protokolle dieser Umgebung. Ein Anwendungsprogramm kommuniziert mit der obersten Protokollschicht. Diese leitet die Informationen an die darunterliegende Schicht weiter und so weiter, bis sie das physikalische Medium erreichen. Dort werden die Informationen physikalisch an den Empfängercomputer übertragen, wo sie dieselben Schichten durchlaufen – diesmal in umgekehrter Reihenfolge –, bis sie die Empfängeranwendung der gesendeten Informationen erreichen. Das folgende Schema zeigt den Weg der Informationen:

Image

Nehmen wir ein Beispiel: die Anwendung FTP, die auf der Ebene der Schicht Application definiert ist und die Dateiübertragungen zwischen Rechnern ermöglicht.

  • Die Anwendung liefert eine Folge von Bytes, die an die Schicht transport. übertragen werden sollen
  • Die Schicht transport unterteilt diese Bytefolge in segments und TCP und fügt am Anfang jedes Segments dessen Nummer hinzu. Die Segmente werden an die Netzwerkschicht weitergeleitet, die vom Protokoll IP gesteuert wird.
  • Die Schicht IP erstellt ein Paket, das das empfangene Segment TCP einkapseln. Am Anfang dieses Pakets fügt sie die Internetadressen des Quell- und des Zielrechners ein. Außerdem ermittelt sie die physikalische Adresse des Zielrechners. Das Ganze wird an die Datenverbindungs- und physikalische Verbindungsebene weitergeleitet, d. h. an die Netzwerkkarte, die den Rechner mit dem physikalischen Netzwerk verbindet.
  • Dort wird das Paket IP seinerseits in einen physikalischen Rahmen gekapselt und über das Kabel an den Empfänger gesendet.
  • Auf dem Zielrechner führt die Schicht „Datenverbindung & Physikalische Verbindung“ den umgekehrten Vorgang durch: Sie entkapseln das Paket IP aus dem physikalischen Rahmen und leitet es an die Schicht IP weiter.
  • Die Schicht IP überprüft, ob das Paket korrekt ist: Sie berechnet anhand der empfangenen Bits eine Prüfsumme (checksum), die im Header des Pakets wiederzufinden sein muss. Ist dies nicht der Fall, wird das Paket verworfen.
  • Wird das Paket als korrekt befunden, entkapseln die Schicht IP das darin enthaltene Segment TCP und leitet es an die darüberliegende Schicht transport weiter.
  • Die Schicht transport – in unserem Beispiel die Schicht TCP – überprüft die Segmentnummer, um die richtige Reihenfolge der Segmente wiederherzustellen.
  • Außerdem berechnet sie eine Prüfsumme für das Segment TCP. Wird diese als korrekt befunden, sendet die Schicht TCP eine Empfangsbestätigung an den Quellrechner; andernfalls wird das Segment TCP abgelehnt.
  • Nun muss die Schicht TCP nur noch den Datenteil des Segments an die Empfängeranwendung in der darüberliegenden Schicht weiterleiten.

8.1.5. Adressierungsprobleme im Internet

Ein noeud in einem Netzwerk kann ein Computer, ein intelligenter Drucker, ein Dateiserver oder eigentlich alles sein, was mithilfe der Protokolle TCP/IP kommunizieren kann. Jeder Knoten verfügt über eine physikalische Adresse, deren Format vom Netzwerktyp abhängt. In einem Ethernet-Netzwerk wird die physikalische Adresse auf 6 Bytes kodiert. Eine Adresse in einem X25-Netzwerk ist eine 14-stellige Zahl.

Die Internetadresse eines Knotens ist eine logische Adresse: Sie ist unabhängig von der Hardware und dem verwendeten Netzwerk. Es handelt sich um eine 4-Byte-Adresse, die sowohl ein lokales Netzwerk als auch einen Knoten dieses Netzwerks identifiziert. Die Internetadresse wird üblicherweise in Form von vier Zahlen dargestellt, die die Werte der vier Bytes angeben und durch einen Punkt voneinander getrennt sind. So lautet die Adresse des Rechners „Lagaffe“ der naturwissenschaftlichen Fakultät in Angers 193.49.144.1 und die des Rechners „Liny“ 193.49.144.9. Daraus lässt sich ableiten, dass die Internetadresse des lokalen Netzwerks 193.49.144.0 lautet. In diesem Netzwerk können bis zu 254 Knoten vorhanden sein.

Da die Internetadressen oder IP-Adressen netzwerkunabhängig sind, kann ein Rechner aus Netzwerk A mit einem Rechner aus Netzwerk B kommunizieren, ohne sich Gedanken über die Art des Netzwerks zu machen, in dem er sich befindet: Es reicht aus, dass er dessen IP-Adresse kennt. Das IP-Protokoll jedes Netzwerks übernimmt die Umwandlung zwischen IP-Adresse und physikalischer Adresse in beide Richtungen.

Die IP-Adressen müssen alle unterschiedlich sein. Offizielle Stellen sind für deren Vergabe zuständig. Tatsächlich vergeben diese Stellen eine Adresse für lokale Netzwerke, zum Beispiel 193.49.144.0 für das Netzwerk der naturwissenschaftlichen Fakultät in Angers. Der Administrator dieses Netzwerks kann dann die Adressen IP 193.49.144.1 bis 193.49.144.254 nach eigenem Ermessen zuweisen. Diese Adresse wird in der Regel in einer speziellen Datei auf jedem mit dem Netzwerk verbundenen Rechner gespeichert.

8.1.5.1. Die Adressklassen IP

Eine IP-Adresse ist eine Folge von 4 Bytes, die oft als I1.I2.I3.I4 bezeichnet wird und tatsächlich zwei Adressen enthält:

  • die Netzwerkadresse
  • die Adresse eines Knotens in diesem Netzwerk

Je nach Größe dieser beiden Felder werden die Adressen IP in drei Klassen unterteilt: Klasse A, B und C.

Klasse A

Die Adresse IP: I1.I2.I3.I4 hat die Form R1.N1.N2.N3, wobei

R1 die Netzwerkadresse ist

N1.N2.N3 die Adresse eines Rechners in diesem Netzwerk ist

Genauer gesagt hat eine Adresse der Klasse A in der Form IP folgende Struktur:

Image

Die Netzwerkadresse umfasst 7 Bit und die Knotenadresse 24 Bit. Somit sind 127 Netzwerke der Klasse A möglich, von denen jedes bis zu 2²⁴ Knoten umfassen kann.

Klasse B

Hier hat die Adresse IP: I1.I2.I3.I4 die Form R1.R2.N1.N2, wobei

R1.R2 die Netzwerkadresse ist

N1.N2 die Adresse eines Rechners in diesem Netzwerk ist

Genauer gesagt hat eine Adresse der Klasse B in der Form IP folgende Struktur:

Image

Sowohl die Netzwerkadresse als auch die Knotenadresse sind 2 Byte (genau 14 Bit) lang. Es können also 2¹⁴ Netzwerke der Klasse B mit jeweils bis zu 2¹⁶ Knoten vorhanden sein.

Klasse C

In dieser Klasse hat die Adresse IP: I1.I2.I3.I4 die Form R1.R2.R3.N1, wobei

R1.R2.R3 die Netzwerkadresse ist

N1 die Adresse eines Rechners in diesem Netzwerk ist

Genauer gesagt hat eine Adresse der Klasse C in der Form IP folgende Struktur:

Image

Die Netzwerkadresse umfasst 3 Bytes (minus 3 Bits) und die Knotenadresse 1 Byte. Somit sind 2²¹ Netzwerke der Klasse C mit jeweils bis zu 256 Knoten möglich.

Da die Adresse des Rechners Lagaffe der naturwissenschaftlichen Fakultät in Angers 193.49.144.1 lautet, sieht man, dass das höchstwertige Byte den Wert 193 hat, also im Binärsystem 11000001. Daraus lässt sich ableiten, dass es sich um ein Netzwerk der Klasse C handelt.

Reservierte Adressen

. Bestimmte Adressen wie IP sind Netzwerkadressen und keine Knotenadressen im Netzwerk. Dies sind diejenigen, bei denen die Knotenadresse auf 0 gesetzt ist. So ist die Adresse 193.49.144.0 die Netzwerkadresse IP der Fakultät für Naturwissenschaften in Angers. Folglich kann kein Knoten eines Netzwerks die Adresse Null haben.

. Wenn in einer Adresse wie IP die Knotenadresse ausschließlich aus 1en besteht, handelt es sich um eine Broadcast-Adresse: Diese Adresse bezeichnet alle Knoten im Netzwerk.

. In einem Netzwerk der Klasse C, das theoretisch 2⁸ = 256 Knoten zulässt, bleiben nach Abzug der beiden unzulässigen Adressen nur noch 254 zulässige Adressen übrig.

8.1.5.2. Konvertierungsprotokolle Internetadresse <--> Physikalische Adresse

Wir haben gesehen, dass bei der Übertragung von Daten von einem Rechner zu einem anderen diese beim Durchlaufen der Schicht IP in Pakete gekapselt wurden. Diese haben folgende Form:

Image

Das Paket IP enthält also die Internetadressen des Quell- und des Zielrechners. Wenn dieses Paket an die Schicht weitergeleitet wird, die für die Übertragung über das physikalische Netzwerk zuständig ist, werden weitere Informationen hinzugefügt, um den physikalischen Rahmen zu bilden, der schließlich über das Netzwerk gesendet wird. Das Format eines Rahmens in einem Ethernet-Netzwerk sieht beispielsweise wie folgt aus:

Image

Im endgültigen Rahmen sind die physikalischen Adressen des Quell- und des Zielrechners enthalten. Wie werden diese ermittelt?

Da der Absender die Adresse IP des Rechners kennt, mit dem er kommunizieren möchte, ermittelt er dessen physikalische Adresse mithilfe eines speziellen Protokolls namens ARP (Address Resolution Protocol).

  • Er sendet ein Paket eines speziellen Typs, das als ARP-Paket bezeichnet wird und die Adresse IP des Rechners enthält, dessen physikalische Adresse ermittelt werden soll. Außerdem hat er darauf geachtet, seine eigene Adresse IP sowie seine physikalische Adresse in das Paket aufzunehmen.
  • Dieses Paket wird an alle Knoten im Netzwerk gesendet.
  • Diese erkennen den besonderen Charakter des Pakets. Der Knoten, der seine Adresse IP im Paket erkennt, antwortet, indem er dem Absender des Pakets seine physikalische Adresse sendet. Wie ist das möglich? Er hat im Paket die Adressen IP und die physikalische Adresse des Absenders gefunden.
  • Der Absender erhält somit die gesuchte physikalische Adresse. Er speichert sie im Speicher, um sie später verwenden zu können, falls weitere Pakete an denselben Empfänger gesendet werden sollen.

Die Adresse IP eines Rechners ist normalerweise in einer seiner Dateien gespeichert, sodass er diese einsehen kann, um sie zu erfahren. Diese Adresse kann geändert werden: Dazu muss lediglich die Datei bearbeitet werden. Die physikalische Adresse hingegen ist im Speicher der Netzwerkkarte hinterlegt und kann nicht geändert werden.

Wenn ein Administrator sein Netzwerk anders organisieren möchte, kann es erforderlich sein, die Adressen IP aller Knoten zu ändern und somit die verschiedenen Konfigurationsdateien der einzelnen Knoten zu bearbeiten. Dies kann mühsam sein und bei einer großen Anzahl von Rechnern zu Fehlern führen. Eine Methode besteht darin, den Rechnern keine IP-Adresse zuzuweisen: Man trägt dann einen speziellen Code in die Datei ein, in der der Rechner seine IP-Adresse finden sollte. Wenn der Rechner feststellt, dass er keine IP-Adresse hat, fordert er diese über ein Protokoll namens RARP (Reverse Address Resolution Protocol) an. Sie sendet daraufhin über ein Netzwerk ein spezielles Paket namens RARP, analog zum vorherigen Paket ARP, in das sie ihre physikalische Adresse einträgt. Dieses Paket wird an alle Knoten gesendet, die es dann als Paket RARP erkennen. Einer von ihnen, der als Server RARP bezeichnet wird, verfügt über eine Datei, die die Zuordnung zwischen physikalischer Adresse und IP-Adresse aller Knoten enthält. Er antwortet daraufhin dem Absender des Pakets RARP und sendet ihm seine Adresse IP zurück. Ein Administrator, der sein Netzwerk neu konfigurieren möchte, muss daher lediglich die Zuordnungsdatei des Servers RARP bearbeiten. Dieser muss normalerweise über eine feste Adresse IP verfügen, die er kennen muss, ohne selbst das Protokoll RARP nutzen zu müssen.

8.1.6. Die sogenannte IP-Netzwerkschicht des Internets

Das Protokoll IP (Internet Protocol) definiert die Form, die die Pakete annehmen müssen, und die Art und Weise, wie sie beim Senden oder Empfangen verarbeitet werden müssen. Diese besondere Art von Paket wird als IP-Datagramm bezeichnet. Wir haben es bereits vorgestellt:

Image

Wichtig ist, dass das IP-Datagramm neben den zu übertragenden Daten auch die Internetadressen des Quell- und des Zielrechners enthält. So weiß der Empfängerrechner, von wem er eine Nachricht erhält.

Im Gegensatz zu einem Netzwerkrahmen, dessen Länge durch die physikalischen Eigenschaften des Netzwerks bestimmt wird, über das er übertragen wird, wird die Länge des Datagramms IP durch die Software festgelegt und ist daher in verschiedenen physikalischen Netzwerken gleich. Wir haben gesehen, dass das Datagramm IP beim Abstieg von der Netzwerkschicht in die physikalische Schicht in einen physikalischen Rahmen gekapselt wurde. Als Beispiel haben wir den physikalischen Rahmen eines Ethernet-Netzwerks angeführt:

Image

Die physikalischen Frames werden von Knoten zu Knoten zu ihrem Ziel weitergeleitet, das sich möglicherweise nicht im selben physikalischen Netzwerk wie der sendende Rechner befindet. Das Paket IP kann daher nacheinander in verschiedene physikalische Frames auf den Knoten gekapselt werden, die die Verbindung zwischen zwei Netzwerken unterschiedlichen Typs herstellen. Es kann auch vorkommen, dass das Paket IP zu groß ist, um in einen physikalischen Frame gekapselt zu werden. Die Software IP des Knotens, an dem dieses Problem auftritt, zerlegt das Paket IP dann nach genauen Regeln in fragments, wobei jedes dieser Pakete anschließend über das physikalische Netzwerk gesendet wird. Sie werden erst an ihrem endgültigen Zielort wieder zusammengesetzt.

8.1.6.1. Das Routing

Das Routing ist die Methode, mit der die Pakete IP an ihren Bestimmungsort geleitet werden. Es gibt zwei Methoden: das direkte Routing und das indirekte Routing.

Direktes Routing

Direktes Routing bezeichnet die Weiterleitung eines Pakets IP direkt vom Absender zum Empfänger innerhalb desselben Netzwerks:

  • Der Absender eines Datagramms IP verfügt über die Empfängeradresse IP.
  • Sie ermittelt die physikalische Adresse des Empfängers über das Protokoll ARP oder aus ihren Tabellen, falls diese Adresse bereits vorliegt.
  • Er sendet das Paket über das Netzwerk an diese physikalische Adresse.

Indirektes Routing

Unter indirekter Weiterleitung versteht man die Weiterleitung eines Pakets mit der Kennung IP an ein Ziel, das sich in einem anderen Netzwerk befindet als das des Absenders. In diesem Fall unterscheiden sich die Netzwerkadressanteile der Adressen IP des Quell- und des Zielrechners. Der Quellrechner erkennt dies. Er sendet das Paket dann an einen speziellen Knoten, den sogenannten Router (router), der ein lokales Netzwerk mit anderen Netzwerken verbindet und dessen Adresse IP er in seinen Tabellen findet – eine Adresse, die ursprünglich entweder aus einer Datei, aus einem permanenten Speicher oder über im Netzwerk zirkulierende Informationen bezogen wurde.

Ein Router ist an zwei Netzwerke angeschlossen und verfügt innerhalb dieser beiden Netzwerke über eine Adresse IP.

Image

In unserem obigen Beispiel:

  • Netzwerk Nr. 1 hat die Internetadresse 193.49.144.0 und Netzwerk Nr. 2 die Adresse 193.49.145.0.
  • Innerhalb von Netzwerk Nr. 1 hat der Router die Adresse 193.49.144.6 und innerhalb von Netzwerk Nr. 2 die Adresse 193.49.145.3.

Die Aufgabe des Routers besteht darin, das empfangene Paket IP, das in einem für Netzwerk Nr. 1 typischen physikalischen Rahmen enthalten ist, in einen physikalischen Rahmen umzuwandeln, der im Netzwerk Nr. 2 übertragen werden kann. Befindet sich die Adresse IP des Paketempfängers im Netzwerk Nr. 2, sendet der Router das Paket direkt an ihn; andernfalls leitet er es an einen anderen Router weiter, der das Netzwerk Nr. 2 mit einem Netzwerk Nr. 3 verbindet, und so weiter.

8.1.6.2. Fehler- und Kontrollmeldungen

Ebenfalls in der Netzwerkschicht, also auf derselben Ebene wie das Protokoll IP, existiert das Protokoll ICMP (Internet Control Message Protocol). Es dient dazu, Meldungen über den internen Betrieb des Netzwerks zu senden: ausgefallene Knoten, Stau an einem Router usw. Die ICMP-Meldungen werden in IP-Pakete gekapselt und über das Netzwerk gesendet. Die IP-Schichten der verschiedenen Knoten ergreifen entsprechend den empfangenen ICMP-Meldungen die geeigneten Maßnahmen. Somit bekommt eine Anwendung selbst diese netzwerkspezifischen Probleme nie mit.

Ein Knoten nutzt die Informationen ICMP, um seine Routing-Tabellen zu aktualisieren.

8.1.7. Die Transportschicht: die Protokolle UDP und TCP

8.1.7.1. Das Protokoll UDP: User Datagram Protocol

Das Protokoll UDP ermöglicht einen unzuverlässigen Datenaustausch zwischen zwei Punkten, d. h., die korrekte Weiterleitung eines Pakets an seinen Bestimmungsort ist nicht garantiert. Die Anwendung kann dies, falls gewünscht, selbst handhaben, indem sie beispielsweise nach dem Senden einer Nachricht auf eine Empfangsbestätigung wartet, bevor sie die nächste sendet.

Bisher haben wir auf Netzwerkebene von IP-Adressen von Rechnern gesprochen. Auf einem Rechner können jedoch gleichzeitig verschiedene Prozesse laufen, die alle miteinander kommunizieren können. Daher muss beim Versenden einer Nachricht nicht nur die Adresse IP des Empfängersystems angegeben werden, sondern auch der „Name“ des Empfängerprozesses. Dieser Name ist eigentlich eine Nummer, die als Portnummer bezeichnet wird. Bestimmte Nummern sind für Standardanwendungen reserviert: Port 69 beispielsweise für die Anwendung TFTP (Trivial File Transfer Protocol).

Die vom Protokoll UDP verwalteten Pakete werden auch als Datagramme bezeichnet. Sie haben folgende Form:

Image

Diese Datagramme werden in IP-Pakete und anschließend in physikalische Rahmen gekapselt.

8.1.7.2. Das Protokoll TCP: Transfer Control Protocol

Für eine sichere Kommunikation reicht das Protokoll UDP nicht aus: Der Anwendungsentwickler muss selbst ein Protokoll entwickeln, mit dem er die korrekte Weiterleitung der Pakete überprüfen kann.

Das Protokoll TCP (Transfer Control Protocol) vermeidet diese Probleme. Es weist folgende Merkmale auf:

  • Der Prozess, der Daten senden möchte, baut zunächst eine Verbindung zu dem Prozess auf, der die zu sendenden Informationen empfangen soll. Diese Verbindung wird zwischen einem Port des sendenden Rechners und einem Port des empfangenden Rechners hergestellt. Zwischen den beiden Ports entsteht so ein virtueller Pfad, der ausschließlich den beiden Prozessen vorbehalten ist, die die Verbindung hergestellt haben.
  • Alle vom Quellprozess gesendeten Pakete folgen diesem virtuellen Pfad und kommen in der Reihenfolge an, in der sie gesendet wurden – was beim Protokoll UDP nicht gewährleistet war, da die Pakete unterschiedliche Pfade nehmen konnten.
  • Die gesendeten Informationen erscheinen als kontinuierlicher Strom. Der sendende Prozess sendet Informationen in seinem eigenen Rhythmus. Diese werden nicht unbedingt sofort gesendet: Das Protokoll TCP wartet, bis genügend Daten vorhanden sind, um sie zu versenden. Sie werden in einer Struktur namens Segment TCP gespeichert. Sobald dieses Segment gefüllt ist, wird es an die Schicht IP weitergeleitet, wo es in ein Paket IP gekapselt wird.
  • Jedes vom Protokoll TCP gesendete Segment wird nummeriert. Das empfangende Protokoll TCP überprüft, ob es die Segmente in der richtigen Reihenfolge erhält. Für jedes korrekt empfangene Segment sendet es eine Empfangsbestätigung an den Absender.
  • Sobald der Absender diese Bestätigung erhält, teilt er dies dem sendenden Prozess mit. Dieser kann somit feststellen, dass ein Segment erfolgreich angekommen ist, was mit dem Protokoll UDP nicht möglich war.
  • Wenn das Protokoll TCP, das ein Segment gesendet hat, nach einer gewissen Zeit keine Empfangsbestätigung erhält, sendet es das betreffende Segment erneut und gewährleistet so die Qualität des Informationsübertragungsdienstes.
  • Die zwischen den beiden kommunizierenden Prozessen hergestellte virtuelle Verbindung lautet full-duplex: Das bedeutet, dass Informationen in beide Richtungen übertragen werden können. So kann der Zielprozess Empfangsbestätigungen senden, während der Quellprozess weiterhin Informationen sendet. Dies ermöglicht es beispielsweise dem Quellprotokoll TCP, mehrere Segmente zu senden, ohne auf eine Empfangsbestätigung zu warten. Stellt es nach einer gewissen Zeit fest, dass es die Empfangsbestätigung für ein bestimmtes Segment mit der Nummer n nicht erhalten hat, setzt es die Übertragung der Segmente an dieser Stelle fort.

8.1.8. Die Anwendungsschicht

Über den Protokollen UDP und TCP gibt es verschiedene Standardprotokolle:

TELNET

Dieses Protokoll ermöglicht es einem Benutzer eines Rechners A im Netzwerk, eine Verbindung zu einem Rechner B (oft als Host-Rechner bezeichnet) herzustellen. TELNET emuliert auf dem Rechner A ein sogenanntes universelles Terminal. Der Benutzer verhält sich somit so, als hätte er ein an Rechner B angeschlossenes Terminal. Telnet basiert auf dem Protokoll TCP.

FTP: (File Transfer Protocol)

Dieses Protokoll ermöglicht den Austausch von Dateien zwischen zwei entfernten Rechnern sowie Dateivorgänge wie beispielsweise das Anlegen von Verzeichnissen. Es basiert auf dem Protokoll TCP.

TFTP: (Trivial File Transfer Control)

Dieses Protokoll ist eine Variante von FTP. Es basiert auf dem Protokoll UDP und ist weniger komplex als FTP.

DNS: (Domain Name System)

Wenn ein Benutzer Dateien mit einem Remote-Rechner austauschen möchte, beispielsweise über FTP, muss er die Internetadresse dieses Rechners kennen. Um beispielsweise FTP auf dem Rechner „Lagaffe“ der Universität Angers auszuführen, müsste man FTP wie folgt starten: FTP 193.49.144.1

Dies erfordert ein Verzeichnis, das die Zuordnung zwischen Rechner und Adresse herstellt, z. B. IP. Wahrscheinlich würden die Rechner in diesem Verzeichnis durch symbolische Namen wie die folgenden bezeichnet:

Rechner DPX2/320 der Universität Angers

Sun-Rechner der Universität Angers mit der Adresse ISERPA

Es liegt auf der Hand, dass es angenehmer wäre, einen Rechner mit einem Namen statt mit seiner Adresse IP zu bezeichnen. Damit stellt sich das Problem der Eindeutigkeit der Namen: Es gibt Millionen miteinander verbundener Rechner. Man könnte sich vorstellen, dass eine zentrale Stelle die Namen vergibt. Das wäre zweifellos recht aufwendig. Die Verwaltung der Namen wurde daher auf Domänen verteilt. Jede Domäne wird von einer in der Regel sehr schlanken Organisation verwaltet, die bei der Wahl der Rechnernamen völlige Freiheit genießt. So gehören die Rechner in Frankreich zur Domäne „fr“, die vom Inria in Paris verwaltet wird. Um die Dinge weiter zu vereinfachen, wird die Verwaltung noch weiter aufgeteilt: Innerhalb der Domäne „fr“ werden weitere Domänen angelegt. So gehört die Universität Angers zur Domäne „univ-Angers“. Die für diese Domäne zuständige Stelle hat völlige Freiheit bei der Benennung der Rechner im Netzwerk der Universität Angers. Bislang wurde diese Domäne noch nicht unterteilt. An einer großen Universität mit vielen vernetzten Rechnern könnte dies jedoch der Fall sein.

Der Rechner DPX2/320 der Universität Angers wurde als Lagaffe benannt, während ein PC und ein 486DX50 als liny benannt wurden. Wie lassen sich diese Rechner von außen referenzieren? Indem man die Hierarchie der Domänen angibt, zu denen sie gehören. Der vollständige Name des Rechners „Lagaffe“ lautet somit:

        Lagaffe.univ-Angers.fr

Innerhalb der Domänen können relative Namen verwendet werden. Innerhalb der Domäne „fr“ und außerhalb der Domäne „univ-Angers“ kann der Rechner „Lagaffe“ also wie folgt referenziert werden:

        Lagaffe.univ-Angers

Schließlich kann er innerhalb der Domäne „univ-Angers“ einfach wie folgt bezeichnet werden:

        Lagaffe

Eine Anwendung kann einen Rechner also anhand seines Namens referenzieren. Letztendlich muss man jedoch die Internetadresse dieses Rechners ermitteln. Wie wird dies bewerkstelligt? Nehmen wir an, man möchte von einem Rechner A aus mit einem Rechner B kommunizieren.

  • Wenn der Rechner B zur gleichen Domäne wie der Rechner A gehört, findet man seine Adresse IP wahrscheinlich in einer Datei auf dem Rechner A.
  • Andernfalls findet Rechner A in einer anderen Datei oder derselben wie zuvor eine Liste mit einigen Nameservern und deren Adressen IP. Ein Nameserver ist dafür zuständig, die Zuordnung zwischen einem Rechnernamen und seiner Adresse IP herzustellen. Rechner A sendet eine spezielle Anfrage an den ersten Nameserver seiner Liste, die sogenannte DNS-Anfrage, die somit den Namen des gesuchten Rechners enthält. Wenn der abgefragte Server diesen Namen in seinen Tabellen hat, sendet er die entsprechende Adresse IP an Rechner A. Andernfalls findet der Server in seinen Dateien ebenfalls eine Liste von Nameservern, die er abfragen kann. Dies wird er dann tun. So werden eine bestimmte Anzahl von Nameservern abgefragt, jedoch nicht wahllos, sondern so, dass die Anzahl der Anfragen minimiert wird. Wird der Rechner schließlich gefunden, gelangt die Antwort zurück zum Rechner A.

XDR: (eXternal-Datendarstellung)

Dieses von Sun entwickelte Protokoll (MicroSystems) legt eine standardisierte, maschinenunabhängige Datendarstellung fest.

RPC: (Remote Procedure Call)

Ebenfalls von Sun definiert, handelt es sich hierbei um ein transportschichtunabhängiges Kommunikationsprotokoll zwischen entfernten Anwendungen. Dieses Protokoll ist wichtig: Es entlastet den Programmierer von der Kenntnis der Details der Transportschicht und macht Anwendungen portabel. Dieses Protokoll basiert auf dem Protokoll XDR

NFS: Network File System

Dieses ebenfalls von Sun definierte Protokoll ermöglicht es einem Rechner, das Dateisystem eines anderen Rechners „zu sehen“. Es basiert auf dem zuvor genannten Protokoll RPC.

8.1.9. Fazit

In dieser Einführung haben wir einige Grundzüge der Internetprotokolle vorgestellt. Wer sich näher mit diesem Thema befassen möchte, kann das hervorragende Buch von Douglas Comer lesen:

Titel: TCP/IP: Architektur, Protokolle, Anwendungen.

Autor: Douglas COMER

Verlag: InterEditions

8.2. Verwaltung von Netzwerkadressen in Java

8.2.1. Definition

Jeder Rechner im Internet wird durch eine eindeutige Adresse oder einen eindeutigen Namen identifiziert. Diese beiden Elemente werden in Java von der Klasse InetAddress verwaltet, von der hier einige Methoden aufgeführt sind:

byte [] getAddress()
gibt die 4 Bytes der Adresse IP der aktuellen Instanz InetAddress zurück
String getHostAddress()
liefert die Adresse IP der aktuellen Instanz InetAddress
String getHostName()
gibt den Internetnamen der aktuellen Instanz InetAddress an
String toString()
gibt die Adress-ID IP bzw. den Internetnamen der aktuellen Instanz InetAddress an
InetAddress getByName(String Host)
erstellt die Instanz InetAddress des durch „Host“ bezeichneten Rechners. Löst eine Ausnahme aus, wenn „Host“ unbekannt ist. „Host“ kann der Internetname eines Rechners oder dessen Adresse IP in der Form I1.I2.I3.I4 sein
InetAddress getLocalHost()
erstellt die Instanz InetAddress des Rechners, auf dem das Programm ausgeführt wird, das diese Anweisung enthält.

8.2.2. Einige Beispiele

8.2.2.1. Den lokalen Rechner identifizieren


import java.net.*;

public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// Ende von try
  }// Ende von main
}// Ende der Klasse

Die Ergebnisse der Ausführung lauten wie folgt:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

Jeder Rechner hat eine interne Adresse IP, die 127.0.0.1 lautet. Wenn ein Programm diese Netzwerkadresse verwendet, greift es auf den Rechner zu, auf dem es ausgeführt wird. Der Vorteil dieser Adresse besteht darin, dass sie keine Netzwerkkarte erfordert. Man kann daher Netzwerkprogramme testen, ohne mit einem Netzwerk verbunden zu sein. Eine weitere Möglichkeit, den lokalen Rechner zu bezeichnen, ist die Verwendung des Namens „localhost“.

8.2.2.2. Einen beliebigen Rechner identifizieren


import java.net.*;

public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // das Argument wird abgerufen
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // Versuch, die Adresse des Rechners abzurufen
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// Ende try
  }// Ende von main
}// Ende der Klasse

Mit dem Java-Aufruf **getbyname** erhält man folgende Ergebnisse:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

Mit dem Aufruf „java getbyname shiva.istia.univ-angers.fr“ erhält man:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

Mit dem Java-Aufruf „getbyname“ www.ibm.com erhält man:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. Meldungen TCP-IP

8.3.1. Allgemeines

Image

Wenn eine Anwendung AppA auf einem Rechner A mit einer Anwendung AppB auf einem Rechner B im Internet kommunizieren möchte, muss sie mehrere Informationen kennen:

  • die Adresse IP oder den Namen des Rechners B
  • die Portnummer, über die die Anwendung AppB arbeitet. Denn der Rechner B kann zahlreiche Anwendungen unterstützen, die über das Internet arbeiten. Wenn er Informationen aus dem Netzwerk empfängt, muss er wissen, für welche Anwendung diese Informationen bestimmt sind. Die Anwendungen auf Rechner B haben über Schnittstellen, die auch als Kommunikationsports bezeichnet werden, Zugriff auf das Netzwerk. Diese Information ist in dem von Rechner B empfangenen Paket enthalten, damit es an die richtige Anwendung weitergeleitet werden kann.
  • Die von Rechner B verstandenen Kommunikationsprotokolle. In unserer Untersuchung werden wir ausschließlich die Protokolle TCP-IP verwenden.
  • Das von der Anwendung AppB akzeptierte Dialogprotokoll. Denn die Maschinen A und B werden miteinander „kommunizieren“. Was sie sich mitteilen, wird in die Protokolle TCP-IP eingekapselt. Wenn jedoch am Ende der Kette die Anwendung AppB die von der Anwendung AppA gesendete Information empfängt, muss sie in der Lage sein, diese zu interpretieren. Dies ist vergleichbar mit der Situation, in der zwei Personen, A und B, telefonisch miteinander kommunizieren: Ihr Gespräch wird über das Telefon übertragen. Die Sprache wird vom Telefon A in Form von Signalen codiert, über Telefonleitungen übertragen und erreicht das Telefon B, wo sie decodiert wird. Person B hört dann Sprache. Hier kommt der Begriff des Dialogprotokolls ins Spiel: Wenn A Französisch spricht und B diese Sprache nicht versteht, können A und B keinen sinnvollen Dialog führen.

Daher müssen sich die beiden kommunizierenden Anwendungen über die Art des Dialogs einigen, den sie anwenden werden. So ist beispielsweise der Dialog mit einem Dienst ftp nicht derselbe wie mit einem Dienst pop: Diese beiden Dienste akzeptieren nicht dieselben Befehle. Sie verfügen über ein unterschiedliches Kommunikationsprotokoll.

8.3.2. Die Merkmale des Protokolls TCP

Wir werden hier nur Netzwerkkommunikationen betrachten, die das Transportprotokoll TCP verwenden. Hier noch einmal die Merkmale dieses Protokolls:

  • Der Prozess, der Daten senden möchte, baut zunächst eine Verbindung zu dem Prozess auf, der die zu sendenden Informationen empfangen soll. Diese Verbindung wird zwischen einem Port des sendenden Rechners und einem Port des empfangenden Rechners hergestellt. Zwischen den beiden Ports entsteht so ein virtueller Pfad, der ausschließlich den beiden Prozessen vorbehalten ist, die die Verbindung hergestellt haben.
  • Alle vom Quellprozess gesendeten Pakete folgen diesem virtuellen Pfad und kommen in der Reihenfolge an, in der sie gesendet wurden
  • Die gesendeten Informationen haben einen kontinuierlichen Charakter. Der sendende Prozess sendet Informationen in seinem eigenen Tempo. Diese werden nicht unbedingt sofort gesendet: Das Protokoll TCP wartet, bis genügend Daten vorhanden sind, um sie zu versenden. Sie werden in einer Struktur namens Segment TCP gespeichert. Sobald dieses Segment gefüllt ist, wird es an die Schicht IP weitergeleitet, wo es in ein Paket IP gekapselt wird.
  • Jedes vom Protokoll TCP gesendete Segment ist nummeriert. Das empfangende Protokoll TCP überprüft, ob es die Segmente in der richtigen Reihenfolge empfängt. Für jedes korrekt empfangene Segment sendet es eine Empfangsbestätigung an den Absender.
  • Sobald der Absender diese erhält, teilt er dies dem sendenden Prozess mit. Dieser kann somit feststellen, dass ein Segment erfolgreich angekommen ist.
  • Wenn das Protokoll TCP, das ein Segment gesendet hat, nach einer gewissen Zeit keine Empfangsbestätigung erhält, sendet es das betreffende Segment erneut und gewährleistet so die Qualität des Informationsübertragungsdienstes.
  • Die zwischen den beiden kommunizierenden Prozessen hergestellte virtuelle Verbindung lautet full-duplex: Das bedeutet, dass die Informationen in beide Richtungen übertragen werden können. So kann der Zielprozess Empfangsbestätigungen senden, während der Quellprozess weiterhin Informationen versendet. Dies ermöglicht es beispielsweise dem Quellprotokoll TCP, mehrere Segmente zu senden, ohne auf eine Empfangsbestätigung zu warten. Stellt es nach einer gewissen Zeit fest, dass es die Empfangsbestätigung für ein bestimmtes Segment mit der Nummer n nicht erhalten hat, setzt es die Übertragung der Segmente an dieser Stelle fort.

8.3.3. Die Client-Server-Beziehung

Häufig ist die Kommunikation im Internet asymmetrisch: Rechner A initiiert eine Verbindung, um einen Dienst von Rechner B anzufordern: Er gibt an, dass er eine Verbindung zum Dienst SB1 von Rechner B herstellen möchte. Dieser akzeptiert oder lehnt ab. Wenn sie akzeptiert, kann Rechner A seine Anfragen an den Dienst SB1 senden. Diese müssen dem vom Dienst SB1 verstandenen Dialogprotokoll entsprechen. So entsteht ein Frage-Antwort-Dialog zwischen Rechner A, dem sogenannten Client, und Rechner B, dem sogenannten Server. Einer der beiden Partner wird die Verbindung beenden.

8.3.4. Architektur eines Clients

Die Architektur eines Netzwerkprogramms, das die Dienste einer Serveranwendung in Anspruch nimmt, sieht wie folgt aus:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
        tant que ce n'est pas fini
            préparer une demande
            l'émettre vers la machine B
            attendre et récupérer la réponse
            la traiter
        fin tant que
finsi

8.3.5. Architektur eines Servers

Die Architektur eines Programms, das Dienste bereitstellt, sieht wie folgt aus:

ouvrir le service sur la machine locale
tant que le service est ouvert
        se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
        lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Das Serverprogramm behandelt die erste Verbindungsanfrage eines Clients anders als dessen nachfolgende Anfragen zur Inanspruchnahme eines Dienstes. Das Programm erbringt den Dienst nicht selbst. Würde es dies tun, könnte es während der Dauer des Dienstes keine Verbindungsanfragen mehr entgegennehmen, und die Clients würden nicht bedient werden. Es geht daher anders vor: Sobald eine Verbindungsanfrage am Listening-Port empfangen und angenommen wird, erstellt der Server eine Aufgabe, die dafür zuständig ist, den vom Client angeforderten Dienst bereitzustellen. Dieser Dienst wird auf einem anderen Port des Serverrechners bereitgestellt, dem sogenannten Dienstport. Auf diese Weise können mehrere Clients gleichzeitig bedient werden.

Eine Dienst-Task hat folgende Struktur:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

8.3.6. Die Socket-Klasse

8.3.6.1. Définition

Das grundlegende Werkzeug, das von Programmen zur Kommunikation über das Internet verwendet wird, ist die socket. Dieses englische Wort bedeutet „Steckdose“. Es wird hier im übertragenen Sinne als „Netzwerksteckdose“ verwendet. Damit eine Anwendung Informationen über das Internet senden und empfangen kann, benötigt sie eine Netzwerksteckdose, eine socket. Dieses Tool wurde ursprünglich in den Unix-Versionen der Universität Berkeley entwickelt. Es wurde seitdem auf alle Unix-Systeme sowie in die Windows-Welt portiert. Es ist auch auf Java-Virtual-Machines in zwei Formen verfügbar: die Klasse Socket für Client-Anwendungen und die Klasse ServerSocket für Server-Anwendungen. Wir erläutern hier einige der Konstruktoren und Methoden der Klasse Socket:

public Socket(String host, int port)
stellt eine Fernverbindung zum Port port des Rechners host her
public int getLocalPort()
gibt die Nummer des vom Socket verwendeten lokalen Ports zurück
  
public int getPort()
gibt die Nummer des Remote-Ports zurück, mit dem der Socket verbunden ist
  
public InetAddress getLocalAdress()
gibt die lokale Adresse zurück, mit der der Socket verknüpft ist
  
public InetAddress getInetAdress()
gibt die Remote-Adresse InetAddress zurück, mit der der Socket verbunden ist
  
public InputStream getInputStream()
gibt einen Eingabestrom zurück, über den die vom Remote-Partner gesendeten Daten gelesen werden können
  
public OutputStream getOutputStream()
gibt einen Ausgabestrom zurück, über den Daten an den Remote-Partner gesendet werden können
  
public void shutdownInput()
schließt den Eingabestrom des Sockets
  
public void shutdownOutput()
schließt den Ausgabestrom des Sockets
  
public void close()
schließt den Socket und seine Ein- und Ausgabeströme
  
public String toString()
gibt eine Zeichenkette zurück, die den Socket „repräsentiert“
 

8.3.6.2. Herstellen einer Verbindung zu einem Server

Wir haben gesehen, dass ein Rechner A zwei Informationen benötigt, um eine Verbindung zu einem Dienst auf einem Rechner B herzustellen:

  • die Adresse IP oder den Namen des Rechners B
  • die Portnummer, unter der der gewünschte Dienst läuft

Der Konstruktor

    public Socket(String  host, int  port);

erstellt einen Socket und verbindet ihn mit dem Rechner host auf dem Port port. Dieser Konstruktor löst in verschiedenen Fällen eine Ausnahme aus:

  • falsche Adresse
  • falscher Port
  • Anfrage abgelehnt

Wir müssen diese Ausnahme behandeln:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // Die Verbindung ist fehlgeschlagen – der Fehler wird behandelt
        ….
    }

Wenn die Verbindungsanfrage erfolgreich ist, wird dem Client lokal ein Port zugewiesen, über den er mit Rechner B kommunizieren kann. Sobald die Verbindung hergestellt ist, kann dieser Port mit der folgenden Methode ermittelt werden:

public int getLocalPort();

Wenn die Verbindung erfolgreich hergestellt wird, haben wir gesehen, dass der Server seinerseits den Dienst durch einen anderen Prozess bereitstellen lässt, der auf einem sogenannten Dienstport läuft. Diese Portnummer kann mit der folgenden Methode ermittelt werden:

public int getPort();

8.3.6.3. Informationen über das Netzwerk senden

Mit der folgenden Methode kann ein Schreibstrom über den Socket und damit über das Netzwerk erzeugt werden:

public OutputStream getOutputStream();

Alles, was in diesen Stream gesendet wird, wird am Service-Port des Servers empfangen. Viele Anwendungen verfügen über einen Dialog in Form von Textzeilen, die mit einem Zeilenumbruch enden. Daher ist die Methode println in solchen Fällen sehr praktisch. Man wandelt dann den Ausgabestrom OutputStream in den Strom PrintWriter um, der über die Methode println verfügt. Das Schreiben kann eine Ausnahme auslösen.

8.3.6.4. Informationen aus dem Netzwerk lesen

Mit der folgenden Methode kann ein Lesestrom der am Socket eingehenden Informationen abgerufen werden:

public InputStream getInputStream();

Alles, was in diesem Stream gelesen wird, stammt vom Dienstport des Servers. Für Anwendungen mit einem Dialog in Form von Textzeilen, die mit einem Zeilenumbruch enden, empfiehlt sich die Verwendung der Methode readLine. Dazu wandelt man den Eingabestrom InputStream in den Strom BufferedReader um, der über die Methode readLine() verfügt. Beim Einlesen kann eine Ausnahme auftreten.

8.3.6.5. Schließen der Verbindung

Dies erfolgt mit der Methode:

public void close();

Die Methode kann eine Ausnahme auslösen. Die verwendeten Ressourcen, insbesondere der Netzwerkport, werden freigegeben.

8.3.6.6. Die Architektur des Clients

Wir verfügen nun über die Elemente, um die grundlegende Architektur eines Internet-Clients zu beschreiben:


    Socket  sClient=null;
    try{
            // Verbindung zum Dienst auf Port P des Rechners M wird hergestellt
        sClient=new Socket(M,P);

        // Die Ein- und Ausgabeströme des Client-Sockets werden erstellt
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);

        // Anfrage-Antwort-Schleife
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // Die Anfrage wird vorbereitet
            demande=…
            // Die Anfrage wird gesendet
            out.println(demande);
            // Die Antwort wird gelesen
            réponse=in.readLine();
            // Antwort wird verarbeitet

        }
        // Fertig
        sClient.close();
    } catch(Exception e){
        // die Ausnahme wird behandelt
        ….
    }

Um das Beispiel nicht zu verkomplizieren, haben wir darauf verzichtet, die verschiedenen Ausnahmetypen zu behandeln, die vom Konstruktor Socket oder den Methoden readline, getInputStream, getOutputStream und close ausgelöst werden. Alles wurde in einer einzigen Ausnahme zusammengefasst.

8.3.7. Die Klasse ServerSocket

8.3.7.1. Définition

Diese Klasse dient der serverseitigen Verwaltung von Sockets. Im Folgenden werden einige der Konstruktoren und Methoden dieser Klasse näher erläutert:

public ServerSocket(int port)
Erstellt einen Listening-Socket auf dem Port port
public ServerSocket(int port, int count)
ebenso, legt jedoch die Warteschlangengröße auf count fest und die maximale Anzahl der in die Warteschlange gestellten Client-Verbindungen auf c.a.d, falls der Server bei Eintreffen der Client-Verbindung ausgelastet ist.
public int getLocalPort()
Gibt die Nummer des vom Socket verwendeten Listening-Ports zurück
public InetAddress getInetAdress()
gibt die lokale Adresse InetAddress zurück, mit der der Socket verknüpft ist
public Socket accept()
versetzt den Server in den Verbindungswartemodus (blockierender Vorgang). Bei Eintreffen einer Client-Verbindung gibt er einen Socket zurück, über den der Dienst für den Client bereitgestellt wird.
public void close()
schließt den Socket und seine E/A-Ströme
public String toString()
gibt eine Zeichenkette zurück, die den Socket „repräsentiert“
public void close()
schließt den Service-Socket und gibt die damit verbundenen Ressourcen frei

8.3.7.2. Öffnen des Dienstes

Dies erfolgt mit den beiden Konstruktoren:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

port ist der Listening-Port des Dienstes: der Port, an den die Clients ihre Verbindungsanfragen senden. count ist die maximale Größe der Warteschlange des Dienstes (standardmäßig 50), in der die Verbindungsanfragen der Clients gespeichert werden, auf die der Server noch nicht geantwortet hat. Wenn die Warteschlange voll ist, werden eingehende Verbindungsanfragen abgelehnt. Beide Konstruktoren lösen eine Ausnahme aus.

8.3.7.3. Annahme einer Verbindungsanfrage

Wenn ein Client eine Verbindungsanfrage an den Listening-Port des Dienstes sendet, akzeptiert dieser sie mit der Methode:

    public Socket accept();

Diese Methode gibt eine Instanz von Socket zurück: Dies ist der Dienst-Socket, über den der Dienst bereitgestellt wird, meist durch einen anderen Task. Die Methode kann eine Ausnahme auslösen.

8.3.7.4. Lesen/Schreiben über den Dienst-Socket

Da der Service-Socket eine Instanz der Klasse Socket ist, verweisen wir auf die vorangegangenen Abschnitte, in denen dieses Thema behandelt wurde.

8.3.7.5. Den Kunden identifizieren

Sobald der Service-Socket erhalten wurde, kann der Client mit der Methode

    public InetAddress getInetAddress()

der Klasse Socket identifiziert werden. Man erhält dann Zugriff auf die Adresse IP und den Namen des Kunden.

8.3.7.6. Den Dienst schließen

Dies erfolgt mit der Methode

    public void close();

der Klasse ServerSocket. Dadurch werden die belegten Ressourcen, insbesondere der Listening-Port, freigegeben. Die Methode kann eine Ausnahme auslösen.

8.3.7.7. Grundlegende Architektur eines Servers

Aus den bisherigen Ausführungen lässt sich die Grundstruktur eines Servers wie folgt beschreiben:


SocketServer sEcoute=null;
try{
    // Dienst wird gestartet
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);

    // Verarbeitung der Verbindungsanfragen
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // Bearbeitung und Annahme eines Antrags
        sService=sEcoute.accept();

        // Der Dienst wird von einer anderen Aufgabe ausgeführt, an die der Dienst-Socket übergeben wird
        new Service(sService).start();

        // Es wird wieder auf Verbindungsanfragen gewartet
    }
    // Fertig – der Dienst wird beendet
    sEcoute.close();
} catch (Exception e){
    // Die Ausnahme wird behandelt

}

Die Klasse Service ist eine thread, die wie folgt aussehen könnte:


public class Service extends Thread{

    Socket sService;        // Der Service-Socket

    // Konstruktor
    public Service(Socket S){
        sService=S;
    }

// Ausführung
public void run(){
    try{
        // Eingabe- und Ausgabeströme werden erstellt
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);

    // Anfrage-Antwort-Schleife
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // die Anfrage wird gelesen
        demande=in.readLine();

        // die Anfrage wird verarbeitet 


        // die Antwort wird vorbereitet
        réponse=…

        // die Antwort wird gesendet
        out.println(réponse);
    }
    // Fertig
    sService.close();
    } catch(Exception e){
    // die Ausnahme wird behandelt
    ….
    }// try
} // Ausführen

8.4. Anwendungen

8.4.1. Echo-Server

Wir wollen einen Echo-Server schreiben, der aus einem Fenster mit der Nummer DOS mit dem folgenden Befehl gestartet wird:

    java serveurEcho port

Der Server läuft auf dem als Parameter übergebenen Port. Er sendet dem Client lediglich die Anfrage zurück, die dieser an ihn gesendet hat, zusammen mit seiner Identität (IP+Name). Er akzeptiert zwei Verbindungen in seiner Warteschlange. Damit sind alle Bestandteile eines TCP-Servers vorhanden. Das Programm lautet wie folgt:

// Aufruf: serveurEcho Port
// Echo-Server
// sendet die vom Client gesendete Zeile an diesen zurück


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

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // Hauptprogramm
    public static void main (String arg[]){

     // Gibt es ein Argument?
     if(arg.length != 1)
        erreur(syntaxe,1);

     // Dieses Argument muss eine ganze Zahl > 0 sein
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // Der Listening-Socket wird erstellt
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // Weiter
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // Service-Schleife
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // Warten auf einen Client
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // Die Verbindung wird identifiziert
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // Der Dienst wird von einer anderen Aufgabe übernommen
        new traiteClientEcho(service).start();
     }// Ende der while-Schleife
    }// Ende von „main“

// Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifiziert
    private static String identifie(InetAddress Host){
        // Host-Identifikation
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// Ende der Klasse


// erbringt den Dienst für einen Client des Echo-Servers

class traiteClientEcho extends Thread{

    private Socket service;            // Dienst-Socket
    private BufferedReader in;        // Eingabestrom
    private PrintWriter out;            // Ausgabestrom

     // Konstruktor
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // Methode „run“
    public void run(){

         // Erstellung der Eingabe- und Ausgabeströme
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// Ende try
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// Ende try

         // Die Identifikation der Verbindung wird an den Client gesendet
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // Schleife: Lesen der Anfrage/Schreiben der Antwort
        String demande,reponse;
        try{
             // Der Dienst wird beendet, wenn der Client ein Ende-der-Datei-Zeichen sendet
            while ((demande=in.readLine())!=null){
                // Echo der Anfrage
                reponse="["+demande+"]";
                out.println(reponse);
                 // Der Dienst wird beendet, wenn der Client „Ende“ sendet
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// Ende der while-Schleife
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// Ende von „try“

         // Der Socket wird geschlossen
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// Ende try
    }// Ende von „run“

     // Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// Ende des Fehlers

     // Identifizierung
    private String identifie(InetAddress Host){
         // Host-Identifikation
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// Ende der Klasse

Die beiden für den Dienst erforderlichen Klassen wurden in einer einzigen Quelldatei zusammengefasst. Nur eine davon, nämlich die mit der Funktion main, verfügt über das Attribut public. Die Struktur des Servers entspricht der allgemeinen Architektur von TCP-Servern. Es wurde eine Methode (identifie) hinzugefügt, mit der die Verbindung zwischen dem Server und einem Client identifiziert werden kann. Hier sind einige Ergebnisse:

Der Server wird mit dem Befehl gestartet

    java serveurEcho 187

Er zeigt dann im Kontrollfenster die folgende Meldung an:

Serveur d'écho lancé sur le port 187

Um diesen Server zu testen, wird das Programm telnet verwendet, das sowohl unter Unix als auch unter Windows verfügbar ist. Telnet ist ein universeller TCP-Client, der für alle Server geeignet ist, die in ihrer Kommunikation Textzeilen akzeptieren, die mit einem Zeilenendezeichen enden. Dies ist bei unserem Echo-Server der Fall. Wir starten einen ersten telnet-Client unter Windows (in diesem Beispiel Windows 2000), indem wir telnet in ein DOS-Fenster eingeben:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1

Le caractère d'échappement est 'CTRL+$'

Microsoft Telnet> help

Les commandes peuvent être abrégées. Les commandes prises en charge sont :

close           ferme la connexion en cours
display         affiche les paramètres d'opération
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' pour afficher la liste)
status          affiche les informations d'état
unset           annule les options (entrez 'unset ?' pour afficher la liste)
? ou help       affiche des informations d'aide

Microsoft Telnet> set ?
NTLM            Active l'authentification NTLM.
LOCAL_ECHO      Active l'écho local.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF

Microsoft Telnet> set local_echo

Microsoft Telnet> open localhost 187

Das Programm telnet gibt standardmäßig keine Rückmeldung zu den über die Tastatur eingegebenen Befehlen aus. Um diese Rückmeldung zu erhalten, geben Sie folgenden Befehl ein:

Microsoft Telnet> set local_echo

Um eine Verbindung zum Server herzustellen und dabei den Port des Echo-Dienstes (187) sowie die Adresse des Rechners, auf dem er läuft (localhost), anzugeben, gibt man folgenden Befehl ein:

Microsoft Telnet> open localhost 187

Im DOS-Fenster des Clients wird dann folgende Meldung angezeigt:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Im Serverfenster erscheint die Meldung:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

Hier bezeichnen tahe und localhost denselben Rechner. Im Client-Fenster telnet kann man Textzeilen eingeben. Der Server gibt diese als Echo zurück:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

Es ist zu beachten, dass der Client-Port (1059) zwar korrekt erkannt wird, der Dienstport (187) jedoch mit dem Listening-Port (187) identisch ist, was unerwartet ist. Man hätte nämlich erwarten können, den Port des Dienst-Sockets zu erhalten und nicht den Listening-Port. Es müsste überprüft werden, ob unter Unix die gleichen Ergebnisse erzielt werden. Starten wir nun einen zweiten Client: telnet. Das Serverfenster sieht nun wie folgt aus:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

Im Fenster des zweiten Clients können wir ebenfalls Textzeilen eingeben:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

Man sieht also, dass der Echo-Server mehrere Clients gleichzeitig bedienen kann. Die Clients telnet können beendet werden, indem man das DOS-Fenster schließt, in dem sie ausgeführt werden.

8.4.2. Ein Java-Client für den Echo-Server

Im vorherigen Abschnitt haben wir einen telnet-Client verwendet, um den Echo-Dienst zu testen. Nun schreiben wir unseren eigenen Client:

// Aufruf: clientEcho Maschinenport
// Client des Echo-Servers
// sendet Zeilen an den Server, der sie zurückgibt

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

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // Hauptprogramm    
    public static void main (String arg[]){

     // Gibt es zwei Argumente?
     if(arg.length != 2)
        erreur(syntaxe,1);

     // Das erste Argument muss der Name eines existierenden Rechners sein
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // Der Port muss eine ganze Zahl größer als 0 sein
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // Es wird eine Verbindung zum Server hergestellt
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // Die Verbindung wird identifiziert
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // Erstellung des Lesestroms für die über die Tastatur eingegebenen Zeilen
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // Erstellung des Eingabestroms für den Client-Socket
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // Erstellung des Ausgabestroms für den Client-Socket
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // Anfrage-Antwort-Schleife
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // Die vom Server unmittelbar nach der Verbindung gesendete Nachricht wird gelesen 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // Anzeige der Antwort
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // Lesen einer über die Tastatur eingegebenen Zeile
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // Anfrage über das Netzwerk senden
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // Warten auf/Lesen der Antwort
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // Anzeige der Antwort
        System.out.println("Serveur : " +reponse);
         // Ist es fertig?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // Es ist fertig
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// Hand

// Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // Identifizieren
    private static String identifie(InetAddress Host){
        // Host-ID
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// Ende der Klasse

Die Struktur dieses Clients entspricht der allgemeinen Architektur der tcp-Clients. Hier wurden die verschiedenen möglichen Ausnahmen einzeln behandelt, was das Programm schwerfälliger macht. Hier sind die Ergebnisse, die beim Testen dieses Clients erzielt wurden:

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

Die Zeilen, die mit Client beginnen, sind die vom Client gesendeten Zeilen, und diejenigen, die mit Serveur beginnen, sind die vom Server zurückgesendeten Zeilen.

8.4.3. Ein generischer TCP-Client

Viele Dienste, die in den Anfängen des Internets entstanden sind, funktionieren nach dem zuvor beschriebenen Echo-Server-Modell: Der Austausch zwischen Client und Server erfolgt über den Austausch von Textzeilen. Wir werden einen generischen TCP-Client schreiben, der wie folgt gestartet wird: java cltTCPgenerique Server Port

Dieser Client TCP stellt eine Verbindung zum Port port des Servers serveur her. Anschließend erstellt er zwei Threads:

  1. einen Thread, der die über die Tastatur eingegebenen Befehle liest und an den Server sendet
  2. ein Thread, der die Antworten des Servers liest und auf dem Bildschirm anzeigt

Warum zwei Threads, obwohl in der vorherigen Anwendung kein Bedarf dafür bestand? In dieser war das Dialogprotokoll bekannt: Der Client sendete eine einzige Zeile und der Server antwortete mit einer einzigen Zeile. Jeder Dienst hat sein eigenes Protokoll, und es gibt auch folgende Situationen:

  • Der Client muss mehrere Textzeilen senden, bevor er eine Antwort erhält
  • die Antwort eines Servers kann mehrere Textzeilen umfassen

Daher ist die Schleife „Senden einer einzelnen Zeile an den Server – Empfang einer einzelnen Zeile vom Server“ nicht immer geeignet. Wir werden daher zwei voneinander getrennte Schleifen erstellen:

  • eine Schleife zum Einlesen der über die Tastatur eingegebenen Befehle, die an den Server gesendet werden sollen. Der Benutzer signalisiert das Ende der Befehle mit dem Schlüsselwort fin.
  • eine Schleife zum Empfangen und Anzeigen der Antworten des Servers. Dabei handelt es sich um eine Endlosschleife, die nur durch das Schließen der Netzwerkverbindung durch den Server oder durch den Benutzer über die Tastatur unterbrochen wird, indem dieser den Befehl fin eingibt.

Um diese beiden Schleifen voneinander zu trennen, benötigen wir zwei unabhängige Threads. Betrachten wir ein Ausführungsbeispiel, bei dem sich unser generischer TCP-Client mit einem Dienst namens SMTP (SendMail Transfer Protocol) verbindet. Dieser Dienst ist für die Weiterleitung von E-Mails an ihre Empfänger zuständig. Er läuft auf Port 25 und verwendet ein Dialogprotokoll, bei dem Textzeilen ausgetauscht werden.


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Erläutern wir diesen Client-Server-Austausch:

  • Der Dienst SMTP sendet eine Begrüßungsnachricht, wenn sich ein Client bei ihm anmeldet:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • Einige Dienste verfügen über den Befehl help, der Hinweise zu den mit dem Dienst verwendbaren Befehlen gibt. Hier ist dies nicht der Fall. Die im Beispiel verwendeten Befehle SMTP lauten wie folgt:
    • mail from: expéditeur, um die E-Mail-Adresse des Absenders der Nachricht anzugeben
    • rcpt to: destinataire, um die E-Mail-Adresse des Empfängers der Nachricht anzugeben. Bei mehreren Empfängern wird der Befehl „rcpt to:“ so oft wie nötig für jeden einzelnen Empfänger wiederholt.
    • data, das dem Server SMTP signalisiert, dass die Nachricht gesendet wird. Wie in der Antwort des Servers angegeben, handelt es sich dabei um eine Folge von Zeilen, die mit einer Zeile endet, die ausschließlich das Zeichen „.“ enthält. Eine Nachricht kann Kopfzeilen enthalten, die durch eine Leerzeile vom Nachrichtentext getrennt sind. In unserem Beispiel haben wir einen Betreff mit dem Schlüsselwort Subject: angegeben
  • Sobald die Nachricht gesendet wurde, kann man dem Server mit dem Befehl „quit“ mitteilen, dass man fertig ist. Der Server schließt daraufhin die Netzwerkverbindung. Der Lesethread kann dieses Ereignis erkennen und sich beenden.
  • Der Benutzer gibt dann „fin“ über die Tastatur ein, um auch den Thread zum Lesen der über die Tastatur eingegebenen Befehle zu beenden.

Wenn man die empfangene E-Mail überprüft, sieht man Folgendes (Outlook):

Image

Es ist zu beachten, dass der Dienst „SMTP“ nicht erkennen kann, ob ein Absender gültig ist oder nicht. Daher darf man dem Feld „from“ einer Nachricht niemals vertrauen. In diesem Fall existierte der Absender „machin@univ-angers.fr“ nicht.

Dieser generische TCP-Client ermöglicht es uns, das Kommunikationsprotokoll von Internetdiensten zu ermitteln und auf dieser Grundlage spezialisierte Klassen für Clients dieser Dienste zu erstellen. Sehen wir uns das Kommunikationsprotokoll des Dienstes POP (Post Office Protocol) an, mit dem man seine auf einem Server gespeicherten E-Mails abrufen kann. Er läuft auf Port 110.


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Die wichtigsten Befehle lauten wie folgt:

  • user login, wobei man seinen Benutzernamen auf dem Rechner angibt, auf dem sich die E-Mails befinden
  • Passwort password eingeben, wobei das zum vorherigen Login gehörende Passwort angegeben wird
  • list, um die Liste der Nachrichten in Form von Nummer und Größe in Byte zu erhalten
  • retr i, um die Nachricht Nr. i zu lesen
  • quit, um den Dialog zu beenden.

Sehen wir uns nun das Dialogprotokoll zwischen einem Client und einem Webserver an, der in der Regel auf Port 80 läuft:


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Ein Webclient sendet seine Befehle nach folgendem Schema an den Server:

commande1
commande2
...
commanden
[ligne vide]

Erst nach Erhalt der leeren Zeile antwortet der Webserver. In diesem Beispiel haben wir nur einen Befehl verwendet:

GET /index.html HTTP/1.0

der den Server nach URL /index.html abfragt und angibt, dass er mit dem Protokoll HTTP Version 1.0 arbeitet. Die aktuellste Version dieses Protokolls ist 1.1. Das Beispiel zeigt, dass der Server mit dem Inhalt der Datei index.html geantwortet und anschließend die Verbindung geschlossen hat, da der Thread zum Lesen der Antworten beendet wurde. Vor dem Senden des Inhalts der Datei index.html hat der Webserver eine Reihe von Headern gesendet, die mit einer Leerzeile endeten:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

Die Zeile <html> ist die erste Zeile der Datei /index.html. Das Vorstehende wird als HTTP-Header (HyperText-Übertragungsprotokoll) bezeichnet. Wir werden hier nicht näher auf diese Header eingehen, aber man sollte bedenken, dass unser generischer Client Zugriff darauf gewährt, was zum Verständnis hilfreich sein kann. Die erste Zeile zum Beispiel:

<-- HTTP/1.1 200 OK

gibt an, dass der kontaktierte Webserver das Protokoll HTTP/1.1 versteht und die angeforderte Datei erfolgreich gefunden hat (200 OK), wobei 200 ein HTTP-Antwortcode ist. Die Zeilen

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

teilen dem Client mit, dass er 11251 Bytes erhalten wird, die den Text HTML (HyperText Markup Language) darstellen, und dass die Verbindung nach Abschluss der Übertragung geschlossen wird.

Wir haben hier also einen sehr praktischen TCP-Client. Er kann zweifellos weniger als das zuvor verwendete Programm telnet, aber es war interessant, ihn selbst zu schreiben. Das Programm für den generischen TCP-Client lautet wie folgt:

// importierte Pakete
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // erhält als Parameter die Merkmale eines Dienstes in der Form
     // Server-Port
     // stellt eine Verbindung zum Dienst her
     // erstellt einen Thread zum Einlesen von Befehlen, die über die Tastatur eingegeben werden
     // diese werden an den Server gesendet
     // erstellt einen Thread zum Lesen der Antworten vom Server
     // diese werden auf dem Bildschirm angezeigt
     // Das Ganze endet mit dem über die Tastatur eingegebenen Befehl „fin“

   // Instanzvariable
  private static Socket client;

    public static void main(String[] args){

         // Syntax
        final String syntaxe="pg serveur port";

         // Anzahl der Argumente
        if(args.length != 2)
            erreur(syntaxe,1);

         // Hier wird der Servername notiert
        String serveur=args[0];

         // Der Port muss eine ganze Zahl > 0 sein
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // Es können Probleme auftreten
        try{
             // Verbindung zum Dienst herstellen
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // Fehler
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // Ende
            return;
        }//catch

         // Lese-/Schreib-Threads werden erstellt
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // Ende des Haupt-Threads
        return;
    }// main

     // Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
         // Fehleranzeige
        System.err.println(msg);
         // Beenden mit Fehler
        System.exit(exitCode);
    }//Fehler
}//Klasse  

class ClientSend extends Thread {
    // Klasse zum Lesen von über die Tastatur eingegebenen Befehlen
     // und diese über einen als Parameter übergebenen TCP-Client an einen Server zu senden

    private Socket client;    // der TCP-Client

     // Konstruktor
    public ClientSend(Socket client){
         // Hier wird der TCP-Client angegeben
        this.client=client;
    }//Konstruktor

     // Run-Methode des Threads
    public void run(){

        // lokale Daten
        PrintWriter OUT=null;            // Netzwerk-Schreibstrom
    BufferedReader IN=null;        // Tastatur-Datenstrom
        String commande=null;            // von der Tastatur gelesener Befehl

         // Fehlerbehandlung
        try{
             // Erstellung des Netzwerk-Schreibstroms
            OUT=new PrintWriter(client.getOutputStream(),true);
      // Erstellung des Tastatureingabestroms
      IN=new BufferedReader(new InputStreamReader(System.in));
            // Schleife für die Eingabe und das Senden von Befehlen
            System.out.println("Commandes : ");
            while(true){
                 // Einlesen des über die Tastatur eingegebenen Befehls
                commande=IN.readLine().trim();
                // Fertig?
                if (commande.toLowerCase().equals("fin")) break;
                // Befehl an den Server senden
                OUT.println(commande);
                 // Nächster Befehl
            }//while
        }catch(Exception ex){
             // Fehler
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // Ende – Streams werden geschlossen
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // das Ende des Threads wird gemeldet
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//run
}//Klasse

class ClientReceive extends Thread{
    // Klasse, die für das Einlesen der Textzeilen zuständig ist, die für einen 
     // als Parameter übergebenen TCP-Client bestimmt sind

    private Socket client;    // der TCP-Client

     // Konstruktor
    public ClientReceive(Socket client){
         // Der TCP-Client wird notiert
        this.client=client;
    }//Konstruktor

     // Run-Methode des Threads
    public void run(){

        // lokale Daten
        BufferedReader IN=null;        // Netzwerk-Lese-Stream
        String réponse=null;        // Serverantwort

         // Fehlerbehandlung
        try{
             // Erstellung des Netzwerk-Lese-Streams
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // Schleife zum Einlesen von Textzeilen aus dem Datenstrom IN
            while(true){
                 // Lesen des Netzwerkstroms
                réponse=IN.readLine();
                 // Datenstrom geschlossen?
                if(réponse==null) break;
                // Anzeige
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // Fehler
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // Ende – die Datenströme werden geschlossen
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // das Ende des Threads wird gemeldet
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//run
}//Klasse

8.4.4. Ein generischer TCP-Server

Nun wenden wir uns einem Server zu,

  • , der die von seinen Clients gesendeten Befehle auf dem Bildschirm anzeigt
  • und ihnen als Antwort die Textzeilen zurücksendet, die ein Benutzer über die Tastatur eingegeben hat. Letzterer fungiert also als Server.

Das Programm wird gestartet mit: java serveurTCPgenerique portEcoute, wobei portEcoute der Port ist, an den sich die Clients verbinden müssen. Die Bedienung der Clients wird von zwei Threads übernommen:

  • ein Thread, der sich ausschließlich dem Einlesen der vom Client gesendeten Textzeilen widmet
  • ein Thread, der sich ausschließlich dem Einlesen der vom Benutzer über die Tastatur eingegebenen Antworten widmet. Dieser signalisiert mit dem Befehl „fin“, dass er die Verbindung zum Client beendet.

Der Server erstellt zwei Threads pro Client. Bei n Clients sind somit 2n Threads gleichzeitig aktiv. Der Server selbst wird niemals beendet, es sei denn, der Benutzer drückt die Tastenkombination Strg+C. Sehen wir uns einige Beispiele an.

Der Server wird auf Port 100 gestartet, und wir verwenden den generischen Client, um mit ihm zu kommunizieren. Das Client-Fenster sieht wie folgt aus:

E:\data\serge\MSNET\c#\Netzwerk\generischer TCP-Client> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

Zeilen, die mit <-- beginnen, wurden vom Server an den Client gesendet, die anderen vom Client an den Server. Das Serverfenster sieht wie folgt aus:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

Die Zeilen, die mit <-- beginnen, sind diejenigen, die vom Client an den Server gesendet wurden. Die Zeilen N: sind die Zeilen, die vom Server an den Client Nr. N gesendet wurden. Der oben genannte Server ist noch aktiv, während Client 1 beendet wurde. Wir starten einen zweiten Client für denselben Server:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

Das Serverfenster sieht dann wie folgt aus:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

Simulieren wir nun einen Webserver, indem wir unseren generischen Server auf Port 88 starten:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

Öffnen wir nun einen Browser und rufen wir die Seite URL http://localhost:88/exemple.html auf. Der Browser stellt dann eine Verbindung zum Port 88 des Rechners localhost her und fordert anschließend die Seite /exemple.html an:

Image

Sehen wir uns nun das Fenster unseres Servers an:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

So entdecken wir die vom Browser gesendeten Header HTTP. Dies ermöglicht es uns, das Protokoll HTTP nach und nach zu entschlüsseln. In einem früheren Beispiel hatten wir einen Web-Client erstellt, der lediglich den Befehl GET sendete. Das hatte ausgereicht. Hier sehen wir, dass der Browser weitere Informationen an den Server sendet. Diese dienen dazu, dem Server mitzuteilen, um welche Art von Client es sich handelt. Wir sehen auch, dass die Header HTTP mit einer leeren Zeile enden.

Erstellen wir eine Antwort für unseren Client. Der Benutzer an der Tastatur ist hier der eigentliche Server und kann eine Antwort manuell erstellen. Erinnern wir uns an die Antwort eines Webservers aus einem früheren Beispiel:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

Versuchen wir, eine ähnliche Antwort zu geben:

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

Zeilen, die mit „2:“ beginnen, werden vom Server an Client Nr. 2 gesendet. Der Befehl fin beendet die Verbindung vom Server zum Client. Wir haben uns in unserer Antwort auf die folgenden HTTP-Header beschränkt:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

Wir geben die Größe der Datei, die wir senden werden (Content-Length), nicht an, sondern geben lediglich an, dass wir die Verbindung (Connection: close) nach dem Senden dieser Datei schließen werden. Das reicht für den Browser aus. Sobald der Browser sieht, dass die Verbindung geschlossen wurde, weiß er, dass die Antwort des Servers abgeschlossen ist, und zeigt die Seite HTML an, die ihm gesendet wurde. Diese lautet wie folgt:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

Anschließend beendet der Benutzer die Verbindung zum Client, indem er den Befehl fin eingibt. Der Browser erkennt daraufhin, dass die Antwort des Servers abgeschlossen ist, und kann sie nun anzeigen:

Image

Wenn man oben den Befehl View/Source eingibt, um zu sehen, was der Browser empfangen hat, erhält man:

Image

Das ist genau das, was vom generischen Server gesendet wurde.

Der Code des generischen TCP-Servers lautet wie folgt:

// Pakete
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // Hauptprogramm
    public static void main (String[] args){

    // empfängt Client-Anfragen über den Listening-Port
     // erstellt einen Thread zum Lesen der Client-Anfragen
     // diese werden auf dem Bildschirm angezeigt
     // erstellt einen Thread zum Einlesen von über die Tastatur eingegebenen Befehlen
     // Diese werden als Antwort an den Kunden gesendet
     // Das Ganze endet mit dem Befehl „fin“, der über die Tastatur eingegeben wird

    final String syntaxe="Syntaxe : pg port";
   // Instanzvariable
         // Gibt es ein Argument?
     if(args.length != 1)
        erreur(syntaxe,1);

         // Der Port muss eine ganze Zahl > 0 sein
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // Der Listening-Dienst wird erstellt
    ServerSocket ecoute=null;
    int nbClients=0;    // Anzahl der bedienten Clients
        try{
             // Der Dienst wird erstellt
            ecoute=new ServerSocket(port);
             // Weiterverfolgung
            System.out.println("Serveur générique lancé sur le port " + port);

             // Schleife für die Kundenbetreuung
            Socket client=null;
            while (true){ // Endlosschleife – wird mit Strg+C beendet
                 // Warten auf einen Client
                client=ecoute.accept();

                 // Der Dienst wird von separaten Threads ausgeführt
                nbClients++;

                 // Lese-/Schreib-Threads werden erstellt
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // Es wird wieder auf Anfragen gewartet
            }// Ende der while-Schleife
        }catch(Exception ex){
             // Der Fehler wird gemeldet
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// Ende von „main“

     // Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
         // Fehleranzeige
        System.err.println(msg);
         // Beenden mit Fehler
        System.exit(exitCode);
    }//Fehler
}//Klasse

class ServeurSend extends Thread{
    // Klasse zum Einlesen von über die Tastatur eingegebenen Antworten
     // und diese über einen an den Konstruktor übergebenen TCP-Client an einen Client zu senden

    Socket client;    // der TCP-Client
    int numClient;        // Client-Nr.

     // Konstruktor
    public ServeurSend(Socket client, int numClient){
         // wird der TCP-Client notiert
        this.client=client;
         // sowie dessen Nummer
        this.numClient=numClient;
    }//Hersteller

     // Run-Methode des Threads
    public void run(){

        // lokale Daten
        PrintWriter OUT=null;        // Netzwerk-Schreibstrom
        String réponse=null;        // von der Tastatur gelesene Antwort
    BufferedReader IN=null;    // Tastatur-Datenstrom

        // Überwachung
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // Fehlerbehandlung
        try{
             // Erstellung des Netzwerk-Schreibstroms
            OUT=new PrintWriter(client.getOutputStream(),true);
      // Erstellung des Tastatur-Datenstroms
      IN=new BufferedReader(new InputStreamReader(System.in));
            // Schleife „Befehlseingabe und -übertragung“
            while(true){
                 // Kundenidentifikation
                System.out.print("--> " + numClient + " : ");
                 // Auslesen der über die Tastatur eingegebenen Antwort
                réponse=IN.readLine().trim();
                // Fertig?
                if (réponse.toLowerCase().equals("fin")) break;
                // Antwort an den Server senden
                OUT.println(réponse);
                 // Nächste Antwort
            }//while
        }catch(Exception ex){
             // Fehler
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // Ende – Streams werden geschlossen
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // das Ende des Threads wird gemeldet
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//run
}//Klasse

class ServeurReceive extends Thread{
    // Klasse, die für das Einlesen der an den Server gesendeten Textzeilen zuständig ist 
     // über einen dem Konstruktor übergebenen TCP-Client

    Socket client;    // der TCP-Client
    int numClient;        // Client-Nr.

     // Konstruktor
    public ServeurReceive(Socket client, int numClient){
         // der TCP-Client wird notiert
        this.client=client;
         // sowie dessen Nummer
        this.numClient=numClient;
    }//Hersteller

     // Run-Methode des Threads
    public void run(){

        // lokale Daten
        BufferedReader IN=null;        // Netzwerk-Lese-Stream
        String réponse=null;        // Serverantwort

         // Überwachung
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // Fehlerbehandlung
        try{
             // Erstellung des Netzwerk-Lese-Streams
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // Schleife zum Einlesen von Textzeilen aus dem Datenstrom IN
            while(true){
                 // Netzwerk-Stream lesen
                réponse=IN.readLine();
                 // Strom geschlossen?
                if(réponse==null) break;
                // Anzeige
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // Fehler
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // Ende – die Datenströme werden geschlossen
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // das Ende des Threads wird gemeldet
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//run
}//Klasse

8.4.5. Ein Web-Client

Im vorherigen Beispiel haben wir einige der HTTP-Header gesehen, die ein Browser gesendet hat:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

Wir werden einen Web-Client schreiben, dem ein URL als Parameter übergeben wird und der den Inhalt dieses URL auf dem Bildschirm anzeigt. Wir gehen davon aus, dass der für die URL kontaktierte Webserver das Protokoll HTTP 1.1 unterstützt. Von den oben genannten Headern verwenden wir nur die folgenden:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • Der erste Header gibt an, welche Seite wir
  • der zweite, welchen Server wir abfragen
  • die dritte, dass der Server die Verbindung nach der Antwort trennen soll.

Wenn wir oben GET durch HEAD ersetzen, sendet uns der Server nur die Header HTTP und nicht die Seite HTML.

Unser Webclient wird wie folgt aufgerufen: java clientweb URL cmd, wobei URL diegewünschte URL und „cmd“ eines der beiden Schlüsselwörter GET oder HEAD, um anzugeben, ob nur die Header (HEAD) oder auch der Inhalt der Seite (GET) haben möchten. Schauen wir uns ein erstes Beispiel an. Wir starten den Server IIS und anschließend den Webclient auf demselben Rechner:

dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

Die Antwort

HTTP/1.1 302 Object moved

bedeutet, dass die angeforderte Seite an einen anderen Ort verschoben wurde (also von URL). Die neue Adresse URL wird durch den Header „Location:“ angegeben:

Location: /IISSamples/Default/welcome.htm

Wenn wir im Aufruf des Web-Clients GET anstelle von HEAD verwenden:

dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private

<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

Wir erhalten das gleiche Ergebnis wie mit HEAD, zusätzlich den Hauptteil der Seite HTML. Das Programm lautet wie folgt:

// importierte Pakete
import java.io.*;
import java.net.*;

public class clientweb{

    // fordert eine URL an
     // zeigt deren Inhalt auf dem Bildschirm an

    public static void main(String[] args){
        // Syntax
        final String syntaxe="pg URI GET/HEAD";

        // Anzahl der Argumente
        if(args.length != 2)
            erreur(syntaxe,1);

         // Es wird die angeforderte URI vermerkt
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // Überprüfung der Gültigkeit von URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI ist falsch
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//Abfangen
         // Überprüfung der Bestellung
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // Fehlerhafte Bestellung
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // Die relevanten Informationen werden aus dem URL extrahiert
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // Es kann weitergearbeitet werden
        Socket  client=null;                        // der Kunde
        BufferedReader IN=null;                    // der Lesestrom des Kunden
        PrintWriter OUT=null;                        // der Schreib-Stream des Kunden
        String réponse=null;                        // Antwort des Servers
        try{
             // Verbindung zum Server herstellen
            client=new Socket(host,port);

            // Die Ein- und Ausgabeströme des Kunden werden erstellt: TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // Anforderung von URL – Senden der Header HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // die Antwort wird gelesen
            while((réponse=IN.readLine())!=null){
                 // die Antwort wird verarbeitet
                System.out.println(réponse);
            }//while
             // ist der Vorgang abgeschlossen
            client.close();
        } catch(Exception e){
            // die Ausnahme wird behandelt
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // Fehleranzeige
    public static void erreur(String msg, int exitCode){
         // Fehleranzeige
        System.err.println(msg);
         // Beenden mit Fehler
        System.exit(exitCode);
    }//Fehler
}//Klasse

Die einzige Neuerung in diesem Programm ist die Verwendung der Klasse URL. Das Programm erhält einen URL (Uniform Resource Locator) oder einen URI (Uniform Resource Identifier) in der Form http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Die Klasse URL ermöglicht es uns, die Zeichenkette des URL in ihre verschiedenen Elemente zu zerlegen. Ein Objekt URL wird aus der als Parameter übergebenen Zeichenkette URLstring erstellt:

         // Gültigkeitsprüfung des URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI falsch
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//Abfangen

Wenn die als Parameter übergebene Zeichenfolge URL kein gültiges URL ist (fehlendes Protokoll, fehlender Server usw.), wird eine Ausnahme ausgelöst. Auf diese Weise können wir die Gültigkeit des empfangenen Parameters überprüfen. Sobald das Objekt URL erstellt wurde, haben wir Zugriff auf dessen verschiedene Elemente. Wenn also das Objekt url aus dem vorherigen Code anhand der Zeichenkette

http://Server:Port/cheminPageHTML?param1=val1;param2=val2;... 

erstellt wurde, ergibt sich Folgendes:

url.getHost() = serveur

url.getPort()=port oder -1, wenn der Port nicht angegeben ist

url.getPath()=cheminPageHTML oder die leere Zeichenfolge, wenn kein Pfad vorhanden ist

url.getQuery()=param1=val1;param2=val2;... oder null, wenn keine Anfrage vorliegt

uri.getProtocol()=http

8.4.6. Web-Client, der Weiterleitungen verwaltet

Der vorherige Web-Client verarbeitet keine mögliche Weiterleitung der von ihm angeforderten URL. Der folgende Client verarbeitet sie.

  1. Er liest die erste Zeile der vom Server gesendeten Header HTTP, um zu überprüfen, ob dort die Zeichenfolge „302 Object moved“ enthalten ist, die eine Weiterleitung signalisiert
  2. Er liest die folgenden Header. Liegt eine Weiterleitung vor, sucht er nach der Zeile „Location: url“, die die neue URL der angeforderten Seite angibt, und notiert sich diese URL.
  3. Er zeigt den Rest der Antwort des Servers an. Liegt eine Weiterleitung vor, werden die Schritte 1 bis 3 mit der neuen URL wiederholt. Das Programm akzeptiert nicht mehr als eine Weiterleitung. Diese Begrenzung wird durch eine Konstante festgelegt, die geändert werden kann.

Hier ein Beispiel:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

Das Programm sieht wie folgt aus:

// importierte Pakete
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // fordert ein URL an
     // zeigt deren Inhalt auf dem Bildschirm an

    public static void main(String[] args){
        // Syntax
        final String syntaxe="pg URL GET/HEAD";

        // Anzahl der Argumente
        if(args.length != 2)
            erreur(syntaxe,1);

         // die angeforderte URI wird notiert
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // Überprüfung der Gültigkeit des URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI ist falsch
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//Abfangen
         // Überprüfung der Bestellung
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // Fehlerhafte Bestellung
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // Es kann weitergearbeitet werden
        Socket  client=null;                        // der Kunde
        BufferedReader IN=null;                    // der Lesestrom des Kunden
        PrintWriter OUT=null;                        // Schreib-Stream des Clients
        String réponse=null;                        // Antwort des Servers
        final int nbRedirsMax=1;                // Es wird nicht mehr als eine Weiterleitung akzeptiert
        int nbRedirs=0;                                    // Anzahl der laufenden Weiterleitungen
        String premièreLigne;                        // erste Zeile der Antwort
        boolean redir=false;                        // gibt an, ob eine Weiterleitung vorliegt oder nicht
        String locationString="";                // die Zeichenfolge URL einer möglichen Weiterleitung

         // Regulärer Ausdruck zum Auffinden einer Weiterleitung mit der Zeichenfolge URL
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // Fehlerbehandlung
        try{
             // Es können mehrere URL abgefragt werden, falls Umleitungen vorliegen
            while(nbRedirs<=nbRedirsMax){

                // Die relevanten Informationen werden aus dem URL extrahiert
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // Man stellt eine Verbindung zum Server her
                client=new Socket(host,port);

                // Es werden die Ein- und Ausgangsströme des Kunden TCP erstellt
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // URL wird angefordert – Senden der Header HTTP
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // Die erste Zeile der Antwort wird gelesen
                premièreLigne=IN.readLine();
                 // Bildschirmausgabe
                System.out.println(premièreLigne);

                // Weiterleitung?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // Es liegt eine Umleitung vor
                    redir=true;
                    nbRedirs++;
                }//if

                // die folgenden Header HTTP, bis die leere Zeile gefunden wird, die das Ende der Header signalisiert
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // die Antwort wird angezeigt
                    System.out.println(réponse);
                     // Wenn eine Weiterleitung vorliegt, wird nach dem Header „Location“ gesucht
                    if(redir && ! locationFound){
                        // die Zeile wird mit dem relationalen Ausdruck „location“ verglichen
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // Wurde ein Treffer gefunden, wird der Umleitungs-URL notiert
                            locationString=résultat.group(1);             
                             // Es wird vermerkt, dass ein Treffer gefunden wurde
                            locationFound=true;
                        }//if
                    }//if
                    // folgender Header
                }//, während

                 // folgende Zeilen der Antwort
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // die Antwort wird angezeigt
                    System.out.println(réponse);
                }//while
                 // die Verbindung wird geschlossen
                client.close();
                 // Sind wir fertig?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // Es muss eine Weiterleitung durchgeführt werden – die neue wird erstellt URL
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // Sendungsverfolgung
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//während
        } catch(Exception e){
            // die Ausnahme wird behandelt
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // Fehleranzeige
    public static void erreur(String msg, int exitCode){
         // Fehleranzeige
        System.err.println(msg);
         // Beenden mit Fehler
        System.exit(exitCode);
    }//Fehler
}//Klasse

8.4.7. Steuerberechnungsserver

Wir greifen die Übung IMPOTS wieder auf, die bereits in verschiedenen Formen behandelt wurde. Zur Erinnerung: Die letzte Version sah wie folgt aus:

Es wurde eine Basisklasse „Steuern“ angelegt. Ihre Attribute sind drei Arrays mit Zahlen:

public class impots{

  // die für die Steuerberechnung erforderlichen Daten
   // stammen aus einer externen Quelle

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // Feld „Hersteller“ leer
  protected impots(){}

   // Konstruktor
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

Die Klasse „impots“ verfügt über zwei Konstruktoren:

  • einen Konstruktor, dem die drei für die Steuerberechnung erforderlichen Daten-Arrays übergeben werden
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • einen Konstruktor ohne Parameter, der ausschließlich von untergeordneten Klassen verwendet werden kann
  protected impots(){}

Von dieser Klasse wurde die Klasse „impotsJDBC“ abgeleitet, mit der die drei Arrays „limites“, „coeffR“ und „coeffN“ anhand des Inhalts einer Datenbank gefüllt werden können:

public class impotsJDBC extends impots{
  // Hinzufügen eines Konstruktors, der die Erstellung ermöglicht
   // die Tabellen „limites“, „coeffr“ und „coeffn“ aus der Tabelle
   // Steuerdatenbank
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: Name der Datenbank DSN
     // userIMPOTS, mdpIMPOTS: Benutzername/Passwort für den Zugriff auf die Datenbank

Es wurde eine grafische Anwendung geschrieben. Die Anwendung verwendete ein Objekt der Klasse impotsJDBC. Die Anwendung und dieses Objekt befanden sich auf demselben Rechner. Wir beabsichtigen, das Testprogramm und das Objekt impotsJDBC auf unterschiedlichen Rechnern zu installieren. Wir erhalten eine Client-Server-Anwendung, bei der das entfernte Objekt impotsJDBC als Server fungiert. Die neue Klasse heißt ServeurImpots und ist von der Klasse impotsJDBC abgeleitet:

// importierte Pakete
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // Attribute
    int portEcoute;                // der Port, auf dem Client-Anfragen abgehört werden
    boolean actif;                // Serverstatus

     // Hersteller
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // übergeordneter Konstruktor
        super(DSNimpots, USERimpots, MDPimpots);
         // der Listening-Port wird vermerkt
        this.portEcoute=portEcoute;
         // derzeit inaktiv
        actif=false;
         // erstellt und startet einen Thread zum Einlesen der über die Tastatur eingegebenen Befehle
         // Der Server wird anhand dieser Befehle verwaltet
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

Der einzige neue Parameter im Konstruktor ist der Port, auf dem Client-Anfragen abgehört werden. Die übrigen Parameter werden direkt an die Basisklasse impotsJDBC übergeben. Der Steuerserver wird über Tastatureingaben gesteuert. Daher wird ein Thread erstellt, um diese Befehle zu lesen. Es gibt zwei mögliche Befehle: start zum Starten des Dienstes und stop zum endgültigen Beenden des Dienstes. Die Methode admin, die diese Befehle verarbeitet, lautet wie folgt:

    public void admin() throws IOException{
        // liest die über die Tastatur eingegebenen Server-Verwaltungsbefehle
         // in einer Endlosschleife
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // fordert zur Eingabe auf
            System.out.print("Serveur d'impôts>");
             // Befehl einlesen
            commande=IN.readLine().trim().toLowerCase();
             // Befehl ausführen
            if(commande.equals("start")){
                // aktiv?
                if(actif){
                     //Fehler
                    System.out.println("Le serveur est déjà actif");
                     // Weiter
                    continue;
                }//if
                 // Listening-Dienst wird erstellt und gestartet
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // Ende aller Ausführungsthreads
                System.exit(0);
            }//if
            else {
                // Fehler
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//Admin

Wenn der über die Tastatur eingegebene Befehl „start“ lautet, wird ein Thread gestartet, der auf Client-Anfragen wartet. Wenn der eingegebene Befehl „stop“ lautet, werden alle Threads beendet. Der Listening-Thread führt die Methode „ecoute“ aus:

    public void ecoute(){
         // Thread zum Abhören von Client-Anfragen
         // Der Listening-Dienst wird erstellt
        ServerSocket ecoute=null;
        try{
            // Der Dienst wird erstellt
            ecoute=new ServerSocket(portEcoute);
             // Weiterverfolgung
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // Dienstschleife
            Socket liaisonClient=null;
            while (true){ // Endlosschleife
                 // Warten auf einen Client
                liaisonClient=ecoute.accept();

                 // Der Dienst wird von einer anderen Aufgabe übernommen
                new traiteClientImpots(liaisonClient,this).start();

                 // Es wird wieder auf Anfragen gewartet
            }// Ende der while-Schleife
        }catch(Exception ex){
             // Der Fehler wird gemeldet
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//Abhör-Thread

Es handelt sich um einen klassischen TCP-Server, der auf dem Port portEcoute lauscht. Die Anfragen der Clients werden von der Methode run des Threads traiteCientImpots verarbeitet, an dessen Konstruktor zwei Parameter übergeben werden:

  1. das Socket-Objekt liaisonClient, über das der Client erreicht werden kann
  2. das Objekt impotsJDBC this, das den Zugriff auf die Methode this.calculer zur Steuerberechnung ermöglicht.
// -------------------------------------------------------
// stellt den Dienst für einen Kunden des Steuerservers bereit

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // Verbindung zum Kunden
    private BufferedReader IN;                // Eingangsdatenfluss
    private PrintWriter OUT;                    // Ausgangsstrom
    private impotsJDBC objImpots;            // Objekt „Steuer“

         // Konstruktor
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//Hersteller

Die Methode run verarbeitet Kundenanfragen. Dabei handelt es sich um Textzeilen, die zwei Formen annehmen können:

  1. Berechnung verheiratet (j/n) nbEnfants salaireAnnuel
  2. fincalculs

Form 1 ermöglicht die Berechnung einer Steuer, Form 2 beendet die Verbindung zwischen Client und Server.

     // Ausführungsmethode
    public void run(){
         // erbringt die Dienstleistung für den Kunden
        try{
             // Eingangsfluss
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // Ausgabestrom
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // Versenden einer Willkommensnachricht an den Kunden
            OUT.println("Bienvenue sur le serveur d'impôts");

             // Schleife zum Lesen der Anfrage und Schreiben der Antwort
            String demande=null;
            String[] champs=null;    // die Elemente der Anfrage
            String commande=null;    // Der Befehl des Kunden: Berechnung oder Abschluss der Berechnungen
            while ((demande=IN.readLine())!=null){
                 // Die Anfrage wird in Felder zerlegt
                champs=demande.trim().toLowerCase().split("\\s+");
                 // Zwei akzeptierte Anfragen: Berechnung und Abschluss der Berechnungen
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // Kundenfehler
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // nächste Bestellung
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // Abschiedsmeldung an den Kunden
                    OUT.println("Au revoir...");
                     // Freigabe der Ressourcen
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // Ende
                    return;
                }//if
                 //nächste Anfrage
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// Ende try
    }// Ende Run

Die Steuerberechnung erfolgt über die Methode calculerImpôt, die als Parameter das Feldarray der vom Kunden gestellten Anfrage erhält. Die Gültigkeit der Anfrage wird überprüft und gegebenenfalls die Steuer berechnet und an den Kunden zurückgesendet.

     // Steuerberechnung
    public void calculerImpôt(String[] champs){
         // bearbeitet den Antrag: Berechnung für Verheiratete nbEnfants salaireAnnuel
         // in Felder in der Feldtabelle aufgeschlüsselt

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // Gültigkeit der Argumente
        try{
             // Es sind mindestens 4 Felder erforderlich
            if(champs.length!=4) throw new Exception();
            // verheiratet
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // Kinder
            nbEnfants=Integer.parseInt(champs[2]);
             // Gehalt
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // Formatfehler
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // fertig
            return;
        }//if
         // Die Steuer kann berechnet werden
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // die Antwort wird an den Kunden gesendet
        OUT.println(""+impot);
    }//berechnen

Ein Testprogramm könnte wie folgt aussehen:

// Aufruf: serveurImpots Port dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // Hauptprogramm
    public static void main (String[] args){

        // Es sind 4 Argumente erforderlich
        if(args.length != 4)
            erreur(syntaxe,1);

         // Der Port muss eine ganze Zahl > 0 sein
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // Der Steuer-Server wird erstellt
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //Fehler
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // Anzeige der Fehler
    public static void erreur(String msg, int exitCode){
         // Fehleranzeige
        System.err.println(msg);
         // Beenden mit Fehler
        System.exit(exitCode);
    }//Fehler
}// Ende der Klasse

Wir übergeben die für die Erstellung eines Objekts ServeurImpots erforderlichen Daten an das Testprogramm, und dieses erstellt daraufhin das Objekt.

Versuchen wir einen ersten Lauf:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

Der Befehl

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

erstellt ein Objekt ServeurImpots, das noch nicht auf Client-Anfragen wartet. Erst der über die Tastatur eingegebene Befehl „start“ löst diesen Empfang aus. Der Befehl „stop“ beendet den Server. Verwenden wir nun einen Client. Wir verwenden den zuvor erstellten generischen Client. Der Server wird gestartet:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

Der generische Client wird in einem weiteren DOS-Fenster gestartet:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

Man sieht, dass der Client die Begrüßungsnachricht des Servers erfolgreich empfangen hat. Wir senden weitere Befehle:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Wir kehren zum Serverfenster zurück, um den Server zu beenden:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. Übungen

8.5.1. Übung 1 – Generischer grafischer Client TCP

8.5.1.1. Vorstellung der Anwendung

Wir wollen ein Programm erstellen, das über das Internet mit den wichtigsten TCP-Diensten kommunizieren kann. Wir nennen es einen generischen TCP-Client. Wenn man diese Anwendung verstanden hat, stellt man fest, dass sich alle TCP-Clients ähneln. Das Programmfenster sieht wie folgt aus:

Image

Die verschiedenen Steuerelemente haben folgende Bedeutung:

Nr.
Name
Typ
Funktion
1
TxtRemoteHost
JTextField
Name des Rechners, der den gewünschten Dienst anbietet
2
TxtPort
JTextField
Port des angeforderten Dienstes
3
TxtSend
JTextField
Text der Nachricht, die vom Client an den Server gesendet wird
4
OptRCLF
OptLF
JCheckBox
Schaltflächen zur Festlegung, wie die Zeilen im Client-Server-Dialog enden
RCLF: Wagenrücklauf (#13) + Zeilenumbruch (#10)
LF: Zeilenumbruch (#10)
5
LstSuivi
JList
zeigt Meldungen zum Kommunikationsstatus zwischen Client und Server an
6
LstDialogue
JList
zeigt die vom Client (->) und vom Server (<-) ausgetauschten Meldungen an
7
CmdAnnuler
JButton
ausgeblendet – befindet sich unterhalb der Dialogliste – wird angezeigt, wenn die Verbindung hergestellt ist, und ermöglicht es, diese zu unterbrechen, falls der Server nicht antwortet

Folgende Menüoptionen stehen zur Verfügung:

Option
Unteroptionen
Rolle
Verbindung
Verbinden
verbindet den Client mit dem Server
 
Trennen
beendet die Verbindung
 
Beenden
Beendet das Programm
Meldungen
Senden
Sendet die Nachricht des Steuerelements TxtSend an den Server
 
RazSuivi
Löscht die Liste LstSuivi
 
RazDialogue
Löscht die Liste LstDialogue
Autor
 
Zeigt ein Copyright-Fenster an

8.5.1.2. FONCTIONNEMENT DE APPLICATION

Initialisierung der Anwendung

Wenn das Hauptblatt der Anwendung geladen wird, finden folgende Aktionen statt:

  • Das Blatt wird auf dem Bildschirm zentriert
  • nur die Menüoptionen Connexion/Quitter und Auteur sind aktiv
  • Die Schaltfläche Annuler ist ausgeblendet
  • Die Listen LstSuivi und LstDialogue sind leer
Menü „Anmelden/Verbinden“

Diese Option ist nur verfügbar, wenn die Felder „Remote-Host“ und „Portnummer“ ausgefüllt sind und derzeit keine Verbindung besteht. Ein Klick auf diese Option führt zu folgenden Vorgängen:

  • Die Gültigkeit des Ports wird überprüft: Es muss sich um eine ganze Zahl > 0 handeln
  • Es wird ein Thread gestartet, um die Verbindung zum Server herzustellen
  • Die Schaltfläche „Annuler“ wird angezeigt, damit der Benutzer die laufende Verbindung unterbrechen kann
  • Alle Menüoptionen werden deaktiviert, mit Ausnahme von „Quitter“ und „Auteur“

Die Verbindung kann auf verschiedene Arten beendet werden:

  1. Der Benutzer hat auf die Schaltfläche „Annuler“ geklickt: Der Verbindungsthread wird beendet und das Menü in seinen Ausgangszustand zurückversetzt. In der Protokollierung wird vermerkt, dass die Verbindung vom Benutzer beendet wurde.
  2. Die Verbindung wird mit einem Fehler beendet: Es wird wie zuvor verfahren, und zusätzlich wird im Protokoll die Fehlerursache angegeben.
  3. Die Verbindung wird erfolgreich beendet: Die Schaltfläche Annuler wird entfernt, im Protokoll wird vermerkt, dass die Verbindung hergestellt wurde, das Menü RazSuivi wird freigegeben, das Menü Connecter wird gesperrt, das Menü Déconnecter wird freigegeben
Menü „Verbinden/Trennen“

Diese Option ist nur verfügbar, wenn eine Verbindung zum Server besteht. Wenn sie aktiviert wird, wird die Verbindung zum Server beendet und das Menü in seinen Ausgangszustand zurückversetzt. In der Protokollierung wird vermerkt, dass die Verbindung vom Client beendet wurde.

Menü „Verbinden/Beenden“

Diese Option beendet eine eventuell aktive Verbindung zum Server und schließt die Anwendung.

Menü „Nachrichten/Senden“

Diese Option ist nur verfügbar, wenn die folgenden Bedingungen erfüllt sind:

  • Die Verbindung zum Server wurde hergestellt

  • es gibt eine zu versendende Nachricht

Sind diese Bedingungen erfüllt, wird der im Feld TxtSend (3) enthaltene Text an den Server gesendet, ergänzt um die Sequenz RCLF, sofern die Option RCLF aktiviert wurde; andernfalls um die Sequenz LF. Ein eventueller Fehler beim Senden wird in der Nachverfolgungsliste angezeigt.

Menüs RazSuivi und RazDialogue

Leeren jeweils die Listen LstSuivi und LstDialogue. Diese Optionen sind deaktiviert, wenn die entsprechenden Listen leer sind.

Die Schaltfläche „Abbrechen“

Diese Schaltfläche am unteren Rand des Formulars wird nur angezeigt, wenn der Client gerade eine Verbindung zum Server herstellt. Diese Verbindung kann fehlschlagen, weil der Server nicht oder nicht richtig antwortet. Die Schaltfläche Annuler bietet dem Benutzer dann die Möglichkeit, die Verbindungsanfrage abzubrechen.

Die Überwachungslisten

Die Liste LstSuivi (5) verfolgt den Verbindungsstatus. Sie zeigt die wichtigsten Zeitpunkte der Verbindung an:

  • ihre Eröffnung durch den Client

  • das Schließen der Verbindung durch den Server oder den Client

  • alle Fehler, die auftreten können, solange die Verbindung aktiv ist

Die Liste LstDialogue (6) verfolgt den Dialog, der zwischen dem Client und dem Server stattfindet. Ein Thread überwacht im Hintergrund, was am Kommunikations-Socket des Clients geschieht, und zeigt dies in Liste 6 an.

Die Option „Autor“

Dieses Menü öffnet ein sogenanntes Copyright-Fenster:

Image

Fehlerbehandlung

Verbindungsfehler werden in der Überwachungsliste 6 angezeigt, Fehler im Zusammenhang mit dem Client-Server-Dialog in der Dialogliste 7. Bei einem Verbindungsfehler wird der Client-Server-Dialog geschlossen und das Formular in seinen Ausgangszustand zurückversetzt, sodass es für eine neue Verbindung bereit ist.

8.5.1.3. TRAVAIL A FAIRE

Führen Sie die zuvor beschriebene Arbeit in zwei Formen durch:

  • Standalone-Anwendung
  • Applet

8.5.2. Übung 2 – Ein Ressourcenserver

8.5.2.1. INTRODUCTION

Eine Einrichtung verfügt über mehrere leistungsstarke Rechenserver, die über das Internet zugänglich sind. Jeder Rechner, der diese Rechen Dienste nutzen möchte, sendet eine Datendatei an Port 756 eines der Server. Diese Datei enthält verschiedene Informationen: Benutzername, Passwort, Befehle zur Angabe der gewünschten Rechenart sowie die Daten, auf denen die Berechnung durchgeführt werden soll. Ist die Datendatei korrekt, verarbeitet der ausgewählte Rechenserver sie und sendet die Ergebnisse in Form einer Textdatei an den Client zurück.

Eine solche Organisation bietet zahlreiche Vorteile:

  • Jeder Client-Typ (PC, Mac, Unix, ...) kann diesen Dienst nutzen
  • Der Kunde kann sich an einem beliebigen Ort im Internet befinden
  • Die Rechenressourcen sind optimiert: Es sind nur wenige leistungsstarke Rechner erforderlich. So kann auch eine kleine Organisation ohne eigene Rechenressourcen diesen Dienst gegen einen finanziellen Beitrag nutzen, der sich nach der in Anspruch genommenen Rechenzeit richtet.

Trotz der Leistungsfähigkeit der Rechner kann eine Berechnung manchmal mehrere Stunden dauern: Der Server steht dann für andere Clients nicht zur Verfügung. Für einen Client stellt sich somit das Problem, einen verfügbaren Rechenserver zu finden. Zu diesem Zweck wird ein „Rechenressourcenmanager“ verwendet, der im Folgenden als GRC-Server bezeichnet wird. Dieser Dienst läuft auf einem einzigen Rechner und arbeitet im TCP-Modus auf Port 864. An ihn wendet sich ein Client, der Zugriff auf einen Rechenserver wünscht. Der Server GRC, der über die vollständige Liste der Rechenserver verfügt, antwortet ihm, indem er den Namen eines derzeit inaktiven Servers übermittelt. Der Client muss dann nur noch seine Daten an den ihm zugewiesenen Server senden.

Wir wollen nun den Server GRC programmieren.

8.5.2.2. Der INTERFACE VISUELLE

Die visuelle Benutzeroberfläche sieht wie folgt aus:

Image

Die Benutzeroberfläche zeigt zwei Serverlisten an:

  • links die Liste der inaktiven und somit für Berechnungen verfügbaren Server
  • rechts die Liste der Server, die gerade mit den Berechnungen eines Kunden beschäftigt sind.

Die Menüstruktur sieht wie folgt aus:

Hauptmenü
Untermenü
Rolle
Dienst
Starten
Startet den TCP-Dienst auf Port 864
 
Beenden
Beendet den Dienst
 
Beenden
Beendet die Anwendung
Autor
 
Urheberrechtshinweise

Die Struktur der Steuerelemente im Formular ist wie folgt:

Name
Typ
Rolle
listLibres
JList
Liste der freien Server
listOccupés
JList
Liste der belegten Server

8.5.2.3. FONCTIONNEMENT DE APPLICATION

Laden der Anwendung

Beim Laden der Anwendung wird die Liste listLibres mit den Namen der vom GRC verwalteten Rechenserver gefüllt. Diese sind in einer als Parameter übergebenen Serverdatei definiert. Diese Datei enthält eine Liste mit Servernamen, wobei pro Zeile ein Name steht, und wird daher zum Ausfüllen der Liste listLibres verwendet. Das Menü „Starten“ ist freigegeben, das Menü „Beenden“ ist gesperrt.

Option „Dienst/Starten“

Diese Option

  • startet den Listening-Dienst auf Port 864 des Rechners
  • sperrt das Menü „Starten“
  • aktiviert das Menü „Beenden“
Option „Dienst/Beenden“

Diese Option beendet den Dienst:

  • Die Liste der belegten Server wird geleert
  • die Liste der freien Server wird mit dem Inhalt der Datei „Server“ gefüllt
  • Das Menü „Starten“ wird freigegeben
  • das Menü „Beenden“ wird gesperrt
Option „Dienst/Beenden“

Die Anwendung wird beendet.

Client-Server-Dialog

Der Client-Server-Dialog erfolgt durch den Austausch von Textzeilen, die mit der Sequenz RCLF enden. Der Server GRC erkennt zwei Befehle: getserveur und finservice. Im Folgenden wird die Funktion dieser beiden Befehle näher erläutert:

  • 1-getserveur

Der Client fragt an, ob ein Rechenserver für ihn verfügbar ist.

Der Server GRC wählt dann den ersten Server aus seiner Liste der freien Server aus und sendet dessen Namen in folgender Form an den Client zurück:

        100-nom du serveur

Außerdem trägt er den dem Kunden zugewiesenen Server in die Liste der belegten Server ein, und zwar in der Form:

        serveur (IP du client)

wie das folgende Beispiel zeigt, in dem der Server calcul1.istia.univ-angers.fr gerade den Client mit der Adresse IP 193.52.43.5 bedient:

Image

Ein Client kann keinen „getserver“-Befehl senden, wenn ihm bereits ein Rechenserver zugewiesen wurde. Bevor der Server GRC dem Client antwortet, überprüft er daher, ob die Client-Adresse IP nicht bereits in der Liste der belegten Server aufgeführt ist. Ist dies der Fall, antwortet der Server GRC:

        501-Vous avez actuellement une demande en cours

Schließlich gibt es noch den Fall, dass kein Rechenserver verfügbar ist: Die Liste der freien Server ist leer. In diesem Fall antwortet der Server GRC:

        502- Il n’y a aucun serveur de calcul disponible

In jedem Fall beendet der Server GRC nach der Antwort an den Client die Verbindung zu diesem, um andere Clients bedienen zu können.

  • 2-finservice

Der Client teilt mit, dass er den bisher genutzten Rechenserver nicht mehr benötigt.

Der Server GRC überprüft zunächst, ob es sich bei dem Client tatsächlich um einen Client handelt, den er bedient hat. Dazu überprüft er, ob die Adresse IP des Clients in der Liste der belegten Server aufgeführt ist. Ist dies nicht der Fall, antwortet der Server GRC:

        503-Aucun serveur ne vous a été attribué

Wird der Client erkannt, antwortet der Server GRC ihm:

        101-Fin de service acceptée

und verschiebt den diesem Client zugewiesenen Rechenserver in die Liste der freien Server. Um das vorherige Beispiel fortzusetzen: Wenn der Client den Befehl „finservice“ sendet, lautet die Anzeige des Servers GRC:

Image

Nach dem Senden der Antwort, unabhängig von deren Inhalt, beendet der Server GRC die Verbindung.

8.5.2.4. TRAVAIL an FAIRE

Schreiben Sie die Anwendung als eigenständiges Programm, das beispielsweise mit einem telnet-Client oder mit dem generischen TCP-Client aus der vorherigen Übung getestet werden kann.

8.5.3. Übung 3 – Ein SMTP-Client

8.5.3.1. INTRODUCTION

Wir möchten hier einen Client für den Dienst SMTP (SendMail Transfer Protocol) erstellen, der das Versenden von E-Mails ermöglicht. Unter Unix oder Windows ist das Programm telnet ein Client, der mit dem Protokoll tcp arbeitet. Es kann mit jedem tcp-Dienst „kommunizieren“, der Befehle im Textformat akzeptiert, die mit der Sequenz RCLF enden, d. h. mit den Codezeichen ASCII 13 und 10. Hier ein Beispiel für eine Kommunikation mit dem Dienst smtp zum Versenden von E-Mails:

$ telnet istia.univ-angers.fr 25        // Aufruf des SMTP-Dienstes

// Antwort des SMTP-Servers


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// Kommentare --------------

Das Programm telnet kann jeden Dienst über die folgende Syntax aufrufen

telnet machine_service port_service

Der Client-Server-Austausch erfolgt über Textzeilen, die mit der Sequenz RCLF enden.

Die Antworten des Dienstes smtp haben folgende Form:

Nachrichtennummer oder

Nachrichtennummer

Der SMTP-Server kann mehrere Antwortzeilen senden. Die letzte Zeile der Antwort wird durch eine Nummer gefolgt von einem Leerzeichen gekennzeichnet, während bei den vorhergehenden Zeilen der Antwort auf die Nummer ein Bindestrich (-) folgt.

Eine Nummer größer oder gleich 500 weist auf eine Fehlermeldung hin.

// Ende der Kommentare

help                        // Befehl über die Tastatur eingegeben

// Antwort des SMTP-Servers

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
mail from: serge.tahe@istia.univ-angers.fr    // neuer Befehl über die Tastatur

// Kommentare ---------

Der Befehl „mail“ hat folgende Syntax:

mail from: E-Mail-Adresse des Absenders der Nachricht

// Ende der Kommentare

// Antwort des SMTP-Servers

250 serge.tahe@istia.univ-angers.fr... Sender ok

// Kommentare

Der Server smtp führt keine Gültigkeitsprüfung der Absenderadresse durch: Er übernimmt sie so, wie sie ihm übermittelt wurde

// Ende der Kommentare

rcpt to: user1@istia.univ-angers.fr        // neuer Befehl über die Tastatur

// Kommentare ---------

Der Befehl „rcpt“ hat folgende Syntax:

rcpt to: E-Mail-Adresse des Empfängers der Nachricht

Wenn es sich bei der E-Mail-Adresse um eine Adresse des Rechners handelt, auf dem der Server smtp läuft, überprüft dieser, ob die Adresse tatsächlich existiert; andernfalls erfolgt keine Überprüfung. Wenn eine Überprüfung stattgefunden hat und ein Fehler festgestellt wurde, wird dieser mit einer Nummer >= 500 gemeldet.

Es können beliebig viele „rcpt to“-Befehle ausgegeben werden: Auf diese Weise lässt sich eine Nachricht an mehrere Personen senden.

// Ende der Kommentare

// Antwort des SMTP-Servers

250 user1@istia.univ-angers.fr... Recipient ok
data                        // neuer Befehl über die Tastatur

// Kommentare ---------

Der Befehl „data“ hat folgende Syntax:

data

Zeile1

Zeile2

...

.

Darauf folgen die Textzeilen, aus denen die Nachricht besteht, wobei diese mit einer Zeile enden muss, die ausschließlich das Zeichen „Punkt“ enthält.

Die Nachricht wird dann an den durch den Befehl rcpt angegebenen Empfänger gesendet.

// Ende der Kommentare

// Antwort des SMTP-Servers

354 Enter mail, end with "." on a line by itself

// per Tastatur eingegebener Nachrichtentext


subject: essai smtp

essai smtp a partir de telnet
.

// Kommentare

In den Textzeilen des Befehls „data“ kann eine Zeile „subject:“ eingefügt werden, um den Betreff der E-Mail anzugeben. Auf diese Zeile muss eine Leerzeile folgen.

// Antwort des SMTP-Servers

250 HAA11627 Message accepted for delivery
quit                            // neuer Befehl über die Tastatur

// Kommentare

Der Befehl „quit“ beendet die Verbindung zum Dienst smtp

// Ende der Kommentare

// Antwort des SMTP-Servers

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. INTERFACE VISUELLE

Wir schlagen vor, ein Programm mit der folgenden grafischen Benutzeroberfläche zu erstellen:

Image

Die Steuerelemente haben folgende Funktionen:

Nummer
Typ
Funktion
1
JTextField
Durch Kommas getrennte E-Mail-Adressen
2
JTextField
Betreff der Nachricht
3
JTextField
Durch Kommas getrennte E-Mail-Adressen
4
JTextField
Durch Kommas getrennte E-Mail-Adressen
5
JTextArea
Nachrichtentext
6
JList
Beobachtungsliste
7
JList
Dialogliste
8
JButton
Die Schaltfläche „Abbrechen“ wird nicht angezeigt und erscheint, wenn der Client eine Verbindung zum Server SMTP anfordert. Ermöglicht es dem Benutzer, diese Anfrage abzubrechen, falls der Server nicht antwortet.

8.5.3.3. LES MENUS

Die Menüstruktur der Anwendung ist wie folgt:

Hauptmenü
Untermenü
Rolle
E-Mail
  
 
Senden
Sende die Nachricht von Steuerung 5
 
Beenden
Die App beenden
Optionen
  
 
Verfolgung ausblenden
Macht das Steuerelement 6 unsichtbar
 
Tracking zurücksetzen
Leert die Beobachtungsliste 6
 
Dialogfeld ausblenden
Macht die Dialogliste unsichtbar 7
 
Dialog löschen
Bezieht die Dialogliste zurück 7
 
Konfigurieren
Ermöglicht es dem Benutzer, Folgendes anzugeben
- die Adresse des vom Programm verwendeten SMTP-Servers
- seine E-Mail-Adresse
 
Speichern...
Speichert die vorherige Konfiguration in einer .ini-Datei
Autor
 
Copyright-Informationen

8.5.3.4. FONCTIONNEMENT DE APPLICATION

Menü „Optionen/Konfigurieren“

Dieses Menü öffnet das folgende Fenster:

Image

Beide Felder müssen ausgefüllt werden, damit die Schaltfläche OK aktiv ist. Beide Angaben müssen in globalen Variablen gespeichert werden, damit sie für andere Module verfügbar sind.

Menü „E-Mail“/„Senden“

Diese Option ist nur verfügbar, wenn die folgenden Bedingungen erfüllt sind:

  • Die Konfiguration wurde vorgenommen
  • es gibt eine zu versendende Nachricht
  • es gibt einen Betreff
  • in den Feldern 1, 3 und 4 ist mindestens ein Empfänger angegeben

Sind diese Bedingungen erfüllt, verläuft die Abfolge der Ereignisse wie folgt:

  • Das Formular wird in einen Zustand versetzt, in dem alle Aktionen, die den Client-Server-Dialog beeinträchtigen könnten, gesperrt sind
  • Es wird eine Verbindung zum Port 25 des in der Konfiguration angegebenen Servers hergestellt
  • Der Client kommuniziert anschließend gemäß dem oben beschriebenen Protokoll mit dem SMTP-Server
  • Die „Mail From“-Adresse verwendet die in der Konfiguration angegebene E-Mail-Adresse des Absenders
  • Das Feld „rcpt to:“ wird für jede der E-Mail-Adressen verwendet, die in den Feldern 1, 3 und 4 gefunden wurden
  • In den Zeilen, die nach dem Befehl data gesendet werden, finden sich folgende Texte:
    • eine Zeile Subject: – Betrefftext der Prüfung 2
    • eine Zeile Cc:: Adressen der Prüfung 3
    • eine Zeile Bcc:: Adressen der Prüfung 4
    • der Text der Meldung der Prüfung 5
    • der Endpunkt
Die Schaltfläche „Abbrechen“

Diese Schaltfläche am unteren Rand des Formulars wird nur angezeigt, wenn der Client gerade eine Verbindung zum Server smtp herstellt. Diese Verbindung kann fehlschlagen, weil der Server smtp nicht oder fehlerhaft antwortet. Die Schaltfläche „Annuler“ bietet dem Benutzer dann die Möglichkeit, die Verbindungsanfrage abzubrechen.

Die Überwachungslisten

Die Liste (6) verfolgt den Verbindungsstatus. Sie zeigt die wichtigsten Zeitpunkte der Verbindung an:

  • die Eröffnung durch den Client
  • die Schließung durch den Server oder den Client
  • alle Verbindungsfehler

Die Liste (7) protokolliert den Dialog smtp, der zwischen dem Client und dem Server stattfindet.

Diese beiden Listen sind mit Menüoptionen verknüpft:

Wiedergabeliste ausblenden
Blendet die Merklisten-Liste 6 sowie die darüber befindliche Beschriftung aus. Wenn die Höhe, die diese beiden Steuerelemente einnehmen, H beträgt, werden alle darunter liegenden Steuerelemente um eine Höhe von H nach oben verschoben und die Gesamtgröße des Formulars um H verringert. Außerdem macht „Verfolgen ausblenden“ die Option RazSuivi darunter unsichtbar.
Verfolgung zurücksetzen
Leert die Nachverfolgungsliste 6
Dialogfeld ausblenden
Blendet die Dialogliste 7, die darüber befindliche Beschriftung sowie die darunter liegende Menüoption RazDialogue aus. Wie bei „Verfolgen ausblenden“ wird die Position der darunter liegenden Steuerelemente (möglicherweise die Schaltfläche Annuler) neu berechnet und die Fenstergröße verringert.
Dialogfeld „Raz“
Leert die Dialogliste 7
Die Option „Autor“

Dieses Menü öffnet ein sogenanntes Copyright-Fenster:

Image

Fehlerbehandlung

Verbindungsfehler werden in der Überwachungsliste 6 gemeldet, Fehler im Zusammenhang mit dem Client/Server-Dialog in der Dialogliste 7. Bei einem Fehler wird der Benutzer durch eine Fehlermeldung benachrichtigt, und die Liste mit der Fehlerursache wird angezeigt, falls sie zuvor ausgeblendet war. Außerdem wird der Client-Server-Dialog geschlossen und das Formular in seinen ursprünglichen Zustand zurückversetzt.

8.5.3.5. GESTION von UN FICHIER DE CONFIGURATION

Es ist wünschenswert, dass der Benutzer die Software nicht bei jeder Nutzung neu konfigurieren muss. Ist dazu die Option „Optionen/Konfiguration beim Beenden speichern“ aktiviert, werden beim Schließen des Programms die beiden über die Option Options/Configurer erfassten Informationen sowie der Status der beiden Überwachungslisten in einer Datei namens sendmail.ini gespeichert, die sich im selben Verzeichnis wie die .exe-Datei des Programms befindet. Diese Datei hat das folgende Format:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

Die Zeilen SmtpServer und ReplyAddress enthalten die beiden Informationen, die über die Option „Optionen/Konfigurieren“ erfasst wurden. Die Zeilen Suivi und Dialogue geben den Status der Listen „Verfolgung“ und „Dialog“ an: 1 (vorhanden), 0 (nicht vorhanden).

Beim Laden des Programms wird die Datei sendmail.ini gelesen, sofern sie vorhanden ist, und das Formular wird entsprechend konfiguriert. Wenn die Datei sendmail.ini nicht vorhanden ist, wird so verfahren, als ob sie vorhanden wäre:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

Wenn die Datei „sendmail.ini“ zwar existiert, aber unvollständig ist (fehlende Zeilen), wird die fehlende Zeile durch die entsprechende Zeile aus der obigen Liste ersetzt. Da also die Zeile „Suivi=...“ fehlt, wird so verfahren, als ob wir „Suivi=1“ hätten.

Alle Zeilen, die nicht der Vorlage entsprechen:

    mot clé= valeur

werden ignoriert, ebenso wie diejenigen, bei denen das Schlüsselwort ungültig ist. Das Schlüsselwort kann in Groß- oder Kleinbuchstaben geschrieben sein: Das spielt keine Rolle.

In der Option Options/Configurer werden die derzeit aktiven Werte SmtpServer und ReplyAddress angezeigt. Der Benutzer kann diese dann nach Belieben ändern.

8.5.3.6. TRAVAIL A FAIRE

Führen Sie die zuvor beschriebene Arbeit durch. Es wird empfohlen, die Bearbeitung der Konfigurationsdatei als Letztes durchzuführen.

8.5.4. Übung 4 – Client POPPASS

8.5.4.1. Introduction

Es soll ein Client mit der Bezeichnung TCP erstellt werden, der mit dem Server POPPASSD kommunizieren kann, der auf Port 106 läuft. Dieser Dienst ermöglicht es, das Passwort auf einem Rechner mit der Bezeichnung UNIX zu ändern. Das Protokoll für die Client-Server-Kommunikation lautet wie folgt:

1 - Die Kommunikation erfolgt durch den Austausch von Nachrichten, die mit der Sequenz RCLF enden

2 - Der Client sendet Befehle an den Server

  • Der Server antwortet mit Nachrichten, die mit dreistelligen Zahlen beginnen: XXX. Wenn XXX=200 ist, wurde der Befehl korrekt ausgeführt, andernfalls ist ein Fehler aufgetreten.

3 - Die Abfolge des Datenaustauschs ist wie folgt:

A    - le client se connecte
  • Der Server antwortet mit einer Begrüßungsnachricht
B    - le client envoie USER login
  • Der Server fordert das Passwort an, wenn die Anmeldung akzeptiert wird; andernfalls gibt er eine Fehlermeldung aus
C    - le client envoie PASS mot_de_passe
  • Der Server fordert das neue Passwort an, wenn das Passwort akzeptiert wird; andernfalls wird eine Fehlermeldung ausgegeben
D    - le client envoie NEWPASS nouveau_mot_de_passe
  • Der Server antwortet mit der Bestätigung, dass das neue Passwort akzeptiert wurde; andernfalls wird eine Fehlermeldung ausgegeben
E    - le client envoie la commande QUIT
  • Der Server sendet eine Abschlussmeldung und schließt die Verbindung

8.5.4.2. Das Formular des Clients

Image

Die verschiedenen Steuerelemente haben folgende Bedeutung:

Nr.
Name
Typ
Funktion
1
txtRemoteHost
JTextField
Servername
2
txtLogin
JTextField
Benutzername
3
txtMdp
JTextField
Benutzerkennwort
4
txtNewMdp
JTextField
Neues Passwort des Benutzers
5
txtConfirmation
JTextField
Bestätigung des neuen Passworts
6
lstSuivi
JList
Meldungen zur Verbindungsüberwachung
7
lstDialogue
JList
Meldungen aus dem Client/Server-Dialog
10
cmdAnnuler
JButton
nicht dargestellt – Schaltfläche, die angezeigt wird, wenn eine Verbindung zum Server hergestellt wird. Ermöglicht das Beenden der Verbindung.

8.5.4.3. Die Menüs

Titel
Name des Steuerelements
Funktion
Verbindung
mnuconnexion
 
Verbinden
mnuconnecter
stellt die Verbindung zum Server her
Beenden
mnuQuitter
beendet die Anwendung
Meldungen
mnuMessages
 
RazSuivi
mnuRazSuivi
Löscht die Liste lstSuivi
RazDialogue
mnuRazDialogue
Löscht die Liste lstDialogue
Autor
mnuAuteur
Zeigt das Copyright-Fenster an

8.5.4.4. Funktionsweise der Anwendung

Initialisierung der Anwendung

Wenn das Hauptfenster der Anwendung geladen wird, finden folgende Aktionen statt:

  • Das Blatt wird auf dem Bildschirm zentriert
  • nur die Menüoptionen Connexion/Quitter und Auteur sind aktiv
  • die Schaltfläche Annuler wird ausgeblendet
  • die Listen LstSuivi und LstDialogue sind leer
Menü „Anmelden/Verbinden“

Diese Option ist nur verfügbar, wenn die Felder 1 bis 5 ausgefüllt wurden. Ein Klick auf diese Option führt zu folgenden Vorgängen:

  • Es wird ein Thread gestartet, um die Verbindung zum Server herzustellen
  • Die Schaltfläche Annuler wird angezeigt, damit der Benutzer die laufende Verbindung unterbrechen kann
  • Alle Menüoptionen werden deaktiviert, mit Ausnahme von Quitter und Auteur

Die Abfolge der Ereignisse ist anschließend wie folgt:

  1. Der Benutzer hat auf die Schaltfläche „Annuler“ geklickt: Der Verbindungsthread wird beendet und das Menü in seinen Ausgangszustand zurückversetzt. In der Protokollierung wird vermerkt, dass die Verbindung vom Benutzer beendet wurde.
  2. Die Verbindungsanfrage wird vom Server akzeptiert. Anschließend wird der Dialog mit dem Server zur Änderung des Passworts aufgenommen. Der Datenaustausch dieses Dialogs wird in der Liste LstDialogue protokolliert. Nach Beendigung des Dialogs wird die Verbindung zum Server geschlossen und das Formularmenü in seinen Ausgangszustand zurückversetzt.
  3. Solange der Dialog aktiv ist, bleibt die Schaltfläche „Abbrechen“ sichtbar, damit der Benutzer die Verbindung bei Bedarf beenden kann.
  4. Tritt während der Kommunikation ein Fehler auf, wird die Verbindung geschlossen und die Fehlerursache in der Protokoll-Liste LstSuivi angezeigt.
Menü „Verbindung/Beenden“

Diese Option beendet eine eventuell aktive Verbindung zum Server und schließt die Anwendung.

Menüs RazSuivi und RazDialogue

Leeren jeweils die Listen LstSuivi und LstDialogue. Diese Optionen sind deaktiviert, wenn die entsprechenden Listen leer sind.

Die Schaltfläche „Abbrechen“

Diese Schaltfläche am unteren Rand des Formulars wird nur angezeigt, wenn der Kunde gerade eine Verbindung zum Server herstellt oder bereits mit dem Server verbunden ist. Die Schaltfläche Annuler bietet dem Benutzer die Möglichkeit, die Kommunikation mit dem Server zu unterbrechen.

Die Überwachungslisten

Die Liste LstSuivi (5) verfolgt den Verbindungsverlauf. Sie zeigt die wichtigsten Zeitpunkte der Verbindung an:

  • ihre Eröffnung durch den Client

  • das Schließen der Verbindung durch den Server oder den Client

  • alle Fehler, die auftreten können, solange die Verbindung aktiv ist

Die Liste LstDialogue (6) verfolgt den Dialog, der zwischen dem Client und dem Server stattfindet.

Die Option „Autor“

Dieses Menü öffnet ein sogenanntes Copyright-Fenster:

Image

Fehlerbehandlung

Kommunikationsfehler werden in der Überwachungsliste 6 angezeigt, Fehler im Zusammenhang mit dem Client-Server-Dialog in der Dialogliste 7. Bei einem Verbindungsfehler wird der Client-Server-Dialog geschlossen und das Formular in seinen Ausgangszustand zurückversetzt, sodass eine neue Verbindung hergestellt werden kann.

8.5.4.5. TRAVAIL A FAIRE

Führen Sie die zuvor beschriebene Aufgabe zunächst als eigenständige Anwendung und anschließend als Applet durch.