Client/Server Programmierung mit Sockets
|
|
|
- Hilko Baumann
- vor 10 Jahren
- Abrufe
Transkript
1 1 Client/Server Programmierung mit Sockets 1 ZUGRUNDELIEGENDE PROTOKOLLE EINFÜHRUNG PROTOKOLLSTAPEL DIE ETHERNETSCHICHT VERPACKEN VON NUTZDATEN DIE IP-SCHICHT DIE TRANSPORTSCHICHT UDP - USER DATAGRAM PROTOCOL TCP - TRANSMISSION CONTROL PROTOCOL ZUSAMMENFASSUNG ANATOMIE EINES PAKETS KONZEPTE DER SOCKETS SOCKETKONZEPTE ADRESSFAMILIEN DER SOCKETS DIE VERSCHIEDENEN SOCKETS VERBINDUNGSORIENTIERTE SERVEROPERATIONEN VERBINDUNGSAUFBAU DATENTRANSFER MITTELS SOCKETS SERVER FÜR EIN VERTEILTES SPIEL MEHRERE CLIENTEN GLEICHZEITIG BEDIENEN PARALLELE SERVER VERMEIDUNG VON ZOMBIES NACH SYSTEM V VERMEIDUNG VON ZOMBIES NACH BSD PARALLE SERVER, DIE SELECT() VERWENDEN PARALLELE DIENSTVERHÄLTNISSE UND STATUS EIN NEUES BEISPIEL: EIN LAGERVERWALTUNGSSERVER VERWENDUNG VON SELECT() DESIGN DES LAGERVERWALTUNGSSERVERS DAS PROGRAMM FÜR DEN LAGERVERWALTUNGSSERVER VERBINDUNGSORIENTIERTE CLIENTEN EIN SOCKET ERZEUGEN EINE LOKALE ADRESSE SETZEN (WENN GEWÜNSCHT) VERBINDUNG MIT DEM SERVER AUFNEHMEN VERBINDUNGSLOSE CLIENTEN UND SERVER DESIGN VERBINDUNGSLOSER SERVER OPERATIONEN VERBINDUNGSLOSER CLIENTEN UND SERVER BEISPIEL FÜR EINEN VERBINDUNGSLOSEN CLIENTEN TFTP-PAKETVERKEHR IMPLEMENTIEREN VON ZEITLIMIT UND WIEDERHOLUNG EINE PARALLELE, VERTEILTE ANWENDUNG EIN NICHT-VERTEILTES PROGRAMM ZUR ERMITTLUNG VON PRIMZAHLEN EIN SERVER ZUR ERMITTLUNG VON PRIMZAHLEN EIN CLIENT ZUR ERMITTLUNG VON PRIMZAHLEN LASTAUSGLEICH IMPLEMENTIEREN EINES PROZESSORPARKS STARTEN DER EXTERNEN SERVER... 69
2 2 1 Zugrundeliegende Protokolle 1.1 Einführung Die Netzdienste wie externe Kommandoausführung und NIS gehören alle zu einem normalen UNIX-Paket. Wir greifen häufig mit Hilfe von standardmäßigen Clientenprogrammen wie rsh und ypmatch auf sie zu. Wir müssen keine Clientenprogramme schreiben. In anderen Fällen brauchen wir Zugang zu diesen Diensten über spezielle Funktionen wie rexec() und gethostbyname(), die eine bequeme Schnittstelle zum Dienst schaffen und die Einzelheiten des zugrundeliegenden Protokolls verbergen. Hier werden wir uns damit befassen, eigene Server und Clienten zu schreiben und somit neue Netzdienste verfügbar zu machen. Wir werden den Mechanismus zum Aufbau verteilter Anwendungen untersuchen, die über das Netz hinweg arbeiten. Dazu müssen wir auf etwas niedrigerer Ebene arbeiten als Clienthülsen wie rexec() und gethostbyname() wir müssen unser eigenes Anwendungsprotokoll entwerfen und uns direkt mit unserem Transportdienst befassen, das heißt, mit den zugrundeliegenden Teilen der Netzsoftware, die die bescheidene, aber wichtige Aufgabe haben, die Daten zwischen Maschinen zu transportieren. Diese zugrundeliegenden Datentransportdienste sind selbst in Schichten organisiert. Sie bilden einen Protokollstapel unterhalb des Anwenderprogramms. Wir müssen nicht viel über die Physik des Verbrennungsmotors wissen, um Auto fahren zu können. Um allerdings die hohe Schule des Autofahrens zu verstehen, ist es günstig, einige Kenntnisse darüber zu haben, was unter der Haube vorgeht. Es hilft uns zum Beispiel zu wissen, warum es keine gute Idee ist, gleichzeitig auf Bremse und Gaspedal zu treten. Ähnlich müssen wir nicht viel über diese niedrigen Transportdienst -Schichten wissen, um verteilte Anwendungen zu schreiben wir sollten lediglich dankbar für sie sein. Auf einige Einzelheiten des Transportdiensts sollten wir allerdings achten. Wir müssen vor allem den Unterschied zwischen einem verbindungslosen Transport und einem kontinuierlichen (stream) Transport verstehen, und wir müssen etwas über die benutzten Adressschemata wissen. Wir beginnen mit einer kurzen Besprechung der zugrundeliegenden Netzprotokolle. 1.2 Protokollstapel Abb.: 1.1 zeigt ein einfaches Beispiel eines Protokollstapels. Die oberen Kästchen, FTP- Client und FTP-Server genannt, sind Beispiele für Anwendungsprogramme, die normalerweise im Programm-Adressraum ausgeführt werden. Sie sind also nicht Teil des Kernel, es sind Programme, die wir ganz normal schreiben, compilieren und ausführen. Die beiden mittleren Schichten des Stapels, TCP und IP genannt, sitzen in erster Linie im Kernel. Ein kleiner Teil kann als Bibliotheksfunktionen implementiert werden, die mit dem Anwendungsprogramm gelinkt sind. Die unterste Schicht des Stapels, Ethernet genannt, ist fast immer in der Hardware einer Netzwerkkarte implementiert.
3 3 Abb.: 1.1 Ein typischer Protokollstapel Präambel Zieladresse Absenderadresse Pakettyp Daten (z. B. IP-Datagram) CRC 64 Bits 48 Bits 48 Bits 16 Bits Bits 32 Bits Abb.: 1.2 Das Ethernet-Rahmenformat In diesem Beispiel kommunizieren Client und Server über das File Transfer Protocol. Das Protokoll definiert natürlich das Format der Nachrichten, die zwischen Client und Server hin und hergeschickt werden. Vom Konzept her stellen wir uns auch tatsächlich vor, Server und Client würden direkt miteinander reden. Diese direkte Kommunikation ist nur eine Illusion. Die Daten wandern in der Realität den Protokollstapel des Senders hinunter und den Stapel des Empfängers wieder hinauf. Einheiten auf der selben Ebene des Protokollstapels werden als Peers (gleichrangige Partner) bezeichnet. Die (konzeptuelle) Kommunikation zwischen ihnen wird Peer to Peer -Kommunikation genannt. Ein Programmierer, der ein Programm wie den FTP-Server schreiben will, muss daher zwei unterschiedliche Schnittstellen verstehen: 1. Das Peer to Peer-Anwendungsprotokoll definiert Format und Bedeutung der Nachrichten, die zwischen Server und Client ausgetauscht werden. 2. Die Schnittstelle zur Schicht des Transportdiensts (in diesem Beispiel TCP) im Kernel. Diese Schnittstelle besteht aus wenigen Systemaufrufen und/oder Bibliotheksroutinen, ü- ber die das Programm die Dienste des Transportunternehmens in Anspruch nimmt.
4 4 Echte Anwendungsprotokolle sind normalerweise eher kompliziert, das Programm für ihre Implementation ist entsprechend umfangreich. Die Beispiele rücken absichtlich den anwendungsspezifischen Teil des Programms in den Hintergrund, um die Anwendungsprotokolle einfach zu halten. Wir lernen, einen Telefonanruf zu führen, nicht aber dem Zuhörer am anderen Ende der Leitung eine Doppelverglasung für die Fenster seines Hauses zu verkaufen. 1.3 Die Ethernetschicht Was tun nun die unteren Schichten des Protokollstapels wirklich? Wir werden uns zunächst die unterste in Abb.: 1.1 gezeigte Schicht ansehen die Ethernetschicht. Im ISO OSI- Referenzmodell entspricht sie in etwa den beiden unteren Schichten (die physikalische Schicht und die Datenverbindungsschicht). Die Ethernetschicht befasst sich mit der Übertragung von Dateneinheiten (Blöcke (frames) genannt) zwischen Maschinen auf demselben physikalischen Netzwerk. Abb.: 1.2 verdeutlicht das Format dieser Blöcke. Die Felder werden kurz erläutert: 1. Eine Einleitung, eine Bitfolge, die der empfangenden Schnittstelle die Synchronisation ermöglicht. 2. Eine 48-Bit Zieladresse (manchmal physikalische Adresse genannt), die bestimmt, für welchen Netzknoten der Block gedacht ist. Jeder Netzknoten verfügt über eine einzigartige physikalische Adresse, damit er unterscheiden kann, welche Blöcke empfangen und welche ignoriert werden müssen. 3. Eine 48-Bit Quelladresse, die anzeigt, von welcher Maschine der Block kam. 4. Ein Typfeld mit einer magic number, die anzeigt, für welches Protokoll höherer Ebene dieser Block transportiert wird. Ein Wert 800 (hex) weist auf ein IP-Paket hin, 6003 (hex) ein Decnet Phase-4-Paket und 809B (hex) ein Appletalk-Paket. Dieses Typfeld ist wichtig, damit Protokolle mehrerer Hersteller auf demselben physikalischen Netz nebeneinander genutzt werden können. 5. Die Daten. Unter TCP/IP wird es ein IP-Datagramm sein oder ein Teil davon. 6. Ein 32-Bit CRC (cyclic redundancy check)-feld, damit der empfangende Netzknoten prüfen kann, ob der Block vollständig und unverändert übertragen wurde. 1.4 Verpacken von Nutzdaten Natürlich befindet sich innerhalb des Blocks ein Feld für die Nutzdaten, die im Auftrag der übergeordneten Schicht transportiert werden. Dieses Feld verfügt über eine variable Länge. Ein wichtiges Konzept der Protokollstapel ist die Verpackung von Nutzdaten. Beim Verpacken wird einem Datenpaket von einer Schicht eine Überschrift (header) (manchmal auch ein Nachtrag (trailer)) hinzugefügt. Dieser Header enthält ausschließlich die Information, die diese Schicht zur Ausführung ihrer Aufgabe benötigt, das heißt, um dem entsprechenden Peer am anderen Ende das Paket zuzustellen. Abb.: 1.3 verdeutlicht, dass dieses Verpacken an jeder Schicht des Protokollstapels stattfindet. Das Paket wird immer größer, wenn ihm auf seinem Weg den Protokollstapel hinunter immer mehr Header hinzugefügt werden. Der Header jeder Schicht wird Teil der Daten der darunter folgenden Schicht. Auf der Empfängerseite baut jede Schicht nach Erledigung ihrer Aufgabe den Header ab.
5 5 Abb.: 1.3 Verpacken von Nutzdaten Folgende Analogie soll uns helfen. Max möchte der in Berlin lebenden Moritz einen Brief schicken. Er schreibt diesen Brief und gibt ihn mit der Anweisung Bitte an Moritz schicken an seine Sekretärin weiter. Die Sekretärin weiß, dass sie nicht einfach ein loses Blatt Papier in die Post geben kann. Sie steckt den Brief in einen Umschlag, schreibt Moritz Adresse auf die Vorderseite und klebt eine Briefmarke darauf. Jetzt kann der Brief der Post anvertraut werden. Mit etwas Glück erhält Moritz Sekretärin am nächsten Tag den Brief. Sie nimmt den Brief aus dem Umschlag, wirft den Umschlag weg und gibt Moritz den Brief. Diese Analogie verdeutlicht zwei wichtige Aspekte: 1. Weder Max noch Moritz sehen jemals den Briefumschlag. 2. Keine der Sekretärinnen muss den Brief lesen. Am Rande: rlogin transportiert einzelne Zeichen im Netz und gehört somit zu den Anwendungen, die die Netzbandbreite sehr schlecht ausnutzen. Jede Ein-Byte Nachricht sammelt insgesamt 74 Byte Verpackung an, bis sie das Netzkabel erreicht hat. Das erinnert an die bedauerliche Verschwendung von Verpackungsmaterial bei Nahrungsmitteln und vielen anderen Dingen. Man sollte sich fragen, ob nicht eine stärker ökologisch orientierte Implementation des TCP/IP geschrieben werden sollte, die die alten Header wiederverwendbar macht. Die Ähnlichkeiten sind deutlich: Keine Schicht des Protokollstapels kennt den von der unteren Schicht hinzugefügten Header, und keine Schicht versucht die Daten zu interpretieren, die sie von der oberen Schicht erhalten hat sie wird sie nur an den entsprechenden Peer am anderen Ende weiterleiten. Die Daten werden daher manchmal als opak bezeichnet. Vor dem Verlassen der Ethernetschicht müssen wir noch festhalten, dass Ethernet zwar das dominierende, aber auf keinen Fall das einzige physikalische Medium ist, das zum Transport der Daten in UNIX-Netzen eingesetzt wird. Andere Standards wie IEEE (Token Ring) und ANSI X3T9.5 (FDDI) sind auch im Einsatz. Der IEEE Standard ist dem E- thernet sehr ähnlich, wurde aber von der UNIX-Gemeinde nicht so gut angenommen. Die von diesen Standards benutzten Blockformate sind zwar nicht mit denen des Ethernet identisch, die grundlegenden Ideen sind Jedoch fast gleich Header- und Trailer-Information, die die physikalische Adresse von Quelle und Ziel enthalten, umgeben ein Datenfeld mit variabler Länge.
6 6 Abb.: 1.4 Das Format eines IP-Datagramms 1.5 Die IP-Schicht Eine Schicht darüber finden wir die Internet-Protocol (IP)-Schicht. Diese Schicht entspricht in etwa der Netzwerkschicht im OSI 7-Schichten-Modell. Sie ist ebenfalls für den Pakettransport von einer Maschine zur anderen verantwortlich, hat aber gegenüber der Ethernetschicht eine sehr wichtige Eigenschaft zwischengeschaltete Netzverbindungssysteme erleichtern es, den Weg des Pakets zu bestimmen. So wird es möglich, Daten auch zu Maschinen zu schicken, die nicht direkt an dasselbe physikalische Netzwerk angeschlossen sind. Die von der IP-Schicht transportierte Dateneinheit wird als IP-Datagramm bezeichnet. Grundlage für die für die Wegbestimmung von IP-Datagrammen im Internet ist nicht eine physikalische Adresse wie Ethernet, sondern eine Adresse höherer Ebene, die Internet- oder IP-Adresse genannt wird. Die IP-Adresse ist eine 32-Bit Zahl, die logisch in zwei Teile zerlegt wird. Der erste Teil identifiziert ein Netz (und wird daher für die Wegbestimmung des Datagramms benutzt), der zweite eine bestimmte Hostverbindung in diesem Netz. IP bietet einen verbindungslosen Transportdienst. Jedes IP-Datagramm wird unabhängig von anderen auf seinem Weg durch das Internet geleitet. Das hat wichtige Folgen: 1. Da ein Netzverbindungssystem für jedes einzelne Datagramm eine Wegentscheidung treffen muss, müssen diese Entscheidungen sehr effizient getroffen werden. (Das hat kaum Auswirkungen für den Anwendungsprogrammierer. Für Designer und Implementatoren der Internet Protokollfolge selbst ist es jedoch eine Herausforderung erster Ordnung). 2. Es kann nicht garantiert werden, dass zwei aufeinanderfolgende Datagramme denselben Weg nehmen, auch wenn sie von derselben Quelle zum selben Ziel laufen. Wichtige Folge ist, dass die IP-Datagramme nicht unbedingt in derselben Reihenfolge ankommen, in der sie abgeschickt wurden.
7 7 Innerhalb der Netzverbindungssysteme gibt es keine dauerhafte Zuordnung von Ressourcen zu bestimmten Verbindungen. Daher müssen die Netzverbindungssysteme manchmal ein Datagramm mangels Puffer oder anderer Ressourcen fallenlassen. Der von IP angebotene Transportdienst entspricht daher eher einem Dienst nach besten Kräften als einer garantierten Zulieferung. 3. Abb.: 1.4 zeigt ein leicht vereinfachtes Format eines IP-Datagramms. 1.6 Die Transportschicht Eine Stufe höher erreichen wir die Transportschicht. Für den Anwendungsprogrammierer ist diese Schicht am interessantesten, da er hiermit direkt zu tun hat. Unter UNIX werden üblicherweise zwei Transportprotokolle benutzt das User-Datagram- Protocol (UDP) und das Transmission-Control-Protocol (TCP). Diese beiden Protokolle bieten entsprechend verbindungslosen und verbindungsorientierten Transport. Wie Abb.: 1.5 verdeutlicht, können UDP und TCP mit Post und Telefondienst verglichen werden. Betrachten wir zunächst die Eigenschaften der Post: Abb.: 1.5 Datagramm-Auslieferung und kontinuierliche Verbindung 1. Jeder Brief muss eine Adresse haben. Der Brief wird ausschließlich aufgrund dieser Adresse durch das Postsystem geschleust. Es gibt keinerlei Möglichkeit, eine Art privater Abmachung mit dem Briefkasten zu treffen. dass alle von mir dort abgelegten Briefe an eine bestimmte Person ausgeliefert werden. Wird dem UDP ein Datagramm für den Transport übergeben, muss in ähnlicher Weise die Adresse des Transportendpunkts angegeben werden, an den es geschickt werden soll. 2. Die Post bestätigt nicht, dass ein Brief ausgeliefert wurde oder dass er nicht ausgeliefert werden konnte (außer wenigen speziellen und teuren Postdiensten, die hier nicht beachtet werden sollen). Wenn ich also einen Brief aufgebe, weiß ich nicht mit Sicherheit, wann oder ob er an seinem Ziel ankommt. Ähnlich hat UDP keinen eingebauten Mechanismus, den Empfang von Datagrammen zu bestätigen. Folglich gibt es auch keine Vorkehrungen, automatisch verlorengegangene Datagramme neu zu senden oder den Sender zu benachrichtigen, dass der Transport nicht erfolgreich war.
8 8 3. Die Post gibt keine Sicherheit dafür, dass Briefe auch in derselben Reihenfolge ankommen, in der sie aufgegeben wurden. Entsprechend liefert UDP nicht notwendigerweise Datagramme in der Reihenfolge, in der sie abgeschickt wurden. 4. Schicke ich drei unterschiedliche Briefe an die gleiche Person, so erhält diese Person drei unterschiedliche Briefe. Auch wenn es sich dabei um drei aufeinanderfolgende Abschnitte meiner Memoiren handelt, verschmelzen sie unterwegs nicht zu einem Dokument. Der Empfänger weiß, wo ein Brief endet und der nächste anfängt. Ähnlich werden UDP- Datagramme als getrennte Einheiten gesendet und empfangen. Wir sagen dazu, UDP hält die Begrenzungen der Nachricht aufrecht. Nun sehen wir uns den Telefondienst an. Die Analogie zu TCP ist nicht so weitgehend, es gibt aber einige wichtige Punkte: 1. Führe ich einen Telefonanruf aus, so muss ich nur einmal eine Adressinformation angeben (in diesem Fall die Telefonnummer), wenn ich wähle. Während des Anrufs weiß mein Telefon irgendwie, dass die von mir gesprochenen Worte zu diesem bestimmten Empfänger gelangen müssen. Ich muss nicht jeden Satz mit bitte diesen Satz an den und den weiterleiten beginnen. Unter TCP gebe ich ebenfalls einmal beim Aufbau der Verbindung eine Adresse an. Danach gebe ich einfach Daten auf diese Verbindung. 2. Die Person am anderen Ende hört die von mir gesprochenen Worte in genau der Reihenfolge, in der ich sie gesagt habe. TCP garantiert ebenfalls, dass die Zeichen in dergleichen Reihenfolge geliefert werden, in der sie abgeschickt wurden. 3. Ist der Anruf einmal durchgeschaltet, arbeitet ein Telefon in beide Richtungen. Ich kann sowohl sprechen als auch zuhören. Eine TCP-Verbindung ist ebenfalls bidirektional. 4. Das Telefon liefert ein kontinuierliches Audiosignal. Es zerlegt es nicht in Teile. TCP bietet ebenso eine rein strom-orientierte Verbindung, es kennt keine Nachrichtenbegrenzungen. Die Analogie ist hier nicht so zutreffend, wir müssen etwas weiter ausholen. Wenn ich 100 Byte an eine TCP-Verbindung sende, dann weitere 100, dann noch einmal 50, so findet das Empfangsende 250 Byte vor, die darauf warten, gelesen zu werden. Es hat keine Ahnung, dass Pakete mit 100, 100 und 50 Byte geschrieben wurden. Das Empfangsende kann nun alle 250 Bytes in einem Arbeitsgang lesen oder nur jeweils 10 Byte oder wie auch immer. Im Gegensatz dazu muss ich unter UDP jeweils genau ein ganzes Datagramm lesen. Über TCP laufende Anwendungsprotokolle sollten daher dem Empfänger eine Möglichkeit geben, zu wissen, wann er eine vollständige Nachricht erhalten hat. Die Anwendung kann zum Beispiel mit Nachrichten unveränderbarer Länge arbeiten, die vielleicht irgendeiner C-Struktur entsprechen. Sind Nachrichten mit variabler Länge nicht zu vermeiden, so können sie einen Anfangswert enthalten, der dem Empfänger ausdrücklich mitteilt, wie viele Daten er zu erwarten hat. Alternativ kann ein Server eine Zeichenfolge an einen Clienten schicken, wobei jede Zeichenfolge durch ein Nullzeichen beendet wird. Diese Analogien sollen allgemein die Unterschiede zwischen einem verbindungslosen (UDP) und einem verbindungsorientierten (TCP) Transportdienst verdeutlichen. Nun werden wir uns beide Protokolle in Einzelheiten ansehen. 1.7 UDP - User Datagram Protocol Abb.: 1.6 zeigt das Format eines UDP-Datagramms. Es ist ausgesprochen einfach und spiegelt die Tatsache wieder, dass UDP nur wenig mehr als der von 1P geleistete Dienst anbietet. Die wichtigste Funktion besteht darin, Datagramme von bestimmten Transportendpunkten (eigentlich spezifischen Prozessen) innerhalb der Maschine anzunehmen und an bestimmte Transportendpunkte zu liefern. Der Transportendpunkt wird durch eine 16-Bit-Port-
9 9 Nummer definiert. Im Rahmen unserer Postanalogie können wir sagen, dass die IP-Schicht dem Briefträger entspricht, der die Briefe an der Haustür unseres Unternehmens abliefert. UDP übernimmt die Rolle eines internen Transportdiensts, der diese Briefe an die einzelnen Abb.: 1.6 Das Format eines UDP-Datagramms Empfänger innerhalb des Unternehmens verteilt. Der UDP-Header beinhaltet eine Kontrollsumme, die der UDP-Schicht am Empfangsende ermöglicht, die Gültigkeit der Daten zu prüfen. Darüber hinaus hat UDP alle Nachteile des IP geerbt es prüft nicht, ob die Datagramme empfangen wurden oder überträgt sie nicht neu, wenn sie verlorengegangen sind. Es kann auch nicht sicherstellen, dass die Datagramme in derselben Reihenfolge empfangen werden, in der sie abgeschickt wurden. 1.8 TCP - Transmission Control Protocol TCP arbeitet wie UDP mit der Port-Nummer, um Nachrichten an spezifische Transportendpunkte innerhalb der Maschine zu leiten. Wie UDP stellt auch TCP dem Empfänger eine Kontrollsumme zur Verfügung, um zu verifizieren, dass die Daten richtig empfangen wurden. Dann enden die Ähnlichkeiten aber. TCP arbeitet mit einer Technik, die positive Bestätigung mit Übertragungswiederholung genannt wird und bietet damit einen Datentransportdienst oberhalb der Netzwerkschicht IP, der zuverlässig und in der richtigen Reihenfolge arbeitet, was IP nicht kann. Abb.: 1.7 veranschaulicht die Grundidee. Immer, wenn TCP ein Datensegment sendet, startet es ein Zeitglied. Erhält der Empfänger das Segment, schickt er sofort eine Bestätigung an den Sender. Erhält der Sender die Bestätigung, weiß er, dass alles in Ordnung ist und setzt das Zeitglied zurück. Verliert die IP-Schicht das abgeschickte Segment (oder die zurückgeschickte Bestätigung), so läuft das Zeitglied des Senders ab und der Sender wiederholt die Übertragung des Segments. Abb.: 1.7 zeigt ein einfaches Gänsemarsch -Schema, in dem der Sender auf die Bestätigung jedes Segments wartet, bevor er das nächste abschickt. Die maximale Kommunikationsbandbreite, die mit diesem Schema erreicht werden kann, entspricht Sie liegt mit Sicherheit unter der vom Netz tatsächlich erreichbaren. (Stellen Sie sich vor, sie möchten mit einem Freund mittels Postkarten einen Dialog führen. Wenn Sie darauf war- (Maximale Paketgröße) / (Übertragungszeit) [Zeichen/sec]
10 10 ten, dass jede Karte bestätigt wird, bevor Sie die nächste schicken, und wenn eine Postkarte durchschnittlich zwei Tage für die Zustellung benötigt, so können Sie nur alle vier Tage eine Karte schicken. Der Briefträger selbst könnte allerdings mehr bewältigen). TCP setzt für dieses Problem ein sliding window -Protokoll ein. Hierbei können mehrere unbestätigte Segmente gleichzeitig im Netz vorhanden sein. Abb.: 1.8 zeigt, wie das Fenster langsam den Datenstrom entlanggleitet, während die Übertragung weitergeht. Bytes hinter dem hinteren Ende des Fensters wurden sowohl übertragen als auch bestätigt. Bytes vor dem Fenster wurden noch nicht abgeschickt. Im TCP-Header gibt es drei Felder zur Kontrolle des Gleitfensters (sliding window). Die Sequenznummer wird vom Sender in den Header gesetzt und weist auf den Bytestartpunkt innerhalb des Datenstroms hin, an dem das Segment beginnt. Diese Daten werden vom Empfänger benutzt, um sicherzustellen, dass Segmente, die in der falschen Reihenfolge ankommen, wieder richtig zusammengefügt werden, sowie um doppelte Segmente zurückzuweisen. Die Bestätigungsnummer wird von den vom Empfänger zurückgegebenen Abb.: 1.7 Positive Rückmeldung und erneute Übertragung Bestätigungen benutzt, sie zeigt dem Sender an, welches Segment bestätigt wird. (Bestätigungspakete nutzen dasselbe Headerformat wie abgeschickte Pakete, aber sie enthalten normalerweise keine Daten). Ein drittes Feld wird zur Kontrolle der Fenstergröße verwendet. Dieses Feld wird bei der Bestätigung der Pakete benutzt. Der Empfänger füllt es auf, und zeigt damit an, wie viel Zeichen er (über das gerade bestätigte hinaus) zu akzeptieren bereit ist, bevor er weitere Bestätigungen schickt. Betrachten wir das Szenario in Abb.: 1.8. Der Empfänger hat den Empfang der Bytes 1-6 bestätigt und eine Fenstergröße von 11 festgelegt. Das erlaubt dem Sender nun, bis Byte 17 abzusenden, aber nicht mehr. An dem im Diagramm gezeigten Punkt hat der Sender bis Byte 13 geschickt. Die Bytes können noch abgeschickt werden, ohne auf weitere Bestätigungen warten zu müssen. Darüber hinaus muss der
11 11 Sender jedoch abwarten, bis der Empfänger weitere Bestätigungen geschickt hat und das Ende des Fensters weitergleiten kann. Die Fenstergröße zeigt die Größe des beim Empfänger verfügbaren Puffers für die Zwischenspeicherung ankommender Daten an. (In unserem Beispiel ist die Datenmenge natürlich unrealistisch klein). Abb.: 1.8 Ein gleitendes Fenster Mit dem Gleitfensterprotokoll hat TCP die genaue Flußkontrolle des Datenstroms. Die Algorithmen für die Anpassung der Fenstergröße sind nit den Jahren verbessert worden. Sie stellen jetzt sicher, dass die Netz-bandbreite bestmöglich ausgenutzt wird und garantieren gleichzeitig, dass der Empfänger niemals Daten fallenlassen muss, weil er mehr geschickt bekommt, als er bearbeiten kann. Abb.: 1.9 zeigt das Format des TCP-Datagramms.
12 12 Abb.: 1.9 Das Format eines TCP-Datagramms Wir müssen noch folgendes festhalten. Die in den UDP- und TCP-Transportdiensten gefundenen Eigenschaftskombinationen sind von den Designern ausgewählt. Sie sind nicht von grundlegender Bedeutung, andere Kombinationen sind möglich. Wir können uns zum Beispiel einen Dienst für Pakete in zuverlässiger Reihenfolge vorstellen, der (wie UDP) Nachrichtengrenzen erhält, aber die richtige Abfolge der Pakete sicherstellt, keine Duplikate zulässt usw. Anwendungen, für die ein solcher Transportdienst ideal geeignet ist, sind leicht zu finden. Alternativ können wir uns einen verbindungsorientierten Transportdienst vorstellen, der die Zustellung aller Daten, nicht aber ihre Reihenfolge garantiert. Es ist ausgesprochen schwierig, Anwendungen zu finden, für die ein derartiger Dienst irgendwie nützlich wäre. Am Rande: Der verstorbene, sehr beliebte Komiker Eric Morecambe führte einst einen Sketch auf, bei dem er versuchte, unter den Augen des Showgasts Andre Previn das Klavierkonzert in A Moll von Grieg zu spielen. Der entrüstete Previn stellte ihn am Schluß der Vorführung zur Rede. Eric verteidigte sich mit der Antwort: Ich habe alle Noten richtig gespielt - nur nicht unbedingt in der richtigen Reihenfolge. 1.9 Zusammenfassung Fassen wir die Eigenschaften der Schichten des TCP/IP-Protokollstapels zusammen:
13 13 Tabelle 1.1 Schicht Entsprechende OSI Schicht Eigenschaften Ethernet Physikalische und Datenverbindungs-schicht Transportiert Blöcke zwischen direkt verbundenen Maschinen. Andere Technologien (z.b. Token Ring, FDD1) bieten Ähnliches. IP Netzwerkschicht Transportiert IP-Datagramme zwischen zwei beliebigen Maschinen auf einem verbundenen Internet. Die Maschinen können durch zwischengeschaltete Gateways getrennt sein. Befasst sich mit der Wegbestimmung und Fragmentierung, garantiert aber weder die Zustellung noch die richtige Reihenfolge der Datagramme UDP Transportschicht Tiansportieit Datagramme zwischen Tiansportendpunkten (d.h. spezifischen Programmen) für zwei beliebige Maschinen auf einem verbundenen Internet. TCP Transportschicht Bietet einen zuverlässigen, flusskontrollierten Datenstromzustelldienst mit richtiger Abfolge zwischen Transportendpunkten (d.h. spezifische Programme) für zwei beliebige Maschinen auf einem verbundenen Internet. Enthält keine Nachrichtenbegrenzungen Anatomie eines Pakets Am Markt sind einige Programme zur Beobachtung des Datenverkehrs verfügbar, die den Netzwerkverkehr einfangen und zeigen. Ein typisches Beispiel ist das Produkt Lanwatch von FTP-Software. Es läuft unter DOS auf einem PC und kann verschiedenste Netzwerkprotokolle zeigen (nicht nur TCP/IP). Es interpretiert Paket-Header bis hinauf zur und einschließlich der Transportschicht. Teil des Solaris-Systems ist das Hilfsprogramm snoop. Es hat den Vorteil, in einem echten UNIX-System zu laufen. Der Schwerpunkt liegt auf TCP/IP, die Interpretation des Protokollstapels kann jedoch auch auf höheren Ebenen erfolgen. Es können zum Beispiel Opcodes eines tftp-pakets oder die Parameter einer NFS-Anfrage abgebildet werden. Dateibeobachter lassen ihren Netzknoten unterschiedslos arbeiten, das heißt, der Knoten akzeptiert alle Pakete unabhängig von ihrer Zieladresse. Diese Programme können sowohl als Diagnosehilfen als auch als Lehrwerkzeuge sehr nützlich sein. Ein Beispiel der Bildschirmausgabe von snoop soll dies verdeutlichen. Die Ausgabe bezieht sich auf ein einzelnes Paket, das heißt ein tftp-paket mit einer Leseanfrage vom Host uranus an pluto. Die Verpackung ist deutlich zu erkennen, der Header jeder Schicht wird nacheinander gezeigt: ETHER: ---- Ether Header- -- ETHER: ETHER: Packet 1 arrived at 15:39:35.08 ETHER: Packet size = 62 bytes
14 14 ETHER: Destination = 8:0:20:e:7e:24,Sun ETHER: Source = 8:0:20:e:7c:9a,Sun ETHER: Ethertype = 0800 (IP) ETHER: IP: IP Header IP: IP: Version = 4 IP: Header length = 20 bytes IP: Type of service = 0x00 IP: xxx... = 0 (precedence) IP: = normal delay IP: = normal throughput IP: = normal reliability IP: Total length = 48 bytes IP: Identification = IP: Flags = 0x0 IP:.0... = may fragment IP: = last fragment IP: Fragment offset = 0 bytes IP: Time to live = 60 seconds/hops IP: Protocol = 17 (UDP) IP: Header checksum = 7c47 IP: Source address = , uranus IP: Destination address = , pluto IP: No options IP: UDP: --- UDP Header --- UDP: UDP: Source port = 3969 UDP: Destination port = 69 (TFTP) UDP: Length =28 UDP: Checksum = 0000 (no checksum) UDP: TFTP: Trivial File Transfer Protocol TFTP: TFTP: Opcode = 1 (read request) TFTP: File name = /etc/passwd TFTP: Transfer mode = octet 2 Konzepte der Sockets Wir haben uns angesehen, wie die TCP-und UDP-Transportdienste arbeiten und weiden uns nun der Frage zuwenden, wie unser Anwendungsprogramm auf diese Dienste zugreifen kann. Die ursprüngliche Schnittstelle zu TCP und UDP stammt aus dem Release BSD 4.2 des Berkeley UNIX. Sie besteht aus etwa acht neuen Systemaufrufen und wird mit dem allgemeinen Namen Sockets bezeichnet. Wir werden diese Systemaufrufe untersuchen und uns damit befassen, wie sie zum Schreiben von Clienten und Servern eingesetzt werden. 2.1 Socketkonzepte Ein Socket ist ein Kommunikationsendpunkt. An dieser Stelle treffen sich Anwendungsprogramm und Transportdienst. Nehmen wir wieder eine Analogie zur Hilfe. An Ihrer Arbeitsstelle gibt es vielleicht einen Kasten, in den Ausgangspost gelegt wird. Zweimal pro Tag entnimmt der Briefträger (der Transportdienst) die Post aus diesem Kasten und liefert sie aus. Auf der Empfängerseite ist wahrscheinlich auch eine Art Briefkasten, in dem der Briefträger die Briefe hinterlässt und die Empfänger sie abholen. Diese
15 15 Abb.: 2.1 Ein Transportendpunkt - Illustrierte Sockets Briefkästen sind wie Datagrammsockets. Sie bilden den Kontaktpunkt zwischen dem Programm des Anwenders und dem verbindungslosen Transportdienst (siehe Abb.: 2.1). Entsprechend ist ein Telefon ein Kommunikationsendpunkt analog einem Streamsocket. Es ist der Kontaktpunkt zwischen Benutzer und einem verbindungsorientierten Transportdienst (der Telefongesellschaft). Aus diesen Analogien erkennen wir, dass Sockets einen Typ haben, der den mit ihnen verbundenen Transportdienst anzeigt. Es sind zwar mehrere Sockettypen definiert, wir werden uns jedoch in erster Linie mit den beiden oben erwähnten befassen, den DATAGRAM Sockets und den STREAM Sockets. Sockets können nur mit Sockets desselben Typs inter-agieren. Das ist sinnvoll: wir können nicht jemandem einen Brief schicken und ihn dann als Monolog aus dem Telefonhörer des Empfängers ankommen lassen. 2.2 Adressfamilien der Sockets Bevor wir uns Programme ansehen, sollten wir untersuchen, wie Sockets adressiert werden. Wenn zwei nicht miteinander in Verbindung stehende Prozesse zusammenkommen wollen, muss es irgendetwas geben, was beide kennen ein vereinbarter Ort, an dem sie sich treffen können. Beim gemeinsam genutzten Speicher oder bei den Nachrichtenschlangen gibt es einen einfachen numerischen Schlüssel, auf den sich die betroffenen Parteien geeinigt haben müssen. Bei Named Pipes gibt es einen Dateinamen, den beide Programme geöffnet haben müssen. Es gibt mehrere verschiedene Wege, Sockets einen Namen zu geben. Diese Namensmethoden werden als Adressfamilien bezeichnet. Das einfachste Adressschema ist die sogenannte UNIX-Adressfamilie, in der mit den Sockets UNIX-Pfadnamen assoziiert sind, zum Beispiel /tmp/mysock. Unter BSD U- NIX werden diese Sockets in der Verzeichnislangform als Einträge des Typs s gezeigt, zum Beispiel: venus% ls -l /tmp/mysock srwxrwxrwx l Max 0 Jul l 21:38 /tmp/mysock
16 16 venus% Unter SVR4 UNIX wird dieser Sockettyp als Named Pipe implementiert und im Verzeichnis als Typ p angezeigt. Named Pipes können auch von der Kommandozeile aus mit mknod fileriame p angelegt werden. Diese UNIX-Adressfamilien Sockets sind zwar einfach, taugen aber nicht als netzweiter IPC-Mechanismus, da Client und Server auf derselben Maschine sein müssen. Wir werden sie daher in keinem unserer Beispiele einsetzen. Ein anderes allgemein gebräuchliches Schema für die Benamung der Sockets wird Internet Domain Adressing genannt. In diesem Schema besteht der Name des Sockets aus zwei Zahlen: die 32-Bit Adresse des Hosts, auf dem das Socket sich befindet, und eine 16-Bit Portnummer. Wir werden in den Beispielen mit dieser Adressfamilie arbeiten. Die Socketsystemaufrufe, die Socketadressen als Argumente benutzen (wie bind() der Aufruf, der den Namen tatsächlich mit dem Socket verbindet), sind flexibel genug, um mit diesen unterschiedlichen Adressfamilien umgehen zu können. Die Funktionsschnittstelle selbst ist so flexibel und verarbeitet jede künstlich geschaffene Art der Socketadresse. Diese Flexibilität kostet ihren Preis im Programm ist etwas höhere Komplexität erforderlich. Für die beiden Arten der Socketadressen werden Strukturen definiert. Eine UNIX- Bereichsadresse sieht so aus: struct sockaddr_un short sun_family; /* Tag: AF_UNIX */ char sun_path[108]; /* path name */ ; Eine Internet-Bereichsadresse so: struct sockaddr_in short sin_family; /* Tag: AF_INET */ u_short sin_port; /* Port number */ struct in_addr sin_addr; /* IP address */ char sin_zero[8]; /* Padding */ ; Beide Strukturen setzten eine Ursprungsmarkierung (Tag), deren Wert gesetzt werden muss, um den tatsächlich benutzten Adresstyp anzuzeigen. Das muss so sein, da Funktionen wie bind() nur einen Zeiger auf die Struktur erhalten sie verlassen sich auf diese Markierung, um zu erfahren, um welche Art Struktur es sich wirklich handelt. Die Struktur sockaddr_in beinhaltet ein struct in_addr, das eine Internetadresse enthält. Diese wird folgendermaßen definiert (auch wenn manche Implementationen sich viel Mühe geben, es komplizierter aussehen zu lassen): struct in_addr u_long s_addr; ;
17 17 Abb.: 2.2 Die Struktur der Socketadressen Diese Datenstrukturen verdeutlicht Abb.: 2.2 Es gibt auch eine generische SocketAdressstruktur, struct sockaddr, die folgendermaßen definiert ist: struct sockaddr u_short sa_family; char sa_data[14] ; Dieser Strukturtyp besteht deshalb, damit die Socketadressen unabhängig von einer Adressfamilie an Funktionen weitergegeben werden können. Die Unabhängigkeit von Adressfamilien der Soeketsystemaufrufe führt zu einer weiteren Komplikation. Immer, wenn eine Socketadresse weitergegeben wird, muss ein zusätzlicher Parameter mitgegeben werden, der die Länge dieser Adresse angibt. Ein Aufruf bind() kann also folgendermaßen aussehen: struct sockaddr_in server; bind (sock, (struct sockaddr *)&server, sizeof server) ; Beachten Sie die Ausführung im zweiten Argument, um den Compiler davon zu überzeugen, dass ein generischer SocketAdresszeiger weitergegeben wird.
18 Die verschiedenen Sockets Außer ihrer Adressfamilie besitzen die Sockets auch einen Typ. Der Typ eines Sockets bezieht sich auf die Art des zugrundeliegenden Transports. Wahrscheinliche Werte sind: SOCK_STREAM /* EinverbindungsorientierterTransport, z.b. TCP */ SOCK_DGRAM /* einverbindungslosertransport, z.b. UDP */ SOCK_RAW SOCK_RAW wird manchmal dafür eingesetzt, direkt mit der IP-Schicht zu sprechen. Unsere Beispiele werden nur mit SOCK_STREAM und SOCK_DGRAM arbeiten. 3 Verbindungsorientierte Serveroperationen 3.1 Verbindungsaufbau Nachdem wir uns mit den grundlegenden Konzepten vertraut gemacht haben, sehen wir Abb.: 3.1 Verbindungsorientierte Client- und Server-Operationen
19 19 uns nun die von einem verbindungsorientierten Server und Clienten ausgeführten Operationen im einzelnen an. Die Client/Serverbeziehung ist asymmetrisch. Ein Server wartet passiv auf Arbeit, er weiß nicht, woher diese Arbeit kommen wird. Ein Client geht aktiv auf die Suche und nimmt mit dem von ihm benötigten Server Verbindung auf. Diese Asymmetrie spiegelt sich in den unterschiedlichen Folgen der bei Server und Client erforderlichen Socketsystemaufrufe wieder, wie Abb.: 3.1 zeigt. Wir werden uns der Reihenfolge nach die vom Server ausgeführten Schritte ansehen: 1. Ein Socket der benötigten Adressfamilie und des benötigten Typs wird angelegt: int sock; sock = socket (AF_INET, SOCK_STREAM, 0) Das erste Argument legt die Adressfamilie fest, dass zweite teilt mit, welchen Sockettyp wir brauchen. Das dritte Argument spezifiziert, welches zugrundeliegende Protokoll benutzt werden soll, wird aber allgemein auf Null gesetzt laß das System wählen. Der Rückgabewert ist ein Socketdeskriptor. Er wird benutzt, wenn wir uns später auf dieses Socket beziehen wollen. 2. Eine Adresse wird an das Socket gebunden. Wie wir bereits gesehen haben, besteht diese Adresse aus einer IP-Adresse und einer Port-Nummer. Sie muss zusammen mit dem Markierungswert AF_INET (gibt den Adresstyp an) in eine sockaddr_in-struktur gesetzt werden. Diese Struktur wird dann an den Systemaufruf bind() weitergegeben. Das Programm sieht meistens so oder ähnlich aus: #define SERVER_PORT 4321 struct sockaddr_in server; server.sin_family = AF_INET; /* Tag value */ server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(server_port); bind (sock, (struct sockaddr *)&server, sizeof server ) Handelt es sich um einen neuen oder experimentellen Dienst, so ist die Port-Nummer in gewissem Maß frei wählbar, allerdings muss sie natürlich mit dem Clienten abgestimmt sein. Da es nicht möglich ist, einen bereits zugewiesenen Port wiederzuverwenden, sollten wir wenigstens die in /etc/services aufgeführten Ports meiden, die mit den standardmäßigen Internetservern korrespondieren. Vielleicht wollen Sie einen Port kleiner als 1024 wählen. Diese sogenannten reservierten Anschlüsse können nur von Programmen mit Super-User- Privilegien benutzt werden. In den Network Support Utilities des ursprünglichen BSD bildeten sie die Grundlage dafür, was als Sicher-heitsmaßnahme galt. Sie wurden als eine Art Verifizierer angesehen, der den Clienten davon überzeugt, dass er wirklich Verbindung zum offiziellen Server und nicht zu einem Schwindler aufgenommen hatte. Natürlich beweist er lediglich, dass der Client Verbindung zu einem Server aufgenommen hat, der mit Root- Privilegien läuft. Die (meist als Makro implementierte) Funktion htons() konvertiert die Port-Nummer von der Host- zur Netzwerk-Byte-Anordung. Wir werden die Byte-Anordnung (byte ordering) später besprechen, nun reicht es zu wissen, dass htons() die Port-Nummer in eine allgemeine Form umwandelt, die alle Maschinen verstehen können. Einzelheiten siehe unter byteorder(3).
20 20 Der der IP-Adresse der Socketadresse zugewiesene Wert INADDR_ANY ist eine Art Metazeichen, es bedeutet dieses Socket kann Verbindungen von jedem der Netzknoten in dieser Maschine akzeptieren. Normalerweise hat die Maschine nur eine Netzschnittstelle und eine IP-Adresse. Jedes Netzwerkverbindungssystem (gateway) hat jedoch mehr als eine. Wir können bestimmen, dass ein Socket nur Verbindungsanfragen über eine davon akzeptieren soll, indem wir hier die entsprechende IP-Adresse benutzen. Beachten Sie bitte, dass I- NADDR_ANY nichts damit zu tun hat, von welchen Clienten IP-Adressen wir Verbindungen akzeptieren. 3. Als nächstes müssen wir den Kernel informieren, dass wir an diesem Socket Verbindungen akzeptieren wollen, etwa so: listen ( sock,5 ); Das zweite Argument legt fest, wie viele anstehende Verbindungsanfragen das System in die Schlange stellen kann. Anfragen stehen an, wenn neue Clienten versuchen, Verbindung aufzunehmen, während der Server mit einem bestehenden Clienten spricht. Ist die Grenze der anstehenden Anfragen erreicht, werden weitere Verbindungsversuche abgewiesen statt in die Warteschlange eingereiht. Ein typischer Wert beträgt 5 (manche Implementationen sagen auch, es sei das Maximum). 4. Der letzte Abschnitt des Verbindungsaufbaus besteht im Warten auf und Akzeptieren einer Verbindungsanfrage eines Clienten. Das Programm sieht üblicherweise so aus: structsockaddr_in clierit; int fd, client_len; client len = sizeof client; fd = accept ( sock, &client, &client_len); Dieser Aufruf wird sperren (das heißt nicht zurückkehren), bis ein Client Verbindung aufgenommen hat. Das zweite Argument auf accept()zeigt auf eine Struktur, in der die Socketadresse des Clienten zurückgegeben wird, der Verbindung aufgenommen hat. Wenn uns gleich ist, wer der Client ist, können wir dies ignorieren. In manchen Fällen will der Server prüfen, ob der Client einen reservierten Anschluß als Verifizierer gebunden hat. In anderen wollen wir vielleicht die IP-Adresse des Clienten prüfen, indem wir sie auf einen symbolischen Namen zurückführen und diesen Namen in einer Liste der Clienten suchen, die wir bedienen wollen. (Der Dämon mountd sieht sich die Datei /etc/exports an, um zu entscheiden, welche Anfrage er annimmt und von wem). Das dritte Argument ist ein sogenanntes Wert-Rückgabe (value-return) Argument, das sowohl einen Wert an eine Funktion weitergibt als auch von dort einen Wert zurückbringt. Wir setzen es für die Mitteilung an accept() ein, wie groß unsere Datenstruktur ist. Bei der Rückkehr modifiziert accept() den Wert für die Mitteilung an uns, wie viele Daten schon bereits dort sind. Der Rückgabewert von accept() ist ein neuer Deskriptor, der in bezug zu der nun mit dem Clienten hergestellten Verbindung steht. Dieser neue Deskriptor benimmt sich wie ein normaler Dateideskriptor. Wir können read() und write() damit ausführen. Manche sind durch das Vorkommen von zwei Deskriptoren verwirrt (in unserem Beispiel sock und fd). Um sie deutlich voneinander zu unterscheiden, werden manchmal die Begriffe Rendezvous-Deskriptor (sock) und Verbindungsdeskriptor (fd) benutzt. Der Rendezvous-Deskriptor kann kein Ein-/Ausgabe ein er ist ja mit nichts verbunden -,
21 21 Abb.: 3.2 Verbindungen zwischen Prozessen er kann aber weitere Verbindungen akzeptieren. Der Verbindungsde-skriptor ist andererseits genau das, was wir für die Kommunikation mit dem Clienten brauchen. Ist die Verbindung aufgebaut, stellt sich die Situation wie in Abb.: 3.2 dar. Beachten Sie, dass die Verbindung zwischen Client und Server durch fünf Merkmale definiert wird: 1. Das benutzte Protokoll 2. Die IP-Adresse des Clienten 3. Die Port-Nummer des Clienten 4. Die IP-Adresse des Servers 5. Die Port-Nummer des Servers Dieser Kommentar ist aus folgendem Grund wichtig: ist irgendeines der fünf Merkmale unterschiedlich, so handelt es sich um eine unterschiedliche Verbindung. Versucht zum Beispiel ein zweiter Client auf derselben Maschine mit demselben Server Verbindung aufzunehmen, so haben wir zwei ganz verschiedene Verbindungen. In diesem Fall ist aber nur eines der fünf die Verbindung definierenden Merkmale unterschiedlich nämlich die Port-Nummer des Clienten. Verschiedene Netzwerkaktivitäten und Statusinforination können mit dem Dienstprogramm netstat gezeigt werden. Mit der Markierung -a werden alle auf der lokalen Maschine vorhandenen Transportendpunkte aufgelistet. Es folgt ein verkürztes Beispiel der Ausgabe von Maschine mars:
22 22 mars% netstat -a Active connections (including servers) Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp 0 0 mars.ftp ford.3099 ESTABLISHED tcp 0 0 mars.1022 saturn.login ESTABLISHED tcp 0 0 mars.1023 saturn.login ESTABLISHED tcp 0 0 *.ftp *.* LISTEN tcp 0 0 *.telnet *.* LISTEN tcp 0 0 *.shell *.* LISTEN tcp 0 0 *.login *.* LISTEN tcp 0 0 *.exec *.* LISTEN tcp 0 0 *.time *.* LISTEN tcp 0 0 *.finger *.* LISTEN tcp 0 0 *.printer *.* LISTEN tcp 0 0 *.smtp *.* LISTEN tcp 0 0 *.1038 *.* LISTEN tcp 0 0 *.1036 *.* LISTEN tcp 0 0 *.1035 *.* LISTEN tcp 0 0 *.1034 *.* LISTEN tcp 0 0 *.sunrpc *.* LISTEN udp *.* udp 0 0 *.llll *.* udp 0 0 *.1109 *.* udp 0 0 *.1108 *.* udp 0 0 *.1104 *.* udp 0 0 *.tftp *.* udp 0 0 *.syslog *.* udp 0 0 *.biff *.* udp 0 0 *.talk *.* udp 0 0 *.time *.* udp 0 0 *.name *.* Die Spalten für lokale und externe Adresse zeigen die Endpunktadressen in der Form machine_adress.port_number. Ist die Maschinenadresse in der Datei /etc/hosts vorhanden, wird sie als symbolischer Name und nicht als numerische IP-Adresse angezeigt. Erscheint die Port-Nummer in der Datei /etc/services, so wird sie entsprechend als Dienstname angezeigt. Die ersten drei Ausgabezeilen zeigen aufgebaute tcp-verbindungen. Die fünf die Verbindung definierenden Merkmale sind klar zu sehen. Die erste Zeile zeigt eine Verbindung zwischen einem ftp-clienten auf Host ford und dem ftp-server auf der lokalen Maschine. Die zweite und dritte Zeile zeigen rlogin Verbindungen, beide von mars auf saturn. Das einzige Merkmal, das zwischen diesen beiden Verbindungen unterscheidet, ist die lokale Port-Nummer. Die Ausgabe zeigt noch mehrere andere tcp-basierende Dienste, diese sind nicht beschäftigt und warten auf Verbindungen. Die Dienste in der Liste, die keinen wohl bekannten Port-Nummern entsprechen, sind RPC-Server. 3.2 Datentransfer mittels Sockets Nach dem Aufbau unserer Verbindung benimmt sich der Deskriptor fd wie jeder andere Dateideskriptor. Wir können read() und write(), dup(), close() und so weiter ausführen. Wir können sogar fopen() aufrufen, um einen stdio-strom zu erhalten und mit fprintf() auf ihn schreiben, hierbei müssen wir allerdings an die Auswirkungen der Pufferaktivitäten innerhalb der stdio-bibliothek denken Der Dialog zwischen Client und Server ist selbstverständlich vom Anwendungsprotokoll abhängig. Es besteht üblicherweise aus Anfragen des Clienten, denen Antworten des Servers
23 23 folgen. Einige Server senden ihre Daten, sobald die Verbindung aufgebaut ist, zum Beispiel der Time-Server, der Client muss gar nichts senden. Häufig beinhaltet das Protokoll einen deutlichen Hinweis darauf, dass der Dialog abgeschlossen ist (eine Antwort des Servers zum Beispiel, die sagt Ich habe keine Daten mehr für dich oder eine Nachricht des Clienten im Sinne Ich habe keine Fragen mehr ). Beide Enden wissen nun jeweils, dass sie die Verbindung schließen können. Manchmal beendet der Client (oder der Server) die Verbindung einfach ohne Warnung. In diesem Fall wird der Server (oder Client) beim nächsten Versuch, aus der Verbindung zu lesen, eine EOF Meldung erhalten. Ist die Interaktion des Servers mit einem Clienten abgeschlossen, so wird er die Verbindung schließen und in eine Schleife gehen, um den nächsten anzunehmen. Das Schema der Warteschleife des Servers sieht häufig wie folgt aus: struct sockaddr_in client; int fd, client_len; char inbuf[1000]; /* Data from client */ char outbuf[1000]; /* Data to client */ client_len = sizeof client; while (1) fd = accept (sock, &client, &client_len); while ( read (fd, inbuf, 1000) > 0) /* Process the data in inbuf, placing the result in outbuf */ write ( fd, outbuf, 1000); /* When client closes its end of the connection, we close our end. Otherwise we would eventually run out of file descriptors. Then we loop round to pick up another connection. */ close (fd); Dieser Servertyp wird allgemein als iterativer Server bezeichnet, da er eine Schleife durchläuft und jeweils einen Clienten bedient. Jeder weitere Client, der Verbindung zu dem so beschäftigten Server aufnehmen will, muss warten. Für Server wie den Timeserver, die nur sehr kurze Verbindungen mit Clienten aufbauen, mag das in Oidnung sein. Es ist jedoch zum Beispiel für den Telnetserver nicht tragbar, der vielleicht stundenlang mit einem Clienten in Verbindung steht. Wir werden uns später noch zwei Arten simultaner Bedienung mehrerer Clienten ansehen. 3.3 Server für ein verteiltes Spiel Wir werden nun all diese Programmfragmente zu einem vollständigen Programm zusammenfügen. Das Programm spielt Hangman, ein Spiel zum Worte erraten. Das Protokoll sieht dabei so aus: Der dient sendet eine Nachricht, die aus einem einzelnen Buchstabenvorschlag besteht. Der Server zeigt als Antwort das von ihm gewählte Wort, in dem noch nicht erratene Buchstaben als - dargestellt sind. Enthält das Wort den Vorschlag des Clienten nicht, wird die Anzahl der Leben des Clienten (ursprünglich 12) herabgesetzt. Der Prozess läuft weiter, bis entweder alle Buchstaben erraten sind (der dient gewonnen hat) oder die Zahl der Leben Null erreicht hat (dann hat der Server gewonnen). Hier ist das Programm (hangserver.c):
24 24 /* Network server for hangman game (iterative schema) */ /* File hangserver.c */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <syslog.h> #include <signal.h> #include <errno.h> extern time_t time(); int maxlives = 12; char *word[] = #include "words" ; #define NUM_OF_WORDS (sizeof(word)/sizeof(word[0])) #define MAXLEN 80 /* Maximum size of any string in the world */ #define HANGMAN_TCP_PORT 1066 main() int sock, fd, client_len; struct sockaddr_in server, client; srand((int)time((long )0)); /* Randomize the seed */ sock = socket(af_inet, SOCK_STREAM, 0); if (sock < 0) perror("creating stream socket"); exit(1); server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(inaddr_any); server.sin_port = htons(hangman_tcp_port); if (bind(sock,(struct sockaddr *) &server, sizeof (server)) < 0) perror("binding socket"); exit(2); listen(sock, 5) ; while (1) client_len = sizeof(client); if ((fd = accept(sock, (struct sockaddr *) &client, &client_len)) < 0) perror("accepting connection"); exit(3); play_hangman(fd, fd); close(fd); /* play_hangman() */ /* Spielt eine Runde Hangman, kehrt zurück, wenn das Wort erraten wurde oder alle "Leben" des Spielers verbraucht sind. Bei jedem "Spielzug" wird eine Zeile aus dem Datenstrom "in" gelesen. Das erste Zeichen dieser Zeile wird als Vorschlag des Spielers angesehen. Nach jedem Vorschlag und vor dem ersten, wird eine Zeile an den Datenstrom "out" geschickt. Dieser besteht aus dem Wort mit allen bisher erratenen Buchstaben und "-" für noch nicht erratene, gefolgt von der Zahl der noch verbliebenen Leben. Beachten Sie, dass diese Funktion weder weiß noch sich darum kümmert, ob ihr Ein- und Ausgabe-Datenstrom sich auf Sockets, Geräte oder Dateien beziehen */ play_hangman(int in, int out) char *whole_word, part_word[maxlen], guess[maxlen], outbuf[maxlen]; int lives = maxlives; /* Number of lives left */
25 25 int game_state = 'I'; /* I ==> Incomplete */ int i, good_guess, word_length; char hostname[maxlen]; gethostname(hostname, MAXLEN); sprintf(outbuf, "Playing hangman on host %s:\n\n", hostname); write(out, outbuf, strlen(outbuf)); /* Pick a word at random from the list */ whole_word = word[rand() % NUM_OF_WORDS] ; word_length = strlen(whole_word) ; syslog(log_user LOG_INFO, "hangman server chose word %s", whole_word) ; /* No letters are guessed initially */ for (i = 0; i < word_length; i++) part_word[i] = '-'; part_word[i] = '\0'; sprintf(outbuf, " %s %d\n", part_word, lives); write(out, outbuf, strlen(outbuf)); while ( game_state == 'I') /* Get guess letter from player */ while (read(in, guess, MAXLEN)<0) if (errno!= EINTR) exit (4) ; printf("re-starting the read\n"); /*Re-start read() if interrupted by signal */ good_guess = 0; for (i=0; i<word_length; i++) if (guess[0] ==whole_word[i]) good_guess = 1; part_word[i] =whole_word[i]; if (! good_guess) lives-- ; if (strcmp(whole_word, part_word) == 0) game_state = 'W' ; /* W ==> User Won */ else if (lives == 0) game_state = 'L'; /* L ==> User Lost / strcpy(part_word, whole_word); / Show User the word */ sprintf(outbuf, " %s %d\n", part_word, lives); write(out, outbuf, strlen(outbuf)); In dieses Programm haben wir Fehlerbehandlung integriert ein unbedingt wichtiger Teil der Netzanwendungen im echten Leben. Ein großer Abschnitt des oben angeführten Programms ist der Funktion play_hangman() gewidmet. Die internen Einzelheiten dieser Funktion sind für uns nicht wichtig. Wir müssen nur wissen, dass sie einen Dialog über zwei Dateideskriptoren führt, die als Argumente weitergegeben werden. Denken wir uns play_hangman als Symbol für die Arbeit, die ein lebensechter Server leisten muss. Wird diese Funktion aufgerufen, so wird die Netzverbindung fd an beide Deskriptoren weitergegeben. Die Wortliste, aus der das Programm wählt, wird aus einer words genannten Datei geholt, die in das Programm compiliert wird. Diese Datei sollte so aussehen: aardvark, abacus, albatross,... zoology
26 26 Ist dieses Programm compiliert, kann es im Hintergrund gestartet und getestet werden. Zum Ausprobieren können wir telnet als Clienten einsetzen. Normalerweise nimmt telnet Verbindung zum telnetd-server an Port 23 auf und beginnt mit der üblichen Protokollverhandlung. Geben wir ein zusätzliches Argument vor, kann sich telnet mit einem anderen Port verbinden. In diesem Fall wird gar keine Protokollabstimmung versucht, sondern nur Text zum Server hin und hertransportiert. (Es handelt sich hier um beobachtetes, nicht um dokumentiertes Verhalten) Es folgt der typische Dialog. Wir compilieren zuerst den Server dann starten wir ihn im Hintergrund. Mit netstat prüfen wir, ob wir wirklich an Port 1066 ein Socket im Zustand LISTEN haben. Schließlich benutzen wir telnet, um mit diesem Port Verbindung aufzunehmen. venus% cc hangserver.c -o hangserver venus% hangserver & [1] 3646 venus% netstat -a grep 1066 tcp 0 0 *.1066 *.* LISTEN venus% telnet localhost 1066 Trying Connected to localhost. Escape character is ^] e ----e s e--s- 12 t e--s- 11 n e--s- 10 i e-is- 10 m e-ism 10 a -a-a--e-ism 10 d -a-a--e-ism 9 r -ara--e-ism 9 P Para--e-ism 9 l parallelism 9 Connection closed by foreign host. venus% 4 Mehrere Clienten gleichzeitig bedienen Iterative Server, die langfristige Verbindungen mit ihren Clienten aufrechterhalten, haben den Nachteil, dass neue Clienten für eine Undefinierte Zeit ihre Verbindungsanfrage in Warteposition halten müssen. Wir brauchen zur Lösung dieses Problems einen Server, der parallele Verbindungen mit mehreren Clienten aufrechterhalten kann. Zwei Ansätze dazu stellen wir in diesem Abschnitt vor.
27 Parallele Server In unserem ersten Ansatz erzeugt der Server für jeden Clienten einen KindProzess. Das Elternteil nimmt die Verbindungen nur mit accept() an, jede wird dann einem neuen Kind übergeben. Auf diese Weise arbeitende Server werden parallele Server genannt. Da ein Kind- Prozess unter UNIX immer offene Dateideskriptoren vom Elternteil erbt, sind parallele Server überraschend leicht zu implementieren. Ein einfaches Schema kann folgendermaßen aussehen: sock = socket (... ); bind ( sock,... ); listen (sock, 2) ; while (1) fd = accept ( sock,...); if (fork() == 0) /* Child - process the request */ play_hangman(fd, fd); exit(0); /* Child is done */ else close (fd); /* Parent does not use the connection */ Beachten Sie folgendes: Nachdem das Elternteil eine Verbindung aufgebaut und sich verzweigt hat, schließt er sie sofort wieder und durchläuft eine Schleife, um die nächste zu akzeptieren. Natürlich bricht die Verbindung mit dem Clienten nicht ab, wenn das Elternteil seinen Deskriptor am Verbindungssocket schließt. Das Elternteil muss fd schließen, sonst wird es bald keine Deskriptoren mehr zur Verfügung haben. 4.2 Vermeidung von Zombies nach System V Das Programm hangserver lässt sich leicht in eineh parallelen Server umwandeln. Danach können mehrere Spiele mit Telnetclienten von mehreren Hosts gleichzeitig ablaufen. Das Programm hat jedoch in seinem derzeitigen Stand eine fatale Schwäche. Wenn wir nach Aufbau und Schließen einiger Verbindungen ein ps auf der Servermaschine laufen lassen, wird es deutlich. Wir sehen dann in etwa folgendes: venus% ps -l F UID PID PPID CP PRI NI SZ RSS WCHAN STAT TT TIME COMMAND l socket IW b 0:00 hangserver Z b 0:00 <defunct> Z b 0:00 <defunct> Z b 0:00 <defunct> R b 0:00 ps -l venus% Die drei mit <defunct> gekennzeichneten Prozesse sind die Leichen dreier vom hangserver-elternteil erzeugter Kinder. (Alle haben 5525 als Prozess-ID des Elternteils). Obwohl diese Prozesse ausgestiegen sind, halten sie noch eine winzige Information ihren Ausstiegsstatus -, die sie ihrem Elternteil zurückgeben wollen. Normalerweise führt ein Eiterteil ausdrücklich einen wait()-systemaufruf durch, um sich mit der Termination des Kindes zu synchronisieren. Dieser Aufruf holt den Ausstiegsstatus des Kindes zurück und lässt den
28 28 KindProzess vollständig verschwinden. In manchen Programmen steigt das Elternteil selbst aus und lässt die Kinder weiterlaufen. In diesem Fall wird jedes Kind von init (Prozess Nr. 1) geerbt, der in jedem Fall einen wait()-aufruf ausstehen hat, um die Beseitigung des Kindes zu ermöglichen. Im obigen Beispiel eines parallelen Servers sind keine der beiden Bedingungen gegeben. Der springende Punkt liegt im Gegenteil darin, dass der Server ohne zu warten die Schleife zu accept() durchläuft. Die Kinder haben nun immer noch einen Ausstiegsstatus zum Zurückgeben, aber keiner will ihn haben. Prozesse in diesem <defunct> Status kann man nicht auf die übliche Art loswerden. Töten hilft nichts sie sind ja bereits tot. (Das Z in der Spalte STAT der ps-ausgabe steht für Zombie ). Diese Zombies werden langsam aber sicher alle Prozesstabelleneinträge im Kernel füllen. Natürlich würden sie alle beim Tod ihres Elternteils (Prozess 5525) verschwinden, da sie dann von init geerbt würden. SVR4 UNIX bietet eine einfache Lösung dieses Problems. Wir fügen nur dem Elternteil folgenden Aufruf hinzu: #include <signal.h> signal (SIGCHLD, SIG_IGN); Dieser Aufruf scheint keinen Zweck zu haben, da die Vorgabedisposition des SIGCHLD Signals SIG_IGN ist. Er hat jedoch den nützlichen (und dokumentierten) Nebeneffekt, dass unter den gerade beschriebenen Umstanden das Bilden von Zombies verhindert wird. 4.3 Vermeidung von Zombies nach BSD Diese Lösung funktioniert unter BSD-UNIX-Derivaten nicht. Dort sind komplexere Maßnahmen erforderlich. Die Grundgedanke sieht so aus: Wir können das Signal SIGCHLD für die Mitteilung nutzen, dass ein Kind gestorben ist. An diesem Punkt können wir einen wait() -Aufruf durchführen (zum Beispiel im SIGCHLD Signalhandler), um das Kind zu beseitigen und sicher sein, dass wait() nicht blockiert. Wenn wir das Senden des Signals SIGCHLD ermöglichen, müssen wir uns aber mit einem ungewünschten Nebeneffekt auseinandersetzen. Bestimmte Systemaufrufe wie read(), accept() und select() können unterbrochen werden, wenn ein Signal ankommt. Geschieht dies, so kommt der Systemaufruf mit einem scheinbaren Fehler zurück, errno ist auf EINTR gesetzt. In unserem Beispiel wird der ElternProzess mit hoher Wahrscheinlichkeit beim Aufruf accept() blockiert, wenn das Signal ankommt. Wir müssen diesen Aufruf abändern, um die EINTR Fehlerrückgabe ausdrücklich abzufangen und accept() neu zu starten. Nun folgen die Programmteile für einen parallelen BSD hangman Server mit den erforderlichen Änderungen: #include <signal.h> /* Additional header file */... /* This is the signal-handling function */ void waiter () wait(0); /* Mop up the child / signal(sigchld, waiter); / Reinstall signal handler */
29 29... /* Somewhere in the initialization code in main(), we add : */ signal (SIGCHLD, waiter);... /* Here is how the main server loop now looks: */ while (1) client_len = sizeof(client); RE_ACCEPT: msgsock = accept(sock, (struct sockaddr *) &client, &client_len) ; if (msgsock < 0) if (errno == EINTR) /* accept() interrupted by signal. Go try again */ goto RE_ACCEPT; else perror( accepting connection ); exit(3); rand () ; / * Advance the random generator * / if (fork() == 0) closesock); /* Child - Process Request */ play_hangman(msgsock, msgsock); exit(0); else close(msgsock); Beachten Sie das Statement goto, dass sich still und heimlich in die Logik eingeschlichen hat, die die Fehlerrückgabe des Aufrufs accept() bearbeitet. Natürlich kann das wie folgt vermieden werden: while ( (msgsock = accept (...) ) < 0) if (errno!= EINTR) perror ( accepting connection ) ; exit(3) ; Es bleibt Ihnen überlassen, wo die Logik am deutlichsten ersichtlich wird. 5 Paralle Server, die select() verwenden 5.1 Parallele Dienstverhältnisse und Status Ein alternativer Ansatz, parallele, verbindungsorientierte Server zu schreiben, erfordert nur einen einzelnen Prozess. Hier wird der Systemaufruf select() benutzt, um zwischen den von jeder Clientenverbindung empfangenen Nachrichten zu multiplexen. Wir werden uns gleich select() in Einzelheiten ansehen, im Augenblick reicht das Wissen, dass einem Prozess ein Weg angeboten wird, abzuwarten, bis irgendwelche spezifizierten Netzverbindungen Daten zum Lesen bereit haben. Im Vergleich mit der KindProzess-pro-Client-Lösung liegt der Vorteil darin, dass weniger Systemressourcen (Speicher und Eintrag in der Prozesstabelle) benötigt werden, da es sich nur um einen einzigen Prozess handelt. Der Nachteil, der
30 30 ganz beträchtlich sein kann, besteht in der Anforderung an den Server, Statusinformationen zu halten, um den Interaktionsfortschritt mit jedem Clienten festzuhalten. (Bei unserem hangman-server würde diese Statusinformation das vollständige Wort, das bis zu diesem Zeitpunkt erratene Wort und die Zahl der verbleibenden Leben beinhalten). In der KindProzesspro-Client-Lösung wird diese Statusinformation natürlich in den lokalen Variablen jedes Kinds gehalten. Prozesse können sehr gut Kontexte aufrechterhalten. In einem einzelnen ServerProzess wird die Statusinformation pro Client üblicherweise in einer Struktur zusammengefaßt und ein Bereich solcher Strukturen deklariert. Der Bereich wird durch den Dateideskriptor indiziert, der sich auf die Clientverbindung bezieht. Der Einsatz von select() hat signifikante Auswirkungen auf den Kontrollstrom im Server. Nach jeder Interaktion mit einem Clienten muss in die Hauptverarbeitungsschleife zurückgekehrt werden (diejenige mit dem Aufruf select() am Anfang). Eine Interaktion besteht im hangman-server aus einem einzelnen Vorschlag/Antwort. Der Kontrolle ist es daher nicht möglich, für die Dauer eines gesamten Spiels innerhalb der Funktion play_hangman() zu bleiben. Aus diesen Gründen ist der Einsatz von select() kein besonders geeigneter Ansatz, einen parallelen hangman-server zu schreiben. (Es ist natürlich möglich, Sie können es auch gern versuchen Sie werden es allerdings ziemlich umständlich und aufwendig finden). 5.2 Ein neues Beispiel: Ein Lagerverwaltungsserver Wenden wir uns einem vollständig neuen Beispie zu. Die Fritterbuck Supermarket Kette hat ein Hauptwarenlager, das Vorräte der von den Verkaufsstellen angebotenen Produkte lagert. Für dieses Lager wird ein Lagerverwaltungsserver benötigt, um den Inventurbestand jedes Produkts zu verfolgen. Die Warendatenbank besteht aus einer Reihe von Datensätzen folgender Form: product_name, stock level Unsere Datenbank soll folgende Einträge enthalten: Messer 1600 Gabel 840 Löffel 1255 Tasse 0 Kanne 200 Der Server muss drei Anfragetypen akzeptieren: BUY product_name quantity SELL product_name quantity SHOW product_name Zum Beispiel: BUY Messer 250 SELL Tasse 85 SHOW Kanne
31 31 Abb.: 5.1 Ein Server für die Lagerverwaltung BUY ist eine von Fritterbucks Zulieferern verlangte Operation und führt zu einem Anstieg des vom Server für das angegebene Produkt gehaltenen Warenbestands. Der Server antwortet folgenderweise auf eine BUY-Anfrage: BOUGHT Messer 250 SELL ist eine von einer der Verkaufsstellen verlangte Operation und führt zu einer Verringerung des Warenbestands des angegebenen Produkts. Der Server antwortet folgendermaßen auf eine SELL-Anfrage: DELIVER Tasse 85 Die Zahl der gelieferten Einheiten sollte entweder gleich der in der SELL-Anfrage angeforderten oder der tatsächlich im Bestand vorhandenen sein, und zwar gleich der kleineren von beiden. Die Begriffe BUY und SELL beziehen sich hier auf die Sichtwelse des Servers. Verkaufen bedeutet, Bestand vom Server zum Clienten zu transferleren. SHOW fordert einen Bericht über den Lagerbestand einer bestimmten Einheit an und führt zu folgender Antwort: STOCK Kanne 200 Server, Clienten und Bestandsliste werden In Abb.: 5.1 verdeutlicht.
32 32 Wir gehen von der Annahme aus, dass die Clientenanfragen häufig genug hereinkommen, dass wir uns den Aufwand sparen können, für jeden eine Verbindung aufzubauen und zu schließen. Unser Server wird folglich mehrere parallele Verbindungen mit seinen Clienten aufrechterhalten müssen. Der Server muss auch Statusinformation pflegen (nämlich die Bestandsliste), diese Information ist jedoch global (das heißt, allen Clienten bekannt und für alle gleich), sie bezieht sich nicht auf jeden einzelnen Clienten. Wenn dieser Server mit Hilfe der Technik Kind-pro-Client geschrieben würde, die für den hangman-server eingesetzt wurde, so müßten wir eine Möglichkeit für die Kindprozesse schaffen, sich die Bestandsliste zu teilen. Wir könnten dafür ein gemeinsam genutztes Speichersegment oder eine Datei nutzen. Weiterhin fordert das Kommando SELL den Server auf, einen Test und eine Verminderungsoperation auf der Bestandsebene durchzuführen. Wir würden irgendeinen Mechanismus brauchen, der sicherstellt, dass diese Operation atomar ist das heißt, unteilbar. Dafür könnten wir mit einer Semaphore oder einer Dateisperre arbeiten. Wenn andererseits der Server als EinzelProzess geschrieben ist, der aus allen Clientverbindungen liest, dann sind die Daten schon von sich aus global. Da es auch nur einen Prozess gibt, der die Bestände verändert, gibt es keine Möglichkeit, vorher unterbrochen zu werden das Thema Unteilbarkeitstest und Verminderung tritt einfach nicht auf. Die konzeptuellen Unterschiede im Design des hangman-server und des Lagerverwaltungsservers sind wichtig. Lassen Sie mich zusammenfassen: Der parallele hangman-server muss für jeden Clienten getrennte Statusinformation pflegen, die Dialoge mit den Clienten interagieren jedoch nicht. In diesem Fall arbeitet ein Kind-pro-Client-Server ganz gut. Der Lagerverwaltungsserver muss nicht für jeden Clienten getrennte Zustandsinformationen pflegen, die Dialoge der Clienten interagieren jedoch via globaler Statusinformation. Hier ist ein Server mit einem einzelnen Aktivitätsstrang die bessere Entscheidung, der mit select() arbeitet. 5.3 Verwendung von select() Die Synopse von select() erscheint formal wie folgt: select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) Die Parameter readfds, writefds und exceptfds zeigen auf drei Deskriptorensätze. Der Aufruf select() kehrt zurück, wenn irgendeiner der Deskriptoren im readfds-bündel lesebereit ist, wenn irgendeiner der Deskriptoren im writefds-bündel schreibbereit ist oder wenn bei irgendeinem der Deskriptoren im exceptfds-bündel eine Ausnahmebedingung anhängig ist. Der Begriff lesebereit (ready for reading) bedeutet einfach, dass auf diesem Deskriptor Daten warten, gelesen zu werden damit zum Beispiel ein read() auf dem Deskriptor nicht blockiert. Es gab Zeiten, da waren wir schon froh, wenn wir eine Grenze von 32 Dateideskriptoren pro Prozess tolerieren konnten, die Deskriptorensätze wurden einfach durch Ganzzahlen repräsentiert. Jedes Bit der Ganzzahl war einem Deskriptor gleichzusetzen. Natürlich ist es heute
33 33 nur noch schlechter Stil, im Programm eine feste Grenze von 32 einzubauen. Daher wird der genaue Typ eines Deskriptorensatzes hinter typedef fd_set versteckt. Am Rande: Damit wir nicht als Assemblerprogrammierer abgestempelt werden, müssen wir auch nicht offen in unserem Programm vorführen, dass wir so etwas ausgefeiltes wie ein Bündel in etwas so bescheidenem wie einer Ganzzahl repräsentieren können. Weiterhin werden löschen, setzen und testen individueller Deskriptoren in vier Makros verborgen: FD_ZERO(&fdset) /* Remove all descriptors from set fdset */ FD_CLR(fd, &fdset) /* Remove descriptor fd from set fdset */ FD_SET(fd, &fdset) /* Add descriptor fd to set fdset */ FD_ISSET(fd, &fdset) /* True if fd is present in the set fdset */ Der Timeout-Parameter kann auf eine timeval-struktur zeigen, die aufgefüllt wird, um die maximale Länge der Zeit anzuzeigen, die der Aufruf blockt, bevor er des Wartens müde sowieso zurückkehrt. In diesem Beispiel brauchen wir keine time out Möglichkeit, daher setzen wir diesen Parameter auf NULL. Abb.: 5.2 verdeutlicht den Einsatz der Deskriptorensätze im Aufruf select().
34 34 Abb.: 5.2 Deskriptorensätze mit select() Um ein einfaches Beispiel zu geben, nehmen wir an, dass wir unbegrenzt warten wollen, bis entweder Dateideskriptor 3 oder 4 lesebereit sind. Der Programmausschnitt kann so aussehen: #include <sys/types.h> fd_set myset; /* Put file descriptors 3 and 4 into myset */ FD_ZERO(&myset); FD_SET(3, &myset); FD_SET(4, &myset); /* In the call below, 5 is the number of descriptors to examine i.e. from 0 to 4. The three NULL parameters mean that we do not want to check if any descriptors are ready for writing, we do not want to check if any descriptors have exceptional conditions pending, andwe do not want to timeout. */ select(5, myset, NULL, NULL, NULL) ; if (FD_ISSET(3, &myset)) /* Go read from descriptor 3... */ else if (FD_ISSET(4, &myset)) /* Go read from descriptor 4... */ else printf ( This should never happen );
35 35 Zwei Nachteile hat der Einsatz von select(). Da die Deskriptorensätze vom Aufruf überschrieben werden, muss es normalerweise noch irgendwo eine getrennte Kopie von ihnen geben. Kehrt der Aufruf zurück, gibt es weiterhin keine sofortige Benachrichtigung, welcher Dateideskriptor aufgeweckt hat. Es gibt wahrscheinlich nur einen, aber um ihn herauszufinden, muss jeder einzeln befragt werden. 5.4 Design des Lagerverwaltungsservers Nachdem wir uns die Einzelheiten zu select() angesehen haben, betrachten wir jetzt das Design unseres Lagerverwaltungsservers. Um den anwendungsspezifischen Teil des Programms so kurz wie möglich zu halten, machen wir eine Reihe vereinfachender Annahmen, die in einer wirklichen Implementation nicht akzeptabel wären. Insbesondere werden wir die Bestandsliste in einem vordeklarierten Bereich mit fester Länge halten. Sie wird nur im Speicher gehalten und nicht in eine Datei geschrieben werden. Wir werden eine einfache lineare Suchstrategie einsetzen, um Produkte in der Liste zu finden. Wir werden mit einer Markierungseingabe (Lagerbestand gleich -1) arbeiten, um das Ende der Liste zu kennzeichnen. Die interessantesten Aspekte des Serverdesigns liegen aus der Sicht des Netzwerks in der Behandlung der Dateideskriptoren und der Bildung eines Testsets für den Einsatz im Aufruf select(). Wir müssen select() benutzen, um zu warten, bis entweder eine Transaktion von einem bestehenden Clienten ankommt oder ein neuer Client eine Verbindungsanfrage schickt. Zu Beginn gibt es noch keine Clientverbindungen. Daher ist der einzige Deskriptor im Testset der Rendezvous-Deskriptor, den wir zum Akzeptieren neuer Verbindungsanfragen benutzen. (Vielleicht ist es überraschend, dass select() einen Rendezvous-Deskriptor, von dem eine Verbindungsanfrage empfangen wurde, als lesebereit ansieht). Daher besteht unser Testset zu jedem beliebigen Zeitpunkt aus einer Reihe von Deskriptoren, die sich auf bereits aufgebaute Verbindungen mit bestehenden Clienten beziehen, sowie aus dem Rendezvous-Deskriptor. Das Programmlisting stock zeigt das Programm für den Lagerverwaltungsserver (stock.c). /* Stock server. This is an example of a single-process, concurrent server using TCP transport and select() File stock.c */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/resource.h> #include <stdio.h> #define STOCK_PORT 7777 /* Our 'well-known' port number */ #define LIST_SIZE 1000 /* Maximum number of stock items */ /* This structure holds all the data about a single stock item */ struct stock_item char product[32]; /* The product name */ long quantity; /* The current stock level */ ; /* This array constitutes the stock list database */ struct stock_item stock_list[list_size]; /* main() */
36 main() int sock, fd, client_len; struct sockaddr_in server, client; int max_fd; FILE **stream; struct rlimit limit_info; /* These are the file descriptor sets used by select() */ fd_set test_set, ready_set; /* We need an array of stream descriptors (of type FILE *). There is no portable way to know a priori how many open descriptors we can have. Thus, we find this out at run time, using getrlimit(), and dynamically allocate an array of the required size.*/ if (getrlimit(rlimit_nofile, &limit_info) < 0) perror("getting file descriptor limit"); exit(1); stream = (FILE **) malloc(limit_info.rlim_cur * sizeof (FILE *)); if (stream == NULL) fprintf(stderr, "failed to allocate stream descriptor table\n"); exit(1); sock = socket(af_inet, SOCK_STREAM, 0); if (sock < 0) perror("creating stream socket"); exit(1); server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(inaddr_any); server.sin_port = htons(stock_port); if (bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) perror("binding socket"); exit(2); listen(sock, 5); max_fd = sock; /* Initially, the 'test set' has just the rendezvous descriptor / FD_ZERO (&test_set) ; FD_SET(sock, &test_set); / The end of the list is marked by an entry with a negative stock level. Mark the list as being initially empty */ stock_list[0].quantity = -1; /* Here is the head of the main service loop */ while (1) /* Because select overwrites the descriptor set, we must not use our 'permanent' set here, we must use a copy */ memcpy(&ready_set, &test_set, sizeof test_set); select(max_fd+1, &ready_set, NULL, NULL, NULL); /* Did we get a new connection reguest? If so, simply accept it and add the new descriptor into the read set */ if (FD_ISSET(sock, &ready_set)) client_len = sizeof client; fd = accept(sock, &client, &client_len); FD_SET(fd, &test_set); if (fd > max_fd) max_fd = fd; stream[fd] = fdopen(fd, "r+"); setbuf(stream[fd], NULL); /* IMPORTANT? */ /* Now we must check each descriptor in the read set in turn. For each one which is ready, we process the client request. Remember NOT to check the rendezvous descriptor. */ for (fd=0; fd<=max_fd; fd++) if ((fd!= sock) && FD_ISSET(fd, &ready_set)) 36
37 37 if (process_request(stream[fd]) < 0) /* If the client has closed its end of the connection, we close our end, and remove the descriptor from the read set */ close(fd); FD_CLR(fd, &test_set); else fflush(stream[fd]); /* This is the application-specific part of the code */ /* find_product() */ /* Return a pointer to the stock_item for the specified product name in the stock list, or NULL if none found */ struct stock_item *find_product(char *name) struct stock_item *p; for (p = stock_list; p->quantity >= 0; p++) if (strcmp(name, p->product) == 0) return p; return NULL; /* add_product() */ void add_product(char *name, int quantity) struct stock_item *p; /* Scan to find the end of the current list */ for (p = stock_list; p->quantity >= 0; p++) ; /* Empty loop body */ if (p - stock_list >= LIST_SIZE-1) return; /* List is already full */ strcpy(p->product, name); p->quantity = quantity; /* Mark new end of list */ (p+1)->quantity = -1; return; /* process_request() */ /* Return 0 for normal return, or -1 if encountered EOF from client */ int process_request(stream) FILE *stream; char req_buffer[100]; char request[32], product[32]; int quantity; struct stock_item *p; /* Read the request from the client */ if (fgets(req_buffer, 100, stream) == NULL) return -1; /* End of file? / / This next call is meaningless for the socket, but is needed to allow the stream to change direction from input to output. (Look up fopen() for details.) */ rewind(stream); sscanf(req_buffer, " %s %s %d", request, product, &quantity); if (strcmp(request, "BUY") == 0) /* Bump up the stock level for this product by the quantity */ if ( (p = find_product(product) ) == NULL) add_product(product, quantity);
38 38 else p->quantity += quantity; /* Confirm transaction to the client */ fprintf(stream, "BOUGHT %s %d\n", product, quantity); else if (strcmp(request, "SELL") == 0) if ((p = find_product(product)) == NULL) fprintf(stream, "ERROR: product %s is unknown\n", product); /* If we have enough, send as many as were asked for */ else if (p->quantity >= quantity) fprintf(stream, "DELIVER %s %d\n", product, quantity); p->quantity -= quantity; /*... otherwise, send as many as we have */ else fprintf(stream, "DELIVER %s %d\n", product, p->quantity); p->quantity = 0; else if (strcmp(request, "SHOW") == 0) product[strlen(product)-1] = '\0'; if ((p = find_product(product)) == NULL) fprintf(stream, "ERROR: product %s is unknovm\n", product); else fprintf(stream, "STOCK %s %d\n", product, p->quantity); else /* Don't recognize command from client */ fprintf(stream, "ERROR: request type not recognized\n"); return 0; /* Normal return */ 5.5 Das Programm für den Lagerverwaltungsserver Die anwendungsspezifischen Programmteile, bestehend aus den Funktionen find_product(), add_product() und process_request() sind für uns relativ uninteressant. Wir werden hier nur noch anmerken, dass process_request() -l zurückbringt, wenn er beim Lesen aus der Clientenverbindung auf eine EOF-Bedingung trifft. Diese Serverimplementation arbeitet mit stdio-ströme (das heißt Deskriptoren des Typs FILE*), um auf die Netzverbindungen bezug zu nehmen. Der Vorteil liegt darin, dass der Server Ausgabefunktionen wie fprintf() einsetzen kann, um Antworten für den Clienten zu formulieren. Die stdio-ströme werden durch fdopen() aus den einfachen Datei- Deskriptoren, die accept() zurückliefert, erzeugt dies geschieht innerhalb der main() - Warteschleife, nachdem eine neue Clientverbindung akzeptiert wurde. Um diese stdio-ströme zu speichern, brauchen wir einen Bereich (indiziert durch den entsprechenden low-level Deskriptor). Da die maximale Zahl der offenen Dateideskriptoren pro Prozess implementationsabhängig ist, rufen wir getrlimit() auf, um die maximale Deskriptorenzahl zu erhalten und nutzen diese, um einen Bereich der richtigen Größe mit malloc() zuzuweisen. (Äl-
39 39 tere UNIX-Versionen auf BSD-Basis haben einen Systemaufruf getdtablesize(), um die Größe der Dateideskriptorentabelle zu erhalten, diesen Aufruf gibt es in SVR4 nicht). getrlimit() bringt eine wie folgt deklarierte Struktur zurück: struct rlimit rlim_t rlim_cur; /* Current limit */ rlim_t rlim_max; /* Hard limit */ ; Das Hardlimit bestimmt den Maximumwert, auf den das derzeitige Limit gesetzt werden kann. Der interessanteste Programmteil ist die Warteschleife in main(). Ein Aufruf memcpy() macht eine Kopie unseres Deskriptorensatzes, den select() überschreiben kann. In der Variablen max_fd wird der höchste benutzte Dateideskriptor vermerkt (für den Einsatz im Aufruf select()). Wir könnten auf max_fd verzichten und stattdessen den Wert von getrlimit() liefern lassen. Bei der Rückkehr vom Aufruf select() testen wir zunächst den Rendezvous-Deskriptor, um zu sehen, ob ein Client auf die Verbindung wartet. Wenn ja, akzeptieren wir die Verbindung, fügen den neuen Deskriptor dem Testset hinzu und erzeugen den entsprechenden Flußdeskriptor. Wir rufen setbuf() auf, um das Puffern der Daten durch die Standard Ein-/Ausgabe-Bibliothek abzuschalten. Die Erfahrung des Autors läuft darauf hinaus, dass solche Puffer schlecht mit der Netzverbindung interagieren. Beachten Sie auch den eigenartigen Aufruf rewind() sofort nach dem Lesen der Clienten-Anfrage durch den Server. Dieser augenscheinliche Unsinn ist notwendig, um die Standard-Ein-/Ausgabe- Bibliothek davon zu überzeugen, die Flußrichtung von Eingabe auf Ausgabe umzuschalten. Die Warteschleife main() prüft auch die Bereitschaft jeder bestehenden Clientverbindung und gibt sie, falls notwendig, an process_request() weiter. Zeigt die Funktion process_request() an, dass der Client die Verbindung geschlossen hat, so schließt die Warteschleife den Deskriptor und entfernt ihn aus dem Testset. Dieser Server arbeitet mit einem einfachen Textprotokoll. Er kann daher wie der hangman-server einfach durch den Einsatz von Telnet als Client getestet werden. Es folgt ein Beispieldialog: mars% stock & [1] 590 mars% telnet localhost 7777 Trying Connected to localhost. Escape character is ^]. BUY Platte 100 BOUGHT Platte 100 SELL Platte 35 DELIVER Platte 35 SHOW Platte STOCK Platte 65 Hier wird natürlich nicht die Fähigkeit des Servers vorgeführt, simultane Konversationen mit mehreren Clienten aufrechtzuerhalten. Dafür brauchen Sie Ihre Vorstellungskraft!
40 40 Am Rande: Hierfür gibt es viele ausgezeichnete Präzedenzfälle (für den Einsatz Ihrer Vorstellungskraft). Shakespeare fordert uns zum Beispiel auf: Ergänze unsere Mängel mit Deinen Gedanken: Teile einen Mann in tausend Teile, und lasse Deine Phantasie herrschen. Wenn wir von Pferden sprechen, denke, dass Du sie siehst, wie sie ihre stolzen Hufe in die empfangende Erde drücken. (Henry V, Akt l, Szene 1). Hätte er ein Schauspiel über UNIX geschrieben, hätte er sicherlich ein angemesseneres Bild gewählt. 6 Verbindungsorientierte Clienten Die vom hangman-server und Lagerverwaltungsserver aus den vorherigen Abschnitten benutzten Protokolle waren so einfach, dass ein bestehender Client (telnet) zum Test eingesetzt werden konnte. Das wird allgemein nicht der Fall sein. Üblicherweise brauchen wir für die Arbeit mit jedem Server spezifische Clientprogramme. In diesem Abschnitt befassen wir uns mit dem Heizplatten-Netzprogramm, das ein verbindungsorientierter Client einsetzt. 6.1 Ein Socket erzeugen Die vom Clienten ausgeführten Schritte sind in Abb.: 6.1 zusammengefaßt. Wie Server beginnen auch Clienten mit dem Erzeugen eines Sockets. Der Aufruf ist identisch: sock = socket (AF_INET, SOCK_STREAM, 0) ; 6.2 Eine lokale Adresse setzen (wenn gewünscht) Im Gegensatz zum Server muss der Client sich nicht ausdrücklich eine Adresse an dieses Socket binden. Er kann einfach weitermachen, denn in diesem Fall wird das System automatisch eine frei wählbare Port-Nummer auswählen und eine Adresse anbinden. Allgemein kümmert sich den Client nicht darum, welche Port-Nummer er benutzt. Eine wichtige Ausnahme bilden Clienten, die sich an eine reservierte Port-Nummer (<1024) als Beweis für den Server binden müssen. In diesem Fall muss der Client ausdrücklich eine Adresse anbinden. Er muss darüber hinaus mit Root-Privilegien ausgestattet sein. Die Funktion rresvport() kann unter BSD (aber nicht unter SVR4) helfen, ein Socket mit einem reservierten Port zu erhalten. Wir nehmen an, dass es in unserem Fall nicht erforderlich ist.
41 41 Abb.: 6.1 Verbindungsorientierte Clienten-Operationen 6.3 Verbindung mit dem Server aufnehmen Im nächsten Schritt muss das Clientensocket mit dem des Servers verbunden werden. Dafür wird der Systemaufruf connect() eingesetzt. Einer der Parameter für diesen Aufruf ist eine sockaddr_in-struktur, die mit der Adresse des Servers versehen werden muss. Erinnern wir uns, dass diese Struktur drei Bestandteile hat: 1. Einen Kennzeichenwert AF_INET, der den benutzten Adresstyp anzeigt 2. Die Port-Nummer, an der der Server wartet 3. Die IP-Adresse der Servermaschine Die einfachste Möglichkeit, Port-Nummer und IP-Adresse zu erhalten, ist sie fest in das Programm zu schreiben. Es reicht oft, die Port-Nummer fest in das Programm einzubauen, da sich besonders bei standardmäßigen Internetservern die Port-Nummern selten oder nie ändern. Eine IP-Adresse fest einzubauen ist dagegen nicht ratsam, da dann die Clientquellen jedesmal neu compiliert werden müßten, wenn eine andere Servermaschine benutzt wird. Der Client wird normalerweise einen Namen einer Hostmaschine aus den Eingabe-Argumenten, in seiner Kommandozeile lesen. (Wir haben bereits gesehen, dass Clienten wie telnet, rsh und rcp so arbeiten). Die Datei /etc/hosts (oder die NIS-Hostsmap) bildet die Maschinennamen auf IP-Adressen ab, und die Umsetzerroutine gethostbyname() bietet die Möglichkeit, dieses Abbild abzufragen. Die Funktion gethostbyname() bringt einen Zeiger auf eine hostent-struktur zurück, die folgendermaßen deklariert wird:
42 42 struct hostent char h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses from name server */ ; Die Flexibilität dieser Struktur ist so groß, dass sie Hosts mit mehreren Namen und mehreren Adressen bewältigen kann. h_addr_list kann ein Zeigerfeld auf IP-Adressen sein. Im Normalfall brauchen wir nur die erste (oder einzige) dieser Adressen. Das folgende Makro sorgt für Einfachheit und Rückwärtskompatibilität: #define h_addrh_addr_list[0] Der Umsetzer getservbyname() kann ähnlich eingesetzt werden, um die Port- Nummer eines gegebenen benannten Diensts zu erhalten, er wird in /etc/services oder in der NIS-Diensttabelle nachgeschlagen (in unserem Beispiel werden wir nicht damit arbeiten). Der Umsetzer bringt eine servent-struktur zurück, deren wichtigster Bestandteil die Port-Nummer ist, s_port. Abb.: 6.2 verdeutlicht, wie die Felder in der Socketadresse des Abb.: 6.2 Finden des gesuchten Dienstes Servers durch gethostbyname() und getservbyname() ergänzt werden. Das typische Programm, jedoch ohne Fehlerroutine:
43 43 #definehangman_tcp_port 1066; struct hostent *host_info; struct sockaddr_in server; /* Take host name from command line */ host_info = gethostbyname(argv[l]); memcpy(&server.sin_addr, host_info->h_addr, host_info->h_length), server.sin_port = htons(hangman_tcp_port); connect(sock, &server, sizeofserver); Warum der Aufruf memcpy()? Das Programm soll so von Form (und Größe) einer Netzwerkadresse unabhängig gemacht werden. Die Länge der Adresse wird in host_info->h_length zurückgebracht, dieser Wert wird zur Steuerung benutzt, wie viel Daten memcpy() kopiert. Unter der Annahme, dass die Adresse genauso groß ist wie eine 8- Byte Ganzzahl (long) (trifft wahrscheinlich für eine IP-Adresse zu), können wir den Aufruf memcpy() durch folgende Zuweisung ersetzen: server.sin_addr = (long)host_info->h_addr; Beachten Sie, dass mit dem Makro htons() (host to network) die Port-Nummer in die normale (big-endian.) Netzwerk-Byte-Anordnung gebracht wird. Davon gehen die Socketroutinen aus. Auf einer normalen Maschine kann der Aufruf htons() ausgelassen werden, so würde jedoch ein häßlicher latenter Fehler entstehen, der nur darauf wartet, dass das Programm auf eine Maschine mit gedrehter Byte-Anordnung (little-endian) portiert wird. Normalerweise würden wir den Rückgabewert von connect() prüfen. Null bedeutet Erfolg, -l weist auf verschiedene Fehlermöglichkeiten hin. Nehmen wir an, die Operation sei erfolgreich gewesen, dann ist nun der Deskriptor sock angeschlossen und kann für Ein- /Ausgabe an den Server eingesetzt werden. Beachten Sie, dass der Client nur einen Deskriptor hat, während der Server über zwei verfügt, den Rendezvous-Deskriptor und den Verbindungsdeskriptor. Nun folgt die vollständige Quelle eines Clienten für unseren hangman-server. Nachdem die Verbindung mittels des eben beschriebenen Heizplatten-Programms aufgebaut wurde, transportiert es lediglich Text von der Tastatur zum Server und vom Server zurück auf den Bildschirm (hangclient.c). /* File hangclient.c - Client for hangman game. */ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #define LINESIZE 80 #define HANGMAN_TCP_PORT 1066 main(argc, argv) int argc ; char *argv[] ; struct sockaddr_in server; /* Server's address assembledhere */ struct hostent *host_info; int sock, count; char _inline [LINESIZE] ; /* Buffer to copy from user to server */
44 44 char outline[linesize] ; /* Buffer to copy from server to user */ char *server_name; /* Get server name from command line. If none, use 'localhost' */ server_name = (argc>1)? argv[1] : "localhost" ; /* Create the socket */ sock = socket(af_inet, SOCK_STREAM, 0) ; if (sock < 0) perror("creating stream socket") ; exit(1); host_info = gethostbyname(server_name); if (host_info == NULL) fprintf(stderr, "%s: unknownhost: %s\n", argv[0], server_name); exit(2); /* Set up the server's socket address, then connect */ server.sin_family = host_info->h_addrtype; memcpy( (char *)&server.sin_addr, host_info->h_addr, host_info->h_length); server.sin_port = htons(hangman_tcp_port) ; if (connect(sock, (struct sockaddr *)&server,sizeof server) < 0) perror("connecting to server"); exit(3); /* OK now we are connected to the server. Collect a line from the server, print it, collect a line from the user, send it to the server, and loop round until the server closes the connection. */ printf ("connected to server %s\n", server_name) ; while ((count = read(sock, _inline, LINESIZE)) > 0) write(1, _inline, count); count = read(0, outline, LINESIZE) ; write(sock, outline, count); Beachten Sie, dass in diesem Fall die Kommunikation vom Server ausgeht, nachdem die Verbindung aufgebaut ist. Die Anordnungen der Aufrufe read() und write() in der Hauptschleife des Clienten spiegeln dies wieder. Dieser Ansatz geht davon aus, dass Austausch zwischen Server und Client sich passend überlappt. Es ist etwas schwieriger, einen Clienten zu schreiben, der Text zwischen Benutzer und Server in frei wählbarer Reihenfolge kopiert. Wir kennen aber die dazu erforderlichen Techniken: wir können einen zweiten Prozess erzeugen oder mit select() arbeiten. 7 Verbindungslose Clienten und Server In diesem Abschnitt betrachten wir die Operationsabfolge bei verbindungslosen Clienten und Servern. Wir verdeutlichen sie am Beispiel eines verbindungslosen Clienten, der mit dem Internet-Standarddienst tftp (trivial file transfer protocol) Verbindung aufnimmt. 7.1 Design verbindungsloser Server Die von verbindungslosen Servern und Clienten ausgeführte Operationsfolge wird in Abb.: 7.1 gezeigt. Hier interagiert ein einzelner Server mit zwei Clienten. Der Server hat ein einziges Socket, mit dem er Datagramme empfängt. Viele Clienten können diesem Socket Datagramme
45 45 Abb.: 7.1 Verbindungslose Clienten und Server senden. Allgemein wird der Server Datagramme verschiedener Clienten in willkürlicher, ineinander verschachtelter Reihenfolge annehmen. Ist der Server zustandslos (braucht er sich von einer Clientinteraktion zur nächsten nichts zu merken), so ist das kein Problem. Der Server liest das Datagramm, formuliert eine Antwort, sendet sie an den Clienten zurück und vergißt den ganzen Vorgang. Einige einfache Internet-Server wie daytime und echo arbeiten so. Server, die Zustandsinformation pflegen, sind komplexer. Ein Ansatz besteht darin, die Statusinformation pro Client in eine Struktur zu verpacken und eine Art indizierter Datenstruktur zu pflegen, die einen aus der Socketadresse des Clienten (d.h., der Port-Nummer und der IP-Adres-se) aufgebauten Suchschlüssel benutzt. Ein anderer Ansatz besteht darin, dass der Server für jeden Clienten, mit dem er zu tun hat, einen KindProzess erzeugt. Jeder Kind-
46 46 Prozess legt ein neues Datagramm-Socket an, dessen Port-Nummer dem Clienten pflichtgemäß gemeldet wird und das für die weitere Interaktion vom Clienten genutzt wird (so arbeitet der tftp-server). Da jeder Client von einem eigenen Prozess behandelt wird, kann die von jedem Clienten benötigte Statusinformation leicht verfolgt werden. Die verbindungslosen Clienten und Server, die UDP als Transportprotokoll einsetzen, sehen sich noch einem anderen Problem gegenüber: der Unzuverlässigkeit des UDP. Darüber sollte man sich jedoch nicht allzuviele Gedanken machen. Wenn wir von einem unzuverlässigen UDP sprechen, heißt das nicht, dass es in sich fehlerbehaftet ist. Wir meinen damit nur, dass es in diesem Protokoll keinen Mechanismus gibt, um Probleme wie verlorene oder falsche Datenauslieferungen zu entdecken (und zu berichtigen). Für Client/Serverpaare, die zwischen Maschinen eines einzigen LAN arbeiten, ist das kein Problem. Verliert das LAN Daten, so ist keine Übertragungswiederholung notwendig, sondern eine Information des Netzverwalters über dieses Problem sowie die Aufforderung, es zu beseitigen. Werden Client und Server durch zwischengeschaltete Verbindungssysteme getrennt, und sind sie darüber hinaus auch noch über ein Weitverkehrsnetz verteilt, so ist die Wahrscheinlichkeit verlorener oder falsch zugestellter Pakete deutlich höher. Der Entwickler eines verbindungslosen Client/Serverpaares hat grundsätzlich zwei Wahlmöglichkeiten: 1. Von der Annahme eines ausreichend zuverlässigen zugrundeliegenden Transportsystems ausgehen und in Kauf nehmen, dass die Anwendung abstürzt, wenn das Netz zusammenbricht. 2. Verlangen, dass die Anwendung zuverlässiger als das Netz ist. In diesem Fall muss auf der Anwendungsebene ein Mechanismus für Zeitlimit/Übertragungswiederholung eingebaut werden. Eine Realisierungsmöglichkeit sehen wir uns später in diesem Abschnitt an. (Alternativ kann natürlich auch zu einem verbindungsorientierten Dienst gewechselt werden. Dann muss sich TCP Gedanken zu Netzwerkabstürzen machen). Die Asymmetrie zwischen Client und Server ist im verbindungslosen Fall viel weniger auffällig als im verbindungsorientierten, wo der Server listen() und accept() aufruft, der Client dagegen connect(). Im verbindungslosen Fall wird keiner dieser Aufrufe eingesetzt. Hier legen Client und Server ein Socket an, binden einen Port daran und senden und empfangen dann Datagramme. Die Beziehung Client/Server ist durch die Frage wer kommt zuerst charakterisiert. Der Client sendet ein Datagramm, der Server antwortet. Bei Diensten wie daytime, Chargen und time sendet der Client ein Null-Datagramm, um seine Anwesenheit kundzutun. Der Server verteilt dann bereitwillig sein Wissen. Im verbindungslosen Fall muss der Client zuerst übertragen, sonst weiß der Server nichts von dessen Existenz. 7.2 Operationen verbindungsloser Clienten und Server Verbindungslose Clienten und Server beginnen beide mit dem Anlegen eines Datagramm-Sockets : int sock; sock=socket(af_inet, SOCK_DGRAM, 0); Die vorhergehenden Anmerkungen zu Adressfamilien treffen auch hier zu, wir entscheiden uns wieder für die AF_INET-Adressierung.
47 47 Der Server muss natürlich seine wohl bekannte Port-Nummer an dieses Socket binden, damit der Client weiß, wo er es findet. Das Programm sieht genauso aus wie beim verbindungsorientierten Fall. Der Client muss nicht ausdrücklich bind() aufrufen. Versucht er, ein Datagramm zu übertragen und findet keine gebundene Adresse, so wird das System automatisch eine anbinden. Bei unverbundenen Sockets kann nicht mit den üblichen Aufrufen read() und write() gearbeitet werden. Sechs neue Aufrufe stehen zur Verfügung, drei zum Senden, drei zum Empfangen. Einzelheiten siehe unter send(3) und recv(3). Die geradlinigsten dieser Aufrufe, sendto() und recvfrom(), werden in Abb.: 7.2 gezeigt. Die ersten drei Argumente dieser Aufrufe entsprechen write() und read(), es handelt sich um einen Deskriptor, einen Zeiger auf den Datenpuffer und eine Bytezahl. Das flags- Argument legt spezielle Lieferoptionen fest, ist aber üblicherweise Null. Wie erwartet, müssen wir die Zieladresse bei jedem Aufruf angeben. Das geschieht über die Argumente addr und addrlen.
48 48 Abb.: 7.2 Senden und Empfangen von Datagrammen Die beiden anderen Aufrufpaare werden wir uns nicht mehr in Einzelheiten ansehen, hier ist eine Kurzbeschreibung: send(sock, data_addr, data_len, flags) recv(sock, data_addr, data_len, flags) sendmsg(sock, msg_info, flags) recvmsg(sock, msg_info, flags) Die Aufrufe send() und recv() bringen in ihrer Argumentliste keine Peer-Adresse. Sie können nur bei Datagramm-Sockets eingesetzt werden, für die Im voraus ein connect() -Aufruf zur Bestimmung der Peer-Adresse durchgeführt wurde. Der Begriff connect() ist irreführend, wenn er auf Datagramm-Sockets angewandt wird, da es nicht wirklich in Kontakt mit dem Peer tritt, sondern nur eine Vorgabeadresse aufbaut, die beim Senden an das Socket benutzt wird. In diesem Zusammenhang bedeutet Peer die Einheit am anderen Ende.
49 49 Der msg_info Parameter in den Aufrufen sendmsg() und recvmsg() zeigt auf eine Struktur, die gather-write- und scatter-read-operationen unterstützt. Gather-write heißt hier, dass eine Sammlung getrennter Datenpuffer an unterschiedlichen Stellen im Speicher in ein einziges Datagramm zusammengeführt werden können. Scatter-read bedeutet, dass ein einzelnes Datagramm aufgespalten und an mehrere getrennte Datenpuffer verteilt werden kann. Nützlich ist das zum Beispiel, wenn das Anwendungsprotokoll eines Benutzers mit Nachrichten aus Header und Datenteil arbeitet. Manchmal ist es nicht vorteilhaft, die beiden nebeneinander im Speicher zu halten (besonders bei Headern variabler Länge). 7.3 Beispiel für einen verbindungslosen Clienten Den Einsatz verbindungsloser Dienste werden wir anhand der Entwicklung eines Clienten für tftp (trivial file transfer protocol) verdeutlichen. Der Client wird eine externe Version des cat-programms sein, rcat genannt, das folgendermaßen aufgerufen wird: rcat saturn /etc/passwd Dieses Kommando zeigt den Inhalt der Datei /etc/passwd auf dem externen Host saturn. Im Vergleich zu anderen Dateitransfer-Protokollen wie ftp, rcp oder ftam (von OSI angeboten) ist tftp wohl trivial, im Verhältnis zu dem einfachen ASCII-Textprotokoll unseres hangman-dienstes jedoch schon um einiges komplexer. Das tftp-protokoll ist in RFC 783 vollständig beschrieben. Nun folgt eine weniger formale Beschreibung: Abb.: 7.3 verdeutlicht, dass das Protokoll fünf Datenpakettypen definiert.
50 50 Jedes Paket beginnt mit einem Opcode (einer zwei-byte Ganzzahl), dessen Wert den Pakettyp anzeigt. Ein Dateitransfer wird vom Clienten ausgelöst. Er sendet ein READ RE- QUEST- (Leseaufruf-) oder ein WRTTE REQUEST-(Schreibaufruf-) Paket. Dieses Paket enthält den Dateinamen (der auf dem Server interpretiert werden soll), ein Null-Byte zum Beenden und eine zweite Zeichenfolge, die den Transfermodus anzeigt. Für unser Beispiel rcat eignet sich ein READ REQUEST im Oktal-Modus. Die Anfrage wird an den TFTP-Server an dessen wohl bekannte Port-Nummer (69) geschickt. Die vom Clienten eingebundene Port-Nummer spielt keine Rolle, sie muss nur für die Dauer der Kommunikation unverändert bleiben. Der Server legt ein neues Socket (mit einer neuen Port-Nummer) für den Dialog mit diesem Clienten an. Bei einer Leseanfrage sendet der Server als Antwort eine Reihe von DATA-Paketen an den Clienten. Jedes Datenpaket enthält genau 512 Bytes Daten (die Gesamtlänge des Datagramms beträgt also 516 Bytes), nur das letzte enthält 0 bis 511 Bytes Daten. Dieses kurze Paket signalisiert dem Clienten das Ende des Datenstroms. Die Blocknummer im Datenpaket beginnt bei l und erhöht sich für jeden Block. Der Client muss jeden Block durch das Senden eines ACKNOWLEDGE-Pakets bestätigen, das die Nummer des zu bestätigenden Blocks enthält. Dann wird der Server den nächsten Block senden. Beachten Sie, dass die Bestätigungen an den UDP-Port gesandt werden, den der Server für das Senden des DATA-Pakets benutzt hat, nicht an Port 69 Abb.: 7.4 zeigt den Dialog. Abb.: 7.3 TFTP-Paketformate
51 51 Abb.: 7.4 Beispiel für den Datenaustausch mittels TFTP Entdeckt der Server einen Fehler (zum Beispiel eine Anfrage, eine nicht vorhandene oder nicht lesbare Datei zu kopieren), so gibt er ein ERROR-Paket statt eines DATA-Pakets zurück. Dieses Fehlerpaket enthält einen numerischen Fehlerkode und einen Fehlerbericht, den der Client ausdrucken kann. Die Opcodes und die Blocknummern werden in Netzwerk-Byte-Anordnung (als erstes das MS-Byte) gesendet. Maschinenspezifische Unstimmigkeiten bei der Interpretation der Dateidaten selbst sind nicht unser Problem. Soweit tftp betroffen ist, sind diese Daten opak (das heißt, sie werden nicht interpretiert). Sie werden feststellen, dass der Client dem Server keine Informationen über die Identität des anfragenden Benutzers gibt. Daher steht dem Server keine sinnvolle Benutzerkennung zur Verfügung, mit der er Dateizugangsberechtigungen prüfen kann. Die meisten Implementationen des tftp-server lösen dieses Problem, indem sie nur zu den Dateien mit world - Leseberechügung Lesezugang gestatten. Schreibzugang erhalten nur Dateien, die bereits bestehen und öffentlich beschreibbar sind. Einige Versionen führen beim Start einen chroot() -Systemaufruf an ein bestimmtes Verzeichnis durch, um mehr Sicherheit zu garantieren. Diese Einschränkungen verringern in der Praxis den Nutzen unseres rcat Clienten. Nachdem wir uns bereits mit verbindungslosen Client- und Serveroperationen befasst und das TFTP-Protokoll beschrieben haben, ist das nun folgende Clientenprogramm sicher keine Überraschung mehr. Der einzige etwas schwierigere Teil ist der Aufbau des READ RE- QUEST-Pakets. Wir können keine Struktur einsetzen, da die Felder variable Länge haben und müssen uns mit Kopieren von Zeichenfolgen und Zeigermanipulation behelfen. Nun folgt der Client-Listing (rcat-1.c).
52 52 /* Externer cat Client nutzt TFTP Server (UDP Socket Implementa-tion). Gebrauch: rcat hostname filename */ #include <sys/types.h> #include <sys/fcntl.h> #include <netinet/in.h> #include <sys/socket.h> #include <netdb.h> #include <stdio.h> #define TFTP_PORT 69 /* tftp's well-known port number */ #define BSIZE 600 /* size of our data buffer */ #define MODE "octet" #define OP_RRQ 1 /* TFTP op-codes */ #define OP_DATA 3 #define OP_ACK 4 #define OP_ERROR 5 main(int argc, char *argv [ ]) int sock; /* Socket descriptor */ struct sockaddr_in server; /* Server's address */ struct sockaddr_in client; /* Client's address */ struct hostent *host; /* Server host info */ char buffer[bsize], *p; int count, server_len; if (argc!= 3) fprintf(stderr, "usage: %s hostname filename\n", argv[0]); exit(1); /* Create a datagram socket */ sock = socket(af_inet, SOCK_DGRAM, 0) ; /* Binden einer lokalenadresse. Jede Port-Nummer ist mögl. Dieser Schritt ist freiwillig fur denclienten. Das System wählt dann selbst eine freie Port-Nummer.*/ client.sin_family =AF_INET; client. sin_addr. s_addr = htonl (INADDR_ANY) ; client.sin_port = 0; /* 0 says choose any port */ if (bind(sock, &client, sizeof client) < 0) fprintf(stderr, "rcat: bindfailed\n"); exit(1); /* Get the server's address */ host = gethostbyname(argv[1]) ; if (host == NULL) fprintf(stderr, "unknownhost: %s\n", argv[1]); exit(2); server.sin_family =AF_INET; memcpy(&server.sin_addr.s_addr, host->h_addr, host->h_length); server.sin_port = htons(tftp_port) ; /* Bilde ein tftp-read-request-paket. Dies ist problematisch weil die Felder variable Lange haben, deshalb sind Strukturen nicht nutzbar.*/ *(short *)buffer = htons(op_rrq); /* The op-code */ /* in network byte order */ p = buffer + 2 ; strcpy(p, argv[2]); /* The file name */ p += strlen(argv[2]) + 1; /* Keep the null */ strcpy(p, MODE); /* The mode */ p += strlen(mode) + 1; /* Keep the null */ /* Send Read Request to tftp server. The length is computed from the pointer difference p-buffer */ count = sendto(sock, buffer, p-buffer, 0, &server, sizeof server); /* Loop, collecting data packets from the server, until a short packet arrives. This indicates the end of the file. */ do
53 53 server_len = sizeof server; count = recvfrom(sock, buffer, BSIZE, 0, &server, &server_len); if (ntohs(*(short*)buffer) ==OP_ERROR) /* Ignore the error code; just print the error message */ fprintf(stderr, "rcat: %s\n", buffer+4); else /* Got a good block. Write it to standard output. */ write(1, buffer+4, count-4); /* Sende ein ack-paket. Die zu bestatigende Blocknummer ist bereits im Puffer, so miissen wir nur den opcode andern und die ersten 4-Byte des Puffers zurucksenden. Achtung die Bestatigung geht an die Port-Nummer des Servers nicht an Port 69. Die erforderliche Adresse ist nach demaufruf von recvfrom() bereits in der Server-Struktur. */ *(short *)buffer = htons(op_ack); sendto(sock, buffer, 4, 0, &server, sizeof server); while (count == 516) ; 7.4 TFTP-Paketverkehr Wir können snoop, das Programm zur Beobachtung des Datenverkehrs, einsetzen, um den von diesem Programm erzeugten Netzverkehr zu untersuchen. Folgender Austausch entsteht aus der Ausführung des Kommandos rcat pluto /etc/passwd auf dem Host uranus. Er entspricht weitgehend dem TFTP-Beispielaustausch in Abb.: 7.4. Die Datei /etc/passwd auf pluto ist offensichtlich 517 Bytes lang. uranus -> pluto TFTP Read /etc/passwd (octet) pluto -> uranus TFTP Ack block l uranus -> pluto TFTP Data block l (512 bytes) uranus -> pluto TFTP Ack block l uranus -> pluto TFTP Data block 2 (15 bytes) (last block) uranus -> pluto TFTP Ack block Implementieren von Zeitlimit und Wiederholung Unser tftp-client trifft keine Vorkehrungen, verlorene Datagramme wiederherzustellen. Verliert das Netzwerk ein ankommendes Datenpaket oder eine ausgehende Bestätigung, wird der Client für ewig blocken. In diesem Fall sind falsch zugestellte Datagramme kein Thema. Das Gänsemarsch-Schema der Bestätigung stellt sicher, dass sich höchstens ein Datagramm unterwegs befindet. Die Implementierung von Zeitlimit und Wiederholungsmöglichkeiten zeigen wir an einem Beispiel. Wir fügen einen Programmteil hinzu, der für die Wiederaufnahme nach Verlust einer abgeschickten Bestätigung sorgt. In einem gesunden LAN ist es
54 54 in der Praxis eher schwierig, das Funktionieren eines solchen Programmteils zu beweisen. Wir simulieren nun ein LAN, das Pakete verliert. Wir werden in zufälliger Verteilung 25% unserer Bestätigungspakete verlieren, dafür ändern wir den sendto() -Aufruf in unserem rcat-clienten folgendermaßen ab: if (rand() % 4 sendto(...); Kommt die Bestätigung abhanden, wird der Server niemals das nächste Datenpaket abschicken. Damit wird der Client beim nächsten recvfrom() -Aufruf für immer blocken. (Das stimmt nicht ganz, da der tftp-server selbst eine Möglichkeit für Zeitlimit und Wiederholung implementiert, das übersehen wir im Moment aber absichtlich!). Es gibt viele Möglichkeiten, eine Datagramm-Leseoperation in ein Zeitlimit laufen zu lassen. In unserem Ansatz kennzeichnen wir das Socket mittels folgendem Aufruf als nichtblockierend (non-blocking): fcntl (sock, F_SETFL, FNDELAY) ; Diesen Aufruf setzen wir ein, wenn das Socket angelegt ist. (Dieser Aufruf kann für die meisten offenen Dateideskriptoren, nicht nur für Sockets, verwendet werden. Die symbolischen Konstanten F_SETFL und FNDELAY werden in <sys/fcntl.h> definiert). Ist das geschehen, wird ein Aufruf recvfrom() sofort mit -l zurückkehren, wenn kein Datagramm zum Lesen vorhanden ist, errno ist dann auf EWOULDBLOCK gesetzt. Ein Zeitlimit kann implementiert werden, indem der recvfrom() -Aufruf in eine Schleife gesetzt wird, die nach einer EWOULDBLOCK-Fehlerrückgabe sucht und bis zu einer maximaler Anzahl Wiederholungen läuft. Dieser Ansatz hat zwei größere Nachteile: Erstens ist es schwer festzustellen, in welcher Beziehung die maximale Wiederholungszahl zur tatsächlich verstrichenen Zeit steht. Welche Wiederholungszahl setzen wir zum Beispiel für ein Zeitlimit von 2 Sekunden an? Das hängt offensichtlich von der Prozessorgeschwindigkeit und von der Zahl der anderen Prozessoren in der Warteschlange beim Zeitscheibenverwalter ab. Ein zweites, schwerwiegenderes Problem ist die Verschwendung von Rechenzeit durch das Kreisen in einer Abfrageschleife wie dieser. Das sollte in einem Multi- Tasking-Betriebssystem tunlichst vermieden werden. Am Rande: Unser eigener Prozess wird sogar selbst bei dieser Vorgehensweise am meisten Schaden nehmen. Der UNIX-Verwaltungsalgorithmus senkt die Priorität der Prozesse, die viel Rechenzeit benötigen. So werden schnelle interaktive Antworten von Programmen wie Shells und Editoren ermöglicht, die viel Zeit damit verbringen, auf Tastatureingaben zu warten. Die Abfrageschleife würde unseren Prozess in eine weniger günstige Zeitscheiben-Verwaltungspriorität setzen. Kehren wir zu unserem Problem mit Zeitlimit und Wiederholung zurück. Wir brauchen eine Möglichkeit zu blocken, bis entweder ein Datagramm ankommt oder eine bestimmte Zeit vergangen ist. Die Lösung ist im Grunde gleich wir lassen ein SIGALARM-Signal nach dem erforderlichen Zeitlimit erzeugen. Dafür sorgt der Aufruf
55 55 alarm(t) wobei t dem erforderlichen Zeitlimit in Sekunden entspricht. Dieses Signal muss auch an den Prozess weitergeleitet werden, und zwar durch die Signalhandlerfunktion signal (SIGALARM, alarm_catcher); Langsame Systemaufrufe (das heißt die, die eher für eine lange Zeit blockieren) werden unterbrochen, wenn ein Signal ankommt und kehren mit einem scheinbaren Fehler des EINTR-Werts zurück. Wir können durch die Prüfung des Rückgabewerts von recvfrom() und des errno-werts zwischen einem gültigen Datagramm und einem Zeitlimit unterscheiden und entsprechend antworten. In diesem Fall muss die alarm_catcher Funktion eigentlich nichts tun außer zurückzukehren. Sie kann daher einfach so geschrieben werden: void alarm_catcher () Kehrt der Aufruf recvfrom() normal zurück (wird das Datagramm ohne Zeitlimit empfangen), so rufen wir alarm(0) auf, um das Alarmsignal zu löschen. Zwischen der Rückkehr von recvfrom() und der Löschung des Alarms ist ein kleines Zeitfenster, innerhalb dessen immer noch ein SIGALARM gesendet werden kann. Da der Signalhandler nichts tut, ist das ohne Auswirkung. Einem weiteren Thema müssen wir uns noch widmen: Welches Datagramm sollen wir wiederholen, wenn das Zeitlimit bei recvfrom() erreicht wird? Natürlich das zuletzt geschickte. Das kann eine Bestätigung oder das erste READ_REQUEST-Paket sein. Im nachfolgenden Programm werden die Variablen buffer, length und server benutzt, um Quelle, Länge und Ziel des zuletzt übertragenen Datagramms festzuhalten. Nun folgt die neue Version des Clienten (rcat-2.c): /* Remote cat client using TFTP server (UDP socket implementation). This version includes a simple timeout/retransmit mechanism Usage: rcat hostname filename */ #include <sys/types.h> #include <sys/fcntl.h> #include <netinet/in.h> #include <sys/socket.h> #include <netdb.h> #include <signal.h> /* Added for this version */ #include <errno.h> /* Added for this version */ #include <stdio.h> #define TFTP_PORT 69 /* tftp's well-known port number */ #define BSIZE 600 /* size of our data buffer */ #define MODE "octet" #define TIMEOUT 2 /* Retransmit timeout in seconds */ #define OP_RRQ 1 /* TFTP op-codes */ #define OP_DATA 3
56 56 #define OP_ACK 4 #define OP_ERROR 5 void alarm_catcher() /* This is the signal handler. It doesn't do anything, except return */ main (int argc, char *argv [ ] ) int sock; /* Socket descriptor */ struct sockaddr_in server; /* Server's address */ struct sockaddr_in client; /* Client's address */ struct hostent *host; /* Server host info */ char buffer[bsize], *p; int count, server_len, length; if (argc!= 3) fprintf(stderr, "usage: %s hostname filename\n", argv[0]); exit(1); /* Create a datagram socket */ sock = socket(af_inet, SOCK_DGRAM, 0); /* Bind a local address. */ client.sin_family =AF_INET; client.sin_addr. s_addr = htonl (INADDR_ANY) ; client.sin_port = 0; /* 0 says choose any port */ if (bind(sock, &client, sizeof client) < 0) fprintf(stderr, "rcat: bindfailed\n"); exit(1); /* Get the server's address */ host = gethostbyname(argv[1]); if (host == NULL) fprintf(stderr, "unknownhost: %s\n", argv[1]); exit(2); server.sin_family =AF_INET; memcpy(&server.sin_addr.s_addr, host->h_addr, host->h_length); server.sin_port = htons(tftp_port); /* Bilde ein tftp-read-request-paket. Dies ist problematisch weil die Felder variable Lange haben, deshalb sind Strukturen nicht nutzbar. */ *(short *)buffer = htons(op_rrq); /* The op-code */ p = buffer + 2; strcpy(p, argv[2]); /* The file name */ p += strlen(argv[2]) + 1; /* Keep the null */ strcpy(p, MODE); /*TheMode */ p += strlen(mode) + 1; /* Send Read Request to tftp server */ length = p-buffer; count = sendto(sock, buffer, length, 0, &server, sizeof server) ; /* Loop, collecting data packets from the server, until a short packet arrives. This indicates the end of the file. */ do await_reply: server_len = sizeof server; /* Arrange for an ALARM signal to be delivered after TIMEOUT */ signal(sigalrm, alarm_catcher); alarm(timeout); count = recvfrom(sock, buffer, BSIZE, 0, &server, &server_len); alarm(0); /* Cancel alarm signal */ if (count == -1) if (errno = EINTR) /* Timed out - resend the last datagram */ sendto(sock, buffer, length, 0, &server, sizeof server); goto await_reply;
57 57 else /* Must be a REAL error! */ fprintf(stderr, "rcat: readerror\n"); exit(3) ; /* Reach here when got a valid datagram */ if (ntohs(*(short*)buffer) ==OP_ERROR) fprintf(stderr, "rcat: %s\n", buffer+4); else write(1, buffer+4, count-4); /* Sende ein ack-paket. Die zu bestatigende Blocknurnmer ist bereits im Puffer, so mussen wir nur den opcode andern und die ersten 4-Byte des Puffers zurücksenden. Achtung die Bestatigung geht an die Port- Nummer des Servers nicht an Port 69. */ *(short *)buffer = htons(op_ack); length = 4; if (rand() % 4) sendto(sock, buffer, length, 0, &server, sizeof server); while (count == 516) ; Im wirklichen Leben müssen Clienten und Server, die Mechanismen wie Zeitlimit und Übertragungswiederholung implementieren, etwas intelligenter sein. Wenn unser Client ein Zeitlimit erreicht, während er auf den nächsten Datenblock des Servers wartet, so weiß er nicht, ob das zuletzt abgeschickte Bestätigungspaket oder das neue ankommende Datenpaket verlorenging. Abb.: 7.5 verdeutlicht beide Möglichkeiten.
58 58 Im ersten Fall geht die Bestätigung für Block 2 verloren und wird neu übertragen. Der Server nimmt nichts unübliches wahr. Im zweiten Fall gehen die Daten für Block 2 verloren. Der Client erreicht ein Zeitlimit und wiederholt die Bestätigung für Block 1. Der Server sieht nun zwei Kopien dieser Bestätigung. Er muss intelligent genug sein, um das zu merken und als Antwort die Daten für Block 2 neu zu schicken. Erhält der Client diesen Block, weiß er, dass der Server ihn mindestens einmal, wahrscheinlich sogar doppelt, übertragen hat. Doppelte Übertragung eines Dateiblocks schadet nicht. Wir müssen nur sicherstellen, dass die Operation mindestens einmal ausgeführt wurde mehrfache Ausführung hat keine Konsequenzen. Nicht alle Operationen sind unkritisch. Sehen wir uns einen für die Verwaltung eines Bankkontos zuständigen Server an. Eine Anfrage kommt an, 250 Mark an das örtliche Finanzamt zu überweisen. Der Server führt die Operation aus und sendet eine Bestätigung, die verlorengeht. Der Client hat keine Lust mehr, auf eine Antwort zu warten, das Zeitlimit greift und die Anfrage wird wiederholt. Natürlich darf der Server die Überweisung nicht noch einmal ausführen. Er muss intelligent genug sein, zu erkennen, dass er das bereits erledigt hat Abb.: 7.5 Verlust von Anfrage und Antwort
59 59 und einfach die Bestätigung wiederholen. Ein jeder Anfrage zugeordneter Transaktionsidentifizierer löst dieses Problem. Der Server pflegt ein Cache, in dem er bereits verarbeitete Transaktionsidentifizierer und die Ergebnisse dieser Transaktion vorhält, und kann daher die doppelte Anfrage ablehnen und das Ergebnis aus dem Cache zurückmelden. So wird sichergestellt, dass die Operation auch bei verlorenen oder doppelten Paketen nur genau einmal ausgeführt wird. 8 Eine parallele, verteilte Anwendung Wir beenden mit einem Beispiel einer wirklich verteilten, parallelen Anwendung. Das von uns angegangene Problem lässt sich einfach beschreiben: Wir wollen wissen, wie viele Primzahlen es zwischen l und gibt. Auch wenn Sie das Ergebnis eigentlich gar nicht wissen wollen, bringt uns dieses Beispiel einige Vorteile: Primzahlen finden ist zeitintensiv. Der Grundgedanke besteht darin, mehrere Server parallel an verschiedenen Teilen des Problems arbeiten zu lassen, um die für die Lösung benötigte Gesamtzeit zu reduzieren. 8.1 Ein nicht-verteiltes Programm zur Ermittlung von Primzahlen Dieses normale, nicht-verteilte Programm erledigt die Aufgabe (primes.c): #define SMALLEST 1L #define BIGGEST L main(argc, argv) int argc; char *argv[]; long count; count = count_primes(smallest, BIGGEST); printf("between %ld and %ld there are %ld primes\n", SMALLEST, BIGGEST, count); long count_primes(min, max) long min, max; long i, count = 0; for (i=min; i<=max; i++) if (isprime(i)) count++; return count; int isprime(n) long n; int i; for (i=2; i*i <= n; i++) if (n%i == 0) return 0; return 1; Auf einer Sparcstation 2 benötigt das Programm für die Ausführung 277 Sekunden, auf einer Sun 3/ Sekunden. Bei derartig hohen Rechenzeiten ist eine parallele, verteilte Lösung sinnvoll. (Es gibt übrigens Primzahlen im angegebenen Bereich).
60 Ein Server zur Ermittlung von Primzahlen Zwei Schlüsselentscheidungen müssen zum Design getroffen werden, bevor eine verteilte Version dieses Programms geschrieben werden kann. Erstens: Wie teilen wir die Arbeit zwischen Client und (mehreren) Servern auf? Zweitens: Sollen die Server verbindungsorientiert oder verbindungslos sein? Für die Arbeitsverteilung zwischen Server und Client gibt es zwei Möglichkeiten. Einmal kann nur die Funktion isprime() auf dem Server implementiert werden. Dann senden wir dem Server eine Zahl, er schickt eine Boolsche Variable zurück, die aussagt, ob diese Zahl eine Primzahl ist. Das ist keine besonders gute Wahl, die vom Server bei jeder Transaktion zu leistende Arbeit ist viel zu klein. Der zusätzliche Aufwand für die Kommunikation würde die tatsächliche Berechnungszeit weit übersteigen. Wir hätten dann eine verteilte Version, die langsamer ist als die ursprüngliche Ein-Prozessor-Version. Die zweite Möglichkeit besteht darin, count_primes auf dem Server zu installieren. Bei jeder Übertragung schicken wir dem Server zwei Zahlen, die die obere und untere Grenze des zu durchsuchenden Zahlenbereiches angeben. Der Server gibt die Zahl der in diesem Bereich liegenden Primzahlen zurück. Ist dieser Bereich recht groß, überwiegt die Rechenlast und der zusätzliche Aufwand für die Kommunikation wird vernachlässigbar. Der Client muss dann nur den gesamten Bereich in eine Reihe von Unterbereichen teilen und jeden Unterbereich an einen Server weiterleiten, dann die Antworten von den Servern einsammeln und zusammenzählen. Dieses Grundmodell werden wir implementieren. Wir wollen Server auf mehreren Maschinen starten, damit sie parallel arbeiten können. Weniger deutlich ist die zweite Entscheidung über den Einsatz verbindungsorientierter oder verbindungsloser Server. In beiden Fällen gibt es zufriedenstellende Lösungen. Für einen verbindungslosen Dienst spricht die Tatsache, dass der Server in sich abgeschlossen ist und keinen Bezug auf vorhergehende Transaktionen nimmt. Es ist auch für den Clienten etwas leichter, die Antworten von mehreren Servern einzusammeln, wenn alle an dasselbe UDP- Socket geschickt werden statt an verschiedene TCP-Verbindungen. Die Arbeit mit UDP kann andererseits da zu zwingen, einen Mechanismus für Zeitlimit und Übertragungswiederholung zu installieren, um mit verlorenen Paketen umgehen zu können, falls das Netz nicht zuverlässig ist. Die Wahl eines angemessenen Werts für das Zeitlimit ist schwierig, da nicht einfach geschätzt werden kann, wielange eine bestimmte Anfrage benötigen sollte. Es ist einfacher, wenn Zeitlimit und Übertragungswiederholung unterhalb des Anwendungsprogramms behandelt werden. Wir werden uns daher für einen verbindungsorientierten Server mit TCP entscheiden. Das Client-an-Server-Protokoll wird wie besprochen nur aus einem den Unterbereich bestimmenden Zahlenpaar bestehen. Als Antwort sendet der Server einen einzelnen Wert an den Clienten zurück, der die Menge der Primzahlen in diesem Bereich angibt. Nachdem diese Entscheidungen getroffen sind, ist der Server leicht zu schreiben. Wir haben das Heizplatten -Programm für einen TCP-Ser-ver bereits kennengelernt, wir haben die count_primes()-funktion auch schon nur wenig muss neu geschrieben werden. Auf einige Dinge müssen sich Server und Client einigen welche Port-Nummer benutzt wird und welches Format die Anfragepakete haben. Diese gemeinsam genutzten Definitionen werden wir in die Infodatei primes.h setzen: /* primes.h */ #define PRIME_PORT 1066 struct subrange
61 61 ; long min; long max; Der vollständige Server (primes-server.c): /* Server for counting prime numbers */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include "primes.h" main() int sock, msgsock, client_len; struct sockaddr_in server, client; long count; struct subrange limits; sock = socket(af_inet, SOCK_STREAM, 0); if (sock < 0) exit(1); server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(inaddr_any); server.sin_port = htons(prime_port); if (bind(sock, (struct sockaddr *) &server, sizeof (server)) < 0) exit(2); listen(sock, 5); while (1) client_len = sizeof(client); msgsock = accept(sock, (struct sockaddr *) &client, &client_len); if (msgsock < 0) exit(3); /* Ist eine Verbindung hergestellt, akzeptieren wir mehrfache Anfragen vom Clienten solange bis der Client die Verbindung beendet. In dem Moment erhalten wir von read() einen Fehler zuruck. */ while (1) if(read(msgsock, &limits, sizeof limits)!= sizeof limits) /* Assume 'end of file'; drop the connection */ break; count = count_primes(limits.min, limits.max); write(msgsock, &count, sizeof count); close(msgsock); /* Go wait for the next client */ long count_primes(min, max) long min, max; long i, count = 0; for (i=min; i<=max; i++)
62 62 if (isprime(i)) count++; return count; int isprime(n) long n; int i; for (i=2; i*i <= n; i++) if (n%i == 0) return 0; return 1; 8.3 Ein Client zur Ermittlung von Primzahlen Der Client ist etwas schwieriger zu realisieren. Woher weiß er, auf welchen Maschinen der Server verfügbar ist? Wie teilt er den Gesamtbereich auf? Wie verfolgt er, welcher Server was erledigt? Wie sammelt er die Antworten ein? Woher weiß er, dass alle Antworten zurück sind? Einige vereinfachende Annahmen halten unseren Clienten simpel. Später werden wir eine ausgefeiltere Version entwickeln. Für den Anfang gehen wir davon aus, dass alle Server per Hand gestartet wurden, zum Beispiel, in dem wir uns auf jeder externen Werkstation der Reihe nach angemeldet haben. Daher wissen wir genau, wo die Server laufen und können dem Clienten in der Kommandozeile die entsprechenden Maschinennamen angeben. Die Bereichsunterteilung wird in Abhängigkeit von der Zahl der verfügbaren Server vorgenommen ( /Anzahl der Server). Wir werden zu jedem Server eine TCP-Verbindung herstellen und ihm einen Unterbereich zur Berechnung überstellen. Dann werden wir (in derselben Reihenfolge) die Antworten der Server abrufen und addieren. Am Rande: Die Wahl eines binären Protokolls setzt implizit voraus, dass die Maschinen, auf denen Client und Server laufen, mit kompatiblen binären Datendarstellungen arbeiten. Hier ist unser erster, etwas einfältiger Client (primes-client-1.c): /* Client for counting prime numbers: Version 1; */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <netdb.h> #include "primes.h" #define MAX_HOSTS 10 #define SMALLEST 1 #define BIGGEST main(int argc, char *argv[]) int connection[max_hosts]; char *host[max_hosts]; int i, nhosts; long count, total; struct subrange limits;
63 63 long start_time; nhosts = argc - 1; if (nhosts < 1) fprintf(stderr, "%s: no hosts specified\n", argv[0]); exit(1); if (nhosts > MAX_HOSTS) nhosts = MAX_HOSTS; fprintf(stderr, "Too many hosts, using only the first %d\n", nhosts); /* Try to obtain connections to each server */ for (i=0; i<nhosts; i++) host[i] = argv[i+1]; switch (connection[i] =connect_to_server(host[i], PRIME_PORT)) case -1: fprintf(stderr, "Problem creating socket - bye!\n"); exit(2); case -2: fprintf(stderr, "Unknown host: %s - bye!\n", host[i]); exit(3); case -3: fprintf(stderr, "Cannot find server on host %s - bye!\n", host[i]); exit(4); default: printf("connected to host %s\n", host[i]); /* Send a subrange to each server */ start_time = time(0); limits.max = SMALLEST - 1; for (i=0; i<nhosts; i++) limits.min = limits.max + 1; limits.max = limits.min + (BIGGEST - SMALLEST + 1)/nhosts; if (i == nhosts-1) limits.max = BIGGEST; printf("sending range (%d,%d) to host %s\n", limits.min, limits.max, host[i]); write(connection[i], &limits, sizeof limits); /* Read responses back from servers, in same order */ total = 0; for (i=0; i<nhosts; i++) read(connection[i], &count, sizeof count); printf("got reply = %ld from host %los after %ld sec\n", count, host[i], time(0)-start_time); total += count; printf("answer is %ld\n", total); int connect_to_server(char *host, int port) int sock; struct sockaddr_in server; struct hostent *host_info; if ((sock = socket(af_inet, SOCK_STREAM, 0)) < 0) return -1; if ((host_info = gethostbyname(host)) == NULL) return -2; server.sin_family = server.sin_port = AF_INET; htons(port);
64 64 memcpy(&server.sin_addr, host_info->h_addr, host_info->h_length); if (connect(sock, &server, sizeof server) < 0) return -3; return sock; Nehmen wir an, dass wir compillert und die Server auf den externen Hosts venus, neptun und jupiter irgendwie gestartet haben. Wir können dann unseren Clienten laufen lassen und ihm folgendermaßen mitteilen, dass er mit diesen drei Hosts arbeiten soll: mars% prime_client venus jupiter neptun connected to host venus connected to host jupiter connected to host neptun sending range (1,333334) to host venus sending range (333335,666668) to host jupiter sending range (666669, ) to host neptun got reply = from host venus after 151 sec got reply = from host jupiter after 163 sec got reply = from host neptun after 404 sec answer is mars% Unser Client funktioniert zwar, hat aber einige Einschränkungen. Erstens geht er nicht gut mit der Situation um, wenn zu einigen Servern kein Kontakt hergestellt werden kann. Er gibt dann einfach völlig auf und steigt aus. Die nicht auffindbaren Server sollten besser nicht beachtet und mit dem Rest weitergearbeitet werden. Die dazu erforderliche weitere Logik werden wir in unserer nächsten Version einbauen. 8.4 Lastausgleich Sehen wir uns die Zeitangaben in der oberen Ausgabe an, wird ein interessanteres Problem mit unserem Clienten deutlich. Die Hosts venus und jupiter benötigen etwa gleich lange für ihre Aufgabe, neptun fast dreimal so lang. Warum? Es liegt daran, dass neptun die großen Zahlen bearbeitet und es im Durchschnitt länger dauert, eine große als eine kleine Zahl auf ihre Primeigenschaft zu prüfen. Die Zeitangaben werden natürlich auch von der Geschwindigkeit der individuellen Prozessoren beeinflußt. Wir nutzen die Maschinen eindeutig nicht so gut wie möglich aus, da die Server von venus und jupiter etwa 60% ihrer Zeit arbeitslos sind. In der Sprache der Gemeinschaft parallel Verarbeitender sagen wir, die Last ist unausgeglichen. Wie können wir das ändern? Eine mögliche Lösung ist die Aufteilung des Bereichs (l ) in ungleiche Teile, wobei die Rechenlast für jede Teilmenge ungefähr gleich ist. Dafür müssen wir die Rechenlast eines gegebenen Unterbereichs irgendwie schätzen können. Leider gibt es dafür keine einfache Möglichkeit. (Es muss nicht immer bei dieser Anwendungsart so sein. Manchmal kann der Aufwand, eine gegebene Serveranfrage zu bearbeiten, recht leicht geschätzt werden, dann kann die Last von vornherein ausgeglichen verteilt werden). Der a-priori Lastausgleich stellt auch bei anderen, weniger trivialen Anwendungen ein Problem dar. Eine verteilte Anwendung zur Bildverarbeitung setzte zum Beispiel räumliche
65 65 Parallelität ein und teilte das Bild in horizontale Streifen, um einen Algorithmus für das räumliche Sehen zu beschleunigen. Die Ausführungszeit dieses Algorithmus war stark von der Menge Kantendetails im Bild abhängig. Daher wurde mit der Aufteilung des Bildes in gleichgroße Streifen nicht immer die Last gut ausgeglichen. Das Problem liegt wie bei unseren Primzahlen in der Schwierigkeit, den bei der Verarbeitung einer gegebenen Untermenge der Daten erforderlichen Aufwand zu schätzen. Selbst wenn eine solche Kostenmetrik zur Verfügung steht, können die Auswirkungen der unterschiedlichen Prozessorgeschwindigkeiten nicht berücksichtigt werden, da diese dem Clienten normalerweise nicht bekannt sind. Die Zeitangaben werden darüber hinaus noch durch die Auslastung der Hostmaschinen durch andere, nicht mit dieser in Beziehung stehende Aufgaben beeinflußt. 8.5 Implementieren eines Prozessorparks Zur Lösung dieses Problems werden wir ein bestimmtes Client/Server Paradigma übernehmen, dass bei den Anhängern der parallelen Algorithmen als Prozessorpark bekannt ist. Folgender Grundgedanke steckt dahinter: Ein einzelner Client (in der Ausdrucksweise des Prozessorparks Master genannt) verteilt die Arbeit auf mehrere Server (oder Sklaven). Er unterteilt die Arbeit in eine große Anzahl Arbeitspakete. Ob diese Pakete alle gleich schwierig sind, spielt keine Rolle, es ist jedoch wichtig, ein Vielfaches der Serveranzahl an Paketen zur Verfügung zu haben. Zuerst schickt er jedem Server ein Arbeitspaket. Dann wartet er auf die Antwort irgendeines Servers. Die Anwort wird festgehalten und diesem Server das nächste Arbeitspaket geschickt. Der Prozess fährt fort. Jeder Server erhält ein neues Arbeitspaket, sobald er das letzte beendet hat, bis alle Pakete abgesandt und alle Anworten eingetroffen sind. Dieser Prozessorpark, bei dem der einzelne Client von vielen Servern bedient wird, ist das genaue Gegenteil des üblichen Client/Server-Modells, in dem ein Server viele Clienten bedient. Der größte Vorteil des Prozessorparks liegt in der Auslastung der Server über die gesamte Zeitdauer, das heißt, die Last wird ausgeglichen. Es muss nicht vorher geschätzt werden, wie lange die Bearbeitung jedes Arbeitspakets dauern wird. Es spielt keine Rolle, ob die Server mit unterschiedlichen Geschwindigkeiten laufen. Der größte Nachteil besteht darin, dass ein Prozessorpark nur Sinn macht, wenn die Arbeit in eine ausreichend große Zahl voneinander unabhängiger Arbeitspakete unterteilt werden kann. Ein Vielfaches der Sklavenpfozessorzahl muss an Arbeitspaketen vorhanden sein. Ist die Abarbeitung eines Arbeitspakets von Ergebnissen vorhergegangener Arbeitspakete abhängig, wird die Clientenlogik viel komplizierter. In unserem Beispiel kann zum Glück jedes Arbeitspaket einer Untermenge ohne Bezug zu irgendeiner anderen Untermenge verarbeitet werden, daher ist hier der Prozessorpark eine gute Möglichkeit. Am Rande: Die Berechnung des Mandelbrotproblems war in den Kinderjahren der Transputer bei den Herstellern ein beliebtes Beispiel für parallele Verarbeitung. (Das Mandelbrotproblem ist ein auf fraktaler Geometrie beruhendes Muster, das auf frei wählbaren Detailstufen untersucht werden kann). Die Beliebtheit hatte drei Gründe: es ist ein sehr rechenintensives Problem, ein Prozessorpark ist sehr einfach zu installieren und das Endergebnis ist ein ganz hübsches Bild. In allen Hallen wurde auf Ausstellungen Bildschirm für Bildschirm mit Mandelbrot geboten. Nach einer Weile ernüchterte der Markt, Mandelbrot-freie Zonen entstanden, und die Hersteller begannen endlich, Werkzeuge für die
66 66 Entwicklung und Fehlersuche zu zeigen, die die Anwender dringend brauchten, wenn sie auch nur eine Spur von Hoffnung haben wollten, irgendetwas anderes als das Mandelbrotproblem zu programmieren... Natürlich gibt es auch wichtige Programme zur Bildbearbeitung, zum Beispiel Strahlverfolgungsbilderzeugung, die mittels eines Prozessorparks parallelisiert werden können. Zur Implementation des Prozessorparks sind beim Server keine Veränderungen nötig. Auf Seiten des Clienten erschwert uns nun unsere Entscheidung für einen verbindungsorientierten Server das Leben ein wenig. Zentrales Thema ist, dass der Client die Antworten der Server in der Reihenfolge ihres Erscheinens einholen muss. Diese Reihenfolge wird im allgemeinen nicht der entsprechen, in der die Anfragen abgeschickt wurden. In einem verbindungslosen Dienst kann der Client die Antworten aller Server durch Lesen aus einem einzigen Datagramm-Socket einsammeln. Bei unserem verbindungsorientierten Clienten müssen wir für jeden Server einen getrennten Dateideskriptor lesen. Wir wissen natürlich nicht, in welcher Reihenfolge sie fertig werden. Wir haben uns bereits den Systemaufruf select() angesehen, der einen Prozess blockieren lässt, bis irgendeiner von verschiedenen, definierten Servern lesebereit ist. Wir hatten ihn beim Server eingesetzt, um einen parallelen Server ohne Einsatz mehrerer Prozessoren zu implementieren. In unserem derzeitigen Beispiel benutzen wir ihn beim Clienten, um die Daten parallel an mehrere Server zu senden. Unsere neue Prozessorpark Version des Clienten (primes-client-2.c): /* Client for counting prime numbers: Version 2 Implements a processor farm, using sockets and select() */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <netdb.h> #include "primes.h" #define FD_TABLE_SIZE 32 #define SMALLEST 1 #define BIGGEST #define GRANULARITY 100 /* This controls the number of subranges (i.e. work packets) which the total range is divided into */ long next_min = SMALLEST; /* Tracks the lower bound of the next work packet to be sent out */ char *host[fd_table_size]; void send_next_work_packet() ; main(int argc, char *argv[]) int i, fd; int prospects; /* Prospective number of hosts */ int outstanding; /* Number of unanswered work packets */ long count, total; long start_time; fd_set select_set; /* Set of descriptors to select on */ fd_set ready_set; start_time = time(0); prospects = argc - 1; if (prospects < 1)
67 67 fprintf(stderr, "%s: no hosts specified\n", argv[0]); exit(1); /* Try to obtain connections to each server */ outstanding = 0; argv++; /* Bump pointer past the command name */ FD_ZERO(&select_set); for (i=0; i<prospects; i++) switch (fd = connect_to_server(argv[i], PRIME_PORT)) case -1: fprintf(stderr, "Problem creating socket\n"); exit(2); case -2: fprintf(stderr, "Unknown host %s: ignored\n", argv[i]); break; case -3: fprintf(stderr,"no server on host %s: ignored\n", argv[i]); break; default: if (fd >= FD_TABLE_SIZE) fprintf(stderr,"too many hosts: %s ignored\n", argv[i]); break; printf("cormected to host %s\n", argv[i]); /* It is not essential to record the host name, but it helps us print more meaningful messages */ host[fd] = argv[i]; /* We do not explicitly record the value of the descriptor for this connection, we simply add it into the select set. Later, when a select returns, we can find out which descriptor is ready for reading. */ FD_SET(fd, &select_set); send_next_work_packet(fd); outstanding++; /* At this point, since we have not retrieved any responses, the value of outstanding tells us how many hosts we found */ if (outstanding == 0) fprintf(stderr, "No servers found!\n"); exit(3) ; total = 0; /* Total count is accumulated in here */ /* Keep the servers busy until the job is done */ while (outstanding > 0) /* We copy select_set onto ready_set because select() over-writes its argument, and we need to keep select_set intact */ memcpy(&ready_set, &select_set, sizeof select_set); select(fd_table_size, &ready_set, NULL, NULL, NULL); /* Now we have to scan through the file descriptor set which select() returned to see which is ready for reading. We always scan the entire set, because it is possible (though unlikely for several descriptors to become ready at the same time. */ for (fd=3; fd < FD_TABLE_SIZE; fd++) if (FD_ISSET(fd, &ready_set)) read(fd, &count, sizeof count); printf("got reply = %ld from host %los after %ld sec\n",
68 68 count, host[fd], time(0)-start_time); total += count; outstanding --; /* If there are more work packets to process, send the next one out. Otherwise, just mop up the outstanding requests */ if (next_min <= BIGGEST) send_next_work_packet(fd) ; outstanding++; printf("answer is %ld\n", total); int connect_to_server(char *host, int port) int sock; struct sockaddr_in server; struct hostent *host_info; if ((sock = socket(af_inet, SOCK_STREAM, 0)) < 0) return -1; if ((host_info = gethostbyname(host)) == NULL) return -2; server.sin_family = AF_INET; server.sin_port = htons(port); memcpy(&server.sin_addr, host_info->h_addr, host_info->h_length); if (connect(sock, &server, sizeof server) < 0) return -3; return sock; void send_next_work_packet(int fd) struct subrange limits; limits.min = next_min; limits.max = limits.min + (BIGGEST - SMALLEST)/GRANULARITY; if (limits.max > BIGGEST) limits.max = BIGGEST; write(fd, &limits, sizeof limits); printf("sent range (%d,%d) to host %s\n", limits.min, limits.max, host[fd]); next_min = limits.max + 1; So sieht der laufende Dialog aus: venus% prime_farm localhost mars uranus pluto neptun connected to host localhost sent range (1,10000) to host localhost connected to host mars sent range (10001,20000) to host mars connected to host uranus sent range (20001,30000) to host uranus connected to host pluto sent range (30001,40000) to host pluto No Server on host neptun: ignored got reply = 1230 from host localhost after l sec sent range (40001,50000) to host localhost got reply = 983 from host uranus after 2 sec sent range (50001,60000) to host uranus got reply = 958 from host pluto after 2 sec sent range (60001,70000) to host pluto got reply = 1033 from host mars after 3 sec sent range (70001,80000) to host mars got reply = 924 from host uranus after 4 sec sent range (80001,90000) to host uranus...und so weiter...
69 69 got reply = 720 from host uranus after 138 sec sent range (980001,990000) to host uranus got reply = 711 from host pluto after 140 sec sent range (990001, ) to host pluto got reply = 710 from host uranus after 144 sec got reply = 732 from host localhost after 146 sec got reply = 721 from host pluto after 146 sec got reply = 717 from host mars after 150 sec answer is Die Arbeitsweise des Prozessorparks ist im oberen Dialog klar ersichtlich. Jeder Host hat eine unterschiedliche Anzahl Arbeitspakete verarbeitet, auch wenn dies aus dem gezeigten nicht offensichtlich ist (das meiste wurde weggelassen): Maschine verarbeitete Pakete localhost 22 mars 16 uranus 31 pluto 31 Da alle Maschinen bei diesem Testlauf nicht noch anderweitig ausgelastet waren, geben die Zahlen einen Hinweis auf die relative Leistung der vier benutzten Maschinen. 8.6 Starten der externen Server Bei unseren Tests haben wir vorausgesetzt, dass die Server auf jeder Maschine bereits irgendwie gestartet wurden. Natürlich kann dafür mit einem entsprechenden Eintrag in die Anweisungsliste beim Start gesorgt werden (der Server kann auch via inetd gestartet werden). Lassen Sie uns dennoch betrachten, wie wir mit unserem Clienten noch einen Schritt weiterkommen und ihn die Server auf den externen Maschinen starten lassen, wie er sie benötigt. Als Arbeitserleichterung verändern wir den Server dahingehend, dass er automatisch einen KindProzess für die echte Arbeit erzeugt. Dazu fügen wir die Zeilen if (fork()) exit (0); close (0) close (1) close (2) an den Anfang der main() Funktion des Servers hinzu. Die erste Zeile veranlaßt den Eltern- Prozess auszusteigen und dem Kind die Arbeit zu überlassen. Die ändern Zeilen schließen den stdin, stdout- und stderr-strom des Servers. Dieser Schritt dient der Absicherung, damit die nachstehend gezeigten rsh Kommandos nicht warten, bis der KindProzess beendet hat. Natürlich schränkt das Schließen der stdout- und stderr-ströme die Möglichkeiten der Fehlermeldungen alle Aufrufe an perror() sind zum Beispiel zum Scheitern verurteilt. Durch diese Veränderung können die benötigten Server leichter per Hand gestartet werden. Wir müssen uns nicht auf jeder Maschine anmelden, sondern können stattdessen eine Folge von rsh-kommandos auf der lokalen Maschine einsetzen:
70 70 mars% rsh venus prime_server mars% rsh neptun prime_server mars% rsh uranus prime_server Diese Operationen können wir auch im Client verstecken. Eine Möglichkeit ist der Einsatz der system() -Funktion: system( rsh venus prime_server ); system( rsh neptun prime_server ); system( rsh uranus prime_server ); Dieser Aufruf lässt das bezeichnete Kommando so ausführen, als se es an einem Terminal eingegeben worden. Für den Programmieraufwand ist das herrlich einfach, der Ausführungsaufwand ist Jedoch sehr hoch. Jeder Aufruf von system() führt ein fork/exec einer Bourne Shell aus und stellt dieser Shell die Argumentenfolge zur Verfügung. fork() und exec() verbrauchen besonders bei einem umfangreichen Programm wie einer Shell signifikant viel Ressourcen. Der Einsatz von Funktionen wie rcmd() oder rexec() ist effizienter, um direkt mit dem externen Ausfuhrungsserver Kontakt aufzunehmen.
15 Transportschicht (Schicht 4)
Netzwerktechnik Aachen, den 16.06.03 Stephan Zielinski Dipl.Ing Elektrotechnik Horbacher Str. 116c 52072 Aachen Tel.: 0241 / 174173 [email protected] zielinski.isdrin.de 15 Transportschicht (Schicht
FOPT 5: Eigenständige Client-Server-Anwendungen (Programmierung verteilter Anwendungen in Java 1)
1 FOPT 5: Eigenständige Client-Server-Anwendungen (Programmierung verteilter Anwendungen in Java 1) In dieser Kurseinheit geht es um verteilte Anwendungen, bei denen wir sowohl ein Client- als auch ein
Kurzanleitung. MEYTON Aufbau einer Internetverbindung. 1 Von 11
Kurzanleitung MEYTON Aufbau einer Internetverbindung 1 Von 11 Inhaltsverzeichnis Installation eines Internetzugangs...3 Ist mein Router bereits im MEYTON Netzwerk?...3 Start des YAST Programms...4 Auswahl
Guide DynDNS und Portforwarding
Guide DynDNS und Portforwarding Allgemein Um Geräte im lokalen Netzwerk von überall aus über das Internet erreichen zu können, kommt man um die Themen Dynamik DNS (kurz DynDNS) und Portweiterleitung(auch
Leichte-Sprache-Bilder
Leichte-Sprache-Bilder Reinhild Kassing Information - So geht es 1. Bilder gucken 2. anmelden für Probe-Bilder 3. Bilder bestellen 4. Rechnung bezahlen 5. Bilder runterladen 6. neue Bilder vorschlagen
Konfiguration eines DNS-Servers
DNS-Server Grundlagen des Themas DNS sind im Kapitel Protokolle und Dienste in meinem Buch (LINUX erschienen im bhv-verlag) beschrieben. Als Beispiel dient ein Intranet mit mehreren Webservern auf verschiedenen
Tutorial - www.root13.de
Tutorial - www.root13.de Netzwerk unter Linux einrichten (SuSE 7.0 oder höher) Inhaltsverzeichnis: - Netzwerk einrichten - Apache einrichten - einfaches FTP einrichten - GRUB einrichten Seite 1 Netzwerk
Netzwerk einrichten unter Windows
Netzwerk einrichten unter Windows Schnell und einfach ein Netzwerk einrichten unter Windows. Kaum ein Rechner kommt heute mehr ohne Netzwerkverbindungen aus. In jedem Rechner den man heute kauft ist eine
Übersicht. Was ist FTP? Übertragungsmodi. Sicherheit. Öffentliche FTP-Server. FTP-Software
FTP Übersicht Was ist FTP? Übertragungsmodi Sicherheit Öffentliche FTP-Server FTP-Software Was ist FTP? Protokoll zur Dateiübertragung Auf Schicht 7 Verwendet TCP, meist Port 21, 20 1972 spezifiziert Übertragungsmodi
Anleitung zur Daten zur Datensicherung und Datenrücksicherung. Datensicherung
Anleitung zur Daten zur Datensicherung und Datenrücksicherung Datensicherung Es gibt drei Möglichkeiten der Datensicherung. Zwei davon sind in Ges eingebaut, die dritte ist eine manuelle Möglichkeit. In
Professionelle Seminare im Bereich MS-Office
Der Name BEREICH.VERSCHIEBEN() ist etwas unglücklich gewählt. Man kann mit der Funktion Bereiche zwar verschieben, man kann Bereiche aber auch verkleinern oder vergrößern. Besser wäre es, die Funktion
Im Folgenden wird die Konfiguration der DIME Tools erläutert. Dazu zählen die Dienste TFTP Server, Time Server, Syslog Daemon und BootP Server.
1. DIME Tools 1.1 Einleitung Im Folgenden wird die Konfiguration der DIME Tools erläutert. Dazu zählen die Dienste TFTP Server, Time Server, Syslog Daemon und BootP Server. 1.2 Voraussetzungen Folgende
Anleitung: Mailinglisten-Nutzung
Anleitung: Mailinglisten-Nutzung 1 Mailingliste finden Eine Übersicht der öffentlichen Mailinglisten des Rechenzentrums befindet sich auf mailman.unihildesheim.de/mailman/listinfo. Es gibt allerdings noch
Technical Note 32. 2 ewon über DSL & VPN mit einander verbinden
Technical Note 32 2 ewon über DSL & VPN mit einander verbinden TN_032_2_eWON_über_VPN_verbinden_DSL Angaben ohne Gewähr Irrtümer und Änderungen vorbehalten. 1 1 Inhaltsverzeichnis 1 Inhaltsverzeichnis...
Erklärung zum Internet-Bestellschein
Erklärung zum Internet-Bestellschein Herzlich Willkommen bei Modellbahnbau Reinhardt. Auf den nächsten Seiten wird Ihnen mit hilfreichen Bildern erklärt, wie Sie den Internet-Bestellschein ausfüllen und
Urlaubsregel in David
Urlaubsregel in David Inhaltsverzeichnis KlickDown Beitrag von Tobit...3 Präambel...3 Benachrichtigung externer Absender...3 Erstellen oder Anpassen des Anworttextes...3 Erstellen oder Anpassen der Auto-Reply-Regel...5
Lineargleichungssysteme: Additions-/ Subtraktionsverfahren
Lineargleichungssysteme: Additions-/ Subtraktionsverfahren W. Kippels 22. Februar 2014 Inhaltsverzeichnis 1 Einleitung 2 2 Lineargleichungssysteme zweiten Grades 2 3 Lineargleichungssysteme höheren als
Der Kalender im ipad
Der Kalender im ipad Wir haben im ipad, dem ipod Touch und dem iphone, sowie auf dem PC in der Cloud einen Kalender. Die App ist voreingestellt, man braucht sie nicht laden. So macht es das ipad leicht,
TCP/UDP. Transport Layer
TCP/UDP Transport Layer Lernziele 1. Wozu dient die Transportschicht? 2. Was passiert in der Transportschicht? 3. Was sind die wichtigsten Protkolle der Transportschicht? 4. Wofür wird TCP eingesetzt?
Folgende Voraussetzungen für die Konfiguration müssen erfüllt sein: - Ein Bootimage ab Version 7.4.4. - Optional einen DHCP Server.
1. Dynamic Host Configuration Protocol 1.1 Einleitung Im Folgenden wird die Konfiguration von DHCP beschrieben. Sie setzen den Bintec Router entweder als DHCP Server, DHCP Client oder als DHCP Relay Agent
Printserver und die Einrichtung von TCP/IP oder LPR Ports
Printserver und die Einrichtung von TCP/IP oder LPR Ports In der Windowswelt werden Drucker auf Printservern mit 2 Arten von Ports eingerichtet. LPR-Ports (Port 515) oder Standard TCP/IP (Port 9100, 9101,9102)
Transmission Control Protocol (TCP)
Transmission Control Protocol (TCP) Verbindungsorientiertes Protokoll, zuverlässig, paketvermittelt stream-orientiert bidirektional gehört zur Transportschicht, OSI-Layer 4 spezifiziert in RFC 793 Mobile
OP-LOG www.op-log.de
Verwendung von Microsoft SQL Server, Seite 1/18 OP-LOG www.op-log.de Anleitung: Verwendung von Microsoft SQL Server 2005 Stand Mai 2010 1 Ich-lese-keine-Anleitungen 'Verwendung von Microsoft SQL Server
Lieber SPAMRobin -Kunde!
Lieber SPAMRobin -Kunde! Wir freuen uns, dass Sie sich für SPAMRobin entschieden haben. Mit diesem Leitfaden möchten wir Ihnen die Kontoeinrichtung erleichtern und die Funktionen näher bringen. Bitte führen
Festplatte defragmentieren Internetspuren und temporäre Dateien löschen
Festplatte defragmentieren Internetspuren und temporäre Dateien löschen Wer viel mit dem Computer arbeitet kennt das Gefühl, dass die Maschine immer langsamer arbeitet und immer mehr Zeit braucht um aufzustarten.
Konfiguration VLAN's. Konfiguration VLAN's IACBOX.COM. Version 2.0.1 Deutsch 01.07.2014
Konfiguration VLAN's Version 2.0.1 Deutsch 01.07.2014 In diesem HOWTO wird die Konfiguration der VLAN's für das Surf-LAN der IAC-BOX beschrieben. Konfiguration VLAN's TITEL Inhaltsverzeichnis Inhaltsverzeichnis...
Client-Server mit Socket und API von Berkeley
Client-Server mit Socket und API von Berkeley L A TEX Projektbereich Deutsche Sprache Klasse 3F Schuljahr 2015/2016 Copyleft 3F Inhaltsverzeichnis 1 NETZWERKPROTOKOLLE 3 1.1 TCP/IP..................................................
Artikel Schnittstelle über CSV
Artikel Schnittstelle über CSV Sie können Artikeldaten aus Ihrem EDV System in das NCFOX importieren, dies geschieht durch eine CSV Schnittstelle. Dies hat mehrere Vorteile: Zeitersparnis, die Karteikarte
TESTEN SIE IHR KÖNNEN UND GEWINNEN SIE!
9 TESTEN SIE IHR KÖNNEN UND GEWINNEN SIE! An den SeniorNETclub 50+ Währinger Str. 57/7 1090 Wien Und zwar gleich in doppelter Hinsicht:!"Beantworten Sie die folgenden Fragen und vertiefen Sie damit Ihr
In diesem Tutorial lernen Sie, wie Sie einen Termin erfassen und verschiedene Einstellungen zu einem Termin vornehmen können.
Tutorial: Wie erfasse ich einen Termin? In diesem Tutorial lernen Sie, wie Sie einen Termin erfassen und verschiedene Einstellungen zu einem Termin vornehmen können. Neben den allgemeinen Angaben zu einem
Rechnernetzwerke. Rechnernetze sind Verbünde von einzelnen Computern, die Daten auf elektronischem Weg miteinander austauschen können.
Rechnernetzwerke Rechnernetze sind Verbünde von einzelnen Computern, die Daten auf elektronischem Weg miteinander austauschen können. Im Gegensatz zu klassischen Methoden des Datenaustauschs (Diskette,
Virtual Private Network
Virtual Private Network Allgemeines zu VPN-Verbindungen WLAN und VPN-TUNNEL Der VPN-Tunnel ist ein Programm, das eine sichere Verbindung zur Universität herstellt. Dabei übernimmt der eigene Rechner eine
Steganos Secure E-Mail Schritt für Schritt-Anleitung für den Gastzugang SCHRITT 1: AKTIVIERUNG IHRES GASTZUGANGS
Steganos Secure E-Mail Schritt für Schritt-Anleitung für den Gastzugang EINLEITUNG Obwohl inzwischen immer mehr PC-Nutzer wissen, dass eine E-Mail so leicht mitzulesen ist wie eine Postkarte, wird die
Adami CRM - Outlook Replikation User Dokumentation
Adami CRM - Outlook Replikation User Dokumentation Die neue Eigenschaft der Adami CRM Applikation macht den Information Austausch mit Microsoft Outlook auf vier Ebenen möglich: Kontakte, Aufgaben, Termine
FTP-Leitfaden RZ. Benutzerleitfaden
FTP-Leitfaden RZ Benutzerleitfaden Version 1.4 Stand 08.03.2012 Inhaltsverzeichnis 1 Einleitung... 3 1.1 Zeitaufwand... 3 2 Beschaffung der Software... 3 3 Installation... 3 4 Auswahl des Verbindungstyps...
Registrierung am Elterninformationssysytem: ClaXss Infoline
elektronisches ElternInformationsSystem (EIS) Klicken Sie auf das Logo oder geben Sie in Ihrem Browser folgende Adresse ein: https://kommunalersprien.schule-eltern.info/infoline/claxss Diese Anleitung
Die Dateiablage Der Weg zur Dateiablage
Die Dateiablage In Ihrem Privatbereich haben Sie die Möglichkeit, Dateien verschiedener Formate abzulegen, zu sortieren, zu archivieren und in andere Dateiablagen der Plattform zu kopieren. In den Gruppen
iphone-kontakte zu Exchange übertragen
iphone-kontakte zu Exchange übertragen Übertragen von iphone-kontakten in ein Exchange Postfach Zunächst muss das iphone an den Rechner, an dem es üblicherweise synchronisiert wird, angeschlossen werden.
Lizenzen auschecken. Was ist zu tun?
Use case Lizenzen auschecken Ihr Unternehmen hat eine Netzwerk-Commuterlizenz mit beispielsweise 4 Lizenzen. Am Freitag wollen Sie Ihren Laptop mit nach Hause nehmen, um dort am Wochenende weiter zu arbeiten.
Outlook und Outlook Express
1 von 8 24.02.2010 12:16 Outlook und Outlook Express Bevor Sie anfangen: Vergewissern Sie sich, dass Sie eine kompatible Version von Outlook haben. Outlook 97 wird nicht funktionieren, wohl aber Outlook
Hochschulrechenzentrum
#91 Version 5 Um Ihre E-Mails über den Mailserver der ZEDAT herunterzuladen oder zu versenden, können Sie das Mailprogramm Thunderbird von Mozilla verwenden. Die folgende bebilderte Anleitung demonstriert
Einrichten eines Postfachs mit Outlook Express / Outlook bis Version 2000
Folgende Anleitung beschreibt, wie Sie ein bestehendes Postfach in Outlook Express, bzw. Microsoft Outlook bis Version 2000 einrichten können. 1. Öffnen Sie im Menü die Punkte Extras und anschließend Konten
Beschreibung E-Mail Regeln z.b. Abwesenheitsmeldung und Weiterleitung
Outlook Weiterleitungen & Abwesenheitsmeldungen Seite 1 von 6 Beschreibung E-Mail Regeln z.b. Abwesenheitsmeldung und Weiterleitung Erstellt: Quelle: 3.12.09/MM \\rsiag-s3aad\install\vnc\email Weiterleitung
Folgende Einstellungen sind notwendig, damit die Kommunikation zwischen Server und Client funktioniert:
Firewall für Lexware professional konfigurieren Inhaltsverzeichnis: 1. Allgemein... 1 2. Einstellungen... 1 3. Windows XP SP2 und Windows 2003 Server SP1 Firewall...1 4. Bitdefender 9... 5 5. Norton Personal
TeamSpeak3 Einrichten
TeamSpeak3 Einrichten Version 1.0.3 24. April 2012 StreamPlus UG Es ist untersagt dieses Dokument ohne eine schriftliche Genehmigung der StreamPlus UG vollständig oder auszugsweise zu reproduzieren, vervielfältigen
Gefahren aus dem Internet 1 Grundwissen April 2010
1 Grundwissen Voraussetzungen Sie haben das Internet bereits zuhause oder an der Schule genutzt. Sie wissen, was ein Provider ist. Sie wissen, was eine URL ist. Lernziele Sie wissen, was es braucht, damit
Fernzugriff auf Kundensysteme. Bedienungsanleitung für Kunden
inquiero Fernzugriff auf Kundensysteme Bedienungsanleitung für Kunden Bahnhofstrasse 1, CH-8304 Wallisellen Tel.: +41 (0)44 205 84 00, Fax: +41 (0)44 205 84 01 E-Mail: [email protected], www.elray-group.com
Zwischenablage (Bilder, Texte,...)
Zwischenablage was ist das? Informationen über. die Bedeutung der Windows-Zwischenablage Kopieren und Einfügen mit der Zwischenablage Vermeiden von Fehlern beim Arbeiten mit der Zwischenablage Bei diesen
4. Network Interfaces Welches verwenden? 5. Anwendung : Laden einer einfachen Internetseite 6. Kapselung von Paketen
Gliederung 1. Was ist Wireshark? 2. Wie arbeitet Wireshark? 3. User Interface 4. Network Interfaces Welches verwenden? 5. Anwendung : Laden einer einfachen Internetseite 6. Kapselung von Paketen 1 1. Was
Anleitung über den Umgang mit Schildern
Anleitung über den Umgang mit Schildern -Vorwort -Wo bekommt man Schilder? -Wo und wie speichert man die Schilder? -Wie füge ich die Schilder in meinen Track ein? -Welche Bauteile kann man noch für Schilder
Das RSA-Verschlüsselungsverfahren 1 Christian Vollmer
Das RSA-Verschlüsselungsverfahren 1 Christian Vollmer Allgemein: Das RSA-Verschlüsselungsverfahren ist ein häufig benutztes Verschlüsselungsverfahren, weil es sehr sicher ist. Es gehört zu der Klasse der
Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang
sysplus.ch outlook - mail-grundlagen Seite 1/8 Outlook Mail-Grundlagen Posteingang Es gibt verschiedene Möglichkeiten, um zum Posteingang zu gelangen. Man kann links im Outlook-Fenster auf die Schaltfläche
Das Leitbild vom Verein WIR
Das Leitbild vom Verein WIR Dieses Zeichen ist ein Gütesiegel. Texte mit diesem Gütesiegel sind leicht verständlich. Leicht Lesen gibt es in drei Stufen. B1: leicht verständlich A2: noch leichter verständlich
Stellen Sie bitte den Cursor in die Spalte B2 und rufen die Funktion Sverweis auf. Es öffnet sich folgendes Dialogfenster
Es gibt in Excel unter anderem die so genannten Suchfunktionen / Matrixfunktionen Damit können Sie Werte innerhalb eines bestimmten Bereichs suchen. Als Beispiel möchte ich die Funktion Sverweis zeigen.
(Hinweis: Dieses ist eine Beispielanleitung anhand vom T-Sinus 154 Komfort, T-Sinus 154 DSL/DSL Basic (SE) ist identisch)
T-Sinus 154 DSL/DSL Basic (SE)/Komfort Portweiterleitung (Hinweis: Dieses ist eine Beispielanleitung anhand vom T-Sinus 154 Komfort, T-Sinus 154 DSL/DSL Basic (SE) ist identisch) Wenn Sie auf Ihrem PC
COMPUTER MULTIMEDIA SERVICE
Umgang mit Web-Zertifikaten Was ist ein Web-Zertifikat? Alle Webseiten, welche mit https (statt http) beginnen, benötigen zwingend ein Zertifikat, welches vom Internet-Browser eingelesen wird. Ein Web
Was ist PDF? Portable Document Format, von Adobe Systems entwickelt Multiplattformfähigkeit,
Was ist PDF? Portable Document Format, von Adobe Systems entwickelt Multiplattformfähigkeit, Wie kann ein PDF File angezeigt werden? kann mit Acrobat-Viewern angezeigt werden auf jeder Plattform!! (Unix,
ISA Server 2004 Erstellen eines neuen Netzwerkes - Von Marc Grote
Seite 1 von 10 ISA Server 2004 Erstellen eines neuen Netzwerkes - Von Marc Grote Die Informationen in diesem Artikel beziehen sich auf: Microsoft ISA Server 2004 Einleitung Microsoft ISA Server 2004 bietet
How to install freesshd
Enthaltene Funktionen - Installation - Benutzer anlegen - Verbindung testen How to install freesshd 1. Installation von freesshd - Falls noch nicht vorhanden, können Sie das Freeware Programm unter folgendem
WOT Skinsetter. Nun, erstens, was brauchen Sie für dieses Tool zu arbeiten:
WOT Skinsetter WOT Skinsetter steht für World of Tanks skinsetter (WOTS von nun an). Mit diesen Tool können Sie Skins importieren und ändern, wann immer Sie möchten auf einfache Weise. Als World of Tanks
FTP-Server einrichten mit automatischem Datenupload für SolarView@Fritzbox
FTP-Server einrichten mit automatischem Datenupload für SolarView@Fritzbox Bitte beachten: Der im folgenden beschriebene Provider "www.cwcity.de" dient lediglich als Beispiel. Cwcity.de blendet recht häufig
Anleitung zur Nutzung des SharePort Utility
Anleitung zur Nutzung des SharePort Utility Um die am USB Port des Routers angeschlossenen Geräte wie Drucker, Speicherstick oder Festplatte am Rechner zu nutzen, muss das SharePort Utility auf jedem Rechner
2. Kommunikation und Synchronisation von Prozessen 2.2 Kommunikation zwischen Prozessen
2. Kommunikation und Synchronisation von Prozessen 2.2 Kommunikation zwischen Prozessen Dienste des Internets Das Internet bietet als riesiges Rechnernetz viele Nutzungsmöglichkeiten, wie etwa das World
1 Mit einem Convision Videoserver über DSL oder ISDN Router ins Internet
1 Mit einem Convision Videoserver über DSL oder ISDN Router ins Internet Diese Anleitung zeigt wie mit einem Draytek Vigor 2600x Router eine Convision V600 über DSL oder ISDN über Internet zugreifbar wird.
Erstellen von x-y-diagrammen in OpenOffice.calc
Erstellen von x-y-diagrammen in OpenOffice.calc In dieser kleinen Anleitung geht es nur darum, aus einer bestehenden Tabelle ein x-y-diagramm zu erzeugen. D.h. es müssen in der Tabelle mindestens zwei
malistor Phone ist für Kunden mit gültigem Servicevertrag kostenlos.
malistor Phone malistor Phone ist die ideale Ergänzung zu Ihrer Malersoftware malistor. Mit malistor Phone haben Sie Ihre Adressen und Dokumente (Angebote, Aufträge, Rechnungen) aus malistor immer dabei.
Herzlich Willkommen bei der BITel!
Herzlich Willkommen bei der BITel! Damit Sie auch unterwegs mit dem iphone Ihre E-Mails abrufen können, zeigen wir Ihnen Schritt für Schritt wie Sie Ihr BITel-Postfach im iphone einrichten. Los geht's:
Virtueller Seminarordner Anleitung für die Dozentinnen und Dozenten
Virtueller Seminarordner Anleitung für die Dozentinnen und Dozenten In dem Virtuellen Seminarordner werden für die Teilnehmerinnen und Teilnehmer des Seminars alle für das Seminar wichtigen Informationen,
Professionelle Seminare im Bereich MS-Office
Serienbrief aus Outlook heraus Schritt 1 Zuerst sollten Sie die Kontakte einblenden, damit Ihnen der Seriendruck zur Verfügung steht. Schritt 2 Danach wählen Sie bitte Gerhard Grünholz 1 Schritt 3 Es öffnet
PHPNuke Quick & Dirty
PHPNuke Quick & Dirty Dieses Tutorial richtet sich an all die, die zum erstenmal an PHPNuke System aufsetzen und wirklich keine Ahnung haben wie es geht. Hier wird sehr flott, ohne grosse Umschweife dargestellt
Sich einen eigenen Blog anzulegen, ist gar nicht so schwer. Es gibt verschiedene Anbieter. www.blogger.com ist einer davon.
www.blogger.com Sich einen eigenen Blog anzulegen, ist gar nicht so schwer. Es gibt verschiedene Anbieter. www.blogger.com ist einer davon. Sie müssen sich dort nur ein Konto anlegen. Dafür gehen Sie auf
Windows 10 > Fragen über Fragen
www.computeria-olten.ch Monatstreff für Menschen ab 50 Merkblatt 103 Windows 10 > Fragen über Fragen Was ist das? Muss ich dieses Upgrade machen? Was bringt mir das neue Programm? Wie / wann muss ich es
Kommunikations-Management
Tutorial: Wie kann ich E-Mails schreiben? Im vorliegenden Tutorial lernen Sie, wie Sie in myfactory E-Mails schreiben können. In myfactory können Sie jederzeit schnell und einfach E-Mails verfassen egal
Kommunikations-Management
Tutorial: Wie importiere und exportiere ich Daten zwischen myfactory und Outlook? Im vorliegenden Tutorial lernen Sie, wie Sie in myfactory Daten aus Outlook importieren Daten aus myfactory nach Outlook
Erstellen einer PostScript-Datei unter Windows XP
Erstellen einer PostScript-Datei unter Windows XP Sie möchten uns Ihre Druckvorlage als PostScript-Datei einreichen. Um Fehler in der Herstellung von vorneherein auszuschließen, möchten wir Sie bitten,
Hilfedatei der Oden$-Börse Stand Juni 2014
Hilfedatei der Oden$-Börse Stand Juni 2014 Inhalt 1. Einleitung... 2 2. Die Anmeldung... 2 2.1 Die Erstregistrierung... 3 2.2 Die Mitgliedsnummer anfordern... 4 3. Die Funktionen für Nutzer... 5 3.1 Arbeiten
DNS-325/-320 und FXP
DNS-325/-320 und FXP Das FXP-Protokoll (File exchange Protocol) erlaubt dem DNS-320/-325 Daten über FTP direkt zu einem anderen FTP-Server zu übertragen. Dabei muss der Datenstrom keinen Client passieren.
IRF2000 Application Note Lösung von IP-Adresskonflikten bei zwei identischen Netzwerken
Version 2.0 1 Original-Application Note ads-tec GmbH IRF2000 Application Note Lösung von IP-Adresskonflikten bei zwei identischen Netzwerken Stand: 27.10.2014 ads-tec GmbH 2014 IRF2000 2 Inhaltsverzeichnis
Einrichtung des Cisco VPN Clients (IPSEC) in Windows7
Einrichtung des Cisco VPN Clients (IPSEC) in Windows7 Diese Verbindung muss einmalig eingerichtet werden und wird benötigt, um den Zugriff vom privaten Rechner oder der Workstation im Home Office über
Proxy. Krishna Tateneni Übersetzer: Stefan Winter
Krishna Tateneni Übersetzer: Stefan Winter 2 Inhaltsverzeichnis 1 Proxy-Server 4 1.1 Einführung.......................................... 4 1.2 Benutzung.......................................... 4 3 1
Alle gehören dazu. Vorwort
Alle gehören dazu Alle sollen zusammen Sport machen können. In diesem Text steht: Wie wir dafür sorgen wollen. Wir sind: Der Deutsche Olympische Sport-Bund und die Deutsche Sport-Jugend. Zu uns gehören
Grundlagen der Theoretischen Informatik, SoSe 2008
1. Aufgabenblatt zur Vorlesung Grundlagen der Theoretischen Informatik, SoSe 2008 (Dr. Frank Hoffmann) Lösung von Manuel Jain und Benjamin Bortfeldt Aufgabe 2 Zustandsdiagramme (6 Punkte, wird korrigiert)
Aufklappelemente anlegen
Aufklappelemente anlegen Dieses Dokument beschreibt die grundsätzliche Erstellung der Aufklappelemente in der mittleren und rechten Spalte. Login Melden Sie sich an der jeweiligen Website an, in dem Sie
MWSoko Erste Schritte
Internetadresse und Einloggen Um die Intranetplattform der Qualitätsgemeinschaft DRK zu erreichen, müssen Sie folgende Internetadresse in die Adresszeile Ihres Browsers eingeben: http://drksaarland.de/
Enigmail Konfiguration
Enigmail Konfiguration 11.06.2006 [email protected] Enigmail ist in der Grundkonfiguration so eingestellt, dass alles funktioniert ohne weitere Einstellungen vornehmen zu müssen. Für alle, die es
Informationen zum Ambulant Betreuten Wohnen in leichter Sprache
Informationen zum Ambulant Betreuten Wohnen in leichter Sprache Arbeiterwohlfahrt Kreisverband Siegen - Wittgenstein/ Olpe 1 Diese Information hat geschrieben: Arbeiterwohlfahrt Stephanie Schür Koblenzer
Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen?
Anleitung für Evident Seite 1 Anleitung für Evident-Anwender: Einbinden der MIZ-Dokumente in Evident. Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen? Zunächst müssen Sie entscheiden,
Lassen Sie sich dieses sensationelle Projekt Schritt für Schritt erklären:
Lassen Sie sich dieses sensationelle Projekt Schritt für Schritt erklären: Gold Line International Ltd. Seite 1 STELLEN SIE SICH VOR: Jeder Mensch auf der Erde gibt Ihnen 1,- Dollar Das wäre nicht schwer
Mit der Maus im Menü links auf den Menüpunkt 'Seiten' gehen und auf 'Erstellen klicken.
Seite erstellen Mit der Maus im Menü links auf den Menüpunkt 'Seiten' gehen und auf 'Erstellen klicken. Es öffnet sich die Eingabe Seite um eine neue Seite zu erstellen. Seiten Titel festlegen Den neuen
EasyWk DAS Schwimmwettkampfprogramm
EasyWk DAS Schwimmwettkampfprogramm Arbeiten mit OMEGA ARES 21 EasyWk - DAS Schwimmwettkampfprogramm 1 Einleitung Diese Präsentation dient zur Darstellung der Zusammenarbeit zwischen EasyWk und der Zeitmessanlage
Software zur Anbindung Ihrer Maschinen über Wireless- (GPRS/EDGE) und Breitbandanbindungen (DSL, LAN)
Software zur Anbindung Ihrer Maschinen über Wireless- (GPRS/EDGE) und Breitbandanbindungen (DSL, LAN) Definition Was ist Talk2M? Talk2M ist eine kostenlose Software welche eine Verbindung zu Ihren Anlagen
mysql - Clients MySQL - Abfragen eine serverbasierenden Datenbank
mysql - Clients MySQL - Abfragen eine serverbasierenden Datenbank In den ersten beiden Abschnitten (rbanken1.pdf und rbanken2.pdf) haben wir uns mit am Ende mysql beschäftigt und kennengelernt, wie man
So die eigene WEB-Seite von Pinterest verifizieren lassen!
So die eigene WEB-Seite von Pinterest verifizieren lassen! Quelle: www.rohinie.eu Die eigene Seite auf Pinterest verifizieren Es ist offiziell. Vielleicht haben auch Sie in den vergangenen Wochen die Informationen
Handbuch Fischertechnik-Einzelteiltabelle V3.7.3
Handbuch Fischertechnik-Einzelteiltabelle V3.7.3 von Markus Mack Stand: Samstag, 17. April 2004 Inhaltsverzeichnis 1. Systemvorraussetzungen...3 2. Installation und Start...3 3. Anpassen der Tabelle...3
Statuten in leichter Sprache
Statuten in leichter Sprache Zweck vom Verein Artikel 1: Zivil-Gesetz-Buch Es gibt einen Verein der selbstbestimmung.ch heisst. Der Verein ist so aufgebaut, wie es im Zivil-Gesetz-Buch steht. Im Zivil-Gesetz-Buch
DFÜ-Netzwerk öffnen Neue Verbindung herstellen Rufnummer einstellen bundesweit gültige Zugangsnummer Benutzererkennung und Passwort
Windows 95/98/ME DFÜ-Netzwerk öffnen So einfach richten Sie 01052surfen manuell auf Ihrem PC oder Notebook ein, wenn Sie Windows 95/98/ME verwenden. Auf Ihrem Desktop befindet sich das Symbol "Arbeitsplatz".
Startmenü So einfach richten Sie 010090 surfen manuell auf Ihrem PC oder Notebook ein, wenn Sie Windows XP verwenden.
Windows XP Startmenü So einfach richten Sie 010090 surfen manuell auf Ihrem PC oder Notebook ein, wenn Sie Windows XP verwenden. Gehen Sie auf Start und im Startmenu auf "Verbinden mit" und danach auf
