Kapitel 5 Interprozesskommunikation PDF
Document Details
Uploaded by FragrantHolly8866
Technische Universität München
Tags
Summary
This document provides an overview of interprocess communication (IPC) methods. It discusses the different types of communication, such as signals, shared memory, and message passing, and how they can be used in computer systems. The article also covers the concept of bandwidth and different communication strategies.
Full Transcript
Kapitel 5 Interprozesskommunikation Ziel: Weitere Mittel zur kontrollierten Interaktion zwischen Prozessen: Wie kön- nen Daten zwischen Prozessen auch dem gleichen oder einem anderen Rechensy- stem ausgetauscht werden? Muss natürlich auch synchronisiert werden. Die Kommunikation zwischen Prozessen...
Kapitel 5 Interprozesskommunikation Ziel: Weitere Mittel zur kontrollierten Interaktion zwischen Prozessen: Wie kön- nen Daten zwischen Prozessen auch dem gleichen oder einem anderen Rechensy- stem ausgetauscht werden? Muss natürlich auch synchronisiert werden. Die Kommunikation zwischen Prozessen/Threads kann unterschiedliche Formen annehmen: Es kann sein, dass nur signalisiert werden soll, in welchen Zustand sich beispielsweise ein Mutex (1Bit) befindet, es kann aber auch sein, dass eine Datei mit mehreren Gigabyte aus dem Internet heruntergeladen werden soll. Arten der Kommunikation: - Ereignisse - Gemeinsamer Speicher - Nachrichten - Datenströme Um diese vielfältigen Anforderungen so effizient wie möglich umzusetzen, kann die gewünschte Kommunikationsstrategie anhand einiger Parameter modelliert werden. Die Verbindung zwischen einem Sender und einem Empfänger wird (Kommuni- kations) Kanal genannt. Einteilung nach Eigenschaften des Kanals: - bandbreite - im explizit - synchronität - nachrichten strom - meldung auftrag Diese Eigenschaften werden in den Abschnitten (1 und 2) detaillierter behandelt. Rendez-Vous-Problem: Adressierung von Prozessen und geeignete Netzarchitek- turen im Abschnitt (3) behandelt. 111 Im Laufe der gennanten Abschnitte werden konkrete Mechanismen zur Kommu- nikation behandelt: - signale - shared memory - synchronisation mittels message passing - pipes - ports und sockets (Netzprogrammierung) - dbus Zuletzt kommt im Abschnitt 4 ein Beispiel für die Netzprogrammierung mit Ports und Sockets, ein einfacher Server und Client. 5.1 Bandbreite der Kommunikation 5.1.1 Breit- und schmalbandige Kommunikation Eines der wichtigsten Merkmale einer Kommunikationsstrategie ist die Menge an Information, die übertragen wird. Diese sogenannte Bandbreite des Kommuni- kationskanales kann schmalbandig oder breitbandig sein. Breitbandige Kanäle ermöglichen das Übertragen von kleinen, aber auch von sehr großen Datenmengen. Dementsprechend gibt es mehr Möglichkeiten, eine breit- bandige Kommunikation aufzubauen. Häufig wird jedoch eine zusätzliche, schmal- bandige Kommunikation, Signale, benötigt, um die Rahmenbedingungen für die breitbandige Kommunikation zu ermöglichen. Damit werden vor allem die Zu- griffe der Teilnehmer auf gemeinsame Kommunikationsmittel synchronisiert. Die Nachrichten können anhand von zwei verschiedenen Ansätzen übertragen wer- den: implizit oder explizit. Erklärung, was ist was (aus Folie 7 und 9). Einteilung von breitbandig in implizit und explizit. Notieren, wo was behandelt wird, Überblick. Begriffsherkunft: Frequenzband - bei der Kommunikation mit unterschiedlichen Frequenzen (vgl. Radio) entstehen im Frequenzbereich (parallele) Kanäle, die un- terschiedlich breit sind. Umso breiter der Kanal, umso mehr Information kann übertragen werden. Man spricht dort von der Bandbreite des Kanals. 112 5.1.2 Beispielmechanismus: Signale Schmalbandige Kanäle, die lediglich wenige Bits an Information überragen, mel- den einem Prozess ein bestimmtes Ereignis durch Senden eines einfachen Signa- les. Der sendende Prozess kann direkt weiter rechnen, er muss nicht auf eine Emp- fangsbestätigung warten. Signale sind entsprechend eine asynchrone Kommuni- kationsform. Ein «empfangender» Prozess muss jedes ihm geschickte Signal aktiv abfangen1. Abfangen heißt in diesem Fall, dass das Programm das Signal registrieren können muss. Ein abgefangenes Signal kann entweder ignoriert werden, oder durch einen Signalhandler eine Aktion auslösen. Die Funktion eines Signalhandlers wird an der entsprechenden C-Funktion ersichtlich: Ein Signalhandler kann mit der Methode signal(signalName, handlerMethode), registriert werden. Die als zweiter Aufrufparameter übergebene Handler-Methode wird aufgerufen, sobald dem Prozess das Signal signalName geschickt wurde. Prozesse können wiederum Signale zum Beispiel durch Aufrufen der Methode kill(), an andere Prozesse senden. Da nur wenige Bits gesendet werden, ist die Menge an möglichen Signalen be- grenzt. Die Programmierschnittstelle Posix definiert beispielsweise 31 Signale, un- ter anderem SIGINT(Interrupt-Signal Wird ausgelöst, wenn ein Programm mit strg + c abgebrochen wird), SIGKILL(Programmprozess sofort beenden, Möglich- keit des Betriebssystem einen Fehlerhaften Prozess zu entsorgen) und SIGILL(signalisiert eine ungültige Instruktion). Weitere Signale sind häufig System-abhängig. Unter Linux wird der Programmablauf beim Auftreten eines Signales asynchron2 unterbrochen. Das bedeutet, dass ein Programm zu jedem Zeitpunkt unterbro- chen werden kann. Entsprechend muss die Handler-Methode in allen möglichen Programmzuständen fehlerfrei ablaufen können. 1 Ausnahme: SIGKILL und SIGSTOP -> Betreffen den Prozess, werden jedoch vom Betriebssy- stem bearbeitet 2 Achtung: Asynchron bezieht sich in diesem Fall nicht auf den Kopplungsgrad zwischen Sen- der und Empfänger. 113 Beispielcode (Quellcode mit Erklärung) 5.1.3 Implizit breitbandige Kommunikation Implizite Kommunikation setzt ein gemeinsames Betriebsmittel (z.B. gemeinsam nutzbarer Speicherbereich) voraus, auf das alle Kommunikationsteilnehmer zu- greifen können. Durch Schreiben und Lesen von Daten aus und in dieses Betriebs- mittel erfolgt die Kommunikation. Dieser gemeinsame Speicher, auf den beide Prozesse zugreifen können liegt in keinem der beiden Adressräume sondern wird passiv vom Kommunikationsme- dium gestellt. Da die Adressräume der Teilnehmer somit nicht angepasst werden müssen und das Betriebssystem nicht in die Kommunikation involviert ist, kann die notwendige Struktur schnell initialisiert werden. Die Daten müssen außerdem nur einmal in dem Betriebsmittel gespeichert werden und nicht, wie bei anderen Kommunikationsformen, jeweils in beiden Adressräumen, ein zusätzliches Ko- pieren der Daten ist folglich nicht nötig. Nachteilig ist, dass ein empfangender Teilnehmer aktiv Warten (busy-waiting)muss, wenn das angefragte Betriebsmittel noch nicht beschrieben wurde oder voll ist. Es wird nicht signalisiert, ob momentan Daten in dem Betriebsmittel gespeichert sind. Dieser Ansatz eignet sich besser für die Kommunikation innerhalb einer Hardware- Einheit, da der notwendige gemeinsame Speicherbereich die Kommunikation über Hardwaregrenzen hinweg erschwert. Als gemeinsames Betriebsmittel bieten sich Komponenten eines Rechners an, auf die jeder Prozess gleichermaßen Zugriff hat: Register, Dateien (vgl. Kapitel 7), Ringpuffer, Queue-Datenstrukturen und der Speicher im Allgemeinen (Nur mit Unterstützung durch das Betriebssystem). Für eine tatsächliche Implementierung ist eine (schmalbandige) Synchronisation nötig. Die Kommunikation an sich muss jedoch nicht zwingend synchron verlaufen. 114 5.1.4 Beispielmechanismus: Shared Memory (Fehlt) 5.2 Explizit breitbandige Kommunikation Bei expliziten Kommunikationsstrategien sendet ein Teilnehmer eine Nachricht, die der Empfänger aktiv empfangen muss. Diese Art der Kommunikation ist nicht nur innerhalb eines Rechners möglich, sondern auch über die Hardware- grenzen hinweg. Der genaue Aufbau einer Nachricht ist allgemein nicht vorgegeben, jedoch, je nach Anwendung, durch allgemeine Standards spezialisiert. Eine Nachricht muss immer einen Header enthalten, der sämtliche Informationen für Übertragung der Daten enthält (z.B. Sende-Adresse, Empfangs-Adresse, Größe,... ). Daraufhin folgt ein Payload, der, die zu verschickenden Informationen, enthält. Der genaue Kommunikationsvorgang ist im Allgemeinen nicht spezifiziert, ba- siert jedoch immer auf einer Methode zum Senden von Daten (send()) und einer Methode zum Empfangen von Daten (recv()). Ablauf der Kommunikation 1. Der Empfangende Prozess muss sich bei dem sendenden Prozess registrie- ren bevor die erste Nachricht versendet/empfangen werden kann. 2. Fortan kann ein Prozess mit einer send()-Methode Daten übermitteln. 3. Der jeweilige Nachrichtendienst übermittelt gesendete Daten. 4. Der empfangende Prozess muss die recv()-Methode aufrufen bevor gesen- dete Daten empfangen werden können. Je nach Implementierung werden empfangene Daten bis zum nächsten recv()-Aufruf gepuffert oder gehen verloren. Jeder Aufruf von recv()-kann maximal eine Nachricht entgegen- nehmen, danach muss die Methode erneut aufgerufen werden. Die genaue Umsetzung und das Verhalten der einzelnen Schritte hängt stark von der jeweiligen Implementierung ab und muss spezifiziert werden. (Überleitung zu Kopplungsgrad und Muster, dadurch kann es eingeteilt werden) 115 Kopplungsgrad Der Kopplungsgrad beschreibt, wie stark die Kommunikationspartner voneinan- der abhängig sind: Synchron: Sender und Empfänger sind stark aneinander gekoppelt. Nach dem Senden wartet der Prozess, bis er eine «Empfangsbestätigung» von dem Empfänger erhält, der Sender blockiert. Erst danach kann er weiter rechnen. Entsprechend ist kein paralleles Rechnen des Senders während der Kommunikation möglich. Asynchron: Bei einer asynchronen Kommunikation werden die Daten le- diglich losgeschickt, der Sender kann direkt weiter rechnen und blockiert nicht, bis er eine «Empfangsbestätigung» erhält. Die Teilnehmer sind in die- sem Fall schwach aneinander gekoppelt. Dadurch, dass die Definitionen bisher sehr allgemein gehalten werden ist die ex- plizite Kommunikation sowohl in synchronen als auch asynchronen Varianten realisierbar. Muster Bisher wurden die übermittelten Daten/Nachrichten nicht näher definiert, doch auch hier gibt es zwei unterschiedliche Prinzipien: Eine Meldung ist eine einfache Nachricht, die dem Empfänger zum Bei- spiel einen bestimmten Zustand oder auch ein bestimmtes Signal signalisie- ren. Eine Meldung ist nicht das Selbe wie Signal. Das schmalbandige Signal, muss immer abgefangen werden und enthält kleinere Datenmengen. Ein Auftrag beinhaltet eine Anweisung, die der Empfänger ausführen soll. Nachdem diese ausgeführt wurde, schickt der Empfänger das Resultat zu- rück an den Sender. Zwei Prozesse oder Threads kommunizieren miteinander, indem sie, auf unter- schiedlichste Weisen, Daten austauschen. In welche Richtung gesendet wird be- schreibt das Muster der Kommunikation. Dabei kann ein Prozess entweder der designierte Sender und der Andere der Empfänger sein (unidirektionale Kom- munikation), es können aber auch beide gleichzeitig sowohl Sender als auch Emp- fänger sein (bidirektionale Kommunikation). 116 5.2.1 Kommunikationsformen Daraus können vier verschiedene Kommunikationsprinzipien zusammengestellt werden: Im Folgenden sind Empfänger E und Sender S beides Prozesse, die blockiert sind, wenn die Lebenslinie durch einen Balken überdeckt wird. Die Datenüber- tragung verläuft anhand der Pfeile, wobei ein durchgezogener Pfeil Daten enthält und ein gestrichelter Pfeil lediglich eine Empfangsbestätigung darstellt. Asynchrone Meldung Der Sender schickt die Nachricht durch die send()-Methode, die daraufhin von dem Nachrichtendienst an dem Empfänger gesendet wird. Die Übertragung im Nachrichtendienst geschieht nicht sofort, sondern benötigt Zeit3 und wird des- halb nicht durch eine waagrechte Linie dargestellt. Der Empfänger ruft zu einem bestimmten Zeitpunkt die recv()-Methode auf, die aktiv ist, bis eine Nachricht empfangen wurde. In dieser Zeitspanne ist er blockiert. Sender S Nachrichtendienst Empfänger E send() Meldung recv() Zeit Abbildung 5.1: Asynchrone Meldung 117 Sender S Nachrichtendienst Empfänger E send() Meldung recv() Quittung Zeit Abbildung 5.2: Synchrone Meldung Synchrone Meldung Bei der synchronen Meldung wird der Sender direkt nach der send()-Methode in den empfangsbereiten Zustand versetzt, alle anderen Aktivitäten blockieren. Der Empfänger blockiert nach Aufruf der recv()-Methode und wartet passiv auf die Meldung. Sobald er diese erhalten hat kann er weiter rechnen. Sofort nach Empfangen der Nachricht wird eine Empfangsbestätigung an den Sender über- mittelt, die keinerlei Daten enthält. Der Sender empfängt die Empfangsbestäti- gung und kann weiter rechnen. Eine synchrone Meldung wird auch Rendezvous-Verfahren genannt, da die bei- den Prozesse sich gegenseitig aus dem blockierten Zustand «abholen». Asynchroner Auftrag Das Prinzip gleicht dem der asynchronen Meldung, mit dem Zusatz, dass der Kommunikationsprozess nicht abgeschlossen ist, sobald der Empfänger die Nach- richt erhalten hat. Der Auftrag muss daraufhin bearbeitet werden und das ent- standene Resultat an den Sender zurück senden werden. Der Sender muss um 3 Sowohl Verwaltungsaufwand durch das Betriebssystem, als auch physischer Datenweg (An- merkung: circa 5cm Strecke können in Kupferdraht pro Taktzeit zurückgelegt werden). 118 Sender S Nachrichtendienst Empfänger E send() Meldung recv() Auftrags- recv() bearbeitung Resultat Resultat Zeit Abbildung 5.3: Asynchroner Auftrag das Resultat zu empfangen seinerseits eine recv()-Funktion aufrufen, die selbi- gen blockiert, bis eine Nachricht empfangen wird. Zwischen dem ursprünglichen send()-Aufruf bleibt somit Zeit, in der der Sender weitere Nachrichten versenden kann. Synchroner Auftrag Das Prinzip arbeitet analog zu der synchronen Meldung mit dem Unterschied, dass das Resultat erst nach einer Bearbeitungszeit, nicht sofort, geschickt wer- den kann und Daten enthält. Der Sender blockiert je nach zu Auftrag sehr lang. Es besteht die Gefahr, dass ein Sender endlos blockiert, wenn bei der Auftrags- bearbeitung oder dem Sendevorgang ein Fehler auftritt. Für diesen Fehler-Fall ist Fehlerbehandlung mittels Abbruchbedingung in der send()-Methode notwendig. Dieses Prinzip des asynchronen Auftrages wird beispielsweise bei Webseiten ver- wendet, bei denen ein Sender solange blockiert, bis alle angefragten Daten voll- ständig übertragen wurden. Sollte das zu lange dauern, kann die Übertragung nach einem Timeout abgebrochen werden. 119 Sender S Nachrichtendienst Empfänger E send() Meldung recv() Auftrags- bearbeitung Resultat Zeit Abbildung 5.4: Synchroner Auftrag Bewertung der Kommunikationsformen Die Implementierung eines asynchronen Kommunikationskanals ist komplexer, da ein sendender Prozess mehrere Nachrichten senden kann, bevor der Empfän- ger anfängt die Nachrichten zu bearbeiten. Alle eingehenden Nachrichten muss das Betriebssystem solange puffern, bis sie aktiv verarbeitet werden. Beim syn- chronisierten Senden hingegen liegt dem Empfänger maximal eine Nachricht, auf einmal, pro Sender vor. In der Realität wird vor allem das asynchrone Senden verwendet, da bei einem Verlust während der Datenübertragung (sowohl beim Senden der ursprünglichen Daten als auch der Bestätigung) der sendende Prozess nicht dauerhaft blockiert. Ergänzen: Threads für Mischung synchron/asynchron + Beispiel Folie 21 5.2.2 Synchronisation mittels Message Passing Message Passing beschreibt eine abstrakte, breitbandige und asynchrone Kom- munikationsstrategie, bei der ein eigenständiges Kommunikationssystem verwen- det wird. Dieses Kommunikationssystem stellt die zwei Methoden: send() und 120 recv() bereit, durch die ein möglicher Anwender Daten senden oder empfangen kann. Da die gesamte Kommunikation somit ausgelagert wird muss keiner der End- punkte des Kommunikationskanals Zugriffe synchronisieren. Dementsprechend muss auch keiner der Teilnehmer Semaphoren oder gemeinsame Speicherberei- che und deren Zugriffe verwalten. 5.3 Nachrichten- vs. Stromorientierung 5.3.1 Streams Streams bilden eine Abstraktion der Verbindung zwischen zwei Kommunikati- onspartnern, die durch einen endlos langen Datenstrom dargestellt wird. Da es sich bei einem Stream nur um eine Abstraktion handelt, kann dieser auf verschiedene Arten implementiert werden. Das Kommunikationssystem besteht allgemein aus einem Puffer, in den mit send() Daten beliebiger Länge gepuffert werden können. Der Puffer speichert einen logischen Bytestrom, die einzelnen Bytes sind nach ihrer Sende-Reihenfolge sortiert. Sollten Datengrenzen benötigt werden, so müssen diese selber hinzugefügt werden. Der logische Bytestrom im Puffer kann in beliebigen Bytemengen (Bytelänge bei recv()-Aufruf angeben) ausgelesen werden. Beispielsweise wird eine gesendete Folge von Chars zu der Folge von Bytes, die dann wiederum als Long ausgelesen werden könnte. Prinzip des logische Bytestrom: Sender Empfänger 1 Byte Abbildung 5.5: Byte-Puffer zwischen den beiden Kommunikationspartnern 121 send(4 Bytes); Sender A A A A Empfänger send(2 Bytes); Sender B B A A A A Empfänger Abbildung 5.6: Puffer nimmt gesendete Daten nach dem FIFO- Prinzip/Sendereihenfolge auf. recv(2 Bytes); Sender B B A A Empfänger recv(4 Bytes); Sender Empfänger Abbildung 5.7: Empfangene Daten werden vorne aus dem Puffer entnommen. Ursprüngliche Datengrenzen werden nicht berücksichtigt. Ergebnis des zweiten recv()-Aufruf enthält Elemente aus A und B. 5.3.2 Beispielmechanismus: Pipes Pipes sind eine spezielle Realisierung von Streams. Es handelt sich um einen unidirektionalen (in eine Richtung verlaufenden) Stream zwischen zwei Kom- munikationspartnern. Wie bei einer FIFO Warteschlange wird hinten eingefügt und vorne entnommen. Bevor aus einer Pipe gelesen (read) oder in eine Pipe geschrieben (write) werden kann, muss sie erst mit dem Systemaufruf pipe() erzeugt werden. Beispiele sind die sogenannten Named Pipes, bei denen eine gemeinsame Datei als Puffer dient. Ein zweites Beispiel ist der Befehl grep «Dateiname» «Dateipfad» | Aktion der Linux Kommandozeile, mit dem bestehende Dateien in eine Pipe geladen werden und die genannte Aktion auf alle Daten in der Pipe angewendet wird Aktion wc -l: bestimmt die Anzahl der Wörter in einer Datei. 122 Prozess 1 Prozess 2 Abbildung 5.8: Wie der Puffer bei einer Pipe beschaffen ist, ist für einen Anwender primär nicht relevant. In der graphischen Darstellung einer Pipe wird der innere Aufbau deshalb nicht gezeigt. 5.4 Adressierung von Prozessen Bisher wurde unbewusst angenommen, dass Sender und Empfänger eindeutig, zum Beispiel über ihre PID/Prozessnamen identifiziert werden können und so- mit als Adresse des Ziels der Kommunikation ausreichend sind. Außerdem wird angenommen, dass Prozesse, empfangene Nachrichten in einem Puffer, in ihrem eigenen Adressraum, entgegennehmen. Das Problem dabei ist, das PID und Prozessname außerhalb eines Rechners nicht eindeutig sind. Der Zielrechner ist also nicht eindeutig bestimmt. Dazu kommt, dass ein Prozess eventuell mit mehr als einem Partner auf einmal kommunizieren muss. In einem gemeinsamen Puffer würden nicht verarbeitete Daten von ver- schiedenen Sendern vermischt werden. 5.4.1 Beispielmechanismus: Ports und Sockets Ports Eine Lösung dieses Problems ist die logische Abstraktion der Kommunikations- endpunkte als Ports: Jeder Port gehört zu einem Zeitpunkt eindeutig zu höchstens einem Prozess und wird ihm für eine spezifische Kommunikation zugeordnet. Einem Prozess kön- nen mehrere Ports zeitgleich zugewiesen werden. Dadurch können Nachrichten, auch an mehrere verschiedene Systeme, gezielt über Hardwaregrenzen hinweg gesendet und empfangen werden. Damit die Daten für jeden Sender separat ver- arbeitet werden können, kann jedem Port ein eigener Thread zugeordnet werden. Ports werden beispielsweise von Internet-Protokollen verwendet. Ein Port kann dynamisch zur Laufzeit des Systemes eingerichtet und gelöscht 123 werden, er existiert somit nur wenn er aktiv verwendet wird. Je nach System gibt es unterschiedliche Möglichkeiten für die Port-Verwaltung eines Rechners: Passive Prozesse: Etwaige Prozesse stellen einen, oder mehrere Ports zur Verfügung, mit denen sich andere Prozesse verbinden können. Sie warten auf eingehende Verbindungen (TCP) oder Nachrichten (UDP): Bei einer TCP-Verbindung enthält der Port eine FIFO-Warteschlange, in der alle eingehenden Verbindungsanfragen gespeichert werden um sie nachein- ander abzuarbeiten. Diese Warteschlange ist in ihrer Größe begrenzt, sodass ein Port irgendwann keine weiteren Verbindungswünsche mehr aufnehmen kann und erst die gepufferten Anfragen bearbeiten muss. Ein Beispiel ist ein Server, der auf Anfragen von Clients wartet. Jede TCP- Verbindung benötigt einen Verbindungsaufbau zwischen den beiden Teil- nehmern, bevor mit der eigentlichen Kommunikation begonnen werde kann. Mit UDP-Verbindungen werden lediglich Nachrichten verbindungslos ver- sandt und in einem Puffer des Empfängers gesammelt. Die Nachrichten werden, mit einem erhöhten Verlustrisiko, an einen freien Port gesendet. UDP ist deutlich unzuverlässiger als TCP, da nicht garantiert werden kann, dass der Empfänger die Nachricht erhält und verarbeitet. Aktive Prozesse: Diese Prozesse initiieren die Interaktion mit einem passi- ven Prozess. Ihnen wird ein Port vom Betriebssystem zugeordnet, von dem aus ein Verbindungswunsch gesendet wird. Ein Client schickt aktiv eine An- frage an einen Server, der passiv auf eine Verbindung gewartet hat. Beide Parteien können über die Verbindung, über ihren Port, sowohl Daten sen- den als auch empfangen. Eine Kommunikationsbeziehung zweier Prozesse über das Internet, basierend auf Ports kann eindeutig über ein 5-Tupel bestehend aus: Quell-IP-Adresse Ziel-IP-Adresse Quell-Port Ziel-Port Protokoll 124 identifiziert werden. Für bestimmte Protokolle sind bestimmte Ports vorgegeben, über die kommuniziert werden muss. Das HTTP Protokoll verwendet beispiels- weise den Port 80, HTTPS benutzt hingegen Port 443 des Servers. Der Port des Clients wird durch das Protokoll nicht direkt beeinflusst. Beispiel: Ein PC verbindet sich mit einem Webserver: https://www.tum.de Die Art des Protokolls (HTTP) steht zu Beginn der URL, die in den Browser ein- gegeben wird. Diese legt den Ziel-Port fest. Die Ziel-IP-Adresse kann aus dem Namen www.tum.de ermittelt werden. Quell-IP-Adresse und Quell-Port werden durch den Client festgelegt. Das notwendige 5-Tupel ist damit komplett und Da- ten können gesendet und empfangen werden. Sockets Sockets ermöglichen eine Stream-basierte Kommunikation zwischen zwei End- punkten (Ports). Diese ist die, in der Regel bidirektionale, logische Verbindung zweier Kommunikationspartner. Abstrahiert betrachtet besteht die Verbindung aus zwei, von beiden Ports unabhängigen Streams, die jeweils Input und Output Stream für einen der beiden Kommunikationspartner sind. Die Socket Deskripto- ren sind gleichzeitig die File-Deskriptoren, der Dateien die zur Kommunikation verwendet werden. Dadurch ist es möglich mit Prozessen in anderen (Hardware-) Systemen zu kommunizieren. Bei einer Socket basierten Kommunikation ist eine komplexere Fehlerbehandlung nötig als bei einer Kommunikation aus einfachen Nachrichten, da die Verbindung, durch die vielfältigere Implementierung, leichter gestört werden kann. 5.4.2 Exkurs zu Betriebssystemressourcen Jede geöffnete Datei, jeder Socket, jede Pipe und alle Standardein-/ausgaben kön- nen eindeutig durch einen I/O-Deskriptor beschrieben werden. Sobald sie ge- nutzt/geöffnet wurde erhält sie neben den ursprünglichen Informationen der Res- source noch einen Reference-Counter. Dieser Referenz-Counter gibt an, wie oft die Ressource aktuell, sowohl innerhalb eines Prozesses, als auch über die Pro- zessgrenzen hinweg, verwendet wird. Dadurch kann festgestellt werden, ob nur noch ein Teilnehmer der Kommunikation übrig ist und eine entsprechende Feh- lerbehandlung getätigt werden müsste. 125 5.4.3 Beispielmechanismus: Pipes (Fortsetzung) Sowohl Pipes als auch Sockets ermöglichen eine Kommunikation über Prozess- grenzen hinweg. Sollte einer der beiden Kommunikationspartner wegfallen muss eine Pipe / ein Socket geschlossen werden. Um dies zu signalisieren gibt es drei verschiedene Möglichkeiten: Entweder kann EOF (End of File) als letztes Zeichen geschickt werden, das Signal SIGPIPE (nur bei Linux) wird ausgelöst, oder eine Fehlermeldung, dass der entsprechende Deskriptor nicht mehr funktioniert, wird gesendet. Bei allen drei Varianten ist eine entsprechende Fehlerbehandlung nö- tig, die dafür sorgt, dass der noch aktive Kommunikationspartner weiter rechnen kann und nicht blockiert. 5.4.4 Netzarchitekturen Client-Server Das Client-Server-Modell basiert auf synchronen Aufträgen, die von einem oder mehreren Clients an einen Server geschickt werden. Server und Client sind in der Regel unterschiedliche Systeme, wobei der Server leistungsstark ist um die syn- chronen Aufträge, die ein eher Ressourcen-armer Client sendet, zu bearbeiten. Der Server kann mit beliebig vielen, unbekannten Clients kommunizieren, ein Cli- ent kann sich jedoch nur mit genau einem, ihm bekannten Server verbinden. Die häufigste Anwendung von Client-Server-Kommunikation sind Webseiten: Ein Heimanwender ruft eine Webseite auf und schickt damit den Auftrag diese anzu- zeigen an den Server. Der Server erstellt die entsprechenden Daten und sendet sie an den Client, der wartet, bis die Kommunikation abgeschlossen ist. Der Client muss nicht besonders leistungsstark sein um die Webseite lediglich anzuzeigen. Peer-2-Peer Das Peer-to-Peer-Modell arbeitet ähnlich wie das Client-Server-Modell mit dem Unterschied, dass nicht zwischen Client und Server unterschieden wird. Beide Kommunikationspartner sind demnach gleichzeitig sowohl Client als auch Ser- ver. Dadurch gibt es keinen übergeordneten Kommunikationsteilnehmer wodurch ein sich selbst organisierendes System entsteht. Alle beteiligten Systeme steuern Ressourcen für die gemeinsame Struktur bei. 126 Bussysteme Alle Kommunikationsteilnehmer innerhalb eines Bussystems sind durch durch den Bus einheitlich miteinander verbunden, sodass nicht jeder Teilnehmer einzeln mit jedem Anderen verknüpft werden muss. Jedem Teilnehmer wird ein Name zugewiesen, über den Daten eindeutig adressiert werden können. 5.4.5 Beispielmechanismus: DBus Ein Beispiel ist der Dbus in Linux über den Signale gesendet werden können. Pro- zesse können sich für die eingehenden Nachrichten selektiv registrieren, sodass nur für diesen Prozess relevante Nachrichten empfangen werden. 5.5 Netzprogrammierung Das folgende Beispielprogramm zeigt die Implementierung eines Servers und ei- nes Clients (Client-Server-Kommunikation) in C, mit dem durch Sockets über Rechnergrenzen hinweg kommuniziert werden kann. Ein Client kann sich mit dem wartenden Server verbinden, ihm daraufhin einen String senden und erhält die Länge des Strings als Antwort. Der Client terminiert anschließend, während der Server auf eine neue Verbindung wartet. Der Code ist auf die wesentlichen Funktionen reduziert, jegliche Fehlermeldungen fehlen. Die Socket Implementie- rung ist von System zu System leicht unterschiedlich. Diese Implementierung ist für ein Linux-System konzipiert: Server 1 # include < stdio.h > 2 # include < string.h > 3 # include < sys / socket.h > 4 # include < netinet / in.h > 5 # include < errno.h > 6 # include < unistd.h > 7 8 int main () { 9 10 // socket 11 int server_fd = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ; 127 12 13 // bind 14 struct sockaddr_in server_info ; 15 memset (& server_info , 0 , sizeof ( server_info ) ) ; 16 server_info. sin_family = AF_INET ; 17 server_info. sin_addr. s_addr = INADDR_ANY ; 18 server_info. sin_port = htons (1237) ; 19 20 bind ( server_fd , ( struct sockaddr *) & server_info , sizeof ( server_info ) ) ; 21 22 // listen 23 listen ( server_fd , 1) ; 24 25 struct sockaddr_in client_info ; 26 socklen_t clen = sizeof ( client_info ) ; 27 int client_fd ; 28 char buffer ; 29 30 while (1) { 31 32 memset (& buffer , 0 , sizeof ( buffer ) ) ; 33 memset (& client_info , 0 , sizeof ( client_info ) ) ; 34 35 // accept 36 client_fd = accept ( server_fd , ( struct sockaddr *) & client_info , & clen ) ; 37 38 // recv 39 int bytes = recv ( client_fd , buffer , sizeof ( buffer ) , 0) ; 40 if ( bytes < 0) { 41 perror ( " recv () failed " ) ; 42 break ; 43 } 44 45 // send 46 sprintf ( buffer , " you sent me % d bytes. " , bytes ) ; 47 send ( client_fd , buffer , strlen ( buffer ) , 0) ; 48 49 // close 50 close ( client_fd ) ; 51 } 128 52 } Listing 5.1: Socket-Server Der Programmablauf des Servers kann in zwei Teile gegliedert werden: Initiali- sieren des Servers sowie die eigentliche Kommunikation innerhalb einer Endlos- schleife. Initialisieren des Servers Die Funktion socket() erzeugt einen neuen Socket, der mit noch keinem End- punkt verbunden ist. Die nächste Funktion bind() bindet den eben erstellten Socket an einen Port des Servers, der Socket ist ab jetzt mit einem Endpunkt ver- bunden. Die Zeilen 14 - 18 erzeugen ein Struct, welches den Verbindungsendpunkt des Servers beschreibt, an den der Socket daraufhin gebunden wird. listen() ist die letzte Methode des Initialisierungsprozesses. Ab diesem Aufruf wartet der Server auf Verbindungswünsche von Clienten an dem vorgegebenen Socket. Serverkommunikation Die eigentliche Kommunikation findet in einer Endlosschleife statt. Jeder Durch- lauf ist dabei genau eine vollständige Kommunikationsphase zwischen Client und Server. Die erste Socket relevante Funktion ist accept(), die nachdem sie aufgerufen wur- de auf einen Verbindungswunsch eines Clients wartet. Erst wenn eine Anfrage eines Clients empfangen wurde, läuft das Programm weiter. Ein von dem Client gesendeter String wird mit der recv()-Funktion in Zeile 40 empfangen. Nach- dem überprüft wurde, ob wirklich Daten, in dem String buffer stehen, wird der Antwort-String für den Client erstellt und mit send() abgeschickt. Andere Pro- grammiersprachen bieten der alternative Funktionen wie write() oder read() an, die Funktionalität bleibt aber identisch. Weitere send() und recv() Aufträge wären an dieser Stelle möglich, jedoch nur mit demselben, verbundenen Client. Anschließend wird die Kommunikation mit einem close()-Aufruf geschlossen. Der Server ist damit bereit sich mit einem neuen Client zu verbinden. Der Aus- 129 gangszustand, den der Server nach dem Initialisierungsschritt hatte, wurde wie- derhergestellt. Ein möglicher dritter Abschnitt eines Servers würde alle verwendeten File-Deskriptoren schließen und den Server geordnet Terminieren. Da die Beispielimplementierung eine Endlosschleife besitzt, würde ein solcher Abschnitt niemals ausgeführt wer- den. Außerdem werden keine Ressourcen verwendet die vor der Terminierung wieder freigegeben werden müssten. Deswegen kann ein solcher Abschnitt im Beispiel weggelassen werden. Client 1 # include < stdio.h > 2 # include < netinet / in.h > 3 # include < arpa / inet.h > 4 5 int main () { 6 7 // socket 8 9 int server_fd = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ; 10 11 // connect 12 13 struct in_addr server_ip ; 14 inet_pton ( AF_INET , " 127.0.0.1 " , & server_ip ) ; 15 16 struct sockaddr_in server_info ; 17 server_info. sin_family = AF_INET ; 18 server_info. sin_addr = server_ip ; 19 server_info. sin_port = htons (1237) ; 20 21 connect ( server_fd , ( struct sockaddr *) & server_info , sizeof ( server_info ) ) ; 22 23 // send 24 25 char buffer ; 26 printf ( " Enter text : " ) ; 27 scanf ( " %1023 s " , buffer ) ; 28 29 send ( server_fd , buffer , strlen ( buffer ) , 0) ; 130 30 31 // recv 32 33 recv ( server_fd , buffer , sizeof ( buffer ) , 0) ; 34 35 printf ( " % s \ n " , buffer ) ; 36 37 return 0; 38 } Listing 5.2: Socket-Client Der C-Code des Clients ist einfacher und kürzer als die Implementierung des Ser- vers. Es wird erneut eine Variable für einen Socket, mit identischen Eigenschaften wie die der Serversocket-Variablen, mit der socket()-Funktion initialisiert. Der Client schickt eine Verbindungswunsch durch die connect()-Funktion an den Server. Als Aufrufparameter werden sämtliche Attribute benötigt um den Port des Servers weltweit eindeutig zu identifizieren. Die Initialisierung des Cli- ents ist damit abgeschlossen, ab jetzt können Daten mit send() und recv() ver- schickt werden. Sollte die Verbindung serverseitig beendet werden, so terminiert der Client. Die Socketvariable wird nur als Zugriffswerkzeug auf den durch den Server erstellten Socket verwendet, er muss nach der Kommunikation also nicht durch close() geschlossen werden Sequenzdiagramm der Client-Server-Interaktion Das Diagramm zeigt die Kommunikation zwischen dem oben beschriebenen Cli- ent und Server. Ergänzung: Konvertierungsfunktionen Unterschiedliche Systeme innerhalb der Kommunikationssequenz könne unter- schiedliche Adressformate und Speicherrepräsentationen verwenden. Die mei- sten Heimcomputer verwenden beispielsweise das Little Endian Format, Netz- werke hingegen das Big Endian Format. Während der Kommunikation muss si- chergestellt werden, dass die Informationen der Daten nicht verändert werden. Systemeigene Konvertierungsfunktionen garantieren, dass die Speicherrepräsen- tation während der gesamten Kommunikation entsprechend angepasst wird. Die Funktion htons() wandelt Daten, egal wie sie auf dem Ausgangssystem vorliegen 131 Client Server Socket erstellen socket() An einen Port binden bind() socket() Socket erstellen Auf Verbindungen warten listen() connect() Verbinden Verbindungsaufbau Informationen accept() send() Anfrage Senden der Verbindung einholen Auf Daten warten, lesen und diese recv() Auf Antwort warten, bearbeiten recv() Antwort lesen Antwort senden send() Verbindung schließen close() close() Verbindung schließen Abbildung 5.9: Sequenzdiagramm Client-Server-Kommunikation. in das 16Bit Format des Netzwerkes um. Dabei ist es egal welche Architektur und Repräsentation (Big Endian/Little Endian) das Ausgangssystem hat, da die Me- thode systemspezifisch implementiert wird. Andere Beispiele sind htonl (Rech- nerrepräsentation zu 32-Bit Netzwerkrepräsentation), ntohl (Netzwerkrepräsen- tation zu 32Bit-Rechner) und ntohs (Netzwerkrepräsentation zu 16Bit-Rechner). Entsprechende Funktionen sollten vor jedem System innerhalb der Nachrichten- 132 übertragung aufgerufen werden um zu garantieren, dass die Datenrepräsentation entsprechende angepasst wird. 133