11. Internet-Programmierung
11.1. Généralités
11.1.1. Internetprotokolle
Hier geben wir eine Einführung in die Kommunikationsprotokolle des Internets, die auch als Protokollsuite TCP/IP (Transfer Control Protocol / Internet Protocol) bezeichnet werden, benannt nach den beiden wichtigsten Protokollen. Es kann für den Leser hilfreich sein, ein allgemeines Verständnis der Funktionsweise von Netzwerken und insbesondere der Protokolle TCP/IP zu haben, 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 durch das 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
11.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:
![]() |
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 Funktionsweise des Netzwerks 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 nach anderen Regeln an die Schicht Session weitergeleitet und so weiter, bis die Informationen auf dem physikalischen Medium ankommen und physikalisch 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:
![]() |
Die verschiedenen Schichten haben folgende Aufgaben:
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 Datenleitungen (E.T.C.D.), wie z. B. Modulatoren/Demodulatoren, Multiplexer und Konzentratoren. Die wichtigsten Aspekte auf dieser Ebene sind:
| |
Versteckt die physikalischen Besonderheiten der physikalischen Schicht. Erkennt und korrigiert Übertragungsfehler. | |
Verwaltet den Weg, den die über das Netzwerk gesendeten Informationen zurücklegen müssen. Dies wird als routage bezeichnet: die Bestimmung der Route, die eine Information nehmen muss, damit sie ihren Empfänger erreicht. | |
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. | |
In dieser Schicht finden sich Dienste, die es einer Anwendung ermöglichen, eine Arbeitssitzung auf einem Remote-Rechner zu eröffnen und aufrechtzuerhalten. | |
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. | |
Auf dieser Ebene befinden sich die Anwendungen, die in der Regel nah am Benutzer angesiedelt sind, wie beispielsweise E-Mail oder Dateiübertragung. |
11.1.3. Das Modell TCP/IP
Das Modell OSI ist ein ideales Modell, das bisher noch nicht realisiert wurde. Die Protokollsuite TCP/IP kommt diesem Modell in folgender Form nahe:
![]() |
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
![]() |
- 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 eine Übertragung starten möchte, überwacht das Kabel – dabei erkennt er, ob eine Trägerwelle vorhanden ist oder nicht; ist dies der Fall, bedeutet dies, dass bereits eine Übertragung stattfindet. Dies ist die CSMA-Technik (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. Das Zugriffsverfahren wird daher als CSMA/CD bezeichnet.
- Eine 48-Bit-Adressierung. Jeder Rechner verfügt über eine Adresse, die hier als physikalische Adresse bezeichnet wird und auf der Karte vermerkt ist, die ihn mit dem Kabel verbindet. Diese Adresse wird als Ethernet-Adresse des Rechners bezeichnet.
Netzwerkschicht
Auf dieser Schicht finden wir die Protokolle IP, ICMP, ARP und RARP.
Übermittelt Pakete zwischen zwei Knoten im Netzwerk | |
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. | |
stellt die Zuordnung von der Internetadresse der Maschine zur physikalischen Adresse der Maschine her | |
stellt die Zuordnung von der physischen Adresse des Rechners zur Internetadresse des Rechners her |
Transport-/Sitzungsschichten
In dieser Schicht finden sich die folgenden Protokolle:
Gewährleistet eine zuverlässige Übermittlung von Informationen zwischen zwei Clients | |
Gewährleistet eine unzuverlässige Übermittlung von Informationen zwischen zwei Clients |
Anwendungs-/Präsentations-/Sitzungsschichten
Hier finden sich verschiedene Protokolle:
Terminalemulator, der es einem Rechner A ermöglicht, sich als Terminal mit einem Rechner B zu verbinden | |
ermöglicht Dateiübertragungen | |
ermöglicht Dateiübertragungen | |
ermöglicht den Austausch von Nachrichten zwischen Netzwerkbenutzern | |
wandelt einen Rechnernamen in die Internetadresse des Rechners um | |
Erstellt von Sun MicroSystems, legt eine standardisierte, maschinenunabhängige Darstellung der Daten fest | |
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 | |
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 |
11.1.4. Funktionsweise der Internetprotokolle
Anwendungen, die in der TCP/IP-Umgebung 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:
![]() |
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 Bytefolge, die an die Schicht transport. übertragen werden soll
- 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, entkapselt 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.
11.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 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 Protokoll IP jedes Netzwerks übernimmt die Umwandlung zwischen IP-Adresse und physikalischer Adresse in beide Richtungen.
Die IP-Adressen müssen alle unterschiedlich sein. In Frankreich ist die INRIA für die Vergabe der IP-Adressen zuständig. Tatsächlich vergibt diese Stelle eine Adresse für Ihr lokales Netzwerk, 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.
11.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:
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:
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:
![]() |
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 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 Einsen besteht, handelt es sich um eine Broadcast-Adresse: Diese Adresse bezeichnet alle Knoten des Netzwerks.
- 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.
11.1.5.2. Die 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:
![]() |
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:
![]() |
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.
11.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 behandelt werden müssen. Diese besondere Art von Paket wird als IP-Datagramm bezeichnet. Wir haben es bereits vorgestellt:
![]() |
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 eine Nachricht gesendet wird.
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:
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.
11.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 Adresse IP des Empfängers.
- 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 indirektem Routing versteht man die Weiterleitung eines Pakets IP an ein Ziel, das sich in einem anderen Netzwerk befindet als dem, zu dem der Absender gehört. 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.
![]() |
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 Empfängeradresse IP des Pakets im Netzwerk Nr. 2, sendet der Router das Paket direkt an den Empfänger; andernfalls leitet er es an einen anderen Router weiter, der das Netzwerk Nr. 2 mit dem Netzwerk Nr. 3 verbindet, und so weiter.
11.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.
11.1.7. Die Transportschicht: die Protokolle UDP und TCP
11.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:
Diese Datagramme werden in IP-Pakete und anschließend in physikalische Rahmen gekapselt.
11.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.
11.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 ziemlich 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 mit
Lagaffe
Eine Anwendung kann einen Rechner also über seinen Namen referenzieren. Letztendlich muss man jedoch die Internetadresse dieses Rechners ermitteln. Wie wird das 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 Anfrage DNS, die somit den Namen des gesuchten Rechners enthält. Wenn der abgefragte Server diesen Namen in seinen Tabellen hat, sendet er an Rechner A die entsprechende Adresse IP. 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 MicroSystems entwickelte Protokoll 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.
11.1.9. Fazit
In dieser Einführung haben wir einige Grundzüge der Internetprotokolle vorgestellt. Um dieses Thema zu vertiefen, empfiehlt sich die Lektüre des ausgezeichneten Buches von Douglas Comer:
Titel TCP/IP: Architektur, Protokolle, Anwendungen.
Autor Douglas COMER
Verlag InterEditions
11.2. Die Klassen .NET der Adressverwaltung IP
Ein Rechner im Internet wird eindeutig durch eine IP-Adresse (Internet Protocol) definiert, die zwei Formen annehmen kann:
- IPv4: 32-Bit-codiert und dargestellt durch eine Zeichenkette der Form „I1.I2.I3.I4“, wobei In eine Zahl zwischen 1 und 254 ist. Dies sind derzeit die am häufigsten verwendeten IP-Adressen.
- IPv6: 128-Bit-codiert und dargestellt durch eine Zeichenkette der Form „[I1.I2.I3.I4.I5.I6.I7.I8]“, wobei In eine Zeichenkette aus 4 hexadezimalen Ziffern ist. In diesem Dokument werden wir die Adressen IPv6 nicht verwenden.
Ein Rechner kann auch durch einen ebenfalls eindeutigen Namen definiert werden. Dieser Name ist nicht zwingend erforderlich, da Anwendungen letztendlich immer die IP-Adressen der Rechner verwenden. Sie dienen lediglich dazu, den Benutzern die Arbeit zu erleichtern. So ist es mit einem Browser einfacher, die Adresse URL http://www.ibm.com abzufragen als die Adresse URL http://129.42.17.99, obwohl beide Methoden möglich sind.
Ein Rechner kann mehrere IP-Adressen haben, wenn er physisch gleichzeitig mit mehreren Netzwerken verbunden ist. In diesem Fall hat er in jedem Netzwerk eine IP-Adresse.
Eine Adresse IP kann in .NET auf zwei Arten dargestellt werden:
- in Form einer Zeichenkette „I1.I2.I3.I4“ oder „[I1.I2.I3.I4.I5.I6.I7.I8]“
- in Form eines Objekts vom Typ IPAddress
Die Klasse IPAddress
Zu den Methoden M, Eigenschaften P und Konstanten C der Klasse IPAddress gehören die folgenden:
P | Familie der Adresse IP. Der Typ AddressFamily ist eine Aufzählung. Die beiden gängigen Werte sind: AddressFamily.InterNetwork: für eine Adresse IPv4 AddressFamily.InterNetworkV6: für eine Adresse IPv6 | |
C | die Adresse IP „0.0.0.0“. Wenn ein Dienst dieser Adresse zugeordnet ist, bedeutet dies, dass er Clients auf allen Adressen IP des Rechners akzeptiert, auf dem er ausgeführt wird. | |
C | die Adresse IP „127.0.0.1“. Wird als „Loopback-Adresse“ bezeichnet. Wenn ein Dienst dieser Adresse zugeordnet ist, bedeutet dies, dass er nur Clients akzeptiert, die sich auf demselben Rechner befinden wie er selbst. | |
C | die Adresse IP „255.255.255.255“. Wenn ein Dienst dieser Adresse zugeordnet ist, bedeutet dies, dass er keine Clients akzeptiert. | |
M | versucht, die Adresse IP ipString in der Form „I1.I2.I3.I4“ als Objekt IPAddress address zu übergeben. Gibt true zurück, wenn der Vorgang erfolgreich war. | |
M | gibt „true“ zurück, wenn die Adresse IP „127.0.0.1“ lautet | |
M | gibt die Adresse IP in der Form „I1.I2.I3.I4“ oder „[I1.I2.I3.I4.I5.I6.I7.I8]“ zurück |
Die Zuordnung der Adresse IP <--> nomMachine wird durch einen verteilten Internetdienst namens DNS (Domain Name System) gewährleistet. Mit den statischen Methoden der Klasse „Dns“ lässt sich die Zuordnung der Adressen IP <--> nomMachine herstellen:
gibt eine Adresse IPHostEntry ausgehend von einer Adresse IP in Form einer Zeichenkette oder ausgehend von einem Rechnernamen zurück. Löst eine Ausnahme aus, wenn der Rechner nicht gefunden werden kann. | |
gibt eine Adresse IPHostEntry ausgehend von einer Adresse IP vom Typ IPAddress zurück. Löst eine Ausnahme aus, wenn der Rechner nicht gefunden werden kann. | |
Gibt den Namen des Rechners zurück, auf dem das Programm ausgeführt wird, das diese Anweisung ausführt | |
gibt die Adressen IP des Rechners zurück, der durch seinen Namen oder eine seiner Adressen IP identifiziert wird. |
Eine Instanz IPHostEntry kapselt die Adressen IP, die Aliase und den Namen eines Rechners. Der Typ IPHostEntry ist wie folgt definiert:
P | Tabelle der IP-Adressen des Rechners | |
P | die Aliase DNS der Maschine. Dies sind die Namen, die den verschiedenen Adressen IP der Maschine entsprechen. | |
P | der primäre Hostname des Rechners |
Betrachten wir das folgende Programm, das den Namen des Rechners anzeigt, auf dem es ausgeführt wird, und anschließend interaktiv die Zuordnungen zwischen Adresse IP und Rechnername angibt:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// zeigt den Namen des lokalen Rechners an
// und gibt anschließend interaktiv Informationen zu den Netzwerkrechnern aus
// die durch einen Namen oder eine Adresse identifiziert werden IP
// lokaler Rechner
Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
// interaktive Fragen und Antworten
string machine;
IPHostEntry ipHostEntry;
while (true) {
// Eingabe des Namens oder der Adresse IP des gesuchten Rechners
Console.Write("Machine recherchée (rien pour arrêter) : ");
machine = Console.ReadLine().Trim().ToLower();
// Fertig?
if (machine == "") return;
// Ausnahmebehandlung
try {
// Maschinensuche
ipHostEntry = Dns.GetHostEntry(machine);
// Maschinenname
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// die Adressen IP des Rechners
Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
Console.Write(", {0}" , ipHostEntry.AddressList[i]);
}
Console.WriteLine();
// Aliasnamen des Rechners
if (ipHostEntry.Aliases.Length != 0) {
Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
Console.Write(", {0}" , ipHostEntry.Aliases[i]);
}
Console.WriteLine();
}
} catch {
// Der Rechner existiert nicht
Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
}
}
}
}
}
Die Ausführung liefert folgende Ergebnisse:
11.3. Grundlagen der Internetprogrammierung
11.3.1. Allgemeines
Betrachten wir die Kommunikation zwischen zwei entfernten Rechnern A und B:
![]() |
Wenn eine Anwendung AppA auf Rechner A mit einer Anwendung AppB auf Rechner B im Internet kommunizieren möchte, muss sie mehrere Dinge wissen:
- die Adresse IP oder den Namen des Rechners B
- die Portnummer, über die die Anwendung AppB arbeitet. Der Rechner B kann nämlich 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 unterstützten Kommunikationsprotokolle. In unserer Untersuchung werden wir ausschließlich die Protokolle TCP-IP verwenden.
- Das von der Anwendung AppB akzeptierte Dialogprotokoll. Tatsächlich werden die Maschinen A und B miteinander „kommunizieren“. Was sie sich mitteilen, wird in den Protokollen TCP-IP gekapselt. 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 per Telefon kommunizieren: Ihr Gespräch wird über das Telefon übertragen. Die Sprache wird vom Telefon A in Form von Signalen codiert, über Telefonleitungen übertragen, erreicht das Telefon B und wird dort decodiert. Die Person B hört dann Sprache. Hier kommt der Begriff des Kommunikationsprotokolls 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 verwenden werden. Beispielsweise unterscheidet sich der Dialog mit einem Dienst ftp von dem mit einem Dienst pop: Diese beiden Dienste akzeptieren nicht dieselben Befehle. Sie verfügen über ein unterschiedliches Dialogprotokoll.
11.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 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.
- 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.
11.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.
11.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
fermer la connexion
11.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, wäre es während der Dauer des Dienstes nicht mehr für Verbindungsanfragen erreichbar, und die Clients würden dann 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
11.4. Entdecken Sie die Kommunikationsprotokolle des Internets:
11.4.1. Einführung
Wenn sich ein Client mit einem Server verbunden hat, wird anschließend ein Dialog zwischen beiden hergestellt. Die Art dieses Dialogs bildet das sogenannte Kommunikationsprotokoll des Servers. Zu den gängigsten Protokollen im Internet gehören die folgenden:
- HTTP: HyperText Transfer Protocol – das Protokoll für die Kommunikation mit einem Webserver (HTTP-Server)
- SMTP: Simple Mail Transfer Protocol – das Protokoll für die Kommunikation mit einem E-Mail-Versandserver (Server SMTP)
- POP: Post Office Protocol – das Protokoll für die Kommunikation mit einem E-Mail-Speicherserver (Server POP). Hier geht es darum, empfangene E-Mails abzurufen, nicht darum, welche zu versenden.
- FTP: File Transfer Protocol – das Protokoll für die Kommunikation mit einem Dateispeicherserver (Server FTP).
All diese Protokolle zeichnen sich dadurch aus, dass es sich um textbasierte Protokolle handelt: Client und Server tauschen Textzeilen aus. Wenn man über einen Client verfügt, der in der Lage ist:
- eine Verbindung zu einem TCP-Server herzustellen
- die vom Server gesendeten Textzeilen auf der Konsole anzuzeigen
- die Textzeilen, die ein Benutzer eingibt, an den Server zu senden
dann ist man in der Lage, mit einem TCP-Server zu kommunizieren, der ein zeilenbasiertes Protokoll verwendet, sofern man die Regeln dieses Protokolls kennt.
Das Programm „telnet“, das auf Unix- oder Windows-Rechnern zu finden ist, ist ein solcher Client. Auf Windows-Rechnern gibt es außerdem ein Tool namens „PuTTY“, das wir hier verwenden werden. PuTTY kann unter der Adresse [http://www.putty.org/] heruntergeladen werden. Es handelt sich um eine direkt ausführbare Datei (.exe). Wir werden es wie folgt konfigurieren:
![]() |
- [1]: Die Adresse IP des TCP-Servers, mit dem wir uns verbinden möchten, oder dessen Name
- [2]: der Listening-Port des TCP-Servers
- [3]: den Modus Raw verwenden, der eine reine TCP-Verbindung bezeichnet.
- [4]: Den Modus Never aktivieren, um zu verhindern, dass das Client-Fenster putty geschlossen wird, wenn der Server die Verbindung trennt.
- [6,7]: Anzahl der Spalten/Zeilen der Konsole
- [5]: Die maximale Anzahl der im Speicher gehaltenen Zeilen. Ein HTTP-Server kann viele Zeilen senden. Es muss möglich sein, darin zu „scrollen“.
![]() |
- [8,9]: Um die vorherigen Einstellungen beizubehalten, geben Sie der Konfiguration [8] einen Namen und speichern Sie sie mit [9].
- [11,12]: Um eine gespeicherte Konfiguration wiederherzustellen, wählen Sie [11] aus und laden Sie sie unter [12].
Nachdem das Tool nun so konfiguriert ist, wollen wir uns einige Protokolle ansehen: TCP.
11.4.2. Das Protokoll HTTP (HyperText Transfer Protocol)
Verbinden wir unseren Client [1] mit dem Webserver des Rechners istia.univ-angers.fr [2], Port 80 [3]:
![]() |
In der Konsole von putty erstellen wir das folgende Dialogfeld „ “ für HTTP:
- Die Zeilen 1–4 sind die vom Kunden über die Tastatur eingegebene Anfrage
- Die Zeilen 5–19 sind die Antwort des Servers
- Zeile 1: Syntax GET UrlDocument HTTP/1.1 – wir fordern die URL / an, c.a.d. Das Stammverzeichnis der Website [istia.univ-angers.fr].
- Zeile 2: Syntax „Host: Rechner:Port“
- Zeile 3: Syntax „Connection: [mode de la connexion]“. Der Modus „[close]“ weist den Server an, die Verbindung zu schließen, sobald er seine Antwort gesendet hat. Der Modus „[Keep-Alive]“ fordert an, die Verbindung offen zu lassen.
- Zeile 4: leere Zeile. Die Zeilen 1–3 werden als HTTP-Header bezeichnet. Es können neben den hier dargestellten noch weitere vorhanden sein. Das Ende der HTTP-Header wird durch eine leere Zeile markiert.
- Zeilen 5–13: Die HTTP-Header der Serverantwort – enden ebenfalls mit einer leeren Zeile.
- Zeilen 14–19: Das vom Server gesendete Dokument, hier ein Dokument mit dem Namen HTML
- Zeile 5: Syntax HTTP/1.1, Msg-Code – der Code 200 zeigt an, dass das angeforderte Dokument gefunden wurde.
- Zeile 6: Datum und Uhrzeit des Servers
- Zeile 7: Identifikation der Software, die den Webdienst bereitstellt – hier ein Apache-Server unter Linux/Debian
- Zeile 8: Das Dokument wurde dynamisch von PHP generiert
- Zeile 9: Cookie zur Identifizierung des Clients – wenn dieser bei seiner nächsten Verbindung wiedererkannt werden möchte, muss er dieses Cookie in seinen Headern zurücksenden: HTTP.
- Zeile 10: Gibt an, dass der Server nach der Bereitstellung des angeforderten Dokuments die Verbindung schließen wird
- Zeile 11: Das Dokument wird in Teilen (chunked) und nicht als Ganzes übertragen.
- Zeile 12: Art des Dokuments: hier ein Dokument HTML
- Zeile 13: Die leere Zeile, die das Ende der Server-Header „HTTP“ signalisiert
- Zeile 14: Hexadezimalzahl, die die Anzahl der Zeichen im ersten Block des Dokuments angibt. Wenn diese Zahl 0 beträgt (Zeile 19), weiß der Client, dass er das gesamte Dokument empfangen hat.
- Zeilen 15–18: Teil des empfangenen Dokuments.
Die Verbindung wurde geschlossen und der Client putty ist inaktiv. Stellen wir die Verbindung zu [1] wieder her und löschen wir die vorherigen Anzeigen vom Bildschirm [2,3]:
![]() |
Der Dialog lautet diesmal wie folgt:
- Zeile 1: Es wurde ein nicht vorhandenes Dokument angefordert
- Zeile 5: Der Server HTTP hat mit dem Code 404 geantwortet, was bedeutet, dass das angeforderte Dokument nicht gefunden wurde.
Wenn man dieses Dokument mit dem Firefox-Browser aufruft:

Wenn wir den Quellcode von [Affichage/Code source] anzeigen lassen:
Erhalten wir die Zeilen 13–22, die unser Client putty empfangen hat. Der Vorteil dieses Clients besteht darin, dass er uns zusätzlich die Header HTTP der Antwort anzeigt. Diese lassen sich auch mit Firefox abrufen.
11.4.3. Das Protokoll SMTP (Simple Mail Transfer Protocol)
![]() |
Die Server SMTP arbeiten in der Regel auf Port 25 [2]. Die Verbindung wird zum Server [1] hergestellt. Hier sollte man in der Regel einen Server wählen,
aus derselben Domäne IP wie der eigene Rechner wählen, da die Server SMTP meist so konfiguriert sind, dass sie nur Anfragen von Rechnern akzeptieren, die zur selben Domäne gehören wie sie selbst. Außerdem sind Firewalls oder Antivirenprogramme auf privaten Rechnern häufig so konfiguriert, dass sie keine Verbindungen zum Port 25 eines externen Rechners zulassen. In diesem Fall kann es erforderlich sein, diese Firewall oder dieses Antivirenprogramm neu zu konfigurieren.
Der Dialog SMTP im Client-Fenster putty sieht wie folgt aus:
Nachfolgend ist (D) eine Anfrage des Clients und (R) eine Antwort des Servers aufgeführt.
- Zeile 1: (R) Begrüßungsmeldung des Servers SMTP
- Zeile 2: (D) Befehl HELO zum Begrüßen
- Zeile 3: (R) Antwort des Servers
- Zeile 4: (D) Absenderadresse, z. B. E-Mail von: someone@gmail.com
- Zeile 5: (R) Antwort des Servers
- Zeile 6: (D) Empfängeradresse, z. B. „rcpt to: someoneelse@gmail.com“
- Zeile 7: (R) Antwort des Servers
- Zeile 8: (D) kennzeichnet den Beginn der Nachricht
- Zeile 9: (R) Antwort des Servers
- Zeilen 10–12: (D) Die zu sendende Nachricht, abgeschlossen durch eine Zeile, die nur einen Punkt enthält.
- Zeile 13: (R) Antwort des Servers
- Zeile 14: (D) Der Client signalisiert, dass er fertig ist
- Zeile 15: (R) Antwort des Servers, der anschließend die Verbindung schließt
11.4.4. Das Protokoll POP (Post Office Protocol)
![]() |
Die Server POP arbeiten in der Regel auf dem Port 110 [2]. Man stellt eine Verbindung zum Server [1] her. Der Dialog POP im Client-Fenster putty sieht wie folgt aus:
- Zeile 1: (R) Begrüßungsmeldung des Servers POP
- Zeile 2: (D) Der Client gibt seine Kennung an: POP, c.a.d. Das Login, mit dem er seine E-Mails abruft
- Zeile 3: (R) Antwort des Servers
- Zeile 4: (D) Das Passwort des Clients
- Zeile 5: (R) Die Antwort des Servers
- Zeile 6: (D) Der Client fordert die Liste seiner E-Mails an
- Zeilen 7–12: (R) Die Liste der Nachrichten im Postfach des Kunden in der Form [N° du message taille en octets du message]
- Zeile 13: (D) Die Nachricht Nr. 64 wird angefordert
- Zeilen 14–25: (R) Nachricht Nr. 64, wobei die Zeilen 15–22 die Kopfzeilen der Nachricht und die Zeilen 23–24 den Nachrichtentext enthalten.
- Zeile 26: (D) Der Client gibt an, dass er fertig ist
- Zeile 27: (R) Antwort des Servers, der anschließend die Verbindung schließt.
11.4.5. Das Protokoll FTP (File Transfer Protocol)
Das Protokoll FTP ist komplexer als die zuvor vorgestellten Protokolle. Um die zwischen Client und Server ausgetauschten Textzeilen zu ermitteln, kann ein Tool wie FileZilla [http://www.filezilla.fr/] verwendet werden.
![]() |
FileZilla ist ein FTP-Client, der eine Windows-Oberfläche für die Dateiübertragung bietet. Die Aktionen des Benutzers auf der Windows-Oberfläche werden in Befehle FTP umgewandelt, die in [1] protokolliert werden. Dies ist eine gute Möglichkeit, die Befehle des Protokolls FTP kennenzulernen.
11.5. Die .NET-Klassen der Internetprogrammierung
11.5.1. Die passende Klasse auswählen
Das Framework .NET bietet verschiedene Klassen für die Arbeit mit dem Netzwerk:
![]() |
- Die Socket-Klasse arbeitet am nächsten am Netzwerk. Sie ermöglicht eine detaillierte Verwaltung der Netzwerkverbindung. Der Begriff socket bezeichnet eine Steckdose. Der Begriff wurde erweitert, um eine softwarebasierte Netzwerkverbindung zu bezeichnen. Bei einer TCP-IP-Kommunikation zwischen zwei Rechnern A und B kommunizieren zwei sockets miteinander. Eine Anwendung kann direkt mit den sockets arbeiten. Dies ist bei der oben genannten Anwendung A der Fall. Ein Socket kann ein client- oder ein serveur-Socket sein.
- Wenn man auf einer weniger detaillierten Ebene als der der Klasse Socket arbeiten möchte, kann man die Klassen
- TcpClient verwenden, um einen TCP-Client
- TcpListener, um einen TCP-Server zu erstellen
Diese beiden Klassen bieten der Anwendung, die sie nutzt, eine vereinfachte Sicht auf die Netzwerkkommunikation, indem sie die technischen Details der Socket-Verwaltung für sie übernehmen.
- .NET bietet protokollspezifische Klassen:
- die Klasse SmtpClient zur Verwaltung des Protokolls SMTP für die Kommunikation mit einem Server SMTP zum Versenden von E-Mails
- die Klasse WebClient zur Verwaltung der Protokolle HTTP oder FTP für die Kommunikation mit einem Webserver.
Es ist zu beachten, dass die Klasse Socket für sich allein ausreicht, um die gesamte TCP/IP-Kommunikation zu verwalten, man wird jedoch vor allem versuchen, die übergeordneten Klassen zu verwenden, um das Schreiben der TCP/IP-Anwendung zu vereinfachen.
11.5.2. Die Klasse TcpClient
Die Klasse TcpClient ist in den meisten Fällen die geeignete Klasse, um den Client für einen Dienst TCP zu erstellen. Zu ihren Konstruktoren C, Methoden M und Eigenschaften P gehören die folgenden:
C | Erstellt eine TCP-Verbindung mit dem Dienst, der auf dem angegebenen Port (port) des angegebenen Rechners (hostname) läuft. Beispiel: new TcpClient("istia.univ-angers.fr", 80), um eine Verbindung zum Port 80 des Rechners istia.univ-angers.fr herzustellen | |
P | der vom Client zur Kommunikation mit dem Server verwendete Socket. | |
M | erhält einen Lese- und Schreib-Stream zum Server. Dieser Stream ermöglicht den Datenaustausch zwischen Client und Server. | |
M | schließt die Verbindung. Der Socket und der Stream NetworkStream werden ebenfalls geschlossen | |
P | wahr, wenn die Verbindung hergestellt wurde |
Die Klasse NetworkStream repräsentiert den Netzwerkdatenstrom zwischen Client und Server. Sie ist von der Klasse Stream abgeleitet. Viele Client-Server-Anwendungen tauschen Textzeilen aus, die mit den Zeilenendezeichen „\r\n“ enden. Daher ist es sinnvoll, die Objekte StreamReader und StreamWriter zu verwenden, um diese Zeilen im Netzwerkstrom zu lesen und zu schreiben. Wenn also ein Rechner M1 mithilfe eines Objekts TcpClient client1 eine Verbindung zu einem Rechner M2 hergestellt hat und beide Textzeilen austauschen, kann sie ihre Lese- und Schreibströme wie folgt erstellen:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
Die Anweisung
bedeutet, dass der Schreibstrom von client1 nicht über einen Zwischenspeicher geleitet wird, sondern direkt ins Netzwerk gelangt. Dieser Punkt ist wichtig. Normalerweise erwartet client1 eine Antwort, wenn es eine Textzeile an seinen Partner sendet. Diese Antwort wird jedoch niemals eintreffen, wenn die Zeile tatsächlich auf dem Rechner M1 gepuffert und nie an den Rechner M2 gesendet wurde.
Um eine Textzeile an den Rechner M2 zu senden, schreibt man:
Um die Antwort von M2 auszulesen, schreibt man:
Wir verfügen nun über die notwendigen Elemente, um die Grundarchitektur eines Internet-Clients zu entwerfen, der das folgende grundlegende Kommunikationsprotokoll mit dem Server verwendet:
- Der Client sendet eine Anfrage, die in einer einzigen Zeile enthalten ist
- Der Server sendet eine Antwort, die in einer einzigen Zeile enthalten ist
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// Es wird eine Verbindung zum Dienst hergestellt
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// Ausgabestrom nicht gepuffert
writer.AutoFlush = true;
// Anfrage-Antwort-Schleife
while (true) {
// Die Anfrage kommt von der Tastatur
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// Fertig?
if (demande.Trim().ToLower() == "bye")
break;
// Die Anfrage wird an den Server gesendet
writer.WriteLine(demande);
// Die Antwort vom Server wird gelesen
réponse = reader.ReadLine();
// Die Antwort wird verarbeitet
...
}
}
}
}
}
} catch (Exception e) {
// Fehler
...
}
}
}
}
- Zeile 11: Herstellung der Verbindung durch den Client – die Anweisung using stellt sicher, dass die damit verbundenen Ressourcen beim Verlassen von using freigegeben werden.
- Zeile 12: Öffnen des Netzwerk-Streams in einer Klausel using
- Zeile 13: Erstellung und Nutzung des Lesestreams in einer Klausel using
- Zeile 14: Erstellung und Nutzung des Schreibstroms in einer Klausel using
- Zeile 16: Den Ausgabestrom nicht puffern
- Zeilen 18–31: Der Zyklus „Client-Anfrage / Server-Antwort“
- Zeile 26: Der Client sendet seine Anfrage an den Server
- Zeile 28: Der Client wartet auf die Antwort des Servers. Dies ist ein blockierender Vorgang, ähnlich wie das Einlesen von der Tastatur. Das Warten endet mit dem Eintreffen einer Zeichenkette, die mit „\n“ endet, oder mit dem Ende des Datenstroms. Letzteres tritt ein, wenn der Server die mit dem Client geöffnete Verbindung schließt.
11.5.3. Die Klasse TcpListener
Die Klasse TcpListener ist in den meisten Fällen die geeignete Klasse, um einen Dienst TCP zu erstellen. Sie verfügt unter ihren Konstruktoren C, Methoden M und Eigenschaften P über folgende:
C | Erstellt einen Dienst TCP, der auf einem als Parameter übergebenen Port (port), dem sogenannten Listening-Port, auf Client-Anfragen wartet (listen). Ist der Rechner mit mehreren Netzwerken verbunden (IP), hört der Dienst in jedem dieser Netzwerke. | |
C | Das Gleiche, jedoch erfolgt das Abhören nur auf der angegebenen IP-Adresse. | |
M | Startet das Abhören von Client-Anfragen | |
M | akzeptiert die Anfrage eines Kunden. Daraufhin wird eine neue Verbindung zu diesem Kunden hergestellt, die als Serviceverbindung bezeichnet wird. Der serverseitig verwendete Port wird zufällig vom System ausgewählt. Er wird als Serviceport bezeichnet. AcceptTcpClient gibt als Ergebnis das Objekt TcpClient zurück, das serverseitig der Serviceverbindung zugeordnet ist. | |
M | beendet das Abhören von Client-Anfragen | |
P | Der Listening-Socket des Servers |
Die Grundstruktur eines TCP-Servers, der mit seinen Clients gemäß dem folgenden Protokoll kommunizieren würde:
- Der Client sendet eine Anfrage, die in einer einzigen Zeile enthalten ist
- Der Server sendet eine Antwort, die in einer einzigen Zeile enthalten ist
könnte wie folgt aussehen:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace ... {
public class ... {
...
// Der Listening-Dienst wird erstellt
TcpListener ecoute = null;
try {
// Der Dienst wird erstellt – er wird auf allen Netzwerkschnittstellen des Rechners lauschen
ecoute = new TcpListener(IPAddress.Any, port);
// Er wird gestartet
ecoute.Start();
// Dienstschleife
TcpClient tcpClient = null;
// Endlosschleife – wird mit Strg+C beendet
while (true) {
// Warten auf einen Client
tcpClient = ecoute.AcceptTcpClient();
// Der Dienst wird von einer anderen Aufgabe übernommen
ThreadPool.QueueUserWorkItem(Service, tcpClient);
// nächster Client
}
} catch (Exception ex) {
// Fehler wird gemeldet
...
} finally {
// Ende des Dienstes
ecoute.Stop();
}
}
// -------------------------------------------------------
// Bedient einen Kunden
public static void Service(Object infos) {
// Der zu bedienende Kunde wird abgerufen
Client client = infos as Client;
// Auswertung der Verbindung TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// nicht gepufferter Ausgabestrom
writer.AutoFlush = true;
// Schleife: Leseanforderung/Schreibantwort
bool fini=false;
while (! fini) != null) {
// Warten auf Client-Anfrage – blockierender Vorgang
demande=reader.ReadLine();
// Antwortvorbereitung
réponse=...;
// Antwort an den Client senden
writer.WriteLine(réponse);
// nächste Anfrage
}
}
}
}
}
} catch (Exception e) {
// Fehler
...
} finally {
// Ende des Kundenvorgangs
...
}
}
}
}
- Zeile 14: Der Listening-Dienst wird für einen bestimmten Port und eine bestimmte IP-Adresse erstellt. Man muss hier bedenken, dass ein Rechner mindestens zwei Adressen IP hat: die Adresse „127.0.0.1“, die seine Loopback-Adresse ist, und die Adresse „I1.I2.I3.I4“, die er in dem Netzwerk hat, mit dem er verbunden ist. Sie kann weitere Adressen IP haben, wenn sie mit mehreren Netzwerken IP verbunden ist. IPAddress.Any bezeichnet alle Adressen IP eines Rechners.
- Zeile 16: Der Listening-Dienst wird gestartet. Er war zuvor bereits erstellt worden, befand sich jedoch noch nicht im Listening-Modus. „Listening“ bedeutet, auf Anfragen von Clients zu warten.
- Zeilen 20–26: Die Schleife „Warten auf Client-Anfrage / Client-Bedienung“ wird für jeden neuen Client wiederholt
- Zeile 22: Die Anfrage eines Kunden wird angenommen. Die Methode AcceptTcpClient gibt eine sogenannte Service-Instanz TcpClient zurück:
- Der Kunde hat seine Anfrage mit seiner eigenen clientseitigen Instanz TcpClient gestellt, die wir TcpClientDemande nennen werden
- Der Server akzeptiert diese Anfrage mit AcceptTcpClient. Diese Methode erstellt eine serverseitige Instanz TcpClient, die wir TcpClientService nennen werden. Nun besteht eine offene TCP-Verbindung, an deren beiden Enden die Instanzen TcpClientDemande <--> TcpClientService stehen.
- Die anschließende Client-Server-Kommunikation erfolgt über diese Verbindung. Der Listening-Dienst ist nicht mehr beteiligt.
- Zeile 24: Damit der Server mehrere Clients gleichzeitig bedienen kann, wird der Dienst durch Threads gewährleistet, wobei pro Client ein Thread zur Verfügung steht.
- Zeile 32: Der Listening-Dienst wird geschlossen
- Zeile 38: Die vom Service-Thread für einen Client ausgeführte Methode. Sie erhält als Parameter die Instanz TcpClient, die bereits mit dem zu bedienenden Client verbunden ist.
- Zeilen 38–71: Hier findet sich ein Code, der dem des zuvor behandelten einfachen TCP-Clients ähnelt.
11.6. Beispiele für TCP-Clients und -Server
11.6.1. Ein Echo-Server
Wir wollen einen Echo-Server schreiben, der über ein DOS-Fenster mit dem folgenden Befehl gestartet wird:
ServeurEcho port
Der Server läuft auf dem als Parameter übergebenen Port. Er sendet lediglich die Anfrage, die der Client an ihn gesendet hat, an diesen zurück. Das Programm sieht wie folgt aus:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
// Aufruf: serveurEcho Port
// Echo-Server
// sendet die vom Client gesendete Zeile an diesen zurück
namespace Chap9 {
public class ServeurEcho {
public const string syntaxe = "Syntaxe : [serveurEcho] port";
// Hauptprogramm
public static void Main(string[] args) {
// Gibt es ein Argument?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// Dieses Argument muss eine ganze Zahl > 0 sein
int port = 0;
if (!int.TryParse(args[0], out port) || port<=0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
return;
}
// Der Listening-Dienst wird erstellt
TcpListener ecoute = null;
int numClient = 0; // nächste Kundennummer
try {
// Der Dienst wird erstellt – er wird auf allen Netzwerkschnittstellen des Rechners lauschen
ecoute = new TcpListener(IPAddress.Any, port);
// Er wird gestartet
ecoute.Start();
// Weiterverfolgung
Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
// Dienst-Threads
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// Dienstschleife
TcpClient tcpClient = null;
// Endlosschleife – wird mit Strg+C beendet
while (true) {
// Warten auf einen Client
tcpClient = ecoute.AcceptTcpClient();
// Der Dienst wird von einer anderen Aufgabe übernommen
ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// nächster Client
numClient++;
}
} catch (Exception ex) {
// Der Fehler wird gemeldet
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
} finally {
// Ende des Dienstes
ecoute.Stop();
}
}
// -------------------------------------------------------
// Bedient einen Client des Echo-Servers
public static void Service(Object infos) {
// Der zu bedienende Client wird abgerufen
Client client = infos as Client;
// erbringt den Dienst für den Client
Console.WriteLine("Début de service au client {0}", client.NumClient);
// Verbindungsauswertung TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// ungepufferter Ausgabestrom
writer.AutoFlush = true;
// Schleife „Anfrage lesen/Antwort schreiben“
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// Konsolenüberwachung
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// Echo der Anfrage an den Client
writer.WriteLine("[{0}]", demande);
// Konsolenüberwachung
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// Der Dienst wird beendet, wenn der Client „bye“ sendet
if (demande.Trim().ToLower() == "bye")
break;
}
}
}
}
}
} catch (Exception e) {
// Fehler
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
// Ende des Clients
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
}
// Client-Informationen
internal class Client {
public TcpClient CanalTcp { get; set; } // Verbindung zum Client
public int NumClient { get; set; } // Kundennummer
}
}
Die Struktur des Echo-Servers entspricht der zuvor beschriebenen grundlegenden Architektur von TCP-Servern. Wir werden nur den Teil „Kundenservice“ näher erläutern:
- Zeile 79: Die Anfrage des Kunden wird gelesen
- Zeile 83: Sie wird in eckigen Klammern an den Client zurückgegeben
- Zeile 79: Der Dienst wird beendet, wenn der Client die Verbindung schließt
In einem DOS-Fenster verwenden wir die ausführbare Datei des C#-Projekts:
Anschließend starten wir zwei Clients putty, die wir mit Port 100 des Rechners localhost verbinden:
![]() |
Die Konsolenausgabe des Echo-Servers sieht nun wie folgt aus:
Zuerst sendet Client 1, dann Client 0 die folgenden Texte:
![]() |
- [1]: Client Nr. 1
- [2]: Client Nr. 0
- [3]: die Konsole des Echo-Servers
![]() |
- in [4]: Client 1 trennt die Verbindung mit dem Befehl bye.
- in [5]: Der Server erkennt dies
Der Server kann mit Strg-C beendet werden. Client Nr. 0 erkennt dies dann mit [6].
11.6.2. Ein Client für den Echo-Server
Wir schreiben nun einen Client für den vorherigen Server. Er wird wie folgt aufgerufen:
ClientEcho nomServeur Port
Er verbindet sich mit dem Rechner nomServeur auf dem Port port und sendet dann Textzeilen an den Server, die dieser ihm als Echo zurücksendet.
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
// stellt eine Verbindung zu einem Echo-Server her
// Jede über die Tastatur eingegebene Zeile wird als Echo zurückgegeben
class ClientEcho {
static void Main(string[] args) {
// Syntax
const string syntaxe = "pg machine port";
// Anzahl der Argumente
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// Der Name des Servers wird notiert
string serveur = args[0];
// Der Port muss eine ganze Zahl größer als 0 sein
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// man kann arbeiten
string demande = null; // Anfrage des Clients
string réponse = null; // Antwort des Servers
try {
// Verbindung zum Dienst wird hergestellt
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// Ausgabestrom nicht gepuffert
writer.AutoFlush = true;
// Anfrage-Antwort-Schleife
while (true) {
// Die Anfrage stammt von der Tastatur
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// Fertig?
if (demande.Trim().ToLower() == "bye")
break;
// Die Anfrage wird an den Server gesendet
writer.WriteLine(demande);
// die Antwort vom Server wird gelesen
réponse = reader.ReadLine();
// Die Antwort wird bearbeitet
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
// Fehler
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Die Struktur dieses Clients entspricht der allgemeinen Grundarchitektur, die für Tcp-Clients vorgeschlagen wird. Hier sind die Ergebnisse, die mit der folgenden Konfiguration erzielt wurden:
- Der Server wird auf Port 100 in einem DOS-Fenster gestartet
- Auf demselben Rechner werden zwei Clients in zwei weiteren DOS-Fenstern gestartet
Im Fenster von Client A (Nr. 0) werden folgende Anzeigen angezeigt:
Im Fenster von Client B (Nr. 1):
Im Serverfenster:
Client A Nr. 0 trennt die Verbindung:
Die Serverkonsole:
11.6.3. Ein generischer TCP-Client
Wir werden einen generischen TCP-Client schreiben, der wie folgt gestartet wird: ClientTcpGenerique Server-Port. Er funktioniert ähnlich wie der PuTTY-Client, verfügt jedoch über eine Konsolenoberfläche und bietet keine Konfigurationsoptionen.
In der vorherigen Anwendung 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 der Zyklus „Senden einer einzelnen Zeile an den Server / Empfangen einer einzelnen Zeile vom Server“ nicht immer geeignet. Um Protokolle zu verarbeiten, die komplexer sind als das Echo-Protokoll, verfügt der generische TCP-Client über zwei Threads:
- Der Haupt-Thread liest die über die Tastatur eingegebenen Textzeilen ein und sendet sie an den Server.
- Ein sekundärer Thread arbeitet parallel dazu und ist für das Einlesen der vom Server gesendeten Textzeilen zuständig. Sobald er eine solche Zeile empfängt, gibt er sie auf der Konsole aus. Der Thread wird erst beendet, wenn der Server die Verbindung schließt. Er arbeitet also kontinuierlich.
Der Code lautet wie folgt:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
// erhält als Parameter die Eigenschaften eines Dienstes in der Form: Server Port
// stellt eine Verbindung zum Dienst her
// sendet jede über die Tastatur eingegebene Zeile an den Server
// erstellt einen Thread, um die vom Server gesendeten Textzeilen kontinuierlich einzulesen
class ClientTcpGenerique {
static void Main(string[] args) {
// Syntax
const string syntaxe = "pg serveur port";
// Anzahl der Argumente
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// Der Name des Servers wird notiert
string serveur = args[0];
// Der Port muss eine ganze Zahl > 0 sein
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// Verbindung zum Dienst herstellen
TcpClient tcpClient = null;
try {
tcpClient = new TcpClient(serveur, port);
} catch (Exception ex) {
// Fehler
Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
// Ende
return;
}
// Es wird ein separater Thread gestartet, um die vom Server gesendeten Textzeilen zu lesen
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// Das Einlesen der Tastaturbefehle erfolgt im Haupt-Thread
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string demande = null; // Anfrage des Clients
try {
// Die Client-Verbindung wird verarbeitet
using (tcpClient) {
// Es wird ein Schreibstrom zum Server erstellt
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// Ausgabestrom wird nicht gepuffert
writer.AutoFlush = true;
// Anfrage-Antwort-Schleife
while (true) {
demande = Console.ReadLine();
// Fertig?
if (demande.Trim().ToLower() == "bye")
break;
// Die Anfrage wird an den Server gesendet
writer.WriteLine(demande);
}
}
}
}
} catch (Exception e) {
// Fehler
Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
}
}
// Lesethread Client <-- Server
public static void Receive(object infos) {
// lokale Daten
string réponse = null; // Antwort vom Server
// Erstellung des Eingabestroms
try {
using (TcpClient tcpClient = infos as TcpClient) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// Schleife zum kontinuierlichen Einlesen der Textzeilen aus dem Eingabestrom
while ((réponse = reader.ReadLine()) != null) {
// Konsolenausgabe
Console.WriteLine("<-- {0}", réponse);
}
}
}
}
} catch (Exception ex) {
// Fehler
Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// Das Ende des Lesethreads wird gemeldet
Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
}
}
}
}
- Zeile 34: Der Client stellt eine Verbindung zum Server her
- Zeile 43: Ein Thread zum Einlesen der Textzeilen vom Server wird gestartet. Er muss die Methode Receive aus Zeile 73 ausführen. An diese Methode wird die Instanz TcpClient übergeben, die mit dem Server verbunden wurde.
- Zeilen 57–64: Die Schleife „Tastatureingabe / Befehl an den Server senden“. Die Verarbeitung der Tastatureingaben erfolgt durch den Hauptthread.
- Zeilen 75–98: Die Methode Receive, die vom Thread zum Einlesen der Textzeilen ausgeführt wird. Diese Methode erhält als Parameter die Instanz TcpClient, die mit dem Server verbunden wurde.
- Zeilen 84–87: Die Endlosschleife zum Einlesen der vom Server gesendeten Textzeilen. Sie endet erst, wenn der Server die offene Verbindung zum Client schließt.
Hier sind einige Beispiele, die denen entsprechen, die im Abschnitt 11.4 mit dem Client putty verwendet wurden. Der Client wird in einer DOS-Konsole ausgeführt.
Protokoll HTTP
Der Leser wird gebeten, die Erläuterungen in Abschnitt 11.4.2 noch einmal durchzulesen. Wir gehen hier nur auf die anwendungsspezifischen Aspekte ein:
- Zeile 28: Nach dem Senden von Zeile 27 hat der Server HTTP die Verbindung geschlossen, was zum Beenden des Lesethreads führte. Der Hauptthread, der die über die Tastatur eingegebenen Befehle liest, ist weiterhin aktiv. Der in Zeile 29 über die Tastatur eingegebene Befehl beendet ihn.
Protokoll SMTP
Der Leser wird gebeten, die Erläuterungen in Abschnitt 11.4.3 noch einmal durchzulesen und die anderen Beispiele mit dem Client putty zu testen.
11.6.4. Ein generischer TCP-Server „ “
Nun widmen wir uns einem Server
- , 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 in einem DOS-Fenster mit folgendem Befehl gestartet: ServeurTcpGenerique portEcoute, wobei portEcoute der Port ist, an den sich die Clients verbinden müssen. Die Bedienung der Clients erfolgt über zwei Threads:
- der Haupt-Thread, der:
- die Clients nacheinander und nicht parallel bearbeitet.
- der die vom Benutzer über die Tastatur eingegebenen Zeilen liest und an den Client sendet. Der Benutzer signalisiert mit dem Befehl „bye“, dass er die Verbindung zum Client beendet. Da die Konsole nicht für zwei Clients gleichzeitig genutzt werden kann, bearbeitet unser Server jeweils nur einen Client.
- ein sekundärer Thread, der sich ausschließlich dem Einlesen der vom Client gesendeten Textzeilen widmet
Der Server selbst wird niemals beendet, es sei denn, der Benutzer gibt über die Tastatur „Strg+C“ ein.
Sehen wir uns einige Beispiele an. Der Server wird auf Port 100 gestartet, und wir verwenden den generischen Client von paragraphe11.6.3, um mit ihm zu kommunizieren. Das Client-Fenster sieht wie folgt aus:
Die Zeilen, die mit <-- beginnen, stammen vom Server und sind an den Client gerichtet, die anderen vom Client an den Server. Das Serverfenster sieht wie folgt aus:
Die Zeilen, die mit <-- beginnen, sind vom Client an den Server gesendet worden, die anderen vom Server an den Client. Zeile 9 zeigt an, dass der Thread zum Lesen der Client-Anfragen angehalten wurde. Der Hauptthread des Servers wartet weiterhin auf über die Tastatur eingegebene Befehle, um diese an den Client zu senden. Man muss daher den Befehl bye aus Zeile 10 über die Tastatur eingeben, um zum nächsten Client zu wechseln. Der Server ist weiterhin aktiv, obwohl Client 1 beendet ist. Wir starten einen zweiten Client für denselben Server:
Das Serverfenster sieht nun wie folgt aus:
Nach der oben genannten Zeile 6 wartet der Server nun auf einen neuen Client. Man kann ihn mit Strg-C beenden.
Simulieren wir nun einen Webserver, indem wir unseren generischen Server auf Port 88 starten:
Öffnen wir nun einen Browser und rufen 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:
![]() |
Sehen wir uns nun das Fenster unseres Servers an:
Wir sehen die vom Browser gesendeten Header HTTP. Dadurch können wir weitere Header HTTP entdecken, die wir bisher noch nicht gesehen haben. Erstellen wir eine Antwort für unseren Client. Der Benutzer am Computer ist hier der eigentliche Server und kann eine Antwort manuell erstellen. Erinnern wir uns an die Antwort eines Webservers aus einem früheren Beispiel:
Versuchen wir, eine ähnliche Antwort zu geben, wobei wir uns auf das absolute Minimum beschränken:
Wir haben uns in unserer Antwort auf die Header HTTP in den Zeilen 1–4 beschränkt. Wir geben die Größe des Dokuments, das wir senden werden (Content-Length), nicht an, sondern beschränken uns darauf, mitzuteilen, dass wir die Verbindung (Connection: close) nach dem Versand des Dokuments schließen werden. Dies reicht für den Browser aus. Sobald er 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. Letztere entspricht den Zeilen 6–9. Der Benutzer schließt anschließend über die Tastatur die Verbindung zum Client, indem er den Befehl bye (Zeile 10) eingibt. Bei diesem Tastaturbefehl schließt der Hauptthread die Verbindung zum Client. Dies löst die Ausnahme in Zeile 11 aus. Der Thread zum Einlesen der Textzeilen vom Client wurde durch das Schließen der Verbindung zum Client abrupt unterbrochen und hat eine Ausnahme ausgelöst. Nach Zeile 12 wartet der Server auf einen neuen Client.
Der Client-Browser zeigt nun Folgendes an:
![]() |
Wenn man oben „Affichage/Source“ eingibt, um zu sehen, was der Browser empfangen hat, erhält man „[2]“, also genau das, was vom generischen Server gesendet wurde.
Der Code des generischen Servers TCP lautet wie folgt:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
public class ServeurTcpGenerique {
public const string syntaxe = "Syntaxe : ServeurGénérique Port";
// Hauptprogramm
public static void Main(string[] args) {
// Gibt es ein Argument?
if (args.Length != 1) {
Console.WriteLine(syntaxe);
Environment.Exit(1);
}
// Dieses Argument muss eine ganze Zahl > 0 sein
int port = 0;
if (!int.TryParse(args[0], out port) || port <= 0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
Environment.Exit(2);
}
// Der Listening-Dienst wird erstellt
TcpListener ecoute = null;
try {
// Der Dienst wird erstellt
ecoute = new TcpListener(IPAddress.Any, port);
// Er wird gestartet
ecoute.Start();
// Weiterverfolgung
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// Warten auf einen Client
Console.WriteLine("Attente du client suivant...");
TcpClient tcpClient = ecoute.AcceptTcpClient();
Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
// Es wird ein separater Thread gestartet, um die vom Client gesendeten Textzeilen zu lesen
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// Das Einlesen der Tastaturbefehle erfolgt im Haupt-Thread
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string réponse = null; // Serverantwort
// Die Client-Verbindung wird genutzt
using (tcpClient) {
// Es wird ein Schreibstrom zum Client erstellt
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// nicht gepufferter Ausgabestrom
writer.AutoFlush = true;
// Schleife zur Erfassung der Tastatureingaben
while (true) {
réponse = Console.ReadLine();
// Fertig?
if (réponse.Trim().ToLower() == "bye")
break;
// Die Anfrage wird an den Client gesendet
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// Fehler melden
Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// Ende des Empfangsvorgangs
ecoute.Stop();
}
}
// Lesethread Server <-- Client
public static void Receive(object infos) {
// lokale Daten
string demande = null; // Anfrage des Clients
string idClient=null; // Client-ID
// Verarbeitung der Client-Verbindung
try {
using (TcpClient tcpClient = infos as TcpClient) {
// Client-Identität
idClient = tcpClient.Client.RemoteEndPoint.ToString();
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// Endlosschleife zum Lesen der Textzeilen aus dem Eingabestrom
while ((demande = reader.ReadLine()) != null) {
// Konsolenausgabe
Console.WriteLine("<-- {0}", demande);
}
}
}
}
} catch (Exception ex) {
// Fehler
Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
} finally {
// Das Ende des Lesethreads wird gemeldet
Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
}
}
}
}
- Zeile 29: Der Listener-Dienst wird erstellt, aber nicht gestartet. Er überwacht alle Netzwerkschnittstellen des Rechners.
- Zeile 31: Der Listening-Dienst wird gestartet
- Zeile 34: Endlosschleife zum Warten auf Clients. Der Benutzer beendet den Server mit Strg+C.
- Zeile 37: Warten auf einen Client – blockierender Vorgang. Wenn der Client eintrifft, stellt die von der Methode AcceptTcpClient zurückgegebene Instanz TcpClient die serverseitige Komponente einer offenen Verbindung mit dem Client dar.
- Zeile 40: Der Lesestrom der Client-Anfragen wird einem separaten Thread übertragen.
- Zeile 45: Verwendung der Verbindung zum Kunden in einer Klausel using, um sicherzustellen, dass sie unter allen Umständen geschlossen wird.
- Zeile 47: Verwendung des Netzwerkstroms in einer using-Klausel
- Zeile 48: Erstellung eines Schreibstroms auf dem Netzwerkstrom in einer Klausel using
- Zeile 50: Der Schreib-Stream wird nicht gepuffert
- Zeilen 52–59: Schleife zur Eingabe der an den Client zu sendenden Befehle über die Tastatur
- Zeile 69: Beendigung des Listening-Dienstes. Diese Anweisung wird hier nie ausgeführt, da der Server mit Strg+C gestoppt wird.
- Zeile 78: Die Methode Receive, die die vom Client gesendeten Textzeilen kontinuierlich auf der Konsole anzeigt. Hier findet sich das wieder, was bereits für den generischen Client TCP besprochen wurde.
11.6.5. Ein Web-Client-
Im vorherigen Beispiel haben wir einige der HTTP-Header gesehen, die ein Browser gesendet hat:
Wir werden einen Web-Client schreiben, dem ein URL als Parameter übergeben wird und der den vom Server gesendeten Text auf dem Bildschirm anzeigt. Wir gehen davon aus, dass dieser das Protokoll HTTP 1.1 unterstützt. Von den oben genannten Headern werden wir nur die folgenden verwenden:
- Der erste Header gibt das gewünschte Dokument an
- der zweite den abgefragten Server
- der dritte, dass der Server die Verbindung nach der Antwort schließen soll.
Wenn wir in Zeile 1 oben GET durch HEAD ersetzen, sendet uns der Server nur die Header HTTP und nicht das in Zeile 1 angegebene Dokument.
Unser Webclient wird wie folgt aufgerufen: ClientWeb URL cmd, wobei URL dasgewü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öchte. Schauen wir uns ein erstes Beispiel an:
- Zeile 1: Wir fordern nur die Kopfzeilen an: HTTP (HEAD)
- Zeilen 2–9: die Antwort des Servers
Wenn wir im Aufruf des Web-Clients GET anstelle von HEAD verwenden, erhalten wir das gleiche Ergebnis wie mit HEAD, zusätzlich jedoch den Hauptteil des angeforderten Dokuments.
Der Code des Web-Clients lautet wie folgt:
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
class ClientWeb {
static void Main(string[] args) {
// Syntax
const string syntaxe = "pg URI GET/HEAD";
// Anzahl der Argumente
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// die angeforderte URI wird notiert
string stringURI = args[0];
string commande = args[1].ToUpper();
// Überprüfung der Gültigkeit von URI
if(! stringURI.StartsWith("http://")){
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI ist falsch
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Überprüfung der Bestellung
if (commande != "GET" && commande != "HEAD") {
// Fehlerhafte Bestellung
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// Verbindung zum Dienst wird hergestellt
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// Ausgabestrom nicht gepuffert
writer.AutoFlush = true;
// URL wird angefordert – Senden der Header HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// die Antwort wird gelesen
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// die Antwort wird auf der Konsole angezeigt
Console.WriteLine(réponse);
}
}
}
}
}
} catch (Exception e) {
// Die Ausnahme wird angezeigt
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Die einzige Neuerung in diesem Programm ist die Verwendung der Klasse „Uri“. Das Programm erhält einen URL (Uniform Resource Locator) oder URI (Uniform Resource Identifier) in der Form http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... Die Klasse „Uri“ ermöglicht es uns, die Zeichenkette des URL in ihre verschiedenen Elemente zu zerlegen.
- Zeilen 26–33: Aus der als Parameter übergebenen Zeichenfolge stringURI wird ein Uri-Objekt erstellt. Wenn die als Parameter übergebene Zeichenfolge „URI“ kein gültiges „URI“ ist (fehlendes Protokoll, fehlender Server usw.), wird eine Ausnahme ausgelöst. So können wir die Gültigkeit des übergebenen Parameters überprüfen. Sobald das Objekt Uri erstellt wurde, haben wir Zugriff auf die verschiedenen Elemente dieser URI. Wenn also das Objekt uri aus dem vorherigen Code anhand der Zeichenfolge http://serveur:port/document?param1=val1¶m2=val2;... erstellt wurde, ergibt sich:
- uri.Host=serveur,
- uri.Port=port,
- uri.Path = document,
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. Ein Webclient, der Weiterleitungen verarbeitet
Der zuvor genannte Webclient unterstützt keine eventuelle Weiterleitung von der von ihm angeforderten URL „URL“. Hier ein Beispiel:
- Zeile 2: Der Code 302 Found weist auf eine Weiterleitung hin. Die Adresse, zu der der Browser weitergeleitet werden soll, befindet sich im Hauptteil des Dokuments, Zeile 16.
Ein zweites Beispiel:
- Zeile 2: Der Code 301 „Moved Permanently“ weist auf eine Weiterleitung hin. Die Adresse, zu der der Browser weitergeleitet werden soll, ist in Zeile 6 im Header „HTTP Location“ angegeben.
Ein drittes Beispiel:
- Zeile 2: Der Code 302 „Moved Temporarily“ weist auf eine Weiterleitung hin. Die Adresse, zu der der Browser weitergeleitet werden soll, ist in Zeile 5 im Header „HTTP Location“ angegeben.
Ein viertes Beispiel mit einem lokalen Server IIS auf dem Rechner:
- Zeile 2: Der Code 302 „Object moved“ weist auf eine Weiterleitung hin. Die Adresse, zu der der Browser weitergeleitet werden soll, ist in Zeile 5 im Header „HTTP Location“ angegeben. Es ist zu beachten, dass die Weiterleitungsadresse im Gegensatz zu den vorherigen Beispielen relativ ist. Die vollständige Adresse lautet tatsächlich http://localhost/localstart.asp.
Wir schlagen vor, Weiterleitungen zu verarbeiten, wenn die erste Zeile der Header „HTTP“ das Schlüsselwort „moved“ (Groß-/Kleinschreibung spielt keine Rolle) enthält und die Weiterleitungsadresse im Header „HTTP Location“ steht.
Wenn wir die letzten drei Beispiele heranziehen, erhalten wir folgende Ergebnisse:
URL: http://www.bull.com
- Zeile 11: Die Weiterleitung erfolgt zur Adresse in Zeile 6
URL: http://www.gouv.fr
- Zeile 11: Die Weiterleitung erfolgt zur Adresse in Zeile 6
URL: http://localhost
- Zeile 13: Die Weiterleitung erfolgt zur Adresse in Zeile 6
- Zeile 15: Der Zugriff auf die Seite http://localhost/localstart.asp wurde uns verweigert.
Das Programm, das die Weiterleitung verwaltet, lautet wie folgt:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Chap9 {
class ClientWebAvecRedirection {
static void Main(string[] args) {
// Syntax
const string syntaxe = "pg URI GET/HEAD";
// Anzahl der Argumente
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// man beachte das angeforderte URI
string stringURI = args[0];
string commande = args[1].ToUpper();
// Überprüfung der Gültigkeit von URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI ist falsch
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Überprüfung der Bestellung
if (commande != "GET" && commande != "HEAD") {
// Fehlerhafte Bestellung
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
const int nbRedirsMax = 1; // Es wird nicht mehr als eine Weiterleitung akzeptiert
int nbRedirs = 0; // Anzahl der laufenden Weiterleitungen
// Regulärer Ausdruck zum Auffinden einer Weiterleitung vom Typ URL
Regex location = new Regex(@"^Location: (.+?)$");
try {
// Es können mehrere URL abgefragt werden, wenn Umleitungen vorhanden sind
while (nbRedirs <= nbRedirsMax) {
// Weiterleitungsverwaltung
bool redir = false;
bool locationFound = false;
string locationString = null;
// Verbindung zum Dienst herstellen
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
// nicht gepufferter Ausgabestrom
writer.AutoFlush = true;
// Anfrage an URL – Senden der Header HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// Die erste Zeile der Antwort wird gelesen
string premièreLigne = reader.ReadLine();
// Bildschirmecho
Console.WriteLine(premièreLigne);
// Weiterleitung?
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
// Es liegt eine Umleitung vor
redir = true;
nbRedirs++;
}
// die folgenden HTTP-Header, bis die leere Zeile gefunden wird, die das Ende der Header signalisiert
string réponse = null;
while ((réponse = reader.ReadLine()) != "") {
// die Antwort wird angezeigt
Console.WriteLine(réponse);
// Wenn eine Weiterleitung vorliegt, wird nach dem Header „Location“ gesucht
if (redir && !locationFound) {
// die aktuelle Zeile wird mit dem relationalen Ausdruck „location“ verglichen
Match résultat = location.Match(réponse);
if (résultat.Success) {
// Wenn ein Treffer gefunden wurde, wird die Umleitungs-ID URL notiert
locationString = résultat.Groups[1].Value;
// Es wird vermerkt, dass ein Treffer gefunden wurde
locationFound = true;
}
}
}
// Die Kopfzeilen HTTP sind erschöpft – es wird eine leere Zeile geschrieben
Console.WriteLine(réponse);
// dann geht man zum Hauptteil des Dokuments über
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// Sind wir fertig?
if (!locationFound || nbRedirs > nbRedirsMax)
break;
// Es muss eine Weiterleitung durchgeführt werden – die neue URI wird erstellt
try {
if (locationString.StartsWith("http")) {
// vollständige HTTP-Adresse
uri = new Uri(locationString);
} else {
// relative HTTP-Adresse zur aktuellen URI
uri = new Uri(uri, locationString);
}
// Konsolenprotokoll
Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
} catch (Exception ex) {
// Problem mit der URI
Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
}
}
} catch (Exception e) {
// Die Ausnahme wird angezeigt
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
Im Vergleich zur vorherigen Version gibt es folgende Änderungen:
- Zeile 46: Der reguläre Ausdruck zum Abrufen der Weiterleitungsadresse aus dem Header „HTTP Location: Adresse“.
- Zeile 49: Der Code, der zuvor für eine einzelne URI ausgeführt wurde, kann nun nacheinander für mehrere URIs ausgeführt werden.
- Zeile 66: Es wird die erste Zeile der vom Server gesendeten HTTP-Header gelesen. Diese enthält das Schlüsselwort moved, falls das angeforderte Dokument verschoben wurde.
- Zeilen 71–75: Es wird geprüft, ob die erste Zeile das Schlüsselwort moved enthält. Wenn ja, wird dies vermerkt.
- Zeilen 79–93: Die übrigen Kopfzeilen HTTP werden gelesen, bis die leere Zeile erreicht wird, die ihr Ende signalisiert. Wenn die erste Zeile eine Weiterleitung angekündigt hat, wird der Header „HTTP Location: Adresse“ ausgewertet, um die Weiterleitungsadresse in „locationString“ zu speichern.
- Zeilen 98–100: Der Rest der Antwort des Servers HTTP wird in der Konsole angezeigt.
- Zeilen 105–106: Die angeforderte URI wurde vollständig ausgewertet und angezeigt. Wenn keine Weiterleitung erforderlich ist oder die zulässige Anzahl an Weiterleitungen überschritten wurde, wird das Programm beendet.
- Zeilen 108–122: Liegt eine Weiterleitung vor, wird die neue anzufragende URI berechnet. Je nachdem, ob die gefundene Weiterleitungsadresse absolut (Zeile 111) oder relativ (Zeile 114) war, sind einige Berechnungen erforderlich.
11.7. Die Klassen .NET, die auf ein bestimmtes Internetprotokoll spezialisiert sind
In den vorangegangenen Beispielen des Web-Clients wurde das Protokoll HTTP mit einem Client TCP verwaltet. Wir mussten also das verwendete spezielle Kommunikationsprotokoll selbst verwalten. Wir hätten analog dazu einen Client SMTP oder POP erstellen können. Das Framework .NET bietet spezialisierte Klassen für die Protokolle HTTP und SMTP. Diese Klassen kennen das Kommunikationsprotokoll zwischen Client und Server und ersparen dem Entwickler die Notwendigkeit, sich darum zu kümmern. Wir stellen sie nun vor.
11.7.1. Die Klasse classeWebClient
Es gibt eine Klasse WebClient, die mit einem Webserver kommunizieren kann. Betrachten wir das Beispiel des Web-Clients aus Abschnitt 11.6.5, das hier mit der Klasse WebClient behandelt wird.
using System;
using System.IO;
using System.Net;
namespace Chap9 {
public class Program {
public static void Main(string[] args) {
// Syntax: [prog] URI
const string syntaxe = "pg URI";
// Anzahl der Argumente
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// Der angeforderte URI wird vermerkt
string stringURI = args[0];
// Überprüfung der Gültigkeit von URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI ist falsch
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
try {
// Anlegen eines Web-Kunden
using (WebClient client = new WebClient()) {
// Hinzufügen eines Headers HTTP
client.Headers.Add("user-agent", "st");
using (Stream stream = client.OpenRead(uri)) {
using (StreamReader reader = new StreamReader(stream)) {
// Anzeige der Antwort des Webservers
Console.WriteLine(reader.ReadToEnd());
// Anzeige der Antwort-Header des Servers
Console.WriteLine("---------------------");
foreach (string clé in client.ResponseHeaders.Keys) {
Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
}
Console.WriteLine("---------------------");
}
}
}
} catch (WebException e1) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
} catch (Exception e2) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
}
}
}
}
- Zeile 35: Der Web-Client wird erstellt, ist aber noch nicht konfiguriert
- Zeile 37: Der Anfrage HTTP, die gestellt werden soll, wird ein Header HTTP hinzugefügt. Wir werden sehen, dass standardmäßig weitere Header gesendet werden.
- Zeile 38: Der Webclient fordert die vom Benutzer angegebene URI an und liest das gesendete Dokument. [WebClient].OpenRead(Uri) stellt eine Verbindung zu Uri her und liest die Antwort. Darin liegt der Nutzen dieser Klasse. Sie übernimmt den Dialog mit dem Webserver. Das Ergebnis der Methode OpenRead ist vom Typ Stream und stellt das angeforderte Dokument dar. Die vom Server gesendeten Header HTTP, die in der Antwort dem Dokument vorangestellt sind, sind nicht Teil davon.
- Zeile 39: Es wird ein StreamReader verwendet, und in Zeile 41 wird dessen Methode ReadToEnd aufgerufen, um die gesamte Antwort auszulesen.
- Zeilen 44–46: Es werden die HTTP-Header der Serverantwort angezeigt. [WebClient].ResponseHeaders stellt eine bewertete Sammlung dar, deren Schlüssel die Namen der Header HTTP sind und deren Werte die diesen Headern zugeordneten Zeichenfolgen sind.
- Zeile 51: Die Ausnahmen, die während eines Client-Server-Austauschs ausgelöst werden, sind vom Typ WebException.
Sehen wir uns einige Beispiele an.
Wir starten den generischen Server TCP, der in Abschnitt 6.4.6 erstellt wurde:
Wir starten den vorherigen Web-Client wie folgt:
Die angeforderte URI ist die des generischen Servers. Dieser zeigt daraufhin die Header HTTP an, die ihm der Webclient gesendet hat:
Man sieht also:
- dass der Webclient standardmäßig drei Header HTTP sendet (Zeilen 3, 5, 6)
- Zeile 4: den Header, den wir selbst generiert haben (Zeile 37 des Codes)
- dass der Webclient standardmäßig die Methode GET verwendet (Zeile 3). Es gibt weitere Methoden, darunter POST und HEAD.
Nun fordern wir eine nicht vorhandene Ressource an:
- Zeile 2: Es ist eine Ausnahme vom Typ WebException aufgetreten, da der Server mit dem Statuscode 404 Not Found geantwortet hat, um anzuzeigen, dass die angeforderte Ressource nicht existiert.
Schließen wir ab, indem wir eine vorhandene Ressource abfragen:
Die durch den Befehl erzeugte Datei istia.univ-angers.txt sieht wie folgt aus:
- Zeile 1: das angeforderte Dokument HTML.
- Zeilen 3–10: Die Header der Antwort HTTP in einer Reihenfolge, die nicht unbedingt der Reihenfolge entspricht, in der sie gesendet wurden.
Die Klasse WebClient verfügt über Methoden zum Empfangen eines Dokuments (Methoden DownLoad) oder zum Senden eines Dokuments (Methoden UpLoad):
zum Herunterladen einer Ressource als Byte-Array (z. B. Bild) | |
um eine Ressource herunterzuladen und in einer lokalen Datei zu speichern | |
um eine Ressource herunterzuladen und als Zeichenkette abzurufen (z. B. eine HTML-Datei) | |
das Gegenstück zu OpenRead, jedoch zum Senden von Daten an den Server | |
das Pendant zu DownLoadData, jedoch zum Server | |
das Gegenstück zu DownLoadFile, jedoch zum Server | |
das Gegenstück zu DownLoadString, jedoch zum Server | |
um die Daten eines Befehls POST an den Server zu senden und die Ergebnisse in Form eines Byte-Arrays abzurufen. Der Befehl POST fordert ein Dokument an und übermittelt dem Server gleichzeitig Informationen, die dieser benötigt, um das tatsächlich zu sendende Dokument zu ermitteln. Diese Informationen werden als Dokument an den Server gesendet, daher der Name UpLoad der Methode. Sie werden hinter der Leerzeile der Kopfzeilen HTTP in der Form param1=valeur1¶m2=valeur2&... gesendet:
Dasselbe Dokument könnte mit der Methode GET angefordert werden:
Der Unterschied zwischen den beiden Methoden besteht darin, dass der Browser, der die angeforderte URI anzeigt, im Fall von POST den Wert /document und im Fall von GET den Wert /document?param1=valeur1¶m2=valeur2&... anzeigt. |
11.7.2. Die Klassen WebRequest / WebResponse
Manchmal ist die Klasse WebClient nicht flexibel genug, um das Gewünschte zu erreichen. Nehmen wir noch einmal das in Abschnitt 11.6.6 behandelte Beispiel des Web-Clients mit Weiterleitung auf. Wir müssen den Header HTTP senden:
Wir haben gesehen, dass die vom Web-Client standardmäßig gesendeten Header HTTP wie folgt lauten:
Wir haben außerdem gesehen, dass es möglich ist, den zuvor genannten Headern mit der Sammlung [WebClient].Headers weitere HTTP-Header hinzuzufügen. Nur Zeile 1 ist kein Header, der zur Sammlung Headers gehört, da sie nicht die Form „Schlüssel:Wert“ aufweist. Ich habe nicht herausgefunden, wie man in Zeile 1 den Wert „GET“ in „HEAD“ ändern kann, ausgehend von der Klasse „WebClient“ (vielleicht habe ich nicht richtig gesucht?). Wenn die Klasse WebClient ihre Grenzen erreicht hat, kann man zu den Klassen WebRequest / WebResponse wechseln:
- WebRequest: stellt die gesamte Anfrage des Web-Clients dar.
- WebResponse: stellt die gesamte Antwort des Webservers dar
Wir haben bereits erwähnt, dass die Klasse WebClient die Schemata http:, https:, ftp: und file: verarbeitet. Die Anfragen und Antworten dieser verschiedenen Protokolle haben nicht dieselbe Form. Daher ist es notwendig, den genauen Typ dieser Elemente zu verwenden, anstatt ihrer generischen Typen WebRequest und WebResponse. Daher werden wir die folgenden Klassen verwenden:
- HttpWebRequest, HttpWebResponse für einen Client HTTP
- FtpWebRequest, FtpWebResponse für einen Kunden FTP
Wir behandeln nun mit den Klassen HttpWebRequest und HttpWebresponse das in Abschnitt 11.6.6 behandelte Beispiel des Web-Clients mit Weiterleitung. Der Code lautet wie folgt:
using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
namespace Chap9 {
class WebRequestResponse {
static void Main(string[] args) {
// Syntax
const string syntaxe = "pg URI GET/HEAD";
// Anzahl der Argumente
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// die angeforderte URI wird notiert
string stringURI = args[0];
string commande = args[1].ToUpper();
// Überprüfung der Gültigkeit von URI
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI ist falsch
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// Überprüfung der Bestellung
if (commande != "GET" && commande != "HEAD") {
// Fehlerhafte Bestellung
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// Die Anfrage wird konfiguriert
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// wird ausgeführt
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// Ergebnis
Console.WriteLine("---------------------");
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
// Kopfzeilen HTTP
Console.WriteLine("---------------------");
foreach (string clé in httpWebResponse.Headers.Keys) {
Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
}
Console.WriteLine("---------------------");
// Dokument
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
// die Antwort wird auf der Konsole angezeigt
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// die Antwort wird abgerufen
HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
} catch (Exception e2) {
// Die Ausnahme wird angezeigt
Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
}
}
}
}
- Zeile 40: Ein Objekt vom Typ WebRequest wird mit der statischen Methode WebRequest.Create(Uri uri) erstellt, wobei uri die URI des herunterzuladenden Dokuments ist. Da bekannt ist, dass das URI-Protokoll HTTP lautet, wird der Typ des Ergebnisses in HttpWebRequest geändert, um Zugriff auf die spezifischen Elemente des HTTP-Protokolls zu erhalten.
- Zeile 41: Wir legen die Methode GET / POST / HEAD aus der ersten Zeile der Header HTTP fest. Hier wäre dies GET oder HEAD.
- Zeile 42: In einem privaten Unternehmensnetzwerk ist es üblich, dass die Rechner des Unternehmens aus Sicherheitsgründen vom Internet isoliert sind. Zu diesem Zweck verwendet das private Netzwerk Internetadressen, die von den Internet-Routern nicht weitergeleitet werden. Das private Netzwerk ist über spezielle Rechner, sogenannte Proxys, mit dem Internet verbunden, die sowohl mit dem privaten Unternehmensnetzwerk als auch mit dem Internet verbunden sind. Dies ist ein Beispiel für Rechner mit mehreren Adressen: IP. Ein Rechner im privaten Netzwerk kann nicht selbst eine Verbindung zu einem Server im Internet, beispielsweise einem Webserver, herstellen. Er muss einen Proxy-Rechner bitten, dies für ihn zu tun. Ein Rechner mit der Adresse proxy kann Server mit der Adresse proxy für verschiedene Protokolle hosten. Als HTTP-Proxy bezeichnet man den Dienst, der die HTTP-Anfragen im Auftrag der Rechner im privaten Netzwerk abwickelt. Wenn ein solcher Proxy-Server HTTP vorhanden ist, muss er im Feld [WebRequest].proxy angegeben werden. Man schreibt beispielsweise:
, wenn der Proxy HTTP auf Port 3128 des Rechners pproxy.istia.uang läuft. Das Feld [WebRequest].proxy wird mit null belegt, wenn der Rechner direkten Zugang zum Internet hat und keinen Proxy benötigt.
- Zeile 44: Die Methode GetResponse() fordert das durch seine URI identifizierte Dokument an und gibt ein Objekt WebRequestResponse zurück, das hier in ein Objekt HttpWebResponse umgewandelt wird. Dieses Objekt stellt die Antwort des Servers auf die Anfrage nach dem Dokument dar.
- Zeile 47:
- [HttpWebResponse].ResponseUri: Dies ist die URI des Servers, der das Dokument gesendet hat. Im Falle einer Weiterleitung kann diese von der URI des ursprünglich abgefragten Servers abweichen. Es ist zu beachten, dass der Code keine Weiterleitung unterstützt. Diese wird automatisch von der Methode GetResponse verwaltet. Auch dies ist ein Vorteil der übergeordneten Klassen gegenüber den Basisklassen des TCP-Protokolls.
- [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription stellen die erste Zeile der Antwort dar, zum Beispiel: HTTP/1.1 200 OK. StatusCode ist 200 und StatusDescription ist OK.
- Zeile 50: [HttpWebResponse].Headers ist die Sammlung der Header HTTP der Antwort.
- Zeile 55: [HttpWebResponse].GetResponseStream: Dies ist der Datenstrom, über den das in der Antwort enthaltene Dokument abgerufen werden kann.
- Zeile 61: Es kann eine Ausnahme vom Typ WebException auftreten
- Zeile 63: [WebException].Response ist die Antwort, die zum Auslösen der Ausnahme geführt hat.
Hier ein Ausführungsbeispiel:
- Zeilen 1 und 3: Der Server, der geantwortet hat, ist nicht derselbe wie der, der abgefragt wurde. Es hat also eine Umleitung stattgefunden.
- Zeilen 5–11: Die vom Server gesendeten Header HTTP
11.7.3. Anwendung: Ein Proxy-Client für einen Web-Übersetzungsserver
Wir zeigen nun, wie wir mit den zuvor vorgestellten Klassen die Ressourcen des Webs nutzen können.
11.7.3.1. L'application
Im Web gibt es Übersetzungswebsites. Die hier verwendete Website ist http://trans.voila.fr/traduction_voila.php:
![]() | Der zu übersetzende Text wird in [1] eingegeben, die Übersetzungsrichtung wird in [2] ausgewählt. Die Übersetzung wird über [3] angefordert und in [4] erhalten. |
Wir werden eine Windows-Client-Anwendung für die oben genannte Anwendung schreiben. Sie wird nicht mehr leisten als die Anwendung auf der Website [trans.voila.fr]. Ihre Benutzeroberfläche wird wie folgt aussehen:
![]() |
11.7.3.2. Die Architektur der Anwendung
Die Anwendung wird die folgende zweischichtige Architektur aufweisen:
![]() |
11.7.3.3. Das Visual Studio-Projekt
Das Visual Studio-Projekt sieht wie folgt aus:
![]() |
- In [1] besteht die Lösung aus zwei Projekten:
- [2]: eines für die Schicht [dao] und die von dieser verwendeten Entitäten,
- [3]: das andere für die Windows-Oberfläche
11.7.3.4. Das Projekt [dao]
Das Projekt [dao] besteht aus folgenden Elementen:
- IServiceTraduction.cs: die Schnittstelle, die der Schicht [ui] zur Verfügung gestellt wird
- ServiceTraduction: die Implementierung dieser Schnittstelle
- WebTraductionsException: eine anwendungsspezifische Ausnahme
Die Schnittstelle IServiceTraduction lautet wie folgt:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// verwendete Sprachen
IDictionary<string, string> LanguesTraduites { get; }
// Übersetzung
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- Zeile 6: Die Eigenschaft LanguesTraduites gibt das Wörterbuch der vom Übersetzungsserver unterstützten Sprachen zurück. Dieses Wörterbuch enthält Einträge der Form ["fe","Français-Anglais"], wobei der Wert eine Übersetzungsrichtung angibt – in diesem Fall vom Französischen ins Englische – und der Schlüssel „fe“ ein vom Übersetzungsserver trans.voila.fr verwendeter Code ist.
- Zeile 8: Die Methode Traduire ist die Übersetzungsmethode:
- texte ist der zu übersetzende Text
- deQuoiVersQuoi ist einer der Schlüssel im Wörterbuch der übersetzten Sprachen
- Die Methode liefert die Übersetzung des Textes
ServiceTraduction ist eine Implementierungsklasse der Schnittstelle IServiceTraduction. Wir gehen im folgenden Abschnitt näher darauf ein.
WebTraductionsException ist die folgende Ausnahmeklasse:
using System;
namespace entites {
public class WebTraductionsException : Exception {
// Fehlercode
public int Code { get; set; }
// Hersteller
public WebTraductionsException() {
}
public WebTraductionsException(string message)
: base(message) {
}
public WebTraductionsException(string message, Exception e)
: base(message, e) {
}
}
}
- Zeile 7: ein Fehlercode
11.7.3.5. Der Web-Client [ServiceTraduction]
Kommen wir noch einmal auf die Architektur unserer Anwendung zurück:
![]() |
Die Klasse [ServiceTraduction], die wir schreiben müssen, ist ein Client des Übersetzungs-Webdienstes [trans.voila.fr]. Um sie zu schreiben, müssen wir verstehen,
- was der Übersetzungsserver von seinem Client erwartet
- was er an seinen Client zurücksendet
Schauen wir uns anhand eines Beispiels den Client-Server-Dialog an, der bei einer Übersetzung stattfindet. Nehmen wir das in der Einleitung der Anwendung vorgestellte Beispiel wieder auf:
![]() | Der zu übersetzende Text wird in [1] eingefügt, die Übersetzungsrichtung wird in [2] ausgewählt. Die Übersetzung wird über [3] angefordert und in [4] erhalten. |
Um die Übersetzung [4] zu erhalten, hat der Browser die folgende Anfrage GET gesendet (die im Adressfeld angezeigt wird):
http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=der+Hund+ist+krank
Sie ist recht einfach zu verstehen:
- http://trans.voila.fr/traduction_voila.php ist die URL des Übersetzungsdienstes
- isText=1 scheint darauf hinzudeuten, dass es sich um Text handelt
- translationDirection bezeichnet die Übersetzungsrichtung, in diesem Fall Français-Anglais
- stext ist der zu übersetzende Text in einer sogenannten URL-kodierten Form. Tatsächlich dürfen bestimmte Zeichen in einer URL nicht vorkommen. Dies gilt beispielsweise für das Leerzeichen, das hier durch ein „+“ kodiert wurde. Das .NET-Framework bietet die statische Methode System.Web.HttpUtility.UrlEncode, um diese Kodierung durchzuführen.
Daraus folgt, dass unsere Klasse [ServiceTraduction] zur Abfrage des Übersetzungsservers die Zeichenfolge
verwenden, wobei die Platzhalter {0} und {1} jeweils durch die Übersetzungsrichtung und den zu übersetzenden Text ersetzt werden.
Woher weiß man, welche Übersetzungsrichtungen der Server akzeptiert? Im obigen Screenshot sind die übersetzten Sprachen in der Dropdown-Liste aufgeführt. Wenn man im Browser (Ansicht / Quelltext) den HTML-Code der Seite betrachtet, findet man für die Dropdown-Liste Folgendes:
Das ist kein besonders sauberer HTML-Code, da jedes <option>-Tag normalerweise durch ein </option>-Tag geschlossen werden sollte. Allerdings liefern uns die „value“-Attribute die Liste der Übersetzungscodes, die an den Server gesendet werden müssen. Im Wörterbuch LanguesTraduites der Schnittstelle IServiceTraduction sind die Schlüssel die oben genannten „value“-Attribute und die Werte die Texte, die in der Dropdown-Liste angezeigt werden.
Schauen wir uns nun (Ansicht / Quelltext) an, wo sich auf der HTML-Seite die vom Übersetzungsserver zurückgegebene Übersetzung befindet:
Die Übersetzung befindet sich mitten in der zurückgegebenen HTML-Seite. Wie lässt sie sich finden? Man kann einen regulären Ausdruck mit der Sequenz <div class="txtTrad">...</div> verwenden, da das Tag <div class="txtTrad"> nur an dieser Stelle der HTML-Seite vorkommt. Der reguläre Ausdruck in C# zum Abrufen des übersetzten Textes lautet wie folgt:
Wir verfügen nun über die notwendigen Elemente, um die Implementierungsklasse ServiceTraduction für die Schnittstelle IServiceTraduction zu schreiben:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;
namespace dao {
public class ServiceTraduction : IServiceTraduction {
// Automatische Konfigurationseigenschaften des Dienstes
public IDictionary<string, string> LanguesTraduites { get; set; }
public string UrlServeurTraduction { get; set; }
public string ProxyHttp { get; set; }
public String RegexTraduction { get; set; }
// Übersetzung
public string Traduire(string texte, string deQuoiVersQuoi) {
// Ist die angeforderte Übersetzung möglich?
if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
}
// zu übersetzender Text
string texteATraduire = HttpUtility.UrlEncode(texte);
// abzufragende URI
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
// Regulärer Ausdruck zum Auffinden der Übersetzung in der Antwort
Regex patternTraduction = new Regex(RegexTraduction);
// Ausnahme
WebTraductionsException exception = null;
// Übersetzung
string traduction = null;
try {
// Die Abfrage wird konfiguriert
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = "GET";
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
// die Abfrage wird ausgeführt
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// Dokument
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
bool traductionTrouvée = false;
string ligne = null;
while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
// Nach Übersetzung in der aktuellen Zeile suchen
MatchCollection résultats = patternTraduction.Matches(ligne);
// Übersetzung gefunden?
if (résultats.Count != 0) {
traduction = résultats[0].Groups[1].Value.Trim();
traductionTrouvée = true;
}
}
// Übersetzung gefunden?
if (!traductionTrouvée) {
exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
}
}
}
} catch (Exception e) {
exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
}
// Ausnahme?
if (exception != null) {
throw exception;
} else {
return traduction;
}
}
}
}
- Zeile 12: Die Eigenschaft LanguesTraduites der Schnittstelle IServiceTraduction – von außen initialisiert
- Zeile 13: Die Eigenschaft UrlServeurTraduction ist die URL, die an den Übersetzungsserver gesendet werden soll: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, wobei der Platzhalter {0} durch die Übersetzungsrichtung und der Platzhalter {1} durch den zu übersetzenden Text ersetzt werden muss – von außen initialisiert
- Zeile 14: Die Eigenschaft ProxyHttp ist der gegebenenfalls zu verwendende HTTP-Proxy, zum Beispiel: pproxy.istia.uang:3128 – von außen initialisiert
- Zeile 15: Die Eigenschaft RegexTraduction ist der reguläre Ausdruck, mit dem die Übersetzung aus dem vom Übersetzungsserver zurückgegebenen HTML-Stream extrahiert wird, zum Beispiel @"<div class=""txtTrad"">(.*?)</div>" – von außen initialisiert
- Diese vier Eigenschaften werden in unserer Anwendung von Spring initialisiert.
- Zeilen 20–22: Es wird überprüft, ob die angeforderte Übersetzungsrichtung tatsächlich im Wörterbuch der übersetzten Sprachen vorhanden ist. Ist dies nicht der Fall, wird eine Ausnahme ausgelöst.
- Zeile 24: Der zu übersetzende Text wird kodiert, damit er Teil einer URL werden kann
- Zeile 26: Die URI des Übersetzungsdienstes wird aufgebaut. Wenn die Eigenschaft UrlServeurTraduction die Zeichenfolge http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, wird der Platzhalter {0} durch die Übersetzungsrichtung und der Platzhalter {1} durch den zu übersetzenden Text ersetzt.
- Zeile 28: Das Suchmuster für die Übersetzung in der HTML-Antwort des Übersetzungsservers wird erstellt.
- Zeilen 33, 60: Die Abfrage des Übersetzungsservers erfolgt in einem try/catch-Block
- Zeile 35: Das Objekt HttpWebRequest, das zur Abfrage des Übersetzungsservers verwendet wird, wird mit der URI des angeforderten Dokuments erstellt.
- Zeile 36: Die Abfragemethode lautet GET. Auf diese Anweisung könnte man verzichten, da GET wahrscheinlich die Standardmethode des Objekts HttpWebRequest ist.
- Zeile 37: Die Eigenschaft Proxy des Objekts HttpWebRequest wird festgelegt.
- Zeile 39: Die Anfrage an den Übersetzungsserver wird gestellt und dessen Antwort vom Typ HttpWebResponse wird abgerufen.
- Zeilen 41–42: Mit einem StreamReader wird jede Zeile der HTML-Antwort des Servers gelesen.
- Zeilen 45–53: In jeder Zeile der Antwort wird nach der Übersetzung gesucht. Sobald sie gefunden wurde, wird das Einlesen der HTML-Antwort beendet und alle geöffneten Datenströme werden geschlossen.
- Zeilen 55–57: Wenn in der HTML-Antwort keine Übersetzung gefunden wurde, wird eine Ausnahme vom Typ WebTraductionsException vorbereitet, um dies zu melden.
- Zeilen 60–62: Wenn während des Client-Server-Austauschs eine Ausnahme aufgetreten ist, wird diese in eine Ausnahme vom Typ WebTraductionsException gekapselt, um dies zu melden.
- Zeilen 64–68: Wenn eine Ausnahme protokolliert wurde, wird sie ausgelöst; andernfalls wird die gefundene Übersetzung zurückgegeben.
Unser Beispiel geht davon aus, dass der HTTP-Proxy keine Authentifizierung erfordert. Wäre dies nicht der Fall, würde man etwa Folgendes schreiben:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
Wir haben hier WebRequest / WebResponse anstelle von WebClient verwendet, da wir nicht die gesamte HTML-Antwort des Übersetzungsservers auswerten müssen. Sobald die Übersetzung in dieser Antwort gefunden wurde, benötigen wir die restlichen Zeilen der Antwort nicht mehr. Die Klasse WebClient lässt dies nicht zu.
Hier ist ein Testprogramm für die Klasse ServiceTraduction:
using System;
using System.Collections.Generic;
using dao;
using entites;
namespace ui {
class Program {
static void Main(string[] args) {
try {
// Übersetzungsdienst anlegen
ServiceTraduction serviceTraduction = new ServiceTraduction();
// Regulärer Ausdruck zum Auffinden der Übersetzung
serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
// URL des Übersetzungsservers
serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
// Wörterbuch der übersetzten Sprachen
Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
languesTraduites["fe"]= "Français-Anglais";
languesTraduites["fs"]= "Français-Espagnol";
languesTraduites["ef"]= "Anglais-Français";
serviceTraduction.LanguesTraduites = languesTraduites;
// Proxy
//serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
// Übersetzung
string texte = "ce chien est perdu";
string deQuoiVersQuoi = "fe";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "l'été sera chaud";
deQuoiVersQuoi = "fs";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "my tailor is rich";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "xx";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
} catch (WebTraductionsException e) {
// Fehler
Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
}
}
}
}
Die erzielten Ergebnisse lauten wie folgt:
Das Projekt [dao] der Lösung wird zu DLL und HttpTraductions.dll kompiliert:
![]() |
11.7.3.6. Die grafische Benutzeroberfläche der Anwendung
Werfen wir noch einmal einen Blick auf die Architektur unserer Anwendung:
![]() |
Wir schreiben nun die Schicht [ui]. Diese ist Gegenstand des Projekts [ui] der im Aufbau befindlichen Lösung:
![]() |
Der Ordner [lib] [3] enthält einige der vom Projekt [4] referenzierten DLL-Dateien:
- die für Spring erforderlichen: Spring.Core, Common.Logging, antlr.runtime
- die der Schicht [dao]: HttpTraductions
Die Datei [App.config] enthält die Spring-Konfiguration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Traductions sur le web</description>
<!-- der Übersetzungsdienst -->
<object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
<property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<property name="LanguesTraduites">
<dictionary key-type="string" value-type="string">
<entry key="fe" value="Français-Anglais"/>
<entry key="ef" value="Anglais-Français"/>
...
<entry key="ei" value="Anglais-Italien"/>
<entry key="ie" value="Italien-Anglais"/>
</dictionary>
</property>
</object>
</objects>
</spring>
</configuration>
- Zeile 15: die von Spring zu instanziierenden Objekte. Es gibt nur eines, nämlich das in Zeile 18, das den Übersetzungsdienst mit der Klasse ServiceTraduction instanziiert, die in DLL und HttpTraductions zu finden ist.
- Zeile 19: Die Eigenschaft UrlServeurTraduction der Klasse ServiceTraduction. Es gibt ein Problem mit dem Zeichen & in der URL. Dieses Zeichen hat in einer XML-Datei eine bestimmte Bedeutung. Es muss daher geschützt werden. Dies gilt auch für andere Zeichen, auf die wir im weiteren Verlauf der Datei stoßen werden. Sie müssen durch die Zeichenfolge [&code;] ersetzt werden: & durch [&], < durch [<], > durch [>], " durch ["].
- Zeile 21: die Eigenschaft ProxyHttp der Klasse ServiceTraduction. Eine nicht initialisierte Eigenschaft bleibt bei null. Diese Eigenschaft nicht zu definieren bedeutet, dass kein HTTP-Proxy vorhanden ist.
- Zeile 23: Die Eigenschaft RegexTraduction der Klasse ServiceTraduction. Im regulären Ausdruck mussten die Zeichen [< > "] durch ihre geschützten Entsprechungen ersetzt werden.
- Zeilen 24–33: Die Eigenschaft LanguesTraduites der Klasse ServiceTraduction.
Das Programm [Program.cs] wird beim Start der Anwendung ausgeführt. Sein Code lautet wie folgt:
using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
namespace ui {
static class Program {
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- Entwicklercode
// Instanziierung des Übersetzungsdienstes
IApplicationContext ctx = null;
Exception ex = null;
ServiceTraduction serviceTraduction = null;
try {
// Spring-Kontext
ctx = ContextRegistry.GetContext();
// Abfrage einer Referenz auf den Übersetzungsdienst
serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
} catch (Exception e1) {
// Ausnahme speichern
ex = e1;
}
// Anzuzeigendes Formular
Form form = null;
// Ist eine Ausnahme aufgetreten?
if (ex != null) {
// Ja – die anzuzeigende Fehlermeldung wird erstellt
StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
Exception e = ex;
while (e != null) {
msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
e = e.InnerException;
}
// Erstellung eines Fehlerfensters, an das die anzuzeigende Fehlermeldung übergeben wird
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
// Dies ist das Fenster, das angezeigt werden soll
form = form2;
} else {
// Alles ist gut gelaufen
// Erstellung der grafischen Benutzeroberfläche [Form1], an die die Referenz auf den Übersetzungsdienst übergeben wird
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
// Dies ist das Fenster, das angezeigt werden soll
form = form1;
}
// Fenster anzeigen
Application.Run(form);
}
}
}
Dieser Code wurde bereits in der Anwendung „Steuern“ Version 6 in Abschnitt 7.6.2 verwendet.
- Der Übersetzungsdienst wird in Zeile 27 von Spring erstellt. Wenn diese Erstellung erfolgreich war, wird das Formular [Form1] angezeigt (Zeilen 52–55), andernfalls wird das Fehlerformular [Form2] angezeigt (Zeilen 36–48).
Das Formular [Form2] wird in der Anwendung „Steuern“ Version 6 verwendet und wurde in Abschnitt 7.6.4 erläutert.
Das Formular [Form1] lautet wie folgt:
![]() |
Nr. | Typ | Name | Rolle |
1 | TextBox | textBoxTexteATraduire | Eingabefeld für den zu übersetzenden Text MultiLine=true |
2 | ComboBox | comboBoxLangues | Liste der Übersetzungsrichtungen |
3 | Button | buttonTraduire | um die Übersetzung des Textes [1] in die Richtung [2] anzufordern |
4 | TextBox | textBoxTraduction | die Übersetzung des Textes [1] |
Der Code des Formulars [Form1] lautet wie folgt:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
namespace ui {
public partial class Form1 : Form {
// Übersetzungsdienst
public ServiceTraduction ServiceTraduction { get; set; }
// Sprachwörterbuch
Dictionary<string, string> languesInversées = new Dictionary<string, string>();
// Konstruktor
public Form1() {
InitializeComponent();
}
// Erstladen des Formulars
private void Form1_Load(object sender, EventArgs e) {
// Erstellung des umgekehrten Sprachwörterbuchs
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// Sprachen
string langues = ServiceTraduction.LanguesTraduites[code];
// Hinzufügen (Sprachen, Code) zum umgekehrten Wörterbuch
languesInversées[langues] = code;
}
// Ausfüllen des Kombinationsfelds in alphabetischer Reihenfolge der Sprachen
string[] languesCombo = languesInversées.Keys.ToArray();
Array.Sort<string>(languesCombo);
foreach (string langue in languesCombo) {
comboBoxLangues.Items.Add(langue);
}
// Auswahl der ersten Sprache
if (comboBoxLangues.Items.Count != 0) {
comboBoxLangues.SelectedIndex = 0;
}
}
private void buttonTraduire_Click(object sender, EventArgs e) {
// Gibt es etwas zu übersetzen?
string texte = textBoxTexteATraduire.Text.Trim();
if (texte == "") return;
// Übersetzung
try {
textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
} catch (Exception ex) {
textBoxTraduction.Text = ex.Message;
}
}
}
}
- Zeile 10: ein Verweis auf den Übersetzungsdienst. Diese öffentliche Eigenschaft wurde durch [Program.cs], Zeile 53, initialisiert. Wenn die Methoden Form1_Load (Zeile 20) oder buttonTraduire_Click (Zeile 40) ausgeführt werden, ist dieses Feld daher bereits initialisiert.
- Zeile 12: Das Wörterbuch der übersetzten Sprachen mit Einträgen vom Typ ["Français-Anglais","fe"], c.a.d. Das Inverse des vom Übersetzungsdienst zurückgegebenen Wörterbuchs LanguesTraduites.
- Zeile 20: Die Methode Form1_Load wird beim Laden des Formulars ausgeführt.
- Zeilen 22–27: Das Wörterbuch serviceTraduction.LanguesTraduites und ["fe","Français-Anglais"] wird verwendet, um das Wörterbuch languesInversées und ["Français-Anglais", "fe"] zu erstellen.
- Zeile 29: languesCombo ist das Schlüsselarray der Wörterbücher languesInversées und c.a.d. Ein Array mit Elementen ["Français-Anglais"]
- Zeile 30: Dieses Array wird sortiert, um die Übersetzungsrichtungen in der Combobox in alphabetischer Reihenfolge anzuzeigen
- Zeilen 31–33: Das Sprachauswahlfeld wird gefüllt.
- Zeile 40: Die Methode, die ausgeführt wird, wenn der Benutzer auf die Schaltfläche [Traduire] klickt
- Zeile 46: Es genügt, die Methode serviceTraduction.Traduire aufzurufen, um die Übersetzung anzufordern. Der erste Parameter ist der zu übersetzende Text, der zweite der Code für die Übersetzungsrichtung. Dieser Code wird im Wörterbuch languesInversées anhand des im Sprach-Dropdown-Menü ausgewählten Elements ermittelt.
- Zeile 48: Liegt eine Ausnahme vor, wird diese anstelle der Übersetzung angezeigt.
11.7.3.7. Conclusion
Diese Anwendung hat gezeigt, dass die Web-Clients des Frameworks .NET es uns ermöglichen, Webressourcen zu nutzen. Die Vorgehensweise ist jeweils ähnlich:
- die abzufragende URI bestimmen. Diese URI ist meist parametrisiert.
- die URI abfragen
- in der Antwort des Servers mithilfe regulärer Ausdrücke das Gesuchte finden
Diese Vorgehensweise ist unzuverlässig. Denn im Laufe der Zeit können sich sowohl die abgefragte URI als auch der reguläre Ausdruck, mit dem das erwartete Ergebnis gefunden wird, ändern. Es ist daher sinnvoll, diese beiden Informationen in einer Konfigurationsdatei zu speichern. Dies kann sich jedoch als unzureichend erweisen. Im nächsten Kapitel werden wir sehen, dass es im Web stabilere Ressourcen gibt: die Webdienste.
11.7.4. Ein SMTP-Client (Simple Mail Transport Protocol) mit der Klasse SmtpClient
Ein Client SMTP ist ein Client eines Servers SMTP, eines E-Mail-Versandservers. Die Klasse .NET SmtpClient kapselt die Anforderungen eines solchen Clients vollständig. Der Entwickler muss die Details des SMTP-Protokolls nicht kennen. Wir kennen dieses Protokoll. Es wurde in Abschnitt 11.4.3 vorgestellt.
Wir stellen die Klasse SmtpClient im Rahmen einer einfachen Windows-Anwendung vor, mit der E-Mails mit Anhängen versendet werden können. Die Anwendung stellt eine Verbindung zum Port 25 eines SMTP-Servers her. Wir weisen darauf hin, dass auf den meisten Windows-Systemen Firewalls oder Antivirenprogramme Verbindungen zum Port 25 blockieren. Daher muss dieser Schutz deaktiviert werden, um die Anwendung zu testen:
![]() |
Der SMTP-Client wird eine einschichtige Architektur aufweisen:
![]() |
Das Visual Studio-Projekt sieht wie folgt aus:
![]() |
Die grafische Benutzeroberfläche [SendMailForm.cs] der Anwendung sieht wie folgt aus:
![]() |
Nr. | Typ | Name | Rolle |
1 | TextBox | textBoxServeur | Name des Servers SMTP, mit dem eine Verbindung hergestellt werden soll |
2 | NumericUpDown | numericUpDownPort | der Port, über den die Verbindung hergestellt werden soll |
3 | TextBox | textBoxExpediteur | Absenderadresse der Nachricht |
4 | TextBox | textBoxTo | Empfängeradressen im Format: Adresse1,Adresse2, ... |
5 | TextBox | textBoxCc | Adressen der Empfänger in Kopie (CC=Carbon Copy) im Format: Adresse1,Adresse2, ... |
6 | TextBox | textBoxBcc | Adressen der Empfänger in der Blindkopie (BCC=Blind Carbon Copy) im Format: Adresse1,Adresse2, ... Alle Adressen in diesen drei Eingabefeldern erhalten dieselbe Nachricht mit denselben Anhängen. Die Empfänger der Nachricht können die Adressen in den Feldern 4 und 5 einsehen, nicht jedoch die in Feld 6. Die Bcc-Funktion ist also eine Möglichkeit, jemanden in Kopie zu setzen, ohne dass die anderen Empfänger der Nachricht davon erfahren. |
7 | Schaltfläche | buttonAjouter | um der E-Mail einen Anhang hinzuzufügen |
8 | ListBox | listBoxPiecesJointes | Liste der der E-Mail beizufügenden Anhänge |
9 | TextBox | textBoxSujet | Betreff des Schreibens |
10 | TextBox | textBoxMessage | Der Text der Nachricht. MultiLine=true |
11 | Schaltfläche | buttonEnvoyer | um die Nachricht und eventuelle Anhänge zu versenden |
12 | TextBox | textBoxRésultat | zeigt eine Zusammenfassung der gesendeten Nachricht oder eine Fehlermeldung an, falls ein Problem aufgetreten ist |
13 | Schaltfläche | buttonEffacer | zum Löschen [12] |
OpenfileDialog | openFileDialog1 | Nicht-visuelle Steuerung, die die Auswahl eines Anhangs im lokalen Dateisystem ermöglicht |
Im vorangegangenen Beispiel lautet die in [12] angezeigte Zusammenfassung wie folgt:
Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc :
Bcc :
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,
Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
Cordialement,
ST
Der Code des Formulars [SendMailForm.cs] lautet wie folgt:
using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;
namespace Chap9 {
public partial class SendMailForm : Form {
public SendMailForm() {
InitializeComponent();
}
// Anhang hinzufügen
private void buttonAjouter_Click(object sender, EventArgs e) {
// Konfiguration des Dialogfelds openfileDialog1
openFileDialog1.InitialDirectory = Application.ExecutablePath;
openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
openFileDialog1.FilterIndex = 0;
openFileDialog1.FileName = "";
// Anzeige des Dialogfelds und Abruf des Ergebnisses
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
// Der Dateiname wird abgerufen
listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
}
}
private void textBoxServeur_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void setStatutEnvoyer() {
buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
}
// einen Anhang entfernen
private void buttonRetirer_Click(object sender, EventArgs e) {
// Anhang ausgewählt?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// Es wird entfernt
listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
// Die Schaltfläche „Entfernen“ wird aktualisiert
buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
}
}
private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
// Anhang ausgewählt?
if (listBoxPiecesJointes.SelectedIndex != -1) {
// Die Schaltfläche „Entfernen“ wird aktualisiert
buttonRetirer.Enabled = true;
}
}
// Nachricht mit Anhängen versenden
private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
}
private void textBoxTo_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void textBoxSujet_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void buttonEffacer_Click(object sender, EventArgs e) {
textBoxResultat.Text = "";
}
}
}
Wir werden diesen Code nicht näher erläutern, da er keine Neuerungen enthält. Um die Methode buttonAjouter_Click in Zeile 14 zu verstehen, wird der Leser gebeten, Abschnitt 7.5.1 noch einmal durchzulesen.
Die Methode buttonEnvoyer_Click in Zeile 55, die die E-Mail versendet, lautet wie folgt:
private void buttonEnvoyer_Click(object sender, EventArgs e) {
try {
// Sanduhr
Cursor = Cursors.WaitCursor;
// der SMTP-Client
SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
// die Nachricht
MailMessage message = new MailMessage();
// Absender
message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
message.From = message.Sender;
// Empfänger
Regex marqueur = new Regex("\\s*,\\s*");
string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
foreach (string destinataire in destinataires) {
if (destinataire.Trim() != "") {
message.To.Add(new MailAddress(destinataire));
}
}
// CC
string[] copies = marqueur.Split(textBoxCc.Text.Trim());
foreach (string copie in copies) {
if (copie.Trim() != "") {
message.CC.Add(new MailAddress(copie));
}
}
// BCC
string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
foreach (string blindCopie in blindCopies) {
if (blindCopie.Trim() != "") {
message.Bcc.Add(new MailAddress(blindCopie));
}
}
// Betreff
message.Subject = textBoxSujet.Text.Trim();
// Nachrichtentext
message.Body = textBoxMessage.Text;
// Anhänge
foreach (string attachement in listBoxPiecesJointes.Items) {
message.Attachments.Add(new Attachment(attachement));
}
// Nachricht senden
smtpClient.Send(message);
// OK – es wird eine Zusammenfassung angezeigt
StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
textBoxSujet.Clear();
msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
textBoxTo.Clear();
msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
textBoxCc.Clear();
msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
textBoxBcc.Clear();
msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
foreach (string attachement in listBoxPiecesJointes.Items) {
msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
}
msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
listBoxPiecesJointes.Items.Clear();
textBoxResultat.Text = msg.ToString();
} catch (Exception ex) {
// Fehler wird angezeigt
textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
}
// Normaler Cursor
Cursor = Cursors.Arrow;
}
- Zeile 6: Der SMTP-Client wird erstellt. Er benötigt zwei Parameter: den Servernamen SMTP und den Port, über den dieser läuft
- Zeile 8: Eine Nachricht vom Typ MailMessage wird erstellt. Diese kapselt die gesamte zu versendende Nachricht.
- Zeile 10: Die E-Mail-Adresse „Sender“ des Absenders wird angegeben. Eine E-Mail-Adresse ist eine Instanz des Typs MailAddress, die aus der Zeichenkette „xx@yy.zz“ gebildet wird. Diese Zeichenkette muss das für eine E-Mail-Adresse erwartete Format aufweisen, andernfalls wird eine Ausnahme ausgelöst. In diesem Fall wird sie im Feld textBoxResultat (Zeile 63) in einer wenig benutzerfreundlichen Form angezeigt.
- Zeilen 13–19: Die E-Mail-Adressen der Empfänger werden in die „An“-Liste der Nachricht eingefügt. Diese Adressen werden aus dem Feld textBoxTo abgerufen. Der reguläre Ausdruck in Zeile 13 ermöglicht es, die verschiedenen, durch Kommas getrennten Adressen zu extrahieren.
- Zeilen 21–26: Der gleiche Vorgang wird wiederholt, um das Feld CC der Nachricht mit den CC-Adressen aus dem Feld textBoxCc zu füllen.
- Zeilen 28–33: Der gleiche Vorgang wird wiederholt, um das Feld „Bcc“ der Nachricht mit den Blindkopie-Adressen aus dem Feld „textBoxBcc“ zu initialisieren.
- Zeile 35: Das Feld „Subject“ der Nachricht wird mit dem Betreff aus dem Feld textBoxSujet initialisiert.
- Zeile 37: Das Feld „Body“ der Nachricht wird mit dem Text der Nachricht aus dem Feld textBoxMessage initialisiert.
- Zeilen 39–41: Die Anhänge werden an die Nachricht angehängt. Jeder Anhang wird als „Attachment“-Objekt zum Feld „Attachments“ der Nachricht hinzugefügt. Ein „Attachment“-Objekt wird anhand des vollständigen Pfads des anzuhängenden Dokuments im lokalen Dateisystem instanziiert.
- Zeile 43: Die Nachricht wird mithilfe der Methode „Send“ des SMTP-Clients versendet.
- Zeilen 45–60: Die Zusammenfassung des Versands wird in das Feld „textBoxResultat“ geschrieben und das Formular wird zurückgesetzt.
- Zeile 63: Anzeige eines eventuellen Fehlers
11.8. Ein generischer asynchroner TCP-Client
11.8.1. Einführung
In allen Beispielen dieses Kapitels erfolgte die Client-Server-Kommunikation im blockierenden Modus, der auch als synchroner Modus bezeichnet wird:
- Wenn ein Client eine Verbindung zu einem Server herstellt, wartet er auf die Antwort des Servers auf diese Anfrage, bevor er fortfährt.
- Wenn ein Client eine vom Server gesendete Textzeile liest, wird er blockiert, solange der Server diese nicht gesendet hat.
- Auf der Serverseite funktionieren die Service-Threads, die den Dienst für den Client bereitstellen, auf die gleiche Weise wie oben beschrieben.
In grafischen Benutzeroberflächen ist es oft notwendig, den Benutzer bei langwierigen Vorgängen nicht zu blockieren. Ein häufig genanntes Beispiel ist das Herunterladen einer großen Datei. Während dieses Downloads muss es dem Benutzer möglich sein, weiterhin mit der grafischen Benutzeroberfläche zu interagieren.
Wir schlagen hier vor, den generischen TCP-Client aus Abschnitt 11.6.3 neu zu schreiben und dabei folgende Änderungen vorzunehmen:
- Die Benutzeroberfläche ist grafisch
- Das Kommunikationswerkzeug mit dem Server wird ein Socket-Objekt sein
- Die Kommunikation erfolgt asynchron:
- Der Client initiiert eine Verbindung zum Server, bleibt jedoch nicht blockiert, während er auf deren Aufbau wartet
- Der Client initiiert eine Datenübertragung an den Server, bleibt jedoch nicht blockiert, während er auf deren Abschluss wartet
- Der Client initiiert den Empfang von Daten vom Server, bleibt jedoch nicht blockiert, um auf dessen Ende zu warten.
Zur Erinnerung: Auf welcher Ebene befindet sich das Socket-Objekt in der TCP-Client-Server-Kommunikation?
![]() |
Die Socket-Klasse arbeitet am nächsten am Netzwerk. Sie ermöglicht eine detaillierte Verwaltung der Netzwerkverbindung. Der Begriff socket bezeichnet eine Steckdose. Der Begriff wurde erweitert, um eine softwarebasierte Netzwerkverbindung zu bezeichnen. Bei einer TCP-IP-Kommunikation zwischen zwei Rechnern A und B kommunizieren zwei sockets miteinander. Eine Anwendung kann direkt mit den sockets arbeiten. Dies ist bei der oben genannten Anwendung A der Fall. Ein Socket kann ein client- oder ein serveur-Socket sein.
11.8.2. Die grafische Benutzeroberfläche des asynchronen TCP-Clients
Die Visual Studio-Anwendung sieht wie folgt aus:
![]() |
[ClientTcpAsynchrone.cs] ist die grafische Benutzeroberfläche. Diese sieht wie folgt aus:
![]() |
Nr. | Typ | Name | Rolle |
1 | TextBox | textBoxNomServeur | Name des TCP-Servers, mit dem eine Verbindung hergestellt werden soll |
2 | NumericUpDown | numericUpDownPortServeur | Der Port, mit dem eine Verbindung hergestellt werden soll |
3 | RadioButton | radioButtonLF radioButtonRCLF | um das vom Client zu verwendende Zeilenendezeichen anzugeben: LF „\n“ oder RCLF „\r\n“ |
4 | Schaltfläche | buttonConnexion | um eine Verbindung zum Port [2] des Servers [1] herzustellen. Die Schaltfläche trägt die Bezeichnung [Connecter], wenn der Client nicht mit einem Server verbunden ist, und [Déconnecter], wenn er verbunden ist. |
5 | TextBox | textBoxMsgToServeur | Nachricht, die nach dem Herstellen der Verbindung an den Server gesendet werden soll. Wenn der Benutzer die Taste [Entrée] drückt, wird die Nachricht mit dem in [3] ausgewählten Zeilenendezeichen gesendet |
6 | ListBox | listBoxEvts | Liste, in der die wichtigsten Ereignisse der Client-Server-Verbindung angezeigt werden: Verbindung, Trennung, Schließen des Datenstroms, Kommunikationsfehler |
7 | ListBox | listBoxDialogue | Liste, in der die Meldungen des Client-Server-Dialogs angezeigt werden |
8 | Schaltfläche | buttonRazEvts | zum Löschen der Liste [6] |
4 | Schaltfläche | buttonRazDialogue | zum Löschen der Liste [7] |
Die Funktionsweise dieser Schnittstelle ist wie folgt:
- Der Benutzer verbindet seinen grafischen TCP-Client über [1, 2, 3, 4] mit einem TCP-Dienst.
- Ein asynchroner Thread nimmt kontinuierlich alle vom TCP-Server gesendeten Daten entgegen und zeigt sie in der Liste [7] an. Dieser Thread ist von den anderen Aktivitäten der Schnittstelle getrennt.
- Der Benutzer kann über [5] nach Belieben Nachrichten an den Server senden. Jede Nachricht wird von einem asynchronen Thread gesendet. Im Gegensatz zum Empfangsthread, der niemals endet, wird der Sendethread beendet, sobald die Nachricht gesendet wurde. Für die nächste Nachricht wird ein neuer asynchroner Thread verwendet.
- Die Client-Server-Kommunikation endet, wenn einer der Partner die Verbindung beendet. Der Benutzer kann dies über die Schaltfläche [4] tun, die nach dem Aufbau der Verbindung die Bezeichnung [Déconnecter] trägt.
Hier ist ein Screenshot einer Ausführung:
![]() |
- in [1]: Verbindung zu einem Dienst POP
- in [2]: Anzeige der Ereignisse, die während der Verbindung stattgefunden haben
- in [3]: Die vom Server POP nach Abschluss der Verbindung gesendete Meldung
- in [4]: Die Schaltfläche [Connecter] wurde zur Schaltfläche [Déconnecter]
![]() |
- in [1] wurde der Befehl quit an den Server POP gesendet. Der Server antwortete mit +OK goodbye und schloss die Verbindung
- In [2] wurde diese serverseitige Trennung erkannt. Der Client hat daraufhin die Verbindung seinerseits geschlossen.
- In [3] wurde die Schaltfläche [Déconnecter] wieder zu einer Schaltfläche [Connecter]
11.8.3. Asynchrone Verbindung zum Server
Durch Drücken der Schaltfläche [Connecter] wird die folgende Methode ausgeführt:
private void buttonConnexion_Click(object sender, EventArgs e) {
// Anmelden oder abmelden?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- Zeile 3: Die Schaltfläche kann die Bezeichnung [Connecter] oder [Déconnecter] haben.
Die Verbindungsmethode lautet wie folgt:
using System.Net.Sockets;
...
namespace Chap9 {
public partial class ClientTcp : Form {
const int tailleBuffer = 1024;
private Socket client = null;
private byte[] data = new byte[tailleBuffer];
private string réponse = null;
private string finLigne = "\r\n";
// Beauftragte
public delegate void writeLog(string log);
public ClientTcp() {
InitializeComponent();
}
....................................
private void connexion() {
// Datenprüfung
string nomServeur = textBoxNomServeur.Text.Trim();
if (nomServeur == "") {
logEvent("indiquez le nom du serveur");
return;
}
// Nachverfolgung
logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
try {
// Socket-Erstellung
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// asynchrone Verbindung
client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
} catch (Exception ex) {
logEvent(String.Format("erreur de connexion : {0}", ex.Message));
return;
}
}
// Die Verbindung wurde hergestellt
private void connecté(IAsyncResult résultat) {
// Der Client-Socket wird abgerufen
Socket client = résultat.AsyncState as Socket;
...
}
// Prozessüberwachung
private void logEvent(string msg) {
....
}
}
}
- Zeile 1: Die Klasse „Socket“ gehört zum Namensraum „System.Net.Sockets“.
Eine Reihe von Daten muss zwischen mehreren Methoden des Formulars gemeinsam genutzt werden. Es handelt sich um folgende:
- Zeile 7: „client“ ist der Socket für die Kommunikation mit dem Server
- Zeilen 6 und 8: Der Client empfängt seine Nachrichten in einem Byte-Array namens „data“.
- Zeile 9: „response“ ist die vom Server gesendete Antwort.
- Zeile 10: „finLigne“ ist das vom TCP-Client verwendete Zeilenendezeichen – es ist standardmäßig auf „RCLF“ initialisiert, kann aber vom Benutzer über die Optionsfelder „[3]“ geändert werden.
Die Prozedur connexion in Zeile 19 stellt die Verbindung zum TCP-Server her:
- Zeilen 21–25: Es wird überprüft, ob der Servername nicht leer ist. Ist dies nicht der Fall, wird das Ereignis in listBoxEvts mithilfe der Methode logEvent in Zeile 49 protokolliert.
- Zeile 27: Es wird gemeldet, dass die Verbindung hergestellt wird
- Zeile 30: Das für die TCP/IP-Kommunikation erforderliche Socket-Objekt wird erstellt. Der Konstruktor akzeptiert drei Parameter:
- AddressFamily addressFamily: die Adressfamilie IP des Clients und des Servers, hier die Adressen IPv4 (AddressFamily.InterNetwork)
- SocketType socketType: der Socket-Typ. Der Typ SocketType.Stream ist für TCP/IP-Verbindungen geeignet
- ProtocolType protocolType: Der verwendete Internetprotokolltyp, hier das TCP-Protokoll
- Zeile 32: Die Verbindung wird asynchron hergestellt. Die Verbindung wird initiiert, die Ausführung wird jedoch fortgesetzt, ohne auf deren Abschluss zu warten. Die Methode [Socket].BeginConnect akzeptiert vier Parameter:
- IPAddress ipAddress: Die IP-Adresse des Rechners, auf dem der Dienst läuft, mit dem eine Verbindung hergestellt werden soll
- Int32 port: der Port des Dienstes
- AsyncCallBack asyncCallBack: AsyncCallBack ist ein Delegat-Typ:
Die Methode asyncCallBack, die als dritter Parameter an die Methode BeginConnect übergeben wird, muss eine Methode sein, die einen Typ IAsyncCallBack akzeptiert und kein Ergebnis zurückgibt. Diese Methode wird aufgerufen, sobald die Verbindung hergestellt wurde. Wir übergeben hier als dritten Parameter die Methode connecté aus Zeile 41.
- (Fortsetzung)
- Objekt state: ein Objekt, das an die Methode asyncCallBack übergeben werden soll. Diese Methode erhält (siehe Delegat oben) einen Parameter „ar“ vom Typ IAsyncResult. Das Objekt state kann in ar.AsyncState (Zeile 43) abgerufen werden. Hier übergeben wir als vierten Parameter den Socket des Clients.
- Zeile 38: Die Methode ist beendet. Der Benutzer kann wieder mit der grafischen Benutzeroberfläche interagieren. Die Verbindung wird im Hintergrund hergestellt, parallel zur Ereignisverarbeitung der grafischen Benutzeroberfläche. Ebenfalls parallel dazu wird die Methode connecté in Zeile 41 am Ende der Verbindung aufgerufen, unabhängig davon, ob diese erfolgreich oder fehlgeschlagen ist.
Der Code der Methode connecté lautet wie folgt:
// Die Verbindung wurde hergestellt
private void connecté(IAsyncResult résultat) {
// Der Client-Socket wird abgerufen
Socket client = résultat.AsyncState as Socket;
try {
// Der asynchrone Vorgang wird beendet
client.EndConnect(résultat);
// Verfolgung
logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
// Formular
buttonConnexion.Text = "Déconnecter";
// asynchrones Einlesen von Daten vom Server
réponse = "";
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
} catch (SocketException e) {
logEvent(String.Format("erreur de connexion : {0}", e.Message));
return;
}
}
// Datenempfang
private void lecture(IAsyncResult résultat) {
// Der Client-Socket wird abgerufen
Socket client = résultat.AsyncState as Socket;
...
}
- Zeile 4: Der Client-Socket wird aus dem Parameter résultat abgerufen, den die Methode erhält. Zur Erinnerung: Dieses Objekt ist dasjenige, das als vierter Parameter an die Methode BeginConnect übergeben wurde.
- Zeile 7: Der Verbindungsversuch wird durch die Methode EndConnect beendet, an die der von der Methode empfangene Parameter résultat übergeben werden muss.
- Zeile 9: Das Ereignis wird in der Ereignisliste protokolliert
- Zeile 11: Die Schaltfläche „[Connecter]“ wird zur Schaltfläche „[Déconnecter]“, damit der Benutzer die Abmeldung anfordern kann.
- Zeile 13: Die Antwort des Servers wird initialisiert. Sie wird durch wiederholte Aufrufe der asynchronen Methode BeginReceive aktualisiert.
- Zeile 14: Erster Aufruf der asynchronen Methode BeginReceive. Diese wird mit folgenden Parametern aufgerufen:
- byte[] buffer: der Puffer, in den die empfangenen Daten geschrieben werden sollen – hier ist der Puffer data
- int offset: Ab welcher Position im Puffer sollen die empfangenen Daten abgelegt werden – hier beträgt der Offset 0, c.a.d, d. h., die Daten werden ab dem ersten Byte des Puffers abgelegt.
- int size: Die Größe des Puffers in Bytes – hier beträgt die Größe tailleBuffer.
- SocketFlags socketFlags: Konfiguration des Sockets – hier keine Konfiguration
- AsyncCallBack asyncCallBack: Die Methode, die aufgerufen werden soll, wenn der Empfang abgeschlossen ist. Dies ist entweder der Fall, weil der Puffer Daten empfangen hat oder weil die Verbindung geschlossen wurde. Hier ist die Callback-Methode die Methode lecture aus Zeile 22.
- Objekt state: Das Objekt, das an die Callback-Methode asyncCallBack übergeben werden soll. Hier wird erneut der Socket des Clients übergeben.
Es ist zu beachten, dass all dies ohne jegliche Aktion des Benutzers abläuft, abgesehen von der anfänglichen Verbindungsanfrage über die Schaltfläche [Connecter]. Am Ende der Methode connecté wird eine weitere Methode im Hintergrund ausgeführt: die Methode lecture, die wir nun näher betrachten.
// Datenempfang
private void lecture(IAsyncResult résultat) {
// Abruf des Client-Sockets
Socket client = résultat.AsyncState as Socket;
int nbOctetsReçus = 0;
bool erreur = false;
try {
// Anzahl der empfangenen Bytes
nbOctetsReçus = client.EndReceive(résultat);
if (nbOctetsReçus == 0) {
// Der Server antwortet nicht mehr
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// Es ist ein Empfangsproblem aufgetreten
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// Beendet?
if (nbOctetsReçus == 0 || erreur) {
// Der Client wird bei Bedarf getrennt
déconnexion();
// Das Ende der Antwort wird angezeigt
afficherRéponseServeur(réponse, true);
// Ende des Lesevorgangs
return;
}
// Die empfangenen Daten werden abgerufen
string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
// man fügt sie zu den bereits empfangenen Daten hinzu
réponse += données;
// die Antwort wird angezeigt
afficherRéponseServeur(réponse, false);
// Weiterlesen
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
}
- Zeile 2: Die Methode lecture wird im Hintergrund ausgelöst, wenn der Puffer data Daten empfangen hat oder die Verbindung vom Server geschlossen wurde.
- Zeile 9: Die asynchrone Leseanforderung wird durch EndReceive beendet. Auch hier muss diese Methode mit dem von der Rückruffunktion empfangenen Parameter aufgerufen werden. Die Methode EndReceive gibt die Anzahl der im Lesepuffer empfangenen Bytes zurück.
- Zeile 10: Ist die Anzahl der Bytes gleich Null, wurde die Verbindung vom Server geschlossen.
- Zeile 12: Das Ereignis wird in der Ereignisliste vermerkt
- Zeile 14: Eine eventuelle Ausnahme wird behandelt
- Zeilen 16–17: Das Ereignis wird in die Ereignisliste eingetragen und der Fehler protokolliert
- Zeile 20: Es wird geprüft, ob die Verbindung geschlossen werden muss
- Zeile 22: Die Verbindung wird auf der Client-Seite mit der Methode déconnexion geschlossen, auf die wir später noch eingehen werden.
- Zeile 24: Die Antwort des Servers, c.a.d. Die globale Variable réponse wird mithilfe einer privaten Methode afficherRéponseServeur in der Dialogliste listBoxDialogue angezeigt.
- Zeile 26: Ende der asynchronen Methode lecture
- Zeile 29: Die empfangenen Bytes werden in eine Zeichenkette im Format UTF8 umgewandelt.
- Zeile 31: Sie werden der gerade erstellten Antwort hinzugefügt
- Zeile 33: Die Antwort wird in der Liste listBoxDialogue angezeigt.
- Zeile 35: Es wird erneut auf Daten vom Server gewartet.
Letztendlich wird die asynchrone Methode lecture nie beendet. Sie liest kontinuierlich die vom Server kommenden Daten ein und lässt sie in der Liste listBoxDialogue anzeigen. Sie wird erst beendet, wenn die Verbindung entweder vom Server oder vom Benutzer selbst geschlossen wird.
11.8.4. Trennung der Verbindung zum Server
Durch Drücken der Schaltfläche [Déconnecter] wird die folgende Methode ausgeführt:
private void buttonConnexion_Click(object sender, EventArgs e) {
// Verbindung herstellen oder trennen?
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- Zeile 3: Die Schaltfläche kann die Beschriftung [Connecter] oder [Déconnecter] tragen.
Die Methode déconnexion sorgt für die Trennung der Verbindung des Clients:
private void déconnexion() {
// Socket schließen
if (client != null && client.Connected) {
try {
// Weiterverfolgung
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
// Trennung
client.Shutdown(SocketShutdown.Both);
client.Close();
// Formular
buttonConnexion.Text = "Connecter";
} catch (Exception ex) {
// Verfolgung
logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
}
}
}
- Zeile 3: Wenn der Client existiert und verbunden ist
- Zeile 6: Die Trennung wird in listBoxEvts angekündigt. Die Eigenschaft client.RemoteEndPoint liefert das Paar (IP-Adresse, Port) des anderen Endes der Verbindung, hier c.a.d vom Server.
- Zeile 8: Der Datenstrom des Sockets wird mit der Methode ShutDown geschlossen. Der Datenstrom eines Sockets ist bidirektional: Der Socket sendet und empfängt Daten. Der Parameter der Methode ShutDown kann wie folgt lauten: ShutDown.Receive zum Schließen des Empfangsstroms, Shutdonw.Send zum Schließen des Sendestroms oder ShutDown.Both zum Schließen beider Ströme.
- Zeile 9: Die mit dem Socket verbundenen Ressourcen werden freigegeben
- Zeile 11: Die Schaltfläche [Déconnecter] wird zur Schaltfläche [Connecter]
- Zeilen 12–15: Behandlung einer möglichen Ausnahme
11.8.5. Asynchrones Senden von Daten an den Server
Wenn der Benutzer die Meldung im Feld textBoxMsgToServeur bestätigt, wird die folgende Methode ausgeführt:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// Taste [Entrée]?
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- Zeilen 3–5: Wenn der Benutzer die Taste [Entrée] gedrückt hat und der Client-Socket verbunden ist, wird die Nachricht aus dem Feld textBoxMsgToServeur mit der Methode envoyerMessage gesendet.
Die Methode envoyerMessage lautet wie folgt:
private void envoyerMessage() {
// Nachricht asynchron senden
// die Nachricht
byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
// Sie wird gesendet
client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
// Dialog
logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
// Nachricht löschen
textBoxMsgToServeur.Clear();
}
- Zeile 4: Die Zeilenende-Markierung des Clients wird der Nachricht hinzugefügt und in das Byte-Array message geschrieben.
- Zeile 6: Mit der Methode BeginSend wird eine asynchrone Übertragung gestartet. Die Parameter von BeginSend sind identisch mit denen der Methode BeginReceive. Nach Abschluss des asynchronen Sendevorgangs der Nachricht wird die Methode écriture aufgerufen.
- Zeile 8: Die gesendete Nachricht wird der Liste listBoxDialogue hinzugefügt, um den Client-Server-Dialog nachverfolgen zu können
- Zeile 10: Die gesendete Nachricht wird aus der grafischen Benutzeroberfläche gelöscht
Die Callback-Methode écriture lautet wie folgt:
private void écriture(IAsyncResult résultat) {
// Ergebnis des Versendens einer Nachricht
Socket client = résultat.AsyncState as Socket;
try {
client.EndSend(résultat);
} catch (Exception e) {
// Es ist ein Problem beim Senden aufgetreten
logEvent(String.Format("erreur d'émission : {0}", e.Message));
}
}
- Zeile 4: Die Callback-Methode écriture erhält einen Ergebnisparameter vom Typ IAsyncResult.
- Zeile 3: Im Parameter résultat wird der Socket des Clients abgerufen. Dieser Socket war der fünfte Parameter der Methode BeginSend.
- Zeile 5: Der asynchrone Sendevorgang wird beendet.
Es wird nicht auf das Ende der Nachrichtenübertragung gewartet, bevor die Kontrolle wieder an den Benutzer übergeben wird. Dieser kann somit eine zweite Nachricht senden, noch bevor die Übertragung der ersten abgeschlossen ist.
11.8.6. Anzeige der Ereignisse und des Client-Server-Dialogs
Die Ereignisse werden über die Methode logEvents angezeigt:
// Prozessverfolgung
private void logEvent(string msg) {
listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
}
private void logEventCallBack(string msg) {
// Anzeige der Nachricht
msg = msg.Replace(finLigne, " ");
listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
- Zeile 2: Die Methode logEvents erhält als Parameter die Nachricht, die in die Liste listBoxEvts aufgenommen werden soll.
- Zeile 3: Die Komponente listBoxEvents kann nicht direkt verwendet werden. Tatsächlich wird die Methode logEvents von zwei Arten von Threads aufgerufen:
- dem Haupt-Thread, der Eigentümer der grafischen Benutzeroberfläche ist, beispielsweise wenn er meldet, dass ein Verbindungsversuch läuft
- ein sekundärer Thread, der einen asynchronen Vorgang ausführt. Dieser Thread-Typ ist nicht Eigentümer der Komponenten, und sein Zugriff auf eine C-Komponente muss durch einen Vorgang vom Typ C.Invoke gesteuert werden. Dieser Vorgang teilt der C-Steuerung mit, dass ein Thread einen Vorgang an ihr ausführen möchte. Die Methode Invoke akzeptiert zwei Parameter:
- eine Rückruffunktion vom Typ delegate. Diese Rückruffunktion wird vom Thread ausgeführt, der Eigentümer der grafischen Benutzeroberfläche ist, und nicht von dem Thread, der die Methode C.Invoke ausführt.
- ein Objekt, das an die Callback-Funktion übergeben wird.
Hier ist der erste Parameter, der an die Methode Invoke übergeben wird, eine Instanz des folgenden Delegaten:
public delegate void writeLog(string log);
Der Delegat writeLog hat einen Parameter vom Typ string und gibt kein Ergebnis zurück. Der Parameter ist die Meldung, die in listBoxEvts gespeichert werden soll.
In Zeile 3 ist der erste Parameter, der an die Methode Invoke übergeben wird, die Methode logEventCallBack aus Zeile 6. Sie entspricht der Signatur des Delegaten writeLog. Der zweite Parameter, der an die Methode Invoke übergeben wird, ist die Nachricht, die als Parameter an die Methode logEventCallBack übergeben wird.
Die Operation Invoke ist eine synchrone Operation. Die Ausführung des sekundären Threads wird blockiert, bis der für das Steuerelement zuständige Thread die Callback-Methode ausführt.
- Zeile 6: Die vom GUI-Thread ausgeführte Rückrufmethode empfängt die im Steuerelement listBoxEvts anzuzeigende Meldung.
- Zeile 9: Das Ereignis wird an erster Stelle der Liste protokolliert, damit die neuesten Ereignisse ganz oben in der Liste stehen.
Die Meldungen des Client-Server-Dialogs werden über die Methode logDialogue angezeigt:
// Verfolgung des Dialogs
private void logDialogue(string msg) {
listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
}
private void logDialogueCallBack(string msg) {
// Anzeige der Meldung
msg = msg.Replace(finLigne, " ");
listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
Das Prinzip ist dasselbe wie bei der Methode logEvent.
Die vom Client empfangenen Meldungen werden über die Methode afficherRéponseServeur angezeigt:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
Der erste Parameter ist die anzuzeigende Meldung. Diese Meldung kann aus mehreren Zeilen bestehen. Der Client liest die vom Server kommenden Daten nämlich in Blöcken von tailleBuffer (1024) Byte. In diesen 1024 Byte können verschiedene Zeilen enthalten sein, die an ihrem Zeilenendezeichen „\n“ zu erkennen sind. Die letzte Zeile kann unvollständig sein, da sich ihr Zeilenendezeichen in den folgenden 1024 Bytes befindet. Die Methode sucht in der Meldung nach Zeilen, die mit „\n“ enden, und fordert anschließend logDialogue auf, diese anzuzeigen. Der zweite Parameter der Methode gibt an, ob die zuletzt gefundene Zeile angezeigt oder im Puffer belassen werden soll, um durch die folgende Nachricht vervollständigt zu werden. Der Code ist recht komplex und für diesen Zusammenhang nicht von Interesse. Daher wird er nicht kommentiert.
11.8.7. Fazit
Das gleiche Beispiel könnte auch mit synchronen Operationen umgesetzt werden. In diesem Fall bietet der asynchrone Aspekt der grafischen Benutzeroberfläche dem Benutzer kaum einen Vorteil. Wenn der Benutzer jedoch eine Verbindung herstellt und anschließend feststellt, dass der Server „nicht mehr antwortet“, hat er die Möglichkeit, die Verbindung zu trennen, da die grafische Benutzeroberfläche während der Ausführung der asynchronen Operationen weiterhin auf Ereignisse reagiert. Dieses recht komplexe Beispiel hat es uns ermöglicht, neue Konzepte vorzustellen:
- die Verwendung von Sockets
- die Verwendung asynchroner Methoden. Das Gesagte ist Teil eines Standards. Es gibt weitere asynchrone Methoden, die nach dem gleichen Prinzip funktionieren.
- die Aktualisierung von Steuerelementen einer grafischen Benutzeroberfläche durch Hintergrund-Threads.
Die asynchrone TCP/IP-Kommunikation bietet für einen Server wesentlich größere Vorteile als die im vorherigen Beispiel gezeigten. Es ist bekannt, dass der Server seine Clients mithilfe von Neben-Threads bedient. Wenn sein Thread-Pool N Threads umfasst, bedeutet dies, dass er nur N Clients gleichzeitig bedienen kann. Wenn alle N Threads eine blockierende (synchrone) Operation ausführen, stehen keine Threads mehr für einen neuen Client zur Verfügung, bis eine der blockierenden Operationen abgeschlossen ist und einen Thread freigibt. Wenn in den Threads asynchrone statt synchroner Operationen ausgeführt werden, wird ein Thread niemals blockiert und kann schnell für neue Clients wiederverwendet werden.
11.9. Beispielanwendung, Version 8: Server zur Steuerberechnung
11.9.1. Die Architektur der neuen Version
Wir greifen die Anwendung zur Steuerberechnung wieder auf, die bereits in verschiedenen Formen behandelt wurde. Erinnern wir uns an ihre letzte Version, nämlich die aus Version 7 in Abschnitt 9.8.
![]() |
Die Daten befanden sich in einer Datenbank, und die Schicht [ui] war eine grafische Benutzeroberfläche:
![]() |
Wir werden diese Architektur wieder aufnehmen und auf zwei Rechner verteilen:
![]() |
- Ein Rechner mit der Bezeichnung [serveur] wird die Schichten [metier] und [dao] der Version 7 hosten. Eine TCP/IP-Schicht [serveur] [1] wird eingerichtet, damit Internetnutzer den Steuerberechnungsdienst abfragen können.
- Ein Rechner mit der Kennung [client] wird die Schicht [ui] der Version 7 hosten. Eine TCP/IP-Schicht [client] [2] wird eingerichtet, damit die Schicht [ui] den Steuerberechnungsdienst abfragen kann.
Die Architektur ändert sich hier grundlegend. Version 7 war eine Windows-Einzelplatzanwendung. Version 8 wird zu einer Client-Server-Anwendung im Internet. Der Server kann mehrere Clients gleichzeitig bedienen.
Zunächst werden wir den Teil [serveur] der Anwendung programmieren.
11.9.2. Der Steuerberechnungsserver
11.9.2.1. Das Visual Studio-Projekt
![]() |
Das Visual Studio-Projekt sieht wie folgt aus:
![]() |
- Das Projekt [1] enthält folgende Elemente:
- [ServeurImpot.cs]: der TCP/IP-Server zur Steuerberechnung in Form einer Konsolenanwendung.
- [dbimpots.sdf]: die Datenbank SQL Compact Server der Version 7, die in Abschnitt 9.8.5 beschrieben wird.
- [App.config]: die Konfigurationsdatei der Anwendung.
- In [2] enthält der Ordner [lib] die für das Projekt erforderlichen Dateien DLL:
- [ImpotsV7-dao]: die Ebene [dao] der Version 7
- [ImpotsV7-metier]: die Schicht [metier] der Version 7
- [antlr.runtime, CommonLogging, Spring.Core] für Spring
- in [3], die Projektreferenzen
11.9.2.2. Anwendungskonfiguration
Die Datei [App.config] wird von Spring verwendet. Ihr Inhalt lautet wie folgt:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- Zeilen 16–20: Konfiguration der Schicht [dao], die mit der Basis SQL „Server compact“ verknüpft ist
- Zeilen 21–23: Konfiguration der Schicht „[metier]“.
Dies ist die Konfigurationsdatei, die in der Schicht [ui] der Version 7 verwendet wird. Sie wurde in Abschnitt 9.8.4 vorgestellt.
11.9.2.3. Funktionsweise des Servers
Beim Start des Servers instanziiert die Serveranwendung die Schichten [metier] und [dao] und zeigt anschließend eine Konsolenoberfläche zur Verwaltung an:
Die Verwaltungskonsole akzeptiert die folgenden Befehle:
um den Dienst auf einem bestimmten Port zu starten | |
um den Dienst zu beenden. Er kann anschließend auf demselben oder einem anderen Port neu gestartet werden. | |
um die Echo-Ausgabe des Client-Server-Dialogs auf der Konsole zu aktivieren | |
um das Echo zu deaktivieren | |
um den Status „aktiv“/„inaktiv“ des Dienstes anzuzeigen | |
um die Anwendung zu beenden |
Starten wir den Server:
Starten wir nun den asynchronen grafischen TCP-Client, der zuvor in Abschnitt 11.8 behandelt wurde.

Der Client ist verbunden. Er kann die folgenden Befehle an den Steuerberechnungsserver senden:
um die Liste der zulässigen Befehle abzurufen | |
um die Steuer für eine Person zu berechnen, die nbEnfants Kinder und ein Gehalt von salaireAnnuel Euro hat. marié ist o, wenn die Person verheiratet ist, andernfalls n. | |
um die Verbindung zum Server zu beenden |
Hier ein Beispiel für einen Dialog:
![]() |
Auf der Serverseite zeigt die Konsole Folgendes an:
Schalten wir das Echo ein und starten wir einen neuen Dialog vom grafischen Client aus:
![]() |
Die Verwaltungskonsole zeigt dann Folgendes an:
- Zeile 1: Das Echo des Client-Server-Dialogs ist aktiviert
- Zeile 2: Ein Client ist eingetroffen
- Zeile 3: Er hat den Befehl [aide] gesendet
- Zeilen 4–7: Die Antwort des Servers auf vier Zeilen.
Beenden wir den Dienst:
- Zeile 1: Der Dienst (nicht die Anwendung selbst) soll beendet werden
- Zeile 2: Eine Ausnahme, da der Server, der auf eine Client-Anfrage wartete, durch das Beenden des Listener-Dienstes abrupt unterbrochen wurde.
- Zeile 3: Der Dienst kann nun über „start port“ neu gestartet oder mit „quit“ beendet werden.
Bevor der Listening-Dienst beendet wurde, wurde ein Client über eine andere Verbindung bedient. Diese Verbindung wird durch das Schließen des Listening-Sockets nicht geschlossen. Der Client kann weiterhin Befehle senden: Der Dienst-Thread, der ihm vor dem Beenden des Listening-Dienstes zugewiesen war, antwortet ihm weiterhin:

11.9.3. Der Code des TCP-Servers zur Steuerberechnung
![]() |
1 ![]() |
Der Code des Servers [ServeurImpot.cs] lautet wie folgt:
...
namespace Chap9 {
public class ServeurImpot {
// Gemeinsam genutzte Daten zwischen Threads und Methoden
private static IImpotMetier metier = null;
private static int port;
private static TcpListener service;
private static bool actif = false;
private static bool echo = false;
// Hauptprogramm
public static void Main(string[] args) {
// Instanziierungen der Schichten [metier] und [dao]
IApplicationContext ctx = null;
metier = null;
try {
// Spring-Kontext
ctx = ContextRegistry.GetContext();
// Es wird eine Referenz auf die Schicht [metier] angefordert
metier = (IImpotMetier)ctx.GetObject("metier");
// Konfiguration des Thread-Pools
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// liest die über die Tastatur eingegebenen Server-Verwaltungsbefehle in einer Endlosschleife
string commande = null;
string[] champs = null;
while (true) {
// Eingabeaufforderung
Console.Write("Serveur de calcul d'impôt >");
// Befehl einlesen
commande = Console.ReadLine().Trim().ToLower();
champs = Regex.Split(commande, @"\s+");
// Befehl ausführen
switch (champs[0]) {
case "start":
// aktiv?
if (actif) {
//Fehler
Console.WriteLine("Le serveur est déjà actif");
} else {
// Portprüfung
if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
Console.WriteLine("Syntaxe : start port. Port incorrect");
} else {
// Listening-Dienst wird gestartet
ThreadPool.QueueUserWorkItem(doEcoute, null);
}
}
break;
case "echo":
// Echo „start“ / „stop“
if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
Console.WriteLine("Syntaxe : echo start / stop");
} else {
echo = champs[1] == "start";
}
break;
case "stop":
// Dienst beendet
if (actif) {
service.Stop();
actif = false;
}
break;
case "status":
// Serverstatus
if (actif) {
Console.WriteLine("Le service est lancé sur le port {0}", port);
} else {
Console.WriteLine("Le service n'est pas lancé}");
}
break;
case "quit":
// Beenden der Anwendung
Console.WriteLine("Fin du service");
Environment.Exit(0);
break;
default:
// Fehlerhafter Befehl
Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
break;
}
}
} catch (Exception e1) {
// Ausnahme anzeigen
Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
return;
}
}
private static void doEcoute(Object data) {
...
}
....
}
}
- Zeilen 18–21: Die Schichten [metier] und [dao] werden von Spring instanziiert, das durch [App.config] konfiguriert ist. Die globale Variable metier in Zeile 6 wird daraufhin initialisiert.
- Zeilen 24–25: Der Thread-Pool der Anwendung wird mit mindestens und höchstens 10 Threads konfiguriert.
- Zeilen 30–86: Die Schleife zur Eingabe der Verwaltungsbefehle des Dienstes (start, stop, quit, echo, status).
- Zeile 32: Server-Eingabeaufforderung für jeden neuen Befehl
- Zeile 34: Einlesen des Administratorbefehls
- Zeile 35: Der Befehl wird zur Analyse in Felder zerlegt
- Zeilen 38–52: Der Befehl „start port“, der den Listener-Dienst starten soll
- Zeile 40: Ist der Dienst bereits aktiv, muss nichts unternommen werden
- Zeile 45: Es wird überprüft, ob der Port vorhanden und korrekt ist. Wenn ja, wird die globale Variable port aus Zeile 7 gesetzt.
- Zeile 49: Der Listening-Dienst wird von einem sekundären Thread verwaltet, damit der Hauptthread weiterhin die Befehle der Konsole ausführen kann. Wenn die Methode doEcoute die Verbindung erfolgreich herstellt, werden die globalen Variablen service in Zeile 8 und actif in Zeile 9 initialisiert.
- Zeilen 53–60: Der Befehl „echo start / stop“, der das Echo des Client-Server-Dialogs auf der Konsole aktiviert bzw. deaktiviert
- Zeile 58: Die globale Variable echo aus Zeile 7 wird gesetzt
- Zeilen 61–67: Der Befehl stop, der den Listener-Dienst beendet.
- Zeile 64: Beenden des Listener-Dienstes
- Zeilen 68–75: Der Befehl status, der den Status „aktiv“/„inaktiv“ des Dienstes anzeigt
- Zeilen 76–80: Der Befehl quit, der alles beendet.
Der Thread, der für das Abhören von Client-Anfragen zuständig ist, führt die folgende Methode doEcoute aus:
private static void doEcoute(Object data) {
// Thread zum Abhören von Client-Anfragen
try {
// Dienst wird erstellt
service = new TcpListener(IPAddress.Any, port);
// Er wird gestartet
service.Start();
// Der Server ist aktiv
actif = true;
// Überwachung
Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
// Schleife für den Kundenservice
TcpClient tcpClient = null;
// Kundennummer
int numClient = 0;
// Endlosschleife
while (true) {
// Warten auf einen Kunden
tcpClient = service.AcceptTcpClient();
// Der Service wird von einer anderen Aufgabe übernommen
ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// nächster Kunde
numClient++;
}
} catch (Exception ex) {
// Fehler wird gemeldet
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
}
}
// Kundeninformationen
internal class Client {
public TcpClient CanalTcp { get; set; } // Verbindung zum Kunden
public int NumClient { get; set; } // Kundennummer
}
Hier handelt es sich um einen Code, der dem des in Abschnitt 11.6.1 behandelten Echo-Servers ähnelt. Wir gehen nur auf die Unterschiede ein:
- Zeile 7: Der Beratungsdienst wird gestartet
- Zeile 9: Es wird vermerkt, dass der Dienst nun aktiv ist
Linie 21: Die Kunden werden von Service-Threads bedient, die die folgende Methode „doService“ ausführen:
private static void doService(Object infos) {
// Der zu bedienende Kunde wird abgerufen
Client client = infos as Client;
// erbringt die Dienstleistung für den Kunden
Console.WriteLine("Début du service au client {0}", client.NumClient);
// Auswertung der Verbindung TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// ungepufferter Ausgabestrom
writer.AutoFlush = true;
// Versenden einer Willkommensnachricht an den Kunden
writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
// Schleife für Leseanforderung/Schreibantwort
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// Konsolenüberwachung
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// Anfrageauswertung
demande = demande.Trim().ToLower();
// Leere Anfrage?
if (demande.Length == 0) {
// Fehlerhafte Anfrage
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// Aufschlüsselung der Anfrage in Felder
string[] champs = Regex.Split(demande, @"\s+");
// Analyse
switch (champs[0].ToLower()) {
case "aide":
writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
break;
case "impot":
// Steuerberechnung
writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
break;
case "aurevoir":
serviceFini = true;
writeClient(writer, client.NumClient, "Au revoir...");
break;
default:
writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
break;
}
}
}
}
}
}
} catch (Exception e) {
// Fehler
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
private static void writeClient(StreamWriter writer, int numClient, string message) {
// Konsolenausgabe?
if (echo) {
Console.WriteLine("---> Client {0} : {1}", numClient, message);
}
// Nachricht an den Kunden senden
writer.WriteLine(message);
}
Auch hier handelt es sich um einen Code, der dem des in Abschnitt 11.6.1 behandelten Echo-Servers ähnelt. Wir gehen nur auf die Unterschiede ein:
- Zeile 15: Sobald der Client verbunden ist, sendet der Server ihm eine Willkommensnachricht.
- Zeilen 19–52: Die Schleife zum Einlesen der Befehle des Clients. Die Schleife endet, wenn der Client den Befehl „aurevoir“ sendet.
- Zeile 27: Fall eines leeren Befehls
- Zeile 34: Die Anfrage wird zur Analyse in Felder zerlegt
- Zeile 37: Befehl aide: Der Client fordert die Liste der zulässigen Befehle an
- Zeile 40: Befehl impot: Der Kunde fordert eine Steuerberechnung an. Die Antwort erfolgt mit der von der Methode calculImpot zurückgegebenen Meldung, auf die wir im Folgenden näher eingehen werden.
- Zeile 44: Befehl aurevoir: Der Kunde gibt an, dass er fertig ist.
- Zeile 45: Es werden Vorbereitungen getroffen, um die Schleife zum Lesen der Kundenanfragen (Zeilen 19–52) zu verlassen
- Zeile 46: Wir antworten dem Kunden mit einer Abschiedsnachricht.
- Zeile 48: Ein ungültiger Befehl. Dem Kunden wird eine Fehlermeldung gesendet.
Die Verarbeitung des Befehls impot erfolgt durch die folgende Methode calculImpot:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
// Abfrage zur Berechnung „verheiratet“ (J/N) nbEnfants salaireAnnuel
// Es sind 4 Felder erforderlich
if (champs.Length != 4) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// Felder [1]
string marié = champs[1];
if (marié != "o" && marié != "n") {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// Felder [2]
int nbEnfants;
if (!int.TryParse(champs[2], out nbEnfants)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// Felder [3]
int salaireAnnuel;
if (!int.TryParse(champs[3], out salaireAnnuel)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// Alles klar – die Steuer wird berechnet
int impot = 0;
try {
impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
return impot.ToString();
} catch (Exception ex) {
return ex.Message;
}
}
- Zeile 1: Die Methode erhält als dritten Parameter das Feldarray des Befehls impot. Wenn dieser korrekt formuliert wurde, hat er die Form „Einkommen verheiratet nbEnfants salaireAnnuel“. Die Methode gibt als Ergebnis die an den Kunden zu sendende Antwort zurück.
- Zeile 4: Es wird überprüft, ob der Befehl 4 Felder enthält
- Zeile 8: Es wird überprüft, ob das Feld marié gültig ist
- Zeile 14: Es wird überprüft, ob das Feld „nbEnfants“ gültig ist
- Zeile 19: Es wird überprüft, ob das Feld salaireAnnuel gültig ist
- Zeile 25: Die Steuer wird mithilfe der Methode CalculerImpot der Schicht [metier] berechnet. Zur Erinnerung: Diese Schicht ist in einer DLL gekapselt.
- Zeile 26: Wenn die Schicht [metier] ein Ergebnis zurückgegeben hat, wird dieses an den Client zurückgegeben.
- Zeile 28: Wenn die Schicht [metier] eine Ausnahme ausgelöst hat, wird die entsprechende Meldung an den Client zurückgegeben.
11.9.4. Der grafische Client des TCP-Servers zur Steuerberechnung
11.9.4.1. Das Visual Studio-Projekt „ “
![]() |
Das Visual Studio-Projekt des grafischen Clients sieht wie folgt aus:
![]() |
- In [1] sind die beiden Projekte der Lösung enthalten, eines für jede der beiden Schichten der Anwendung
- in [2] der TCP-Client, der die Rolle der Schicht [metier] für die Schicht [ui] übernimmt. Daher werden wir beide Bezeichnungen verwenden.
- in [3] die Schicht [ui] der Version 7, bis auf ein Detail, auf das wir noch eingehen werden
11.9.4.2. Die Ebene [metier]
Die Schnittstelle IImpotMetier hat sich nicht geändert. Es handelt sich weiterhin um die Schnittstelle der Version 7:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
Die Implementierung dieser Schnittstelle ist die folgende Klasse [ImpotMetierTcp]:
using System.Net.Sockets;
using System.IO;
namespace Metier {
public class ImpotMetierTcp : IImpotMetier {
// Informationen [serveur]
private string Serveur { get; set; }
private int Port { get; set; }
// Steuerberechnung
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
// Verbindung zum Dienst herstellen
using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// Ausgabestrom nicht gepuffert
writer.AutoFlush = true;
// Die Begrüßungsmeldung wird übersprungen
reader.ReadLine();
// Anfrage
writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
// Antwort
return int.Parse(reader.ReadLine());
}
}
}
}
}
}
}
- Zeile 7: Name oder IP-Adresse des TCP-Servers für die Steuerberechnung
- Zeile 8: der Listening-Port dieses Servers
- Diese beiden Eigenschaften werden von Spring bei der Instanziierung der Klasse [ImpotMetierTcp] initialisiert.
- Zeile 11: die Methode zur Steuerberechnung. Bei ihrer Ausführung sind die Eigenschaften Serveur und Port bereits initialisiert. Im Code findet sich der klassische Ablauf eines TCP-Clients wieder
- Zeile 13: Die Verbindung zum Server wird hergestellt
- Zeilen 14–16: Es wird (Zeile 14) der dieser Verbindung zugeordnete Netzwerkstrom abgerufen, aus dem ein Lesestrom (Zeile 15) und ein Schreibstrom (Zeile 16) abgeleitet werden.
- Zeile 18: Der Schreib-Stream muss ungepuffert sein
- Zeile 20: Hier ist zu beachten, dass der Server beim Öffnen der Verbindung dem Client eine erste Zeile sendet, nämlich die Begrüßungsnachricht „Willkommen auf dem Steuerberechnungsserver“. Diese Nachricht wird gelesen und ignoriert.
- Zeile 22: Man sendet dem Server den Befehl vom Typ: „impot o 2 60000“, um ihn aufzufordern, die Steuer für eine verheiratete Person mit zwei Kindern und einem Jahresgehalt von 60.000 Euro zu berechnen.
- Zeile 24: Der Server antwortet mit dem Steuerbetrag in der Form „4282“ oder mit einer Fehlermeldung, falls der Befehl falsch formatiert war (was hier nicht vorkommen wird) oder bei der Steuerberechnung ein Problem aufgetreten ist. Dieser letzte Fall wird hier nicht behandelt, aber es wäre sicherlich „sauberer“ gewesen, dies zu tun. Denn wenn die gelesene Zeile eine Fehlermeldung ist, wird eine Ausnahme ausgelöst, da die Konvertierung in eine Ganzzahl fehlschlägt. Die von der grafischen Benutzeroberfläche abgefangene Ausnahme wird als Konvertierungsfehler angezeigt, obwohl die ursprüngliche Ausnahme ganz anderer Natur ist. Der Leser ist aufgefordert, diesen Code zu verbessern.
- Zeilen 25–28: Freigabe aller mit einer „using“-Klausel verwendeten Ressourcen.
Die Schicht [metier] wird in die DLL ImpotsV8-metier.dll kompiliert:

11.9.4.3. Die Schicht [ui]
![]() |
Die Ebene [ui] [1,3] ist diejenige, die in Version 7 in Abschnitt 9.8.4 behandelt wird, mit drei Unterschieden:
- Die Konfiguration der Schicht [metier] in [App.config] ist anders, da sich deren Implementierung geändert hat
- Die grafische Benutzeroberfläche [Form1.cs] wurde geändert, um eine mögliche Ausnahme anzuzeigen
- Die Schicht [metier] befindet sich in der DLL [ImpotsV8-metier.dll].
Die Datei [App.config] lautet wie folgt:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
<property name="Serveur" value="localhost"/>
<property name="Port" value="27"/>
</object>
</objects>
</spring>
</configuration>
- Zeile 16: Instanziierung der Schicht [metier] mit der Klasse Metier.ImpotMetierTcp aus der DLL ImpotsV8-metier.dll
- Zeilen 17–18: Die Eigenschaften „Server“ und „Port“ der Klasse Metier.ImpotMetierTcp werden initialisiert. Der Server befindet sich auf dem Rechner localhost und läuft auf Port 27.
Die dem Benutzer angezeigte grafische Benutzeroberfläche sieht wie folgt aus:
![]() |
- In [1] wurde ein Feld TextBox hinzugefügt, um eine eventuelle Ausnahme anzuzeigen. Dieses Feld gab es in der vorherigen Version noch nicht.
Abgesehen von diesem Detail entspricht der Code des Formulars dem bereits in Abschnitt 6.4.3 behandelten. Der Leser wird gebeten, dort nachzuschlagen. In [2] ist ein Ausführungsbeispiel zu sehen, das mit einem Server erzielt wurde, der wie folgt gestartet wurde:
Der Screenshot [2] des Kunden entspricht den Zeilen des Kunden 9 oben.
11.9.5. Fazit
Erneut konnten wir bestehenden Code ohne Änderungen (Server-Schichten [metier], [dao]) oder mit nur sehr geringen Änderungen (Client-Schicht [ui]) wiederverwenden. Möglich wurde dies durch unseren konsequenten Einsatz von Schnittstellen und deren Instanziierung mit Spring. Hätten wir in Version 7 den Geschäftslogik-Code direkt in die Ereignisbehandler der grafischen Benutzeroberfläche geschrieben, wäre dieser Geschäftslogik-Code nicht wiederverwendbar gewesen. Das ist der größte Nachteil von Ein-Schicht-Architekturen.
Schließlich ist anzumerken, dass die Schicht [ui] keine Kenntnis davon hat, dass ein Remote-Server den Steuerbetrag für sie berechnet.























































