Skriptum
Skriptum
Skriptum
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
Netzwerktechnologie 4<br />
Begleitmaterial zur Vorlesung NET 4 der FH-Studiengänge<br />
Software Engineering,<br />
Software Engineering für Business und Finanz<br />
und<br />
Software Engineering für Medizin<br />
in Hagenberg<br />
Sommersemester 2004<br />
FH-Prof. Dipl.-Ing. Dr. Gerhard Jahn<br />
email: Gerhard.Jahn@fh-hagenberg.at<br />
2. März 2004
Inhaltsverzeichnis<br />
1 TCP/IP 3<br />
1.1 Entstehung und Überblick . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
1.2 Schichtenmodell, Einordnung der Protokolle . . . . . . . . . . . . . . 4<br />
1.3 Internet Layer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
1.3.1 Aufgaben und Charakteristika . . . . . . . . . . . . . . . . . . 5<br />
1.3.2 IP-Header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
1.3.3 Fragmentierung . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />
1.3.4 IP-Adressen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8<br />
1.3.5 Subnetting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
1.3.6 ICMP – Internet Control Message Protocol . . . . . . . . . . . 10<br />
1.3.7 ARP und RARP . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />
1.3.8 Classless Interdomain Routing (CIDR) . . . . . . . . . . . . . 12<br />
1.3.9 Network Adress Translation (NAT) . . . . . . . . . . . . . . . 12<br />
1.3.10 IPv6 – IP Version 6 . . . . . . . . . . . . . . . . . . . . . . . . 14<br />
1.4 Transport Layer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
1.4.1 User Datagram Protocol (UDP) . . . . . . . . . . . . . . . . . 17<br />
1.4.2 Einschub: Gesicherte Übertragung . . . . . . . . . . . . . . . . 18<br />
1.4.3 Transmission Control Protocol (TCP) . . . . . . . . . . . . . . 19<br />
1.5 Domain Name Services (DNS) . . . . . . . . . . . . . . . . . . . . . . 23<br />
1.5.1 Aufgabenstellung . . . . . . . . . . . . . . . . . . . . . . . . . 26<br />
1.5.2 Lokale Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . 26<br />
1.5.3 Verteilte Datenbank . . . . . . . . . . . . . . . . . . . . . . . 27<br />
2 Programmierschnittstellen unter UNIX 30<br />
2.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />
2.2 Socket-Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />
2.2.1 Einführung in Clients, Server, Daemons und Protokolle . . . . 31<br />
2.2.2 Adressierung von Diensten . . . . . . . . . . . . . . . . . . . . 31<br />
2.2.3 Allgemeines zum Socket Interface . . . . . . . . . . . . . . . . 32<br />
2.2.4 Verbindungsorientierte Server . . . . . . . . . . . . . . . . . . 34<br />
1
INHALTSVERZEICHNIS 2<br />
2.2.5 Parallele Server . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
2.2.6 Client zur verbindungsorientierten Kommunikation . . . . . . 39<br />
2.2.7 Client für mehrere Streams . . . . . . . . . . . . . . . . . . . . 41<br />
2.2.8 Verbindungslose Kommunikation . . . . . . . . . . . . . . . . 44<br />
2.3 Socket-Programmierung in C unter Windows . . . . . . . . . . . . . . 46<br />
2.4 Socket-Programmierung mit Java . . . . . . . . . . . . . . . . . . . . 47<br />
2.4.1 Generelles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47<br />
2.4.2 Host-Adressen . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />
2.4.3 Client zur verbindungsorientierten Kommunikation . . . . . . 48<br />
2.4.4 Server zur verbindungsorientierten Kommunikation . . . . . . 50<br />
2.4.5 Verbindungslose Kommunikation . . . . . . . . . . . . . . . . 52<br />
2.5 Remote Procedure Calls . . . . . . . . . . . . . . . . . . . . . . . . . 53<br />
2.5.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53<br />
2.5.2 XDR – Extended Data Representation . . . . . . . . . . . . . 54<br />
2.5.3 RPC-Programmierung . . . . . . . . . . . . . . . . . . . . . . 57<br />
2.5.4 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58<br />
Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Kapitel 1<br />
TCP/IP<br />
Am Anfang schuf die Arpa das Arpanet.<br />
Und das Arpanet war wüst und leer.<br />
Und es war finster in der Tiefe.<br />
Und der Geist der Arpa schwebte auf dem Netzwerk,<br />
und die Arpa sprach: ” Es werde ein Protokoll.“<br />
Und es ward ein Protokoll.<br />
Und die Arpa sah, dass es gut war.<br />
Und die Arpa sagte: ” Es seien mehr Protokolle.“<br />
Und es geschah so.<br />
Und die Arpa sagte: ” Es seien mehr Netzwerke.“<br />
Und so geschah es.<br />
[Hafner und Lyon 1996]<br />
1.1 Entstehung und Überblick<br />
Die Entstehung und Verbreitung der TCP/IP-Protokollfamilie ist eng mit UNIX<br />
verbunden: Praktisch jedes UNIX-System enthielt auch immer eine Implementierung<br />
von TCP/IP. Mittlerweile ist der TCP/IP-Protokollstack auch bei praktisch<br />
allen anderen Betriebssytemen zum Standard geworden. Auch das Internet basiert<br />
auf TCP/IP. Die ursprüngliche Entwicklung initiierte das Department of Defence<br />
(DoD), genauer die US Defense Advanced Research Projects Agency (DARPA) in<br />
den frühen 70er Jahren. Dieses Netz der DARPA hieß ARPANET. Es verband Universitäten<br />
mit Einrichtungen des DoD und Industriepartnern. Ursprünglich umfasste<br />
es nur eine kleine Anzahl von Netzwerken und Rechnern. Primär wurden damit auf<br />
Applikationsebene die (rudimentären) Dienste Telnet, FTP und EMail realisiert.<br />
Seit damals wächst dieses Netz ständig an. 1983 spaltete sich der militärische Teil in<br />
ein eigenes Netzwerk (MILNET) ab, 1990 ging das ARPANET in das heute populäre<br />
3
KAPITEL 1. TCP/IP 4<br />
Internet über.<br />
Der große Erfolg von TCP/IP liegt zum Teil an seinen grundlegenden Design-<br />
Gedanken: Das DoD war an Protokollen interessiert, die ohne zentrale Wartung zu<br />
betreiben sind. Zusätzlich soll ein solches Netzwerk fehlertolerant sein, d.h. auch<br />
bei Ausfall einzelner Verbindungen soll das gesamte Netzwerk seine Funktionen zumindest<br />
noch eingeschränkt erfüllen können. Diese Anforderungen sind typisch für<br />
Anwendungen im militärischen Bereich. Aber auch für ein an sich chaotisches Netzwerk<br />
wie das Internet – das praktisch ohne zentrale Wartung auskommt, bei dem<br />
ständig neue Teilnetze und Rechner dazukommen und auch laufend Rechner stillschweigend<br />
entfernt werden – ist TCP/IP bestens geeignet.<br />
Ein Großteil der Dokumente über TCP/IP und auch das Internet über Architektur,<br />
Protokolle und auch Geschichte liegt als Serie von Berichten den sogenannten<br />
Request for Comments (RFCs) vor. Dabei handelt es sich um eine eher lose koordinierte<br />
Sammlung, die ziemlich bunt gemischt ist. Unter anderem finden sich in den<br />
RFCs alle Festlegungen von TCP/IP wie Protokolle, vergebene Nummern, usw. Die<br />
RFCs sind numerisch indizierte Texte, welche auf vielen Servern im Internet abgelegt<br />
sind. Ein RFC hat zu Beginn Vorschlagscharakter und wird nach einer gewissen<br />
Diskussionsphase durch die Internetgemeinde zu einem verbindlichen Dokument.<br />
Änderungen oder Klarstellungen zu einem RFC werden als neuer RFC – mit einer<br />
neuen Nummer – herausgebracht. Dadurch steigt die Anzahl der RFCs ständig an,<br />
derzeit sind es knapp 3500 (Stand März 2003). Jeder Server mit RFCs einhält aus<br />
diesem Grund auch eine Liste der aktuellen RFCs mit Verweisen auf ältere RFCs<br />
zum gleichen Thema (vgl. z.B. http://www.rfc-editor.org [RFC-Editor 2001]).<br />
Dieses Dokument basiert im Wesentlichen auf den RFCs, [Tanenbaum 1998],<br />
[Comer 1995], [Halsall 1996], [Hart und Rosenberg 1995], [Douba 1995], sowie diversen<br />
WWW- Seiten.<br />
1.2 Schichtenmodell, Einordnung der Protokolle<br />
Wie jede andere Protokollfamilie, besteht TCP/IP aus hierarchisch angeordneten<br />
Schichten, die jeweils bestimmte Teilaufgaben übernehmen. Damit wird die Komplexität<br />
der gesamten Aufgabe geordnet in kleinere Problembereiche zerlegt. Verglichen<br />
zum OSI-Modell für offene Kommunikation ist das Modell von TCP/IP wesentlich<br />
einfacher: Es besitzt statt sieben Schichten lediglich vier. Zu jeder dieser Schichten<br />
gehört in der Regel mehr als ein Protokoll:<br />
• Der Application Layer ist die oberste Ebene, er entspricht im OSI-Modell<br />
den Schichten 5 bis 7. Hier sind die Applikationsprotokolle wie Telnet, SMTP<br />
(Simple Mail Transfer Protocol für EMail), FTP (File Transfer Protocol) usw.<br />
enthalten.
KAPITEL 1. TCP/IP 5<br />
• Der Transport Layer oder Host to Host Layer ist die Basis der Applikationsprogramme.<br />
Er geht von einer existierenden Verbindung zwischen den Endteilnehmern<br />
(Hosts) im Netzwerk aus. Der Host to Host Layer kümmert sich<br />
um die Zustellung zu den richtigen Prozessen innerhalb des Zielrechners. Vertreter<br />
sind hier die Protokolle UDP (User Datagram Protocol, ungesichert und<br />
verbindungslos) und TCP (Transmission Control Protocol, gesichert und verbindungsorientiert).<br />
Der Transport Layer entspricht der gleichnamigen Schicht<br />
4 des OSI-Modells.<br />
• Die Basis für den Transport Layer ist der Internet Layer. Das wichtigste<br />
Protokoll ist hier IP (Internet Protocol). Seine primäre Aufgabe ist die Übertragung<br />
von Nachrichten zwischen Endgeräten über ein Netzwerk von Routern.<br />
IP ist für die Router transparent: Router verwendet die Informationen<br />
im IP-Header. IP ist datagramm-orientiert, d.h. die Zustellung erfolgt ohne<br />
Verbindungsaufbau und wird von IP nicht garantiert. Wichtige Aufgaben sind<br />
die korrekte Adressierung, das Weiterleiten zu den richtigen Vermittlungsknoten<br />
(Routing) und ggf. die Fragmentierung und Defragmentierung der zu<br />
übertragenden Nachrichten. Weitere Protokolle sind ARP (Address Resolution<br />
Protocol) und ICMP (Internet Control Message Protocol). Im OSI-Modell<br />
entspricht der Internet Layer der Schicht 3.<br />
• Den Abschluß nach unten bildet der Network Access Layer, er entspricht<br />
nach OSI den Schichten 1 und 2. Wie z.B. Netware von Novell, setzt auch<br />
TCP/IP auf bekannten und bewährten Schicht-2-Protokollen wie Ethernet,<br />
Token Ring u. ä. auf.<br />
Die Bezeichnung TCP/IP steht für die gesamte Protokollfamilie, Namensgeber<br />
waren hier zwei der wichtigsten Protokolle.<br />
1.3 Internet Layer<br />
1.3.1 Aufgaben und Charakteristika<br />
Die Schicht 3 aus dem ISO/OSI-Modell ist für die Kommunikation zwischen Geräten<br />
zuständig, die nicht direkt miteinander verbunden sind. Der Internet Layer ist das<br />
Gegenstück dazu. Hier sind zwei Protokolle angesiedelt. Sie haben die folgenden<br />
Aufgaben:<br />
Internet Protocol (IP)<br />
• Routing der IP-Datagramme
KAPITEL 1. TCP/IP 6<br />
• Adressierung der Rechner<br />
• ggf. Fragmentierung<br />
• keine End-to-End-Sicherung zwischen Sender und Empfänger<br />
• aber: meist Punkt-zu-Punkt-Sicherung durch Schicht 2<br />
• Prüfsumme nur über Header, nicht über Daten<br />
• endliche Lebensdauer der Datagramme vermeidet Zyklen<br />
• Best Effort Zustellung<br />
Internet Control Message Protocol (ICMP)<br />
• Source Quench zur Flusskontrolle (veraltet)<br />
• Host unreachable: Problem beim Routing<br />
• Echo request / echo reply: Kommando Ping<br />
• ... (diverse Management-Aufgaben)<br />
1.3.2 IP-Header<br />
IP ist verbindungslos, d.h. vor dem Senden der eigentlichen Daten muss die Verbindung<br />
nicht aufgebaut werden. Damit ist es mit dem IPX-Prototokoll von NetWare<br />
vergleichbar.<br />
Der Header ist in Blöcke zu je 32 Bit unterteilt und besteht aus einem festen<br />
Teil mit 5 x 32 Bit und ev. weiteren Optionen. Falls notwendig wird der Platz hinter<br />
den Optionen mit Füllbytes auf ein Vielfaches von 32 Bit aufgefüllt. Die Felder des<br />
Headers:<br />
Version (4 Bit) enthält die Version des IP-Layers der abgebenden Stelle. Dieses<br />
Feld bestimmt damit die Struktur der nachfolgenden Datenfelder. Damit kann<br />
gleichzeitig mit unterschiedlichen Versionen gearbeitet werden. Derzeit ist die<br />
Version 4 in Verwendung (Codierung 0100 binär).<br />
Header Length (4 Bit) gibt die tatsächliche Länge des Header in 32-bit Worten<br />
an. Der Header kann ja wegen der Optionen auch mehr als 5 Worte zu je 32<br />
Bit enthalten.<br />
Type of service enthält Informationen über die gewünschten Übertragungswege.<br />
Diese Daten werden von Routern bei alternativen Wegen berücksichtigt. Konkret<br />
besteht Type of service aus den Einzelfeldern:<br />
Precedence (3 Bit) nimmt die Priorität (0 − 7) des Paketes auf.<br />
Low delay (1 Bit) signalisiert ein Paket, bei dem auf geringste Verzögerungszeiten<br />
zu achten ist (z.B. bei telnet).
KAPITEL 1. TCP/IP 7<br />
High throughput (1 Bit) zeigt an, daß hier auf hohen Durchsatz zu achten<br />
ist (z.B. ftp).<br />
High reliability (1 Bit) zeigt an, daß hier eine Verbindung mit hoher Zuverlässigkeit<br />
zu wählen ist.<br />
Unused (2 Bit); die letzten beiden Bit des Feldes Type of service sind ungenutzt.<br />
Total length (16 Bit) enthält die Gesamtlänge des Datagrammes inkl. Header und<br />
Nutzdaten. Die maximale Länge ist 2 16 Bytes.<br />
Identification (16 Bit) identifiziert das Datagramm eindeutig. Damit kann ein<br />
längeres Datagramm auf mehrere – der darunterliegenden Schicht 2 genehme<br />
– Fragmente zerlegt werden. Alle Fragmente haben dann den gleichen Wert in<br />
Identification.<br />
Bit flags (3 Bit) ist ein Feld mit 3 Bit, von denen nur die ersten beiden genutzt<br />
werden:<br />
Don’t fragment, D bit wird wieder von Routern benutzt: Ein gesetztes Dbit<br />
zeigt an, dass ein Router eine Alternative wählen muss, deren Schicht<br />
2 das vorliegende Datagramm als Ganzes übertragen kann.<br />
More fragments, M bit Ist für das Zusammenstellen der Fragmente in Ziel-<br />
Host wichtig: Ein gesetztes M-bit zeigt an, dass zu diesem Datagramm<br />
noch weitere Fragmente folgen.<br />
Fragment offset (13 Bit) zeigt die Lage des vorliegenden Fragmentes im gesamten<br />
Datagramm an. Der Wert von fragment offset entspricht dem Offset in Bytes<br />
vom Anfang geteilt durch 8. (Hinweis: Ein Fragment enthält immer einen Datenteil,<br />
dessen Länge durch 8 teilbar ist. Die einzige Ausnahme ist das letzte<br />
Fragment eines Datagrammes.)<br />
Time-to-live (8 Bit) definiert die maximale Zeit, die ein Datagramm für die Zustellung<br />
zum Zielrechner (Destination Host) benötigen darf. Der Wert – angegeben<br />
in Sekunden – wird vom Quellrechner (Source Host) gesetzt. Da die tatsächliche<br />
Zeit schwer zu bestimmen ist, dekrementiert jeder Router den Wert von<br />
time to live um einen bestimmten Wert – in der Regel um 1. Damit gibt dieses<br />
Feld eigentlich die Anzahl der möglichen hops zum Zielrechner an. Erreicht<br />
time to live vor seiner Ankunft im Zielrechner den Wert 0, so entfernt der<br />
aktuelle Router das Datagramm vom Netzwerk. Damit werden zirkulierende<br />
Pakete, die durch schlecht eingestellte Router entstehen können, eliminiert.<br />
Protocol (8 Bit) zeigt das höhere Protokoll – den Benutzer der IP-Schicht – an.<br />
Damit ist die Zustellung im Zielrechner zur richtigen höheren Schicht möglich.
KAPITEL 1. TCP/IP 8<br />
Konkret steht 1 für ICMP, 6 für TCP und 17 für UDP. Details dazu enthält<br />
der RFC 1700 (Assigned Numbers).<br />
Header checksum (16 Bit) ist die Prüfsumme über den Header, jedoch nicht über<br />
die Daten. Eigentlich fällt diese Aufgabe in den Bereich der Schicht 2. Da<br />
jedoch fehlerhafte Daten im Header unter Umständen beim Routing zu völlig<br />
falschen Wegen führen können, wird diese Information in IP noch zusätzlich<br />
gesichert.<br />
Source IP address, Destination IP address (jeweils 32 Bit) identifizieren<br />
Quell- und Zielrechner.<br />
Options nimmt ggf. weitere Optionen über z.B. die folgenden Themen auf:<br />
Security: Die Daten sind vertraulich zu behandeln.<br />
Source routing: Der Quellrechner bestimmt selber die zu wählende Route<br />
zum Zielrechner.<br />
1.3.3 Fragmentierung<br />
Ein IP-Datagramm kann maximal 64 kByte lang werden. Ethernet lässt z.B. aber<br />
in den meisten Varianten nur Frames mit max. 1500 Byte Payload zu. In solchen<br />
Fällen zerlegt (fragmentiert) IP ein Datagramm in mehrere Frames. Dies kann auch<br />
bei einem Router geschehen, der Pakete aus einer Verbindung mit hoher maximaler<br />
Paketgröße empfängt und in eine Verbindung mit geringerer maximaler Paketgröße<br />
weiterleitet. Prinzipiell fügen umgekehrt Router die einzelnen Fragmente nicht mehr<br />
zusammen. Dies macht lediglich der Zielrechner: Erst wenn alle Fragmente eines Datagrammes<br />
bei ihm eintreffen, gibt er das Datagramm an seinen Benutzer weiter. Bei<br />
einem Router würde dieses Defragmentieren viel Speicherbedarf und hohe Verzögerungszeiten<br />
verursachen. Die bei IP gewählte Variante bei der Fragmente erst am<br />
Ziel zusammengefügt werden, nennt man internet fragmentation. Sie hat gegenüber<br />
der intranet fragmentation den Nachteil, dass beim Übergang von Verbindungen mit<br />
kurzer maximaler Frame-Länge zu Verbindungen mit höherer Frame-Länge trotzdem<br />
weiter kurze Frames übertragen werden. Allerdings ist besonders bei ungesicherten<br />
Protokollen – wie IP – das Sammeln der Fragmente in Routern eine aufwändige<br />
Aufgabe bzw. wegen eventuell vorhandener alternativer Wege gar nicht möglich.<br />
1.3.4 IP-Adressen<br />
Jeder Host oder Router im gesamten Netzwerk (z.B. dem Internet) bekommt eine<br />
numerische IP-Adresse, die ihn eindeutig identifiziert. Maschinen, die an mehrere<br />
Netze angeschlossen sind, haben auch in jedem Netz eine eigene IP-Adresse. Diese
KAPITEL 1. TCP/IP 9<br />
Adressen sind 32 Bit breit. Damit ist die Adressierung in IP und den darüber liegenden<br />
Schichten unabhängig von den Adressen der verwendeten Schicht 2. Meist<br />
werden die einzelnen Bytes einer Adresse dezimal mit einem Punkt als Trennzeichen<br />
notiert (dotted decimal). Die Adresse besteht aus einem Teil für das Netzwerk und<br />
einem Teil, der den Rechner innerhalb des Netzwerkes identifiziert. Alle Rechner<br />
im gleichen Netzwerk – die direkt (d.h. ohne Router) miteinander kommunizieren<br />
können – führen in ihren Adressen den gleichen Wert im Netzwerkteil. Auf dieser<br />
Konvention basiert das Routing. Um Netzwerke unterschiedlicher Größe realisieren<br />
zu können, ist die Grenze zwischen Netzwerk-Nummer und Host-Nummer in<br />
den Adressen innerhalb gewisser Grenzen variabel. Primär existieren die folgenden<br />
Klassen von Adressen:<br />
Klasse A: Die Adresse beginnt mit einer binären 0, gefolgt von 7 Bit für die Kennung<br />
des Netzwerkes. Die restlichen 3 Bytes identifizieren den Rechner innerhalb<br />
des Netzwerkes. Gültige Netzwerk-Nummern gehen von 0 bis 126, der<br />
Wert 127 ist unabhängig von der aktuellen Klasse für das lokale Netzwerk<br />
reserviert.<br />
Klasse B: Die ersten beiden Bit enthalten die Kombination 10, die nächsten 14 Bit<br />
bestimmen das Netzwerk. Damit verbleiben 16 Bit für die Rechner-Nummer.<br />
Klasse C: Die Adresse beginnt mit 110, für das Netzwerk sind die nächsten 21 Bit<br />
vorgesehen. Der Rechner wird innerhalb des Netzwerkes mit dem verbleibenden<br />
Byte identifiziert.<br />
Klasse D: Dieser Bereich ist für Multicasts vorgesehen. Multicast-Adressen beginnen<br />
mit dem Bitmuster 1110, die restlichen 28 bit sind die eigentliche<br />
Multicast-Adresse.<br />
Klasse E: Adressen, die mit dem Bitmuster 11110 sind für künftige Nutzungen<br />
reserviert.<br />
Bei den Klassen A, B und C sind im hinteren Teil der Adresse – also bei den<br />
Hostnummern – zwei Werte reserviert:<br />
• Eine Adresse, mit einer Host-ID 0 bezieht sich allgemein auf das Netzwerk und<br />
nicht auf einen bestimmten Host.<br />
• Umgekehrt steht eine Adresse, bei der alle Bit der Host-ID gesetzt sind, für<br />
alle Rechner dieses Netzwerkes.<br />
Diese Konventionen schränken die Anzahl der möglichen Rechner in einem Netzwerk<br />
noch ein, so kann z.B. ein Netzwerk der Klasse C bis zu 254 Hosts enthalten.
KAPITEL 1. TCP/IP 10<br />
Nachrichtentyp Beschreibung<br />
Destination Unreachable Ein Paket kann nicht zugestellt werden, da entweder<br />
das Zielnetz oder der nächste Router nicht gefunden<br />
wurde.<br />
Timer Exeeded Das Feld Time to Live erreichte 0, das Paket läuft<br />
vermutlich im Kreis.<br />
Parameter Problem Ungültiger IP-Header<br />
Source Quench Nachricht zur Flusskontrolle (veraltet), damit soll der<br />
Empfänger der ICMP-Nachricht zu einer Drosselung<br />
veranlasst werden.<br />
Redirect Der Sender dieser ICMP-Nachricht ist der Meinung,<br />
dass ein Paket fälschlicher Weise an ihn gerichtetet<br />
wurde. Er weist damit den Sender des Paketes auf<br />
einen möglichen Fehler hin.<br />
Echo Request Anklopfen bei einem Host oder Router (ping)<br />
Echo Reply Antwort auf Echo Request<br />
Timestamp Request wie Echo Request, zusätzlich mit Zeitstempel<br />
Timestamp Reply Antwort auf Timestamp Request<br />
1.3.5 Subnetting<br />
Tabelle 1.1: Wichtige ICMP-Meldungen<br />
In manchen Fällen ist eine Unterteilung eines aufgrund der Klasse großen oder mittleren<br />
Netzwerkes in mehrere kleine Netzwerke sinnvoll. Ein Beispiel ist die Unterteilung<br />
einer größeren Organisation in Divisionen oder Abteilungen. Dann wird die<br />
Grenze zwischen Netzwerk-ID und Host-ID um ein oder mehrere Bit nach rechts<br />
verschoben. Nach außen ändert sich nichts: Die einzelnen Netzwerke treten für externe<br />
Hosts und Router als ein gemeinsames Netzwerk auf. Lediglich der Router zum<br />
Umfeld und die lokalen Hosts müssen mit einer geänderten Grenze, d.h. geänderten<br />
Subnet-Mask arbeiten.<br />
1.3.6 ICMP – Internet Control Message Protocol<br />
Neben IP existieren im Internetlayer einige andere Protokolle, die für Managementaufgaben<br />
zuständig sind. Eines davon ist ICMP, es wird von den Routern verwendet,<br />
um aussergewöhnliche Ereignisse zu melden. Auch Hosts können mit ICMP-Paketen<br />
die Erreichbarkeit anderer Hosts testen. ICMP-Nachrichten werden in IP-Frames<br />
eingepackt. Die Tabelle 1.1 zeigt einige wichtige Typen von ICMP-Nachrichten.
KAPITEL 1. TCP/IP 11<br />
1.3.7 ARP und RARP<br />
IP muss zur Datenübertragung die darunterliegende Schicht 2 (Network Access<br />
Layer in TCP/IP-Terminologie) verwenden. Die Schicht 2 führt aber eigene Adressen<br />
(die sog. MAC-Adressen od. Kartennummern) ein, die nicht mit den IP-Adressen<br />
kompatibel sind und auch keine Rückschlüsse auf das Netzwerk, in dem sich ein<br />
bestimmter Rechner befindet, zulassen.<br />
Allerdings kann der Internet-Layer durch einen Vergleich der IP-Adresse des<br />
Kommunikationspartners mit der eigenen IP-Adresse feststellen, ob er die Nachricht<br />
direkt zustellen kann oder ob er dazu einen Router benötigt. Bei der Direktzustellung<br />
muss der IP-Layer die MAC-Adresse des Ziel-Host erfahren, bei der Zustellung über<br />
einen Router ist die MAC-Adresse des Routers gefragt.<br />
Die Zuordnung zwischen MAC-Adresse und IP-Adresse eines Host wird nicht<br />
im Netzwerk zentral abgelegt, sondern mit dem Address Resolution Protocol<br />
(ARP) ermittelt. Vorraussetzung dazu ist, dass jeder Host die eigene IP-Adresse<br />
und auch die eigene MAC-Adresse kennt. Die MAC-Adressen sind nur für die Rechner<br />
im gleichen Netz interessant, sie werden nach außen nicht bekannt gegeben.<br />
Um die MAC-Adresse zu einer bekannten IP-Adresse eines Fremdrechners (im<br />
gleichen Netzwerk!) zu erfahren, setzt ein Rechner einen Schicht-2-Broadcast ab.<br />
Hier ein Szenario am Beispiel Ethernet:<br />
• ARP-Request von Rechner aaa.aaa.aaa.aaa / Kartennummer xx-xx-xx-xx-xxxx<br />
an Kartennummer ff-ff-ff-ff-ff-ff mit dem Inhalt: Ich will wissen, welche<br />
Kartennummer der Rechner bbb.bbb.bbb.bbb hat!<br />
• Alle Rechner im Netzwerk hören diese Broadcast-Nachricht, der Rechner<br />
bbb.bbb.bbb.bbb reagiert jedoch als einziger darauf:<br />
• ARP-response von Rechner bbb.bbb.bbb.bbb an aaa.aaa.aaa.aaa mit dem Inhalt:<br />
Meine Kartennummer ist yy-yy-yy-yy-yy-yy.<br />
Gleichzeitig hat so auch der Zielrechner die Kartennummer des ersten Rechners<br />
erfahren. Der Internet-Layer speichert die so erfahrenen Daten in einem Cache.<br />
Damit werden weitere Zugriffe innerhalb des Netzwerkes beschleunigt. In Unix zeigt<br />
das Kommando arp -a den aktuellen Inhalt des Cache an.<br />
Auch für den umgekehrten Weg, also die Umsetzung der Kartennummer zur<br />
IP-Adresse existiert ein Prokoll, das Reverse Address Resolution Protocol. Es<br />
wird vor allem von Discless Workstations verwendet, die von einem Startprogramm<br />
auf der Netzwerkkarte hochfahren.
KAPITEL 1. TCP/IP 12<br />
Adressbereich Kontinent<br />
194.0.0.0 - 195.255.255.255 Europa<br />
198.0.0.0 - 199.255.255.255 Nordamerika<br />
200.0.0.0 - 201.255.255.255 Mittel- und Südamerika<br />
202.0.0.0 - 203.255.255.255 Asien und Pazifischer Raum<br />
Tabelle 1.2: Regionale Trennung für Netze der Klasse C<br />
1.3.8 Classless Interdomain Routing (CIDR)<br />
Mit dem rasanten Wachsen des Internet in den letzten Jahren ist die Anzahl der<br />
Rechner und damit auch der Bedarf an IP-Adressen gestiegen. Dazu kommt noch,<br />
dass durch die starre Einteilung in die Klassen A, B und C viele Adressen vergeudet<br />
werden. Die Vergabe mehrerer Netze der Klasse C an eine einzige Organisation bläht<br />
die Routing-Tabellen auf und belastet dadurch die Router zusätzlich.<br />
Eine kurzfristige Erleichterung schafft hier das Classless Interdomain Routing<br />
(vgl. RFC 1519). Es weicht die starren Grenzen zwischen den Klassen auf und erlaubt<br />
so die Vergabe von Netzen individueller Größe. Dazu ist es notwendig, dass zu jeder<br />
Netz-ID auch die Subnetmask angegeben wird. In der Praxis findet man häufig<br />
stattdessen die Anzahl der (führenden) binären 1er in der Subnetmask. So benutzen<br />
die FH-Studiengänge in O.Ö. derzeit das Netz 193.170.124.0/22, was für die Netze<br />
193.170.124.0 bis 193.170.127.0 steht.<br />
Um die Routing-Tabellen nicht zu stark wachsen zu lassen, existiert ein Plan für<br />
die weitere Vergabe der noch freien Netze der Klasse C nach regionalen Kriterien,<br />
den Zonen (vgl. Tabelle 1.2). Damit benötigt ein Router lediglich genaue Kenntnis<br />
über die Netze in seiner Zone. Von Netzen anderer Zonen reicht die Kenntnis des<br />
Standard-Routers für diese Zone.<br />
1.3.9 Network Adress Translation (NAT)<br />
Hinweis: Dieser Abschnitt gehört inhaltlich zum Internet Layer, setzt<br />
jedoch Fachwissen über den Transport Layer von TCP/IP – vor allem<br />
die Ports und deren Rolle bei der Kommunikation – vorraus.<br />
Wenn jedem Host weltweit eine eindeutige Adresse zugewiesen wird, gehen die<br />
IP-Adressen bald aus. Neben CIDR hilft vor allem die hier beschriebene Adressumsetzung<br />
das Problem der ausgehenden IP-Adressen um einige Jahre zu verschieben.<br />
Zu jeder Klasse (A, B und C) wurden von der IANA einige IP-Adressen für die<br />
lokale bzw. private Nutzung reserviert (vgl. den RFC 1918). Es sind dies die Netze:<br />
• 10.0.0.0 - 10.255.255.255 (10/8 prefix)
KAPITEL 1. TCP/IP 13<br />
• 172.16.0.0 - 172.31.255.255 (172.16/12 prefix)<br />
• 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)<br />
Mit privater Nutzung ist gemeint, dass solche Adressen zwar lokal und ohne Absprache<br />
mit anderen Organisiationen verwendet werden können aber nicht am Internet<br />
aufscheinen dürfen. Für Netze ohne Internetanbindung sind natürlich keine weiteren<br />
Maßnahmen notwendig. Wenn ein solches Netz aber auch an das Internet angebunden<br />
ist, muss an der Schnittstelle d.h. dem Router zum Internet eine Umsetzung<br />
der Adressen erfolgen. Neben einigen anderen Vorteilen (vgl. unten) können sich<br />
so mehrere Hosts eines lokalen Netzes wesentlich weniger öffentliche IP-Adressen<br />
teilen. Für die lokale Kommunikation ist hingegen keine Umsetzung der Adressen<br />
notwendig.<br />
Prinzipiell existieren mehrere Arten der technischen Realisierung:<br />
1. Single Pool: Die NAT-Realisierung besitzt einen Pool von öffentlichen IP-<br />
Adressen, aus dem jeweils eine Adresse bei Bedarf einem lokalen Host temporär<br />
zugeordnet wird.<br />
2. Multiple Pool: Diese Variante entstand historisch aus der Vorhergehenden.<br />
Werden bei Single Pool nachträglich weitere öffentliche IP-Adressen vom Provider<br />
angefordert, so liegen diese praktisch immer in einem anderen Adressbereich.<br />
Jedem Adressbereich wird ein Pool zugeordnet.<br />
3. Port Adress Translation (PAT): Dies ist die gebräuchlichste Form. Hier<br />
verwenden mehrere lokale Hosts sehr viel weniger öffentliche IP-Adressen gemeinsam<br />
und gleichzeitig. Meist wird nur eine einzige IP-Adresse – nämlich<br />
die des Routers – benötigt. Zur Unterscheidung der gebündelten Verbindungen<br />
wird zusätzlich die verwendete Port-Nummer des lokalen Hosts herangezogen.<br />
Die NAT-Komponente verwaltet die Requests in einer Tabelle und ersetzt die<br />
Quell-Adresse und den Quell-Port des Requesters durch die eigene (externe)<br />
Adresse und einen eigenen freien Port. Der angesprochene externe Host sieht<br />
so den Router als Requester und antwortet auch diesem. Die Response wird<br />
von der NAT-Komponente anhand des Tabelleneintrages wieder manipuliert<br />
(Adressdaten des lokalen Host als Ziel-Adresse und Ziel-Port eintragen) und<br />
kann dem lokalen Host zugestellt werden. Erst wenn viele lokale Hosts mit<br />
PAT konzentriert werden, ist das Verwenden mehrerer öffentlicher IP-Adressen<br />
sinnvoll. Hier werden – aus Sicht des Client – die Quelladressen geändert,<br />
deshalb heißt dieses Verfahren auch Source NAT.<br />
Probleme bereiten manche Applikationsprotokolle, bei denen mehrere Ports<br />
gemeinsam mit einer weiteren Initiierung durch den Server vorkommen wie<br />
z.B. FTP im active mode.
KAPITEL 1. TCP/IP 14<br />
Allen drei Lösungen ist gemein, dass an den Hosts keine Änderungen notwendig<br />
sind. NAT bedarf lediglich einer Manipulation der Pakete am Router.<br />
Die Tabelleneintragungen erfolgen dynamisch. Lokal vorhandene Server sind<br />
zunächst nicht von außen erreichbar. Für sie werden zusätzlich statisch weitere<br />
öffentliche IP-Adressen in der NAT-Komponente definiert. Die Server selber behalten<br />
die private IP-Adresse. Aus Sicht des Client werden hier Zieladressen geändert,<br />
deshalb heißt dieses Verfahren auch Destination-NAT. Manchmal werden auch einzelne<br />
Ports des öffentlich zugänglichen Gateway auch bestimmte interne Server weiter<br />
geleitet. Clients erreichen diese eigentlich internen Server dann über die öffentliche<br />
Adresse des Gateway. Man spricht dann von Port Forwarding.<br />
Neben der entsprechenden Einsparung an öffentlichen IP-Adressen ist ein weiterer<br />
entscheidender Vorteil, dass Hosts ohne eine aktuelle Verbindung nach außen<br />
von außerhalb des Netzes nicht erreichbar sind. Damit sind sie natürlich vor externen<br />
Angriffen geschützt. Dies ersetzt zwar keine Firewall, zeigt aber Ansätze in<br />
Richtung Sicherheit. Auch ein gewisser Schutz vor dem Missbrauch von lokaler Seite<br />
ist vorhanden: Ohne entsprechende Konfiguration der NAT-Komponente können<br />
lokal keine öffentlich erreichbaren Server installiert werden. Öffentliche IP-Adressen<br />
werden nicht verschenkt, NAT führt hier zu Einsparungen.<br />
Ein Nachteil von NAT ist natürlich der Aufwand für die Konfiguration. Die<br />
eigentliche Umsetzung belastet den Router zusätzlich und führt zu einer – meist<br />
kaum merkbaren – zusätzlichen Verzögerung.<br />
1.3.10 IPv6 – IP Version 6<br />
Das Problem der ausgehenden IP-Adressen kann mit CIDR und NAT um einige<br />
Jahre hinausgeschoben werden, allerdings muss langfristig eine andere Lösung gefunden<br />
werden. Zusätzlich werden an das Internet ganz neue Anforderungen gestellt:<br />
Einerseits betrifft es die Sicherheit und Vertraulichkeit der übertragenen Daten –<br />
IP bietet in der derzeit verwendeten Version 4 hier praktisch nichts – andererseits<br />
werden oft nicht nur Daten im klassischen Sinn übertragen. Ein Beispiel ist Videoon-Demand.<br />
Die wesentlichen Ziele bei der Entwicklung einer neuen Version für IP<br />
waren:<br />
• Eignung für Milliarden von Hosts<br />
• möglichst kurze Routing-Tabellen<br />
• bessere Sicherheit (Authentifikation und Datenschutz)<br />
• echte Unterstützung von Dienstarten, speziell für Echtzeitanforderungen<br />
• gleitender Übergang d.h. Koexistenz mit alter Version über Jahre
KAPITEL 1. TCP/IP 15<br />
Die Arbeit an der neuen Version begann 1990, der RFC 2460 beschreibt IPv6.<br />
Hier die wichtigsten Merkmale:<br />
• Adressen sind 16 Byte lang. Damit ist die Anzahl der verwaltbaren Hosts<br />
praktisch unbegrenzt.<br />
• Der Header wurde vereinfacht. Er enthält zunächst nur 7 Felder. Das erleichtert<br />
die Arbeit für Router. Bisher zwingend vorhandene Felder sind jetzt optional<br />
und können in Erweiterungs-Headern angegeben werden.<br />
• Die Sicherheit wurde erhöht: IPv6 erlaubt die Authentifizierung und Verschlüsselung.<br />
• Die schon in der Version 4 ansatzweise vorhandenen Dienstarten wurden ausgeweitet.<br />
Jedes Datagramm beginnt mit dem Hauptheader. Er enthält die folgenden Felder:<br />
Version (4 Bit) Hier steht der Wert 6. Damit kann in der Übergangsphase gleichzeitig<br />
auch noch mit der alten Version gearbeitet werden.<br />
Traffic Class (8 Bit) Hier soll dem Paket eine bestimmte Klasse oder Priorität<br />
zugewiesen werden. Details sind noch in Ausarbeitung. Neben einer Prioritätsangabe<br />
wird auch zwischen Paketen mit Flusssteuerung und solchen ohne<br />
Flusssteuerung unterschieden. Die Übertragung mit Flusssteuerung kann<br />
und soll sich bei Überlastung verlangsamen. Bei Echtzeitübertragung (ohne<br />
Flusssteuerung) soll eine konstante Übertragungsrate eingehalten werden. Im<br />
Überlastfall dürfen dann auch Pakete verloren gehen. In diese Kategorie fallen<br />
z.B. die Echtzeitübertragung von Audio und Video.<br />
Flow Label (20 Bit) Mit diesem Feld soll den Routern eine spezielle Behandlung<br />
dieses Paketes abverlangt werden, die sich nicht im Feld Traffic Class spezifizieren<br />
läßt. Die genaue Bedeutung dieses Feldes ist noch immer nicht festgelegt.<br />
Payload Length (16 Bit) Dieses Feld gibt die Länge der diesem Header folgenden<br />
Nutzdaten an, es erfüllt damit die gleiche bzw. eine ähnliche Aufgabe wie das<br />
Feld Total Length in der Version 4.<br />
Next Header (8 Bit) Hier steht die Kennung des nächsten Headers. Damit können<br />
weitere Header mit optionalen Feldern folgen. Im letzten Erweiterungs-Header<br />
enthält dieses Feld die Kennung des Transportprotokolls dieses Paketes. Derzeit<br />
sind die folgenden Erweiterungs-Header definiert:<br />
• Optionen für Teilstrecken (Unterstützung für sehr große Datagramme<br />
> 64 kB)
KAPITEL 1. TCP/IP 16<br />
Präfix (binär) Verwendung Bruch (Anteil<br />
am Adressraum)<br />
0000 0000 reserviert (einschließlich IPv4) 1/256<br />
010 Adressen für Service Provider 1/8<br />
100 Adressen für geographische Bereiche 1/8<br />
1111 1110 10 Verbindungsspezifische lokale Adressen 1/1024<br />
1111 1110 11 Standortspezifische lokale Adressen 1/1024<br />
1111 1111 Multicast 1/256<br />
• Source Routing<br />
• Fragmentierung<br />
• Authentifikation<br />
• Verschlüsselung<br />
Tabelle 1.3: Auszug IPv6 Adressraum<br />
Hop Limit (8 Bit) Hop Limit entspricht dem Feld Time to Live.<br />
Source Address, Destination Address (jeweils 16 Byte) Hier werden Quellund<br />
Zieladresse angegeben. Der riesige Adressraum ist in Bereiche unterteilt.<br />
Die Tabelle 1.3 zeigt Teile des gesamten Adressraums.<br />
1.4 Transport Layer<br />
Der Transport oder Host-to-Host Layer verbindet Applikationen, d.h. er geht von<br />
einer existierenden Verbindung zwischen den beteiligten Stationen aus. Er enstpricht<br />
im OSI-Modell der Schicht 4 (Transport).<br />
Dazu stützt sich dieser Layer auf dem Protokoll IP aus dem Internet Layer ab.<br />
Bei IP werden die Stationen durch die IP-Adresse identifiziert. Der Host-to-Host<br />
Layer verfeinert Adressen mit ports. Sie kennzeichnen die Applikationen innerhalb<br />
der Rechner. Die Applikationen können aus zwei Protokollen mit unterschiedlichen<br />
Charakteristika auswählen:<br />
Das User Datagram Protocol (UDP) übernimmt im Wesentlichen die Eigenschaften<br />
von IP. Es arbeitet verbindungslos nach einer best-try-Philosopie. Damit<br />
wird die Übertragung nicht garantiert. Das Transmission Control Protocol<br />
(TCP) erweitert die Funktionalität von IP. Es garantiert Zustellung und richtige<br />
Reihenfolge der Nachrichten. Auch eine Flusskontrolle ist vorhanden. Damit wird<br />
verhindert, dass ein schneller Sender einen langsamen Empfänger unnötig überlastet.<br />
Zur Unterscheidung dieser beiden Protokolle wird das Feld Protocol im IP-Header<br />
verwendet (vgl. Abschnitt 1.3.2 ab Seite 6).
KAPITEL 1. TCP/IP 17<br />
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16<br />
Source port<br />
Destination port<br />
Length<br />
Checksum<br />
Abbildung 1.1: UDP-Header<br />
1.4.1 User Datagram Protocol (UDP)<br />
UDP übernimmt die Fähigkeiten von IP und zeichnet sich deshalb durch einen<br />
minimalen Overhead und einen guten Durchsatz aus. Es arbeitet verbindungslos,<br />
zwischen den einzelnen Datagrammen besteht keine Beziehung. Die Abbildung 1.1<br />
zeigt den Header von UDP. Er folgt unmittelbar nach dem IP-Header. An ihn sind<br />
die Applikationsdaten angefügt. Die Felder im Einzelnen:<br />
Source Port (16 Bit) kennzeichnet die sendende Applikation. Falls keine Antwort<br />
auf eine Nachricht erwartet wird, kann seine Angabe auch entfallen. Dann hat<br />
Source Port den Wert 0.<br />
Destination Port (16 Bit) kennzeichnet die angesprochene Applikation. Er muss<br />
in jedem Fall angegeben werden.<br />
Length (16 Bit) enthält die Anzahl der Oktetts (Bytes) im ganzen UDP-<br />
Datagramm, d.h. inkl. Header.<br />
Checksum (16 Bit) ist die Prüfsumme über Daten und Header. Sie ist notwendig,<br />
da ja IP zwar eine eigene Prüfsumme mitführt, diese jedoch nur den IP-Header<br />
abdeckt. Die Angabe der Prüfsumme kann auch entfallen, dann hat Checksum<br />
den Wert 0.<br />
Die sendende Applikation muss die Adressdaten der empfangenden Station (IP-<br />
Nummer und Port-Nummer) selbst kennen. Ihre Ermittlung ist nicht Aufgabe des<br />
Host-to-Host-Layers, dies fällt sowohl bei UDP als auch bei TCP in den Aufgabenbereich<br />
der Applikationen. Bei einer typischen Client-/Server-Applikation meldet<br />
der Server zunächst seine Dienste an einem bestimmten lokalen Port an. Der Port<br />
wird dabei von der Applikation vorgegeben. Nur so kann der Client dann später<br />
die Server-Applikation ansprechen. Für Standard-Applikationen wie Telnet, FTP,<br />
. . . sind vordefinierte Ports festgelegt, die auch Well Known Ports heißen (vgl. unter<br />
Unix die Datei /etc/services). Ein Client bekommt bei der Kontaktaufnahme mit<br />
dem Server einen eigenen lokalen Port dynamisch zugewiesen.
KAPITEL 1. TCP/IP 18<br />
1.4.2 Einschub: Gesicherte Übertragung<br />
Im Gegensatz zu UDP garantiert TCP die Übertragung d.h. fehlerhafte Pakete werden<br />
als solche erkannt und – auch bei völligem Verlust der Pakete – wiederholt. TCP<br />
verwendet dazu ein ausgeklügeltes Verfahren, wobei sich die Güte dieses Verfahrens<br />
nicht in seiner Korrektheit und Robustheit – diese werden ohnehin vorausgesetzt<br />
– sondern in seiner Effizienz im Sinne von Performanz ausdrückt. Zum besseren<br />
Verständnis von TCP werden zunächst die Basisverfahren untersucht. Es wird hier<br />
auf die entsprechende Literatur verwiesen [Halsall 1996, S. 170-200]. An dieser Stelle<br />
erfolgt lediglich eine kurze Übersicht mit Stichworten. Sie soll das Durcharbeiten der<br />
entsprechenden Seiten erleichtern.<br />
• Einführung<br />
– Einordnung in 7-Schichtenmodell<br />
– Varianten (gesichert vs. best try)<br />
• Idle-RQ [Halsall 1996, S. 170 – 174]<br />
– implicit retransmission<br />
– explicit retransmission<br />
• Continous RQ [Halsall 1996, S. 188 – 200]<br />
– Selective Repeat<br />
– Go-Back-N<br />
– Flusskontrolle<br />
• Realisierung (Schichtenarchitektur)<br />
– Primitives, Services, Event Control Blocks<br />
– Schichten und Warteschlangen zur Kommunikation zwischen den Schichten<br />
– Realisierung als single task (Hauptschleife mit Pseudo Tasks)<br />
– Remote und local Services
KAPITEL 1. TCP/IP 19<br />
1.4.3 Transmission Control Protocol (TCP)<br />
Einführung<br />
Der Einsatz von UDP ist immer dann sinnvoll, wenn eine Quittierung und die ev.<br />
notwendige Fehlerbehandlung nicht notwendig ist. Dies trifft vor allem bei einmaligen<br />
und kurzen Nachrichten zu. In den meisten Fällen wird jedoch gerade auf solche<br />
Eigenschaften besonders Wert gelegt. Auch das Einhalten der richtigen Reihenfolge<br />
ist meist wichtig. All diese Anforderungen erfüllt TCP.<br />
Es betrachtet die zu übertragenden Nachrichten als einen konsekutiven Strom<br />
von Daten (Oktetts) der mit dem Verbindungsaufbau beginnt und erst beim Verbindungsabbau<br />
endet. Genauer kann TCP zwei solcher Datenströme auf einer Verbindung<br />
gleichzeitig übertragen: TCP arbeitet bidirektional. Die Flusskontrolle verhindert<br />
das Überlasten des Empfängers. Die TCP-Komponente einer empfangenden<br />
Station kann das Senden von Nachrichten durch die abgebende Station verzögern,<br />
falls die Empfangspuffer zu stark belastet werden.<br />
TCP überträgt den Datenstrom in Einheiten (Segmenten). Dabei bestimmt ein<br />
TCP-Sender in der Regel selbständig wie groß die einzelnen Segmente sind. Der<br />
TCP-Empfänger legt die eingehenden Segmente in einem Empfangspuffer ab und<br />
gibt den Inhalt im allgemeinen erst an die Applikation weiter, wenn der Puffer voll<br />
ist. Ein Segment kann daher mehrere Nachrichten enthalten (bei kurzen Nachrichten).<br />
Andererseits kann – im Fall von langen Applikationsdaten, wie ganzen Dateien<br />
– ein Segment auch nur einen Teil der Applikationsnachricht enthalten. Der Benutzer<br />
von TCP hat jedoch noch die Möglichkeit einer Einflussnahme: Parameter an<br />
der Schnittstelle zu TCP gestatten den sofortigen Transport einer Nachricht (vgl.<br />
das push flag im TCP Header), und mit dem urgent flag (vgl. dazu den Header<br />
von TCP) kann die Nachricht auch unter Umgehen der Flusskontrolle übertragen<br />
werden.<br />
Der RFC 793 definiert TCP. Seit seiner Veröffentlichung wurden einige Fehler<br />
und Inkonsistenzen gefunden und im RFC 1122 behandelt. Der RFC 1323 definiert<br />
Erweiterungen von TCP.<br />
Header von TCP<br />
Die Abbildung 1.2 auf der nächsten Seite zeigt den Aufbau des Headers von TCP:<br />
Source, Destination Port (jeweils 16 Bit) haben die gleiche Funktion wie bei<br />
UDP, hier ist jedoch auch die Angabe des Source Port zwingend.<br />
Sequence Number (32 Bit) dient der Identifikation des Paketes. TCP betrachtet<br />
alle Daten, die im Lauf einer Verbindung übertragen werden als einen Strom,
KAPITEL 1. TCP/IP 20<br />
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16<br />
Source port<br />
Destination port<br />
Sequence Number<br />
Acknowledgement number<br />
Header Length Reserved<br />
Code bits<br />
Options (if any)<br />
Window<br />
Checksum<br />
Urgent Pointer<br />
Abbildung 1.2: TCP-Header<br />
Padding
KAPITEL 1. TCP/IP 21<br />
der durch einzelne Übertragungen in Teile zerschnitten ist. Die Sequence Number<br />
gibt die Position des ersten Oktetts der vorliegenden Nachricht innerhalb<br />
des Datenstromes an. Beim Verbindungsaufbau wählt jede Station ihre Sequence<br />
Number nach eigenem Ermessen, bei einem Überlauf beginnt Sequence<br />
Number wieder mit dem Wert 0.<br />
Acknowledgement Number (32 Bit) dient zum Bestätigen von zuvor eingelangten<br />
Paketen. Die hier sendende Station bestätigt den Erhalt aller zuvor empfangenen<br />
Oktetts bis zum Oktett mit der Nr. Acknowledgement Number −1.<br />
Das nächste erwartete Oktett hat damit den Offset Acknowledgement Number.<br />
Damit kann eine Station mit dem zum Bestätigen notwendigen Paket selber<br />
wieder Daten senden.<br />
Header Length (4 Bit) gibt die Länge des Headers in 32-Bit Worten an. Wegen<br />
der ev. vorhandenen Optionen (vgl. Feld Options) kann die Länge variieren.<br />
Im RFC 793 heißt dieses Feld Data Offset.<br />
Reserved (6 Bit) ist für künftige Erweiterungen vorgesehen und wird derzeit nicht<br />
verwendet.<br />
Code bits (6 Bit) enthält einige Flags. Die Tabelle 1.4 auf der nächsten Seite zeigt<br />
die einzelnen Positionen und deren Bedeutung.<br />
Window (16 Bit) dient der Flusskontrolle. Dieses Feld enthält die Anzahl der Oktetts,<br />
die der Sender derzeit, d. h. ab dem Wert von Acknowledgement Number<br />
aufnehmen kann.<br />
Checksum (16 Bit) enthält wie bei UDP die Prüfsumme über das komplette Paket.<br />
Urgent pointer (16 Bit) gibt die Lage der dringenden Daten im Segment an, falls<br />
das URG flag gesetzt ist.<br />
Options nimmt weitere Optionen auf, wie z. B. das Verabreden auf eine maximale<br />
Paketgröße zwischen Sender und Empfänger (der Vorschlagswert für die max.<br />
Paketgröße ist 536 Oktett).<br />
Padding füllt den Header auf ganze 32-Bit Worte auf.<br />
Verbindungsmanagement<br />
Bei Client-/Server-Anwendungen bereitet sich der Server zunächst passiv auf einen<br />
bevorstehenden Verbindungsaufbau durch einen Client vor. Damit ist die Reihenfolge<br />
der für den Aufbau notwendigen Übertragungen vorgegeben. Die Abbildung<br />
1.3 auf der nächsten Seite zeigt den Ablauf bei dieser Art des Verbindungs-
KAPITEL 1. TCP/IP 22<br />
Bit<br />
Pos. Code - Funktion<br />
11 URG - urgent pointer field valid: Die Nachricht enthält wichtige Daten.<br />
Die durch das Feld urgent pointer angegebenen Oktetts werden<br />
unter Umgehung der Flusskontrolle übertragen.<br />
12 ACK - acknowledgement field valid: Diese Nachricht bestätigt den Empfang<br />
von Daten aus der Gegenrichtung.<br />
13 PSH - push: Die Oktetts dieser Nachricht sind sofort an die Applikation<br />
weiterzugeben.<br />
14 RST - reset: Die Verbindung wird hart – d.h. ohne weitere Formalitäten<br />
beendet.<br />
15 SYN - sequence number: Diese Nachricht enthält eine Initial Sequence<br />
Number.<br />
16 FIN - end of byte stream from sender: Die Verbindung wird (zumindest)<br />
einseitig beendet. Der Kommunikationspartner kann jedoch<br />
noch Nachrichten übertragen.<br />
Tabelle 1.4: Code bits im TCP-Header<br />
Client Server<br />
✾<br />
SYN, Seq=X<br />
SYN, ACK, Seq=Y, Ack=X+1<br />
ACK, Seq=X+1, Ack=Y+1<br />
Abbildung 1.3: Sequentieller Verbindungsaufbau in TCP<br />
③<br />
③
KAPITEL 1. TCP/IP 23<br />
Station A Station B<br />
SYN, Seq=X<br />
✾<br />
ACK, Seq=X+1, Ack=Y+1<br />
SYN, Seq=Y<br />
✾ ③<br />
③<br />
ACK, Seq=Y+1, Ack=X+1<br />
Abbildung 1.4: Gleichzeitiger Verbindungsaufbau in TCP<br />
Primitive Typ Client/<br />
Server<br />
Parameter<br />
PASSIVE OPEN Request S Source port, timeout, timeout-action<br />
ACTIVE OPEN Request C Source port, destination port, destination<br />
address, timeout, timeout-action<br />
OPEN RECEIVED Indication S Local connection name, destination<br />
CLOSE Request C/S<br />
port, destination address<br />
Local connection name<br />
CLOSING Indication C/S Local connection name<br />
TERMINATE Confirm C/S Local connection name, reason code<br />
ABORT Request C/S Local connection name<br />
Tabelle 1.5: TCP-User: Service-Primitiven und ihre Parameter<br />
aufbaus. Der Client übermittelt zunächst seine eigene Sequence Number. Diese wird<br />
dann vom Server bestätigt. Mit der Bestätigung übermittelt auch der Server seine<br />
eigene Sequence Number an den Client. Auch diese wird vom Client bestätigt. Dieses<br />
Verfahren heißt three-way handshake.<br />
TCP erlaubt jedoch auch den ev. zufällig gleichzeitig erfolgenden Verbindungsaufbau<br />
zwischen gleichrangigen Applikationen. Die Abbildung 1.4 zeigt die dabei<br />
auftretenden Aktionen der beiden Stationen.<br />
Die Abbildungen 1.5 auf der nächsten Seite und 1.6 auf Seite 25 zeigen die dazugehörigen<br />
Spezifikationen für den Auf- und Abbau von Verbindungen in Form<br />
endlicher Automaten. Die Tabelle 1.5 beschreibt die in diesen Spezifikationen vorkommenden<br />
Service-Primitiven an der Schnittstelle zum TCP-User.<br />
1.5 Domain Name Services (DNS)<br />
Dieser Abschnitt basiert zum Großteil auf [Douba 1995, Kap. 6].
KAPITEL 1. TCP/IP 24<br />
ACK or TIMEOUT<br />
/ TERMINATE<br />
Wait<br />
ACK<br />
✻<br />
CLOSE /<br />
Closing<br />
✻<br />
FIN<br />
FIN / ACK,<br />
CLOSING<br />
✲<br />
Closed<br />
✻<br />
RST /<br />
Data<br />
✛<br />
Transfer<br />
PASSIVE OPEN<br />
Abbildung 1.5: Zustandsautomat TCP-Server<br />
❄<br />
Listen<br />
SYN / SYN+ACK,<br />
TERMINATE OPEN RECEIVED<br />
❄<br />
SYN<br />
Recvd<br />
ACK
KAPITEL 1. TCP/IP 25<br />
TERMINATE, ACK✲<br />
Wait<br />
FIN 2<br />
✻<br />
Wait<br />
FIN 1<br />
FIN /<br />
ACK<br />
■<br />
Timed<br />
wait<br />
✻<br />
Closing<br />
✻<br />
TIMEOUT<br />
ACK<br />
FIN / ACK,<br />
CLOSING<br />
CLOSE / FIN<br />
✲<br />
Closed<br />
✻<br />
ABORT /<br />
RST<br />
Data<br />
✛<br />
Transfer<br />
Abbildung 1.6: Zustandsautomat TCP-Client<br />
ACTIVE OPEN /<br />
SYN<br />
✎<br />
SYN<br />
Sent<br />
SYN+ACK /<br />
OPEN SUCCESS, ACK
KAPITEL 1. TCP/IP 26<br />
1.5.1 Aufgabenstellung<br />
In TCP/IP ist jedem Host zumindest eine IP-Adresse zugeordnet. Diese Adresse<br />
identifiziert den Host im ganzen Netzwerk eindeutig, sie ist auch die Basis für das<br />
Routing. Solche IP-Adressen sind kompakt zu speichern, sie sind jedoch für den<br />
Menschen schwer zu merken und wenig aussagekräftig.<br />
Mit den Domain Name Services werden den IP-Adressen sprechende Namen zugeordnet.<br />
Menschen können mit diesen Bezeichnungen besser umgehen. Zusätzlich<br />
haben sie noch einen weiteren Vorteil: Die IP-Adresse ist starr mit dem Teilnetz<br />
verbunden. Wird ein bekannter Server in ein anderes Teilnetz verlegt, so ändert sich<br />
damit auch seine IP-Adresse. Trotzdem kann er auch im neuen LAN – zumindest<br />
nach einer Aktualisierung der DNS-Datenbank – unter dem alten Namen angesprochen<br />
werden.<br />
1.5.2 Lokale Datenbank<br />
Ursprünglich wurde die Zuordnung zwischen IP-Adresse und Rechnername in jedem<br />
Rechner lokal getroffen. Solche Daten sind auch heute noch in der Datei /etc/hosts<br />
enthalten. Darin sind neben den eigenen Daten auch die Daten der bekannten Rechner<br />
abgelegt. Hier ein – mitlerweile nicht mehr aktueller – Auszug aus der entsprechenden<br />
Datei eines unserer Server:<br />
# Internet Address Hostname # Comments<br />
127.0.0.1 loopback localhost # loopback<br />
193.170.124.101 EDV201.fhs-hagenberg.ac.at EDV201<br />
193.170.124.102 EDV202.fhs-hagenberg.ac.at EDV202<br />
193.170.124.103 EDV203.fhs-hagenberg.ac.at EDV203<br />
193.170.124.104 EDV204.fhs-hagenberg.ac.at EDV204<br />
193.170.124.105 EDV205.fhs-hagenberg.ac.at EDV205<br />
Neben der IP-Nummer steht zumindest der offizielle Name. Weitere alternative<br />
Namen (Aliases) sind optional. Auch im Internet wurde in seiner Anfangsphase<br />
die Namenszuordnung ausschließlich mit lokalen Konfigurationsdateien realisiert.<br />
Offizielle Rechnernamen vergab das NIC (Network Information Center). Es hielt eine<br />
offizielle Version der Datei /etc/hosts die laufend an alle teilnehmenden Rechner<br />
verteilt wurde. Diese Methode ist für kleine Netzwerke durchaus praktikabel. Bei<br />
größeren Netzwerken steigt der Aufwand für die Aktualisierung der lokalen Kopien<br />
dieser Datei jedoch dramatisch an. Auch heute wird die lokale Konfigurationsdatei<br />
als Backup verwendet, in ihr sind meist wichtige, im lokalen Umfeld angesiedelte<br />
Hosts eingetragen. Damit sind diese Hosts auch bei einem Ausfall der Verbindung<br />
zum Internet zu erreichen.
KAPITEL 1. TCP/IP 27<br />
Die durch eine wachsende Anzahl von Rechnern entstehenden Nachteile dieser<br />
Methode sind klar:<br />
• Kollisionen von Namen<br />
• hoher Verwaltungsaufwand in der Zentrale<br />
• Konsistenz durch viele Änderungen nur schwer zu gewährleisten<br />
• vermehrter Datenverkehr am Netz durch große Datenbank und viele Hosts<br />
1.5.3 Verteilte Datenbank<br />
Aufgrund dieser Nachteile ging das NIC bald zu einer hierarchisch organisierten<br />
Datenbank über, den eigentlichen Domain Name Services. Sie sind in den RFCs<br />
1034 und 1035 festgelegt.<br />
Namensraum<br />
In DNS sind Rechnernamen hierarchisch aufgebaut. Statt einem einzigen Namen, besteht<br />
ein Name aus einem Teil der den Rechner innerhalb seines logischen Umfeldes<br />
(Bereich/Domain) identifiziert und einem Domain-Anteil, der hierarchisch aufgebaut<br />
ist. Im Internet vergibt das NIC lediglich die Haupt-Domain (toplevel domains), es<br />
tritt die Verantwortung für Sub-Domains an lokale Verantwortliche ab. Diese können<br />
weitere Domain- und Rechnernamen innerhalb der eigenen Domain vergeben. Dadurch<br />
enstehen die heute verwendeten hierarchisch organisierten Domain-Namen.<br />
Sie werden von unten nach oben gelesen. Das Problem der Namenskonflikte ist damit<br />
weitestgehend entschärft.<br />
Name Server<br />
Jeder Betreiber einer Domain ist für die unter ihm befindlichen Sub-Domains verantwortlich.<br />
Nach außen vertritt er den ganzen unter seiner Obhut befindlichen<br />
Teilbaum. Bei größern Domains delegiert er die Zuständigkeit für die Sub-Domains<br />
an die darin befindlichen Institutionen.<br />
Aus dieser Hierarchie resultiert eine verteilte Datenbank für die Zuordnung zwischen<br />
Namen und IP-Adressen: Jeder Verantwortliche einer Domain verwaltet seinen<br />
Teil mit einem eigenen lokalen Name Server. Dieser kennt die Rechner, die direkt<br />
in der eigenen Domain angesiedelt sind. Existieren Sub-Domains, so bieten sich zwei<br />
Alternativen an:<br />
1. Die Sub-Domains betreiben eigene Name Server. Der übergeordnete Name<br />
Server kennt von seinen Sub-Domains lediglich die Namen und IP-Adressen<br />
der jeweiligen Name Server.
KAPITEL 1. TCP/IP 28<br />
2. Die Adressdaten der einzelnen Rechner der Sub-Domains sind ebenfalls in der<br />
Datenbank des übergeordneten Name Servers enthalten. Diese Variante ist vor<br />
allem für kleine Sub-Domains sinnvoll.<br />
Root Name Server<br />
Im ganzen Internet existieren einige Name Server, denen die vom NIC festgelegte<br />
oberste Hierarchieebene bekannt ist. Diese Root Name Server sind für einen besseren<br />
Durchsatz lokal verstreut. Ihr Datenbestand ist ident. Damit ist ein Zugriff auf diese<br />
Datenbestände auch bei einem Teilausfall des Internet möglich. Jeder Name Server<br />
kennt einige dieser Root Name Server.<br />
Resolver<br />
Gewöhnliche Rechner (Workstations) nehmen die Dienste lokaler Name Server in<br />
Anspruch. Die in Workstations dafür vorhandene Komponente heißt Resolver, sie<br />
wird z.B. von der Funktion gethostbyname() verwendet. Der Resolver wird in Unix<br />
durch die Datei /etc/resolv.conf konfiguriert. Hier der Inhalt dieser Datei einer<br />
Workstation:<br />
domain fh-hagenberg.at<br />
nameserver 193.170.124.100<br />
Sie enthält den Namen der eigenen Domain und die IP-Adresse des für diese Domain<br />
zuständigen Name Servers. Die Nennung mehrerer Name Server ist möglich, dies<br />
steigert die Fehlertoleranz. Abhängig von der Konfiguration des Resolvers verwendet<br />
er zum Ermitteln von IP-Adressen aus Namen entweder die lokale Hosts-Datei, den<br />
lokalen Name Server oder beides (Regelfall). Der Resolver setzt ein dns_query an<br />
den lokalen Name Server ab. In dieser Query ist der Rechnername angegeben. Er<br />
erwartet vom Name Server ein dns_reply mit der korrespondierenden IP-Adresse.<br />
Auflösung im Name Server<br />
Ein Name Server hält den Datenbestand über die lokal in seiner Domain existierenden<br />
Rechner. Er muss seinen Clients jedoch über alle Rechner des gesamten<br />
Netzwerkes Auskunft geben können. Dazu nimmt er Verbindung zu anderen Name<br />
Servern auf. In der Hochlaufphase sind ihm zunächst nur die folgenden Daten<br />
bekannt:<br />
• Adressdaten der in seiner Domain vorhandenen Rechner
KAPITEL 1. TCP/IP 29<br />
• IP-Adressen der in der DNS-Hierarchie untergelagerten Name Server, bzw.<br />
direkt die Adressdaten der Rechner aus den Subdomains<br />
• IP-Adressen der Root Name Server<br />
Durch einen Vergleich des Domain-Teils aus dem gesuchten Namen stellt der<br />
Name Server fest, ob er die Anfrage aus einem dns_request direkt beantworten<br />
kann. Ist dies nicht der Fall, so befragt er selber einen Root Server. Der sendet<br />
entweder direkt die Antwort oder teilt dem lokalen Name Server die IP-Adresse eines<br />
untergelagerten Name Servers mit, der für die gewünschte Subdomain zuständig ist.<br />
Dieses Spiel wiederholt sich, bis ein Name Server gefunden wird, der den gesuchten<br />
Eintrag in seiner Datenbank hat. Das Motto bei diesem Verfahren ist:<br />
Ich weiß es selber nicht, aber ich kenne jemanden, der es wissen sollte!<br />
Die schließlich erhaltene Antwort gibt der lokale Name Server an seinen Client<br />
(den Resolver eines lokalen Rechners) weiter. Bei diesem Vorgang lernt der lokale<br />
Name Server einerseits die Adressdaten des gewünschten Rechners kennen, andererseits<br />
werden ihm aber auch die Adressdaten einiger Name Server bekannt. Diese<br />
Daten hält ein Name Server einige Zeit im lokalen Speicher. Damit werden andere<br />
Name Server (ganz besonders die Root Name Server) entlastet.<br />
Einen Sonderfall im Namensbaum des Internet stellt die Domain in_addr.arpa<br />
dar. Sie enthält die inverse Zuordnung, also die Abbildung von IP-Adressen zu Rechnernamen.<br />
Mit dieser Domain kann ein beliebiger Applikations-Server den Domain-<br />
Namen eines Rechners aus der IP-Adresse erfahren. Dies wird z.B. zum Prüfen von<br />
IP-Adressen auf Authentizität verwendet. Jeder Name Server muss auch auf solche<br />
inversen Anfragen antworten können.
Kapitel 2<br />
Programmierschnittstellen unter<br />
UNIX<br />
2.1 Einführung<br />
Das Betriebssystem Unix war von Beginn an eng mit Netzwerken verbunden. Aus<br />
diesem Grund sind Software-Schnittstellen für den Zugriff auf die Kommunikationsteile<br />
des Betriebssystems integraler Bestandteil jeder Unix-Implementierung.<br />
Dem Programmierer bieten sich viele unterschiedliche Alternativen zum Realisieren<br />
seines Programmes an. Die high level Schnittstellen verbergen die Details der<br />
Kommunikation vor dem Applikationsprogrammierer. In den folgenden Kapiteln<br />
werden zwei typische Vertreter behandelt:<br />
Sockets bauen auf Streams, also den Zugriffen auf Dateien, auf. Die Socket-<br />
Schnittstelle implementiert viele Routinen aus der Dateibehandlung auch für<br />
den Datenaustausch zwischen einzelnen Rechnern. Einmal als Stream geöffnete<br />
Socket-Kanäle können mit den konventionellen Stream-Befehlen bearbeitet<br />
werden.<br />
Remote Procedure Calls (RPCs) gehen einen völlig anderen Weg: Funktionen,<br />
welche auf einem anderen Rechner (Server) implementiert sind, können wie<br />
lokal vorhandene Funktionen aufgerufen werden. Auch hier werden die Details<br />
der Kommunikation vor dem Anwendungsprogramm weitestgehend verborgen.<br />
2.2 Socket-Interface<br />
Hinweis: Dieser Abschnitt entstammt weitestgehend [Rago 1993, Kap. 7]. Diverse<br />
Web-Server bieten einführende Seiten zur Socket-Programmierung an (vgl. z. B.<br />
30
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 31<br />
[Hall 1996]). Comer und Stevens [1996] behandeln das Thema umfassend, in [Comer<br />
und Stevens 1997] wird auf die Socket-Programmierung unter Windows eingegangen.<br />
2.2.1 Einführung in Clients, Server, Daemons und Protokolle<br />
Unter Server versteht man ein Programm, das einen Dienst zur Verfügung stellt.<br />
Ein Server macht Ressourcen, die irgendwo im Netz liegen, für andere Programme<br />
verfügbar. Ressource ist in diesem Zusammenhang ein äußerst weitläufiger Begriff.<br />
Eine Ressource kann sowohl Hardware (Drucker, Faxmodem, . . . ) als auch Software<br />
(Datenbank, Filesystem, Telnet, . . . ) sein. Der Serverprozess läuft auf dem Rechner,<br />
an dem die Ressource angeschlossen ist und wartet darauf, dass der zur Verfügung<br />
gestellte Dienst von einem anderen Host angefordert wird. Serverprozesse werden<br />
meistens schon beim Hochlauf durch Start-up-Skripts gestartet.<br />
Ein Client ist ein Programm, das eine solche Ressource nutzt. Ein wichtiger<br />
Punkt beim Client-Server-Konzept ist die Nutzung von Ressourcen unabhängig<br />
von deren physikalischem Standort. Ein Client muss lediglich über das Netzwerk<br />
eine Verbindung zum Server herstellen.<br />
In der UNIX-Welt wird ein Server meist auch Daemon bezeichnet. Die Verbindung<br />
von Serverprozessen zu Daemons könnte man vielleicht so sehen: Daemons<br />
tauchen plötzlich auf und verschwinden genau so plötzlich wieder. Ein Daemon unter<br />
UNIX wird meist beim Hochlauf gestartet. Anschließend wartet der Daemon<br />
passiv auf eine Anforderung. Fordert nun ein Client einen Dienst an, so wird der<br />
Daemon aktiv und stellt dem Client eine Ressource zur Verfügung. Benötigt nun<br />
der Client diese Ressource nicht mehr, dann geht der Daemon wieder in einen passiven<br />
Wartezustand über. Unter UNIX erkennt man einen Daemon an der Endung<br />
des Filenamens mit d. Beispiele: inetd, telnetd, . . . (vgl. dazu den Output des<br />
Kommandos ps -ef | grep telnetd).<br />
Unter Protokoll versteht man die Beschreibung der Interaktion zwischen Server<br />
und Client. Das Protokoll definiert das genaue Format der Daten, die zwischen den<br />
beiden Prozessen ausgetauscht werden.<br />
2.2.2 Adressierung von Diensten<br />
Um einen Dienst im Netzwerk zu finden, muss der Client erst einmal wissen, mit<br />
welchem Rechner er Kontakt aufnehmen muss. Dies erfolgt meist über eine Internetdomainadresse<br />
(Bsp.: www.fh-hagenberg.at). Diese Domainadresse wird dann<br />
umgewandelt in eine 32-Bit Netzadresse (IP-Adresse) und adressiert einen Rechner<br />
im Netzwerk. Auf diesem Rechner gibt es nun aber mehrere Kommunikationsendpunkte.<br />
Diese Kommunikationsendpunkte, die sogenannten Ports, werden mit einer
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 32<br />
16-Bit-Portnummer angesprochen. Jeder Daemon hört nun einen solchen Port auf<br />
Anfragen von Clients ab (Bsp.: ftpd überprüft Port 21).<br />
Unter UNIX sind alle Portnummern, die kleiner als 1024 sind, reserviert. Das<br />
bedeutet, dass nur Prozesse, die unter der UID root laufen, diese Ports benutzen<br />
dürfen. Das stellt eine einfache Form von Sicherheitsmechanismus dar. Die meisten<br />
Daemons arbeiten mit reservierten Ports. So hat der Client die Sicherheit, mit einem<br />
echten Daemon zu kommunizieren und nicht mit einem Programm, das ein<br />
gewöhnlicher Benutzer geschrieben hat um Passworte abzufangen. Der Zusammenhang<br />
zwischen Diensten und Portnummern ist in der Datei /etc/services definiert.<br />
Hier ein Auszug daraus:<br />
daytime 13/tcp<br />
telnet 23/tcp<br />
time 37/udp timeserver<br />
time 37/tcp timeserver<br />
fax_modem 2055/tcp faxmodem<br />
Die erste Spalte eines Eintrages bezeichnet den Namen des Dienstes, die zweite<br />
die Portnummer sowie das verwendete Protokoll und der dritte Eintrag ist ein alternativer<br />
Name für den Dienst. In größeren UNIX-Netzwerken wird diese Information<br />
über Dienste nicht auf jedem Rechner in einer Datei abgespeichert, sondern liegt in<br />
einer zentralen Datenbank (Network Information Service).<br />
2.2.3 Allgemeines zum Socket Interface<br />
Sockets<br />
Die Transportprotokolle TCP und UDP der TCP/IP-Protokollfamilie sind die Basis<br />
der Sockets. Ein Socket ist ein Kommunikationsendpunkt, an dem sich Anwendungsprogramm<br />
und Transportschicht treffen. Die ursprüngliche Schnittstelle zu TCP und<br />
UDP stammt aus dem Release BSD 4.2 des Berkeley UNIX. Sie besteht aus acht<br />
neuen Sytemaufrufen und wird mit dem allgemeinen Namen Sockets bezeichnet.<br />
Adressfamilien von Sockets<br />
Die einfachste Adressschema ist die sogenannte UNIX-Adressfamilie. Dabei wird<br />
ein Socket mit einem UNIX-Pfadnamen assoziiert. Unter BSD-UNIX werden diese<br />
Sockets in der Verzeichnisstruktur als Einträge mit dem Typ s gezeigt:<br />
turing% ls -l /tmp/mysocket<br />
srwxrwxrwx 1 chris 0 Jul 1 22:00 /tmp/mysocket<br />
turing%
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 33<br />
Unter SVR4 UNIX wird dieser Typ als Named Pipe implementiert und im Verzeichnis<br />
mit der Kennung p angezeigt. Diese Art von Adressierung ist zwar einfach,<br />
aber für eine Kommunikation über das Netzwerk unbrauchbar.<br />
Ein anderes Konzept für die Adressierung von Sockets ist das Internet Domain<br />
Addressing. Dabei stehen hinter dem Socket zwei Zahlenwerte: Die 32-Bit Internetadresse<br />
des Hosts, auf welchem sich der Socket befindet und die 16-Bit Portnummer.<br />
Socket-Aufrufe (z.B. bei der hier exemplarisch gewählten Routine bind())<br />
müssen nun flexibel genug sein, um mit den unterschiedlichen Adressfamilien umgehen<br />
zu können. Dies wird durch eine flexible Funktionsschnittstelle erreicht. Für<br />
beide Arten von Socket-Adressen werden Strukturen definiert.<br />
UNIX-Bereichsadresse:<br />
struct sockaddr_un {<br />
short sun_family; /* Tag: AF_UNIX */<br />
char sun_path[108]; /* path name */<br />
};<br />
Internet-Bereichsadresse:<br />
struct sockaddr_in {<br />
short sin_family; /* Tag: AF_INET */<br />
u_short sin_port; /* Port Number */<br />
struct in_addr sin_addr; /* IP-address */<br />
char sin_zero[8]; /* Padding */<br />
};<br />
struct in_addr {<br />
u_long s_addr;<br />
};<br />
Beide Strukturen enthalten am Beginn ein Tag, das unbedingt gesetzt sein muss.<br />
Dieses Tag benutzen Funktionen wie bind(), die nur einen Zeiger auf die Struktur<br />
(Übergabeparameter) erhalten, um herauszufinden, ob es sich um eine Struktur<br />
vom Typ sockaddr_in oder um eine vom Typ sockaddr_un handelt. Weiters muss<br />
noch ein zusätzlicher Parameter – der die Länge der Adresse enthält – übergeben<br />
werden. Dies vereinfacht die Implementierung dieser Systemfunktionen. Ein Aufruf<br />
der Funktion bind() könnte wie folgt aussehen:<br />
#include <br />
#include <br />
int bind(int fd, struct sockaddr *addrp, int alen);<br />
bind (sock, (struct sockaddr *) &server, sizeof(server));
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 34<br />
Hier ist server z. B. vom Typ struct sockaddr_in.<br />
Typen von Sockets<br />
Zusätzlich zu den Adressfamilien besitzen die Sockets auch noch einen Typ, der sich<br />
auf die Art des zugrundeliegenden Protokolls bezieht:<br />
SOCK_STREAM /* verbindungsorientierter Transport (TCP) */<br />
SOCK_DGRAM /* verbindungsloser Transport (UDP) */<br />
SOCK_RAW<br />
Der Typ SOCK_RAW wird dazu eingesetzt, um direkt mit der IP-Schicht zu kommunizieren.<br />
Dazu sind root-Rechte nötig.<br />
2.2.4 Verbindungsorientierte Server<br />
Bei dieser Art der Kommunikation müssen Client und Server ein bestimmtes Prozedere<br />
einhalten. Die Abbildung 2.1 auf der nächsten Seite zeigt die einzelnen Etappen<br />
mit den zugehörigen Systemaufrufen.<br />
Verbindungsaufbau<br />
Der wesentliche Unterschied zwischen Client und Server ist der, dass der Server<br />
passiv auf Arbeit wartet, der Client hingegen aktiv mit einem Server Verbindung<br />
aufnimmt.<br />
Die vom Server auszuführenden Schritte:<br />
1. Ein Socket der benötigten Adressfamilie und des benötigten Typs muss angelegt<br />
werden:<br />
#include <br />
#include <br />
int socket(int family, int type, int protocol);<br />
int my_sock;<br />
my_sock = socket (AF_INET, SOCK_STREAM, 0)<br />
if (my_sock < 0) {<br />
perror ("cannot get socket");’<br />
exit (1);<br />
}
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 35<br />
✗<br />
Server<br />
✖<br />
Socket anlegen<br />
socket()<br />
✔<br />
✕<br />
❄<br />
an Port binden<br />
bind()<br />
❄<br />
am Kernel anmelden<br />
listen()<br />
❄<br />
auf Client warten<br />
accept()<br />
❄<br />
Dialog mit Client<br />
read(), write()<br />
❄<br />
Verbindung beenden<br />
close()<br />
✛<br />
✛ ✲<br />
✛ ✲<br />
✗<br />
Client<br />
✖<br />
Socket anlegen<br />
socket()<br />
✔<br />
✕<br />
❄<br />
Verbindung aufbauen<br />
connect()<br />
❄<br />
Dialog mit Server<br />
write(), read()<br />
❄<br />
Verbindung beenden<br />
close()<br />
Abbildung 2.1: Verbindungsorientierte Client-/Serveroperationen
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 36<br />
Das erste Argument gibt die Adressfamilie an, das zweite Argument spezifiziert<br />
den Sockettyp und das dritte Argument gibt das Protokoll an, wird aber im<br />
Allgemeinen auf 0 gesetzt – d.h. lass das System wählen. Ist das Anlegen<br />
des Socket nicht erfolgreich, dann retourniert socket mit dem Wert −1. In<br />
diesem Fall enthält die globale Variable errno den Fehlercode. Die Funktion<br />
perror gibt einen mit errno korrespondierenden Text auf stderr aus (vgl.<br />
man perror und man strerror). Viele Systemroutinen teilen so die Ursachen<br />
für einen Fehler mit. Zum Zweck der besseren Lesbarkeit wird das Testen der<br />
Return-Werte jedoch in den folgenden Code-Fragmenten weggelassen.<br />
2. Eine Adresse wird an den Socket gebunden. Dabei handelt es sich je nach<br />
Adressfamilie entweder um einen Pfadnamen oder um eine IP-Adresse und<br />
eine Portnummer. Beispiel:<br />
#define SERVER_PORT 2222<br />
struct sockaddr_in server;<br />
server.sin_family = AF_INET;<br />
server.sin_addr.s_addr = INADDR_ANY;<br />
server.sin_port = htons (SERVER_PORT);<br />
bind (<br />
my_sock,<br />
(struct sockaddr *) &server,<br />
sizeof(server)<br />
);<br />
Handelt sich ein einen experimentellen Server, so ist darauf zu achten, dass<br />
die Portnummer größer als 1024 ist. Die Portnummer muss natürlich mit dem<br />
Client abgestimmt sein, da dieser sonst den Serverdienst nicht findet. Das Macro<br />
htons konvertiert einen short-Wert von der internen Darstellung in eine<br />
(standardisierte) Netzwerk-Darstellung (host to network short). Damit werden<br />
die Unterschiede zwischen little und big endian ausgeglichen. Das Macro<br />
für die Gegenrichtung heißt ntohs. Weiters gibt es noch entsprechende Macros<br />
für long-Werte: htonl und ntohl. Diese sind für die IP-Adressen zu verwenden.<br />
Der der IP-Adresse des Sockets zugewiesene Wert INADDR_ANY (vgl.<br />
include netinet/in.h) ist ein vordefinierter Wert und bedeutet, dass dieser<br />
Socket Verbindungen an jedem Netzwerk-Interface dieser Maschine akzeptiert.<br />
INADDR_ANY hat den Wert FF...FF. Daher wurde hier das an dieser Stelle eigentlich<br />
notwendige Macro htonl weggelassen. Normalerweise hat jeder Rechner<br />
nur eine Netzwerkkarte. Ist ein Rechner mit mehreren Netzwerk-Interfaces
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 37<br />
ausgestattet (Gateway), so gehört zu jedem Interface eine eigene IP-Adresse.<br />
Sollen nur Verbindungen von einem bestimmten Interface akzeptiert werden,<br />
so kann die IP-Adresse des gewünschten Interface hier angegeben werden. Es<br />
ist jedoch zu beachten, dass diese Adresse überhaupt nichts damit zu tun hat,<br />
von welchen Clients Verbindungen akzeptiert werden.<br />
3. Nun muss der Kernel informiert werden, dass von diesem Socket Verbindungen<br />
akzeptiert werden.<br />
#include <br />
#include <br />
int listen(int fd, int backlog);<br />
listen (my_socket, 5);<br />
Das zweite Argument legt die Anzahl der wartenden Verbindungsanfragen fest.<br />
Eine Verbindungsanfrage liegt dann vor, wenn ein Client versucht mit einem<br />
Server Verbindung aufzunehmen, während ein anderer Client gerade mit dem<br />
Server kommuniziert. Die Anfragen werden der Reihe nach in eine Warteschlange<br />
eingereiht. Ist der angegebene Wert erreicht, wird die Verbindung<br />
zurückgewiesen.<br />
4. Jetzt muss nur noch darauf gewartet werden, dass ein Client eine Verbindung<br />
aufbaut. Versucht ein Client nun eine Verbindung aufzubauen, so muss diese<br />
vom Server akzeptiert werden.<br />
#include <br />
#include <br />
int accept(int fd, struct sockaddr *addrp, int *alenp);<br />
struct sockaddr_in client;<br />
int fd, client_len;<br />
client_len = sizeof(client);<br />
fd = accept (my_sock, &client, &client_len);<br />
Das zweite Argument liefert bei Akzeptieren der Verbindung die IP-Adresse<br />
und den Port des Client, der die Verbindung aufgebaut hat. Akzeptieren wir<br />
von jedem beliebigen Rechner Verbindungen, so können wir diesen Parameter<br />
ignorieren. Wollen wir eine Zugriffskontrolle durchführen, so können wir
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 38<br />
über diesen Parameter die Adresse des Client erfahren. Der Rückgabewert<br />
von accept() ist ein Deskriptor, der in Bezug zu der mit dem Client hergestellten<br />
Verbindung steht. Wir können nun mit den Funktionen read() und<br />
write() auf den Socket zugreifen.<br />
Datentransport mittels Sockets<br />
Nach dem Verbindungsaufbau verhält sich unser Deskriptor fd wie jeder andere<br />
Dateideskriptor. Man kann Operationen wie read(), write(), dup(), close(),<br />
. . . darauf anwenden. Die Kommunikation zwischen Client und Server ist natürlich<br />
vom Anwendungsprogramm abhängig. Üblicherweise sieht ein typischer Dialog folgendermaßen<br />
aus: Ein Client stellt eine Anfrage an den Server und der Server antwortet.<br />
Die Verbindung wird durch ein Schließen des Deskriptors mit close() beendet.<br />
Der Partner erkennt dies durch einen Return-Wert von 0 beim nächsten read(). In<br />
der weiteren Folge ist der Server bereit für den nächsten Client – d.h. er sollte alle<br />
Aktionen ab einschließlich accept() in eine Endlosschleife einbetten.<br />
2.2.5 Parallele Server<br />
Server, die nur einen Client bedienen, werden iterative Server genannt. Sie sind<br />
nur für Dienste geeignet, die eine äußerst kurze Verbindungsdauer aufweisen (z.B.:<br />
timed), da weitere Clients, die einen solchen Dienst anfordern, unbestimmte Zeit<br />
warten müssen. Das ist z.B. für einen Telnet-Daemon untragbar. Aus diesem Grund<br />
wollen wir uns parallelen Servern zuwenden. Der Server erzeugt mit dem UNIX-<br />
Befehl fork() für jeden Client einen Kindprozess. Der Elternprozess übernimmt<br />
lediglich mit accept() die Verbindung und übergibt sie dem Kind. Da der Kindprozess<br />
die offenen Deskriptoren vom Elternteil erbt, ist der Server sehr einfach zu<br />
implementieren:<br />
a_socket = socket ( ....);<br />
bind (a_socket, ....);<br />
listen (a_socket, 5);<br />
while(1) {<br />
fd = accept (a_socket,...); /* await and accept connection */<br />
if (fork()==0) { /* child: */<br />
server_process (fd); /* do server application */<br />
close (fd); /* close connection */<br />
exit(0); /* end child */<br />
}<br />
else { /* parent: */<br />
close(fd); /* close connection */
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 39<br />
} /* end parent part */<br />
} /* while */<br />
Diese Implementierung hat noch einen Nachteil: Terminierende Kind-Prozesse<br />
warten auf die Übernahme ihres Exit-Status durch den Vater-Prozess. Holt dieser<br />
den Exit-Status seiner Kinder nicht ab, so leben die Kind-Prozesse als ” Zombies“<br />
weiter. In der nachfolgenden Programmskizze löst der Vater-Prozess dieses Problem<br />
mit Signalen:<br />
/*<br />
* waiter accepts the exit code from a child created previously<br />
* /<br />
void waiter()<br />
{<br />
int cpid, stat;<br />
}<br />
cpid = wait (&stat); /* wait for a child to terminate */<br />
signal (SIGCHLD, waiter); /* reinstall signal handler */<br />
/*<br />
* main procedure of the fork based socket server<br />
*/<br />
void main(void)<br />
{<br />
signal (SIGCHLD, waiter); /* install signal handler */<br />
a_socket = socket ( ... );<br />
bind (a_socket, ... );<br />
listen (a_socket, ... );<br />
.<br />
.<br />
} /* end of program */<br />
2.2.6 Client zur verbindungsorientierten Kommunikation<br />
Für den Client sind wesentlich weniger Schritte notwendig. Gemäß Abb. 2.1 auf<br />
Seite 35 legt er den Socket genau wie ein Server an. Der nächste Schritt ist bereits<br />
der Verbindungsaufbau mittels connect().<br />
#include <br />
#include <br />
#include
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 40<br />
int connect (int Socket, struct sockaddr *Name, int NameLength);<br />
Vor dem Aufruf sind in Name die Adressdaten des gewünschten Servers einzustellen,<br />
NameLength gibt – ähnlich wie bei bind() – die Länge der verwendeten<br />
Adressstruktur an. connect() vergibt für die Client-Seite einen lokalen (dynamisch<br />
bestimmten) Port. Fehler werden durch den Return-Wert -1 angezeigt, die globale<br />
Variable errno spezifiziert dann die Ursache des Fehlers genauer. Zu beachten ist,<br />
dass – im Gegensatz zu accept() – hier lediglich ein Fehler-Code retourniert wird,<br />
der nachfolgende Datenaustausch erfolgt beim Client direkt mit dem Socket.<br />
Der eigentliche Datentransfer erfolgt wie beim Server z.B. mit den Funktionen<br />
read() und write(). Natürlich müssen Client und Server ein gemeinsames Applikationsprotokoll<br />
abwickeln. D.h. es muss klar sein, wer wann sendet bzw. empfängt.<br />
Oft erhalten Clients die Adresse des Servers durch die Kommandozeile oder<br />
durch ein GUI-Element vom Benuzter. Dann liegt diese Adresse intern als String<br />
vor. Meist wird statt der IP-Adresse der Domain-Name des Servers angegeben. Für<br />
die Konvertierung solcher Daten in eine von connect() akzeptierte Form ist die<br />
Funktion gethostbyname() sehr nutzbringend:<br />
#include <br />
struct hostent *gethostbyname (char *Name);<br />
Sie ermittelt zu einem Rechnernamen (z.B. jersey.fh-hagenberg.at) unter anderem<br />
die IP-Nummer. gethostbyname() retourniert einen Zeiger auf eine Struktur:<br />
struct hostent {<br />
char* h_name;<br />
char** h_aliases;<br />
int h_addrtype;<br />
int h_length;<br />
char** h_addr_list;<br />
};<br />
#define h_addr h_addr_list[0]<br />
Diese Felder haben die folgende Bedeutung:<br />
h_name - offizieller Name des Host<br />
h_aliases - ein NULL-terminiertes Feld von alternativen Namen<br />
h_addrtype - der Typ der retournierten Adresse, in der Regel AF_INET<br />
h_length - die Länge der retournierten Adresse<br />
h_addr_list - ein NULL-terminiertes Feld von Netzwerkadressen des Host, bereits<br />
in der Darstellungsform des Netzwerkes<br />
h_addr - die erste Adresse in h_addr_list
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 41<br />
gethostbyname() retourniert einen Zeiger auf die vollständig ausgefüllte Struktur.<br />
Im Fehlerfall enthält der Zeiger den Wert NULL, dann ist die globale Variable<br />
h_errno entsprechend eingestellt. Zur Demonstration der Anwendung wird an dieser<br />
Stelle ein Programm von Hall [1996] übernommen:<br />
#include <br />
#include <br />
#include <br />
#include <br />
#include <br />
#include <br />
int main(int argc, char *argv[])<br />
{<br />
struct hostent *h;<br />
}<br />
if (argc != 2) { /* error check the command line */<br />
fprintf(stderr,"usage: getip address\n");<br />
exit(1);<br />
}<br />
/*<br />
* get the host info<br />
*/<br />
if ((h=gethostbyname(argv[1])) == NULL) {<br />
herror("gethostbyname");<br />
exit(1);<br />
}<br />
printf("Host name : %s\n", h->h_name);<br />
printf("IP Address : %s\n",inet_ntoa(<br />
*((struct in_addr *)h->h_addr)<br />
));<br />
return 0;<br />
2.2.7 Client für mehrere Streams<br />
Applikationen müssen manchmal gleichzeitig mehrere Streams überwachen und von<br />
diesen eingehende Daten lesen und verarbeiten. Eine Möglichkeit ist der non blocking<br />
mode von Streams. Ein read() retourniert dann sofort, auch wenn gerade keine<br />
Daten zu lesen sind. Nachteilig ist hier, dass dauernd zu lesen bzw. prüfen ist und<br />
dabei der Prozessor belastet d.h. nicht für andere Prozesse frei gegeben wird (busy
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 42<br />
Ereignis Beschreibung<br />
POLLIN Daten sind zu lesen<br />
POLLPRI hochpriore Daten sind zu lesen<br />
Tabelle 2.1: Spezifizierbare Ereignisse für poll()<br />
waiting).<br />
Deshalb existieren für das Überwachen mehrerer Streams Funktionen wie z.B.<br />
poll():<br />
#include <br />
#include <br />
#include <br />
int poll(struct pollfd *parray, ulong_t nfds, int timeout);<br />
Der Rufer stellt die zu überwachenden Streams im Array parray zusammen,<br />
die Anzahl des Arrays steht in nfds, timeout gibt die maximale Wartezeit in Millisekunden<br />
an. Tritt binnen Ablauf der Timeout-Zeit kein Ereignis an einem der<br />
Streams auf, so retourniert poll() mit 0, sonst wird unmittelbar beim Auftreten<br />
mit der Anzahl der zu behandelnden Streams retourniert. −1 signalisiert wie üblich<br />
einen Fehler. Ein Timeout-Wert von −1 steht für ∞, der Wert 0 wartet gar nicht<br />
sondern testet lediglich die Streams. Die Struktur pollfd ist wie folgt definiert:<br />
struct pollfd {<br />
int fd; /* file descriptor */<br />
short events; /* requested events */<br />
short revents; /* returned events */<br />
};<br />
Dabei ist vom Rufer in fd der File Deskriptor des Streams einzutragen, events<br />
ist ein Bit-Feld in dem die zu überwachenden Ereignisse einzutragen sind. In revents<br />
stehen nach dem Retournieren die davon tatsächlich aufgetretenen Ereignisse. Die<br />
Tabelle 2.1 zeigt einige sinnvolle Ereignisse, für eine vollständige Liste wird auf die<br />
man-Page von poll() verwiesen. Einige weitere Ereignisse können immer auftreten<br />
(vgl. Tab. 2.2 auf der nächsten Seite). Sie werden auch immer von poll() in revents<br />
gemeldet und sollten nicht in events angegeben werden.<br />
Ein kleines Beispiel zeigt die Anwendung von poll(). Es koppelt einfach zwei –<br />
zuvor geöffnete – Streams d.h. es liest von einem und schreibt diese Daten auf den<br />
anderen Stream. Wenn ein Stream für eine geöffnete Socket-Verbindung und der<br />
zweite für STDIN/STDOUT verwendet wird, lässt sich damit ein einfaches Terminal<br />
realisieren. Dieses Beispiel wurde aus Rago [1993, S. 125–127] übernommen.
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 43<br />
Ereignis Beschreibung<br />
POLLERR Stream meldet Fehler<br />
POLLHUP Stream ging verloren (hang up)<br />
POLLNVAL ungültiger File Deskriptor<br />
Tabelle 2.2: Nicht maskierbare Ereignisse für poll()<br />
#include <br />
#include <br />
extern void error(const char *fmt, ...);<br />
void<br />
comm(int tfd, int nfd)<br />
{<br />
int n, i;<br />
struct pollfd pfd[2];<br />
char buf[256];<br />
pfd[0].fd = tfd; /* terminal */<br />
pfd[0].events = POLLIN;<br />
pfd[1].fd = nfd; /* network */<br />
pfd[1].events = POLLIN;<br />
for (;;) {<br />
/*<br />
* Wait for events to occur.<br />
*/<br />
if (poll(pfd, 2, -1) < 0) {<br />
error("poll failed");<br />
break;<br />
}<br />
/*<br />
* Check each file descriptor.<br />
*/<br />
for (i = 0; i < 2; i++) {<br />
/*<br />
* If an error occurred, just return.<br />
*/<br />
if (pfd[i].revents&(POLLERR|POLLHUP|POLLNVAL))<br />
return;<br />
/*<br />
* If there are data present, read them from
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 44<br />
}<br />
}<br />
}<br />
* one file descriptor and write them to the<br />
* other one.<br />
*/<br />
if (pfd[i].revents&POLLIN) {<br />
n = read(pfd[i].fd, buf, sizeof(buf));<br />
if (n > 0) {<br />
write(pfd[1-i].fd, buf, n);<br />
} else {<br />
if (n < 0)<br />
error("read failed");<br />
return;<br />
}<br />
}<br />
Manchmal wird statt poll() auch die praktisch gleichwertige Funktion select()<br />
verwendet. Sie unterscheidet sich im Wesentlichen durch die Parameter, leistet aber<br />
prinzipiell das Gleiche. Details liefert die man-Page von select(), der Umgang ist<br />
z.B. in Hall [1996, Kap. 6.2] beschrieben.<br />
2.2.8 Verbindungslose Kommunikation<br />
Die verbindungslose Kommunikation basiert auf dem Protokoll UDP. Damit ist die<br />
Übertragung nicht gesichert. Anwendungen, die auf UDP aufsetzen, sollten selber für<br />
die Sicherung und Quittierung sorgen. Die für die beteiligten Stationen notwendigen<br />
Aktionen sind in Abb. 2.2 auf der nächsten Seite dargestellt.<br />
Zunächst wird ein Socket angelegt (socket()). Die Operation bind() ist eigentlich<br />
optional, sie dient lediglich dem Binden an einen bestimmten lokalen Port.<br />
Wird bind() nicht aufgerufen, dann wählt das Socket-Interface willkürlich einen<br />
freien Port. In der Regel übernimmt auch hier eine der beteiligten Stationen die<br />
Rolle des Servers. Dann ist bei ihr das Verwenden von bind() sinnvoll, nur so kann<br />
der Client den Server-Prozess identifizieren.<br />
Bei verbindungsloser Kommunikation entfallen die Aufrufe von listen(),<br />
accept() und connect(). Die Adressdaten der Partnerstation werden direkt beim<br />
Senden von Nachrichten angegeben:<br />
#include <br />
#include <br />
int sendto (<br />
int Socket,<br />
char* Message,<br />
int Length,
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 45<br />
✗<br />
Station A<br />
✖<br />
Socket anlegen<br />
socket()<br />
❄<br />
an Port binden<br />
bind()<br />
❄<br />
Dialog mit Partner<br />
recvfrom(), sendto()<br />
❄<br />
beenden<br />
close()<br />
✔<br />
✕<br />
optional<br />
✛ ✲<br />
✗<br />
Station B<br />
✖<br />
Abbildung 2.2: Verbindungslose Kommunikation<br />
int Flags,<br />
struct sockaddr* To,<br />
int ToLength);<br />
Socket anlegen<br />
socket()<br />
❄<br />
an Port binden<br />
bind()<br />
❄<br />
Dialog mit Partner<br />
sendto(), recvfrom()<br />
❄<br />
beenden<br />
close()<br />
✔<br />
✕<br />
Die durch Message und Length spezifizierte Nachricht wird an den mit To und<br />
ToLength angegebenen Host gesendet. Broadcasts sind nur nach einem vorhergehenden<br />
Setzen der Option SO_BROADCAST (vgl. die Manual-Seite zu setsockopt())<br />
möglich. Mit dem Parameter Flags können noch einige Attribute gesteuert werden<br />
(vgl. die Manual-Seite zu sendto()). Treten Fehler auf, dann retourniert sendto()<br />
mit dem Wert -1, errno ist dann wieder entsprechend eingestellt. Andernfalls retourniert<br />
diese Funktion die Anzahl der tatsächlich gesendeten Bytes. Wurden weniger<br />
Zeichen gesendet, als angegeben wurden, dann muss das Senden der verbleibenden<br />
Zeichen erneut durchgeführt werden.<br />
Umgekehrt bekommt eine empfangende Station auch die Adressdaten des Senders<br />
übermittelt:<br />
#include <br />
#include <br />
int recvfrom (<br />
int Socket,
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 46<br />
char* Buffer,<br />
int Length,<br />
int Flags,<br />
struct sockaddr* From,<br />
int* FromLength);<br />
Hier werden maximal Length Zeichen empfangen und in Buffer abgelegt. Die<br />
Adressdaten des Senders werden in From abgelegt. From muss beim Aufruf auf allokierten<br />
Speicher ausreichender Größe zeigen. Die Größe dieses Speicherbereiches ist<br />
in FromLength anzugeben. Ein Server erhält so die für eine ev. Antwort notwendigen<br />
Adressdaten. recfrom() retourniert die Anzahl der empfangenen Zeichen bzw. -1<br />
im Fehlerfall. Auch FromLength wird auf die tatsächliche Größe der Adressstruktur<br />
eingestellt.<br />
2.3 Socket-Programmierung in C unter Windows<br />
Microsoft hat das Socket-Interface von Unix unter Windows in einer abgemagerten<br />
Version als WinSocks nachgebaut. Die wesentlichen Unterschiede sind:<br />
1. Anstelle der üblichen Include-Dateien aus der Socket-Library wird lediglich<br />
das System-Include winsock.h verwendet.<br />
2. Vor dem ersten Aufruf einer Socket-Funktion muss die richtige DLL geladen<br />
werden:<br />
#include <br />
{<br />
WSADATA wsaData; // if this does’nt work<br />
//WSAData wsaData; // then try this instead<br />
if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {<br />
fprintf(stderr, "WSAStartup failed\n");<br />
exit(1);<br />
}<br />
3. Dem Linker muss die richtige Library angegeben werden (wsock32.lib bzw.<br />
winsock32.lib).<br />
4. Vor dem Terminieren einer Socket-Anwendung sollte WSACleanup() aufgerufen<br />
werden.<br />
5. Windows Sockets sind nicht mit File-Deskriptoren kompatibel. Das hat die<br />
folgenden Konsequenzen:
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 47<br />
(a) An Stelle von close() ist closesocket() zu verwenden.<br />
(b) select() arbeitet nur mit Socket-Deskriptoren und nicht mit File-<br />
Deskriptoren.<br />
(c) Die Funktionen read() und write() können für Windows-Sockets nicht<br />
verwendet werden. An ihrer Stelle sind send() und recv() zu benutzen.<br />
Diese beiden Funktionen existieren auch für das klassische<br />
Socket-Interface unter Unix, wegen ihrer Inkompatibilität zu den File-<br />
Deskriptoren werden sie dort aber eher selten verwendet.<br />
6. fork() exitiert unter Windows nicht, an seiner Stelle ist CreateProcess() zu<br />
verwenden.<br />
Detaillierte Informationen über die Socket-Programmierung unter Windows liefern<br />
z.B. Comer und Stevens [1997].<br />
2.4 Socket-Programmierung mit Java<br />
2.4.1 Generelles<br />
Java legt großen Wert auf Plattformunabhängigkeit was besonders für verteilte<br />
oder Netzwerk orientierte Anwendungen wichtig ist. Die Sprache ist wesentlich<br />
jünger als C und auch komfortabler. Das zeigt sich auch im Bereich der Socket-<br />
Programmierung. Grundlegende Konzepte wie z.B. der gesamte Ablauf wurden aus<br />
der Socket-Library für C übernommen, auch hier wird ja letztendlich auf der gleichen<br />
Schnittstelle des Protokollstack von TCP/IP aufgebaut.<br />
In einigen Aspekten unterscheidet sich die Socket-Programmierung allerdings<br />
doch:<br />
• Für Adressen steht eine eigene Klasse InetAddress zur Verfügung, welche<br />
selber Methoden zur Namensauflösung mit DNS besitzt.<br />
• Bei verbindungsorientierten Sockets existieren getrennte Klassen für Client<br />
und Server (Socket und ServerSocket).<br />
• Auch für verbindungslose Sockets ist eine eigene Klasse DatagramSocket vorhanden.<br />
• Fehler werden natürlich über Exceptions gemeldet.<br />
All diese Klassen sind – gemeinsam mit einigen Anderen und auch einigen Interfaces<br />
– im Package java.net definiert.<br />
Die folgenden Abschnitte geben einen Überblick über die Programmierung am<br />
Socket-Interface mit Java. Sie entstammen weitgehend Krüger [2000]. Weitere Details<br />
liefert die API-Spezifikation zu Java.
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 48<br />
2.4.2 Host-Adressen<br />
Die Adressierung erfolgt mit der Klasse InetAddress. Ein solches Objekt<br />
enthält sowohl die IP-Adresse aus auch den DNS-Namen des jeweiligen<br />
Hosts. Diese Bestandteile liefern die Methoden String getHostName() und<br />
String getHostAddress(). Die Methode byte[] getAddress() liefert die IP-<br />
Adresse als byte-Array. Solche Adress-Objekte werden mittels der statischen Methoden<br />
InetAddress getByName(String host) und InetAddress getLocalHost()<br />
erzeugt. Beide werfen die Exception UnknownHostException wenn die Adresse nicht<br />
zu ermitteln ist. getByName() erwartet als Argument die IP-Adresse oder den DNS-<br />
Namen. Auch Multicast-Adressen sind möglich. Das folgende Progrämmchen demonstriert<br />
die Anwendung von InetAddress:<br />
import java.net.*;<br />
public class Resolver<br />
{<br />
public static void main(String[] args)<br />
{<br />
if (args.length != 1) {<br />
System.err.println("Usage: java Resolver ");<br />
System.exit(1);<br />
}<br />
try {<br />
// get requested address<br />
InetAddress addr = InetAddress.getByName(args[0]);<br />
System.out.println(addr.getHostName());<br />
System.out.println(addr.getHostAddress());<br />
} catch (UnknownHostException e) {<br />
System.err.println(e.toString());<br />
System.exit(1);<br />
}<br />
} // main<br />
} // class Resolver<br />
2.4.3 Client zur verbindungsorientierten Kommunikation<br />
Ähnlich wie beim Socket-Interface in C muss natürlich auch in Java die Verbindung<br />
zuerst aufgebaut werden. Die Initiative ergreift hier selbstverständlich ebenfalls der<br />
Client. Ihm steht die Klasse Socket zur Verfügung. Sie erlaubt – zumindest im<br />
Vergleich zum C-Interface – eine elegante Programmierung. Socket verfügt über
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 49<br />
verschiedene Konstruktoren. Mit ihnen wird ein Socket zur verbindungsorientierten<br />
Kommunikation angelegt. Sie wichtigsten Konstruktoren sind:<br />
public Socket(String host, int port)<br />
throws UnknownHostException, IOException<br />
public Socket(InetAddress address, int port)<br />
throws IOException<br />
Beide Varianten bauen auch gleich die Verbindung zum Server auf. Der erste dieser<br />
Konstruktoren erspart dem Programmierer auch das Hantieren mit der IP-Adresse<br />
des Servers. Der zweite Konstruktor ist besser geeignet, wenn die IP-Adresse des<br />
Servers mehrfach benötigt wird, dann muss die Adressauflösung nur einmal erfolgen.<br />
Die IOException signalisiert, dass der Socket nicht geöffnet werden konnte. Der<br />
Datenaustausch mit dem Server erfolgt über Streams. Die folgenden Methoden von<br />
Socket retournieren diese Streams:<br />
public InputStream getInputStream()<br />
throws IOException<br />
public OutputStream getOutputStream()<br />
throws IOException<br />
Die Methode close() beendet die Kommunikation und schließt den Socket. Ein<br />
Day-Time-Client sieht in Java so aus:<br />
import java.net.*;<br />
import java.io.*;<br />
public class DayTime<br />
{<br />
public static void main(String[] args)<br />
{<br />
if (args.length != 1) {<br />
System.err.println("Usage: java DayTime ");<br />
System.exit(1);<br />
}<br />
try {<br />
Socket sock = new Socket(args[0], 13);<br />
InputStream in = sock.getInputStream();<br />
int len;<br />
byte[] b = new byte[100];
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 50<br />
while ((len = in.read(b)) != -1) {<br />
System.out.write(b, 0, len);<br />
}<br />
in.close();<br />
sock.close();<br />
} catch (IOException e) {<br />
System.err.println(e.toString());<br />
System.exit(1);<br />
}<br />
} // main<br />
} // class DayTime<br />
2.4.4 Server zur verbindungsorientierten Kommunikation<br />
Ein Socket-Server unterscheidet sich in seinem Verhalten stark von dem des Clients.<br />
Deshalb existiert dafür auch eine eigene Klasse ServerSocket. Ihre wichtigsten Methoden<br />
sind der Konstruktor und accept():<br />
public ServerSocket(int port)<br />
throws IOException<br />
public Socket accept()<br />
throws IOException<br />
Der Konstruktor erzeugt einen Socket und verwendet default-Werte für die Routinen<br />
listen() und bind() (vgl. dazu die Beschreibung des C-Interface ab Seite<br />
34). Andere Konstruktoren erlauben hier Feineinstellungen. Socket accept() wartet<br />
genau wie beim C-Interface auf einen eingehenden Request eines Client. Die<br />
eigentliche Kommunikation erfolgt wieder mit Hilfe des von accept() retournierten<br />
Socket. Auch hier schließt close () wiederum den Socket.<br />
Auch in Java sind spezielle Maßnahmen notwendig, wenn mehr als ein Client<br />
gleichzeitig servisiert werden soll. Anders als die mit fork() erzeugten schwergewichtigen<br />
Prozesse werden in Java gerne leichtgewichtige Threads erzeugt. Bei ihnen<br />
werden u.a. keine getrennen Datenbereiche eingerichtet. Ein Echo-Server der diese<br />
Anforderungen erfüllt könnte folgendermaßen aussehen:<br />
import java.net.*;<br />
import java.io.*;<br />
public class EchoServer<br />
{
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 51<br />
public static void main(String[] args)<br />
{<br />
final int port = 7;<br />
int cnt = 0;<br />
try {<br />
System.out.println("Waiting for connection requests on port "<br />
+ port + "...");<br />
ServerSocket echod = new ServerSocket(port);<br />
while (true) {<br />
Socket socket = echod.accept();<br />
(new EchoClientThread(++cnt, socket)).start();<br />
}<br />
} catch (IOException e) {<br />
System.err.println(e.toString());<br />
System.exit(1);<br />
}<br />
} // main<br />
} // class EchoServer<br />
class EchoClientThread<br />
extends Thread<br />
{<br />
private int name;<br />
private Socket socket;<br />
public EchoClientThread(int name, Socket socket)<br />
{<br />
this.name = name;<br />
this.socket = socket;<br />
} // constructor EchoClientThread<br />
public void run()<br />
{<br />
String msg = "EchoServer: connection " + name;<br />
System.out.println(msg + " established");<br />
try {<br />
InputStream in = socket.getInputStream();<br />
OutputStream out = socket.getOutputStream();<br />
out.write((msg + "\r\n").getBytes());<br />
int c;
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 52<br />
while ((c = in.read()) != -1) {<br />
out.write((char)c);<br />
System.out.print((char)c);<br />
}<br />
System.out.println("connection " + name + " terminated");<br />
socket.close();<br />
} catch (IOException e) {<br />
System.err.println(e.toString());<br />
}<br />
} // run<br />
} // class EchoClientThread<br />
2.4.5 Verbindungslose Kommunikation<br />
Die Kommunikation mit UPD erfolgt mit Hilfe der Klasse DatagramSocket. Sie<br />
gestattet auch das Senden an Broadcast-Adressen. Ein – typischer – Konstruktor<br />
ist:<br />
public DatagramSocket(int port)<br />
throws SocketException<br />
Er erzeugt einen UDP-Socket und bindet ihn an den lokalen Port port. Der Datenaustausch<br />
erfolgt mit den folgenden Methoden:<br />
public void send(DatagramPacket p)<br />
throws IOException<br />
public void receive(DatagramPacket p)<br />
throws IOException<br />
Zu beachten ist, dass die Daten in Instanzen von DatagramPacket enthalten sind.<br />
Diese Objekte enthalten auch die Zieladresse und den Zielport.<br />
DatagramPacket selber besitzt wiederum einige Konstruktoren. Für das nachfolgende<br />
Senden von Daten ist<br />
public DatagramPacket(byte[] buf,<br />
int length,<br />
InetAddress address,<br />
int port)<br />
geeignet, während das Empfangen mit einem durch den Konstruktor<br />
public DatagramPacket(byte[] buf,<br />
int length)<br />
erzeugten Objekt erfolgen kann.
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 53<br />
2.5 Remote Procedure Calls<br />
2.5.1 Einführung<br />
Der RPC-Mechanismus (Remote Procedure Call) wurde ursprünglich von SUN entwickelt<br />
und propagiert. Die grundlegende Idee ist hier das nahezu völlige Verbergen<br />
der Details der Kommunikation vor den Applikationsprogrammen. Der Ansatz ist<br />
dabei der Aufruf von Prozeduren: Ein Server bietet eine Sammlung von Prozeduren<br />
an, die von einem oder mehreren Clients genutzt werden können.<br />
In der Applikationsebene erfolgt der Aufruf solcher Remote Procedures auf die<br />
gleiche Weise, wie der Aufruf konventioneller (lokaler) Prozeduren. Lediglich die Anzahl<br />
der Parameter ist etwas eingeschränkt. Natürlich kann der RPC-Mechanismus<br />
auch – zumindest in der Testphase – lokal genutzt werden. Ein späteres Verteilen<br />
der gesamten Applikation auf mehrere Rechner in einem Netzwerk ist dann ohne<br />
wesentliche Änderungen möglich.<br />
Prinzipiell sind für das Realisieren der RPCs einige Schritte beim Aufruf notwendig:<br />
• Die Eingangsparameter der aufzurufenden Prozedur müssen gemeinsam mit<br />
einer Kennung der gewünschten Prozedur vom Client an den Server übermittelt<br />
werden. Dazu ist es notwendig, diese Information in einen sequentiellen<br />
Datenstrom zu konvertieren. Dieser Vorgang heißt Serialisieren.<br />
• Am Server muss eine Verwaltungskomponente den Request und die serialisierten<br />
Daten entgegennehmen und an die gewünschte Prozedur übermitteln.<br />
Dazu ist es notwendig, die Daten zu Deserialisieren.<br />
• Der Aufruf der Prozedur liefert Werte für die Ausgangsparmeter, die analog<br />
an den Client retourniert werden.<br />
Der RPC-Mechanismus erlaubt lediglich jeweils einen Eingangs- und einen Ausgangsparameter<br />
für jede Prozedur. Diese können allerdings aus beliebigen Strukturen<br />
bestehen.<br />
Ein RCP-Server verwaltet eine Sammlung von Funktionen, die er am Netz anbietet.<br />
Diese Menge der Funktionen bildet ein Programm in einer bestimmten Version.<br />
Auf einem Rechner können natürlich mehrere Server – mit unter Umständen verschiedenen<br />
Versionen des gleichen Programmes – laufen. Die Identifizierung erfolgt<br />
auf allen drei Hierarchieebenen durch Nummern. Diese muss der Client, zusätzlich<br />
zur Adresse des Server-Rechners, angeben. Um Überschneidungen zu vermeiden,<br />
hat SUN den Bereich der Programm-Nummern aufgeteilt (vgl. Tabelle 2.3 auf der<br />
nächsten Seite).
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 54<br />
Bereich Programm-Nummer Beschreibung<br />
0x00000000 - 0x1FFFFFFF Reserviert für über SUN zu registrierende<br />
Programme von globalem Interesse<br />
0x20000000 - 0x3FFFFFFF Vorgesehen für lokale Dienste und Debugging<br />
0x40000000 - 0x5FFFFFFF Dynamisch vergebene Nummern für kurzfristige<br />
Aktivitäten<br />
0x60000000 - 0xFFFFFFFF Reserviert<br />
Tabelle 2.3: Nummernbereiche für RPC-Programme<br />
2.5.2 XDR – Extended Data Representation<br />
Probleme beim Transfer der Parameter entstehen durch die unter Umständen unterschiedliche<br />
interne Darstellung von Daten in den beteiligten Rechnern. Konkret<br />
sind die folgenden Unterschiede zu beachten:<br />
• Repräsentation der einzelnen Datentypen (Bytefolge, Länge, Zeichensatz)<br />
• Alignment (manchmal auch Unterschiede zwischen Compiler für den gleichen<br />
Prozessor)<br />
All diese potentiell vorhandenen Unterschiede zwischen Client und Server verhindern<br />
eine offene Kommunikation. Der RPC-Mechanismus löst dieses Problem mit<br />
einer einheitlichen Darstellung bei der Übertragung. Es wird auch der Vorrat der<br />
verfügbaren Datentypen festgelegt.<br />
Die gewählte Darstellung heißt Extended Data Representation (XDR). Die Beilage<br />
enthält die in XDR vorhandenen Datentypen und deren Repräsentation bei der<br />
Übertragung. Dazu einige generelle Bemerkungen:<br />
• Für ganze Zahlen verwendet XDR die Notation Big-endian. Hier beginnt<br />
die Übertragung mit dem höherwertigen Byte. In der Regel verwenden die<br />
SPARC-Prozessoren von SUN und die Prozessoren von Motorola diese Darstellung<br />
auch für die interne Repräsentation. Andererseits arbeiten Intel-CPUs<br />
mit der Little-endian-Notation. Ganze Zahlen belegen in XDR 4 oder 8 Byte.<br />
• Jede Variable beginnt auf einem durch 4 teilbaren Offset (Alignment).<br />
• Für Wiederholungen (arrays) ist neben solcher mit konstanter Länge auch ein<br />
Datentyp mit variabler Länge vorhanden. In diesem Fall wird auch die aktuell<br />
belegte Länge übermittelt.<br />
• Varianten (union) sind erlaubt, die Angabe eines tag ist zwingend notwendig.
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 55<br />
• XDR codiert prinzipiell nur die Dateninhalte, nicht jedoch den zugrundeliegenden<br />
Datentyp. Es muss also eine implizite Absprache zwischen Client und<br />
Server über die Struktur der Parameterdaten bestehen.<br />
Die XDR-Bibliothek stellt Routinen für das Serialisieren bzw. Deserialisieren von<br />
Daten zur Verfügung. Die Konvertierung erfolgt hier immer zwischen den Daten in<br />
der internen Darstellung und einem sog. XDR-Stream, der die Daten in serialisierter<br />
Form aufnehmen kann. Für jeden Datentyp (einfach oder strukturiert) ist eine Routine<br />
zur Konvertiertung vorhanden, diese Routinen heißen auch XDR-Filter. Die<br />
Gesamtstruktur des zu konvertierenden Parameters gibt hier die Reihenfolge des<br />
Aufrufes vor. Die Routinen der XDR-Bibliothek arbeiten wahlweise in eine bestimmte<br />
Richtung (intern→XDR-Stream: serialisieren, XDR-Stream→intern: deserialisieren).<br />
Die tatsächliche Richtung wird beim Anlegen des XDR-Streams festgelegt.<br />
Hier ein Beispiel:<br />
Die Funktion xdrstdio_create() verbindet einen XDR-Stream mit der Console.<br />
#include <br />
#include <br />
void xdrstdio_create (xdrs, file, op)<br />
XDR *xdrs;<br />
FILE *file;<br />
enum xdr_op op;<br />
Das folgende Fragment bereitet den XDR-Stream handle für das Serialisieren, d.h.<br />
für das Konvertieren von der internen Darstellung und das Versenden an die Console<br />
vor. Die umgekehrte Richtung wird mit XDR_DECODE selektiert:<br />
XDR handle;<br />
xdrstdio_create(&handle, stdout, XDR_ENCODE);<br />
Die Funktion xdr_int() konvertiert einen Wert vom Typ int:<br />
#include <br />
bool_t<br />
xdr_int (xdrs, ip)<br />
XDR *xdrs;<br />
int *ip;
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 56<br />
Es retourniert TRUE, falls die Konvertierung erfolgreich war; im Fehlerfall ist der<br />
Return-Wert FALSE. Für jeden einfachen Datentyp ist ein solches Filter vorhanden.<br />
Die XDR-Bibliothek kennt auch komplexe Datentypen für Wiederholungen:<br />
• xdr_string() für Zeichenketten<br />
• xdr_opaque() für anonyme Bytefolgen fester Länge<br />
• xdr_bytes() für anonyme Bytefolgen variabler Länge<br />
• xdr_vector() für beliebige Arrays fester Länge<br />
• xdr_array() für beliebige Arrays variabler Länge<br />
So hat z.B. xdr_array() den Prototyp:<br />
#include <br />
bool_t<br />
xdr_array (xdrs, arrp, sizep, maxsize, elsize, elproc)<br />
XDR *xdrs;<br />
char **arrp;<br />
u_int *sizep;<br />
u_int maxsize;<br />
u_int elsize;<br />
xdrproc_t elproc;<br />
Der Parameter xdrs ist der XDR-Stream, *arrp zeigt auf das Feld variabler Länge,<br />
sizep zeigt auf die Anzahl der Array-Elemente, maxsize gibt die maximale Anzahl<br />
der Elemente an, elsize gibt die Größe eines Eintrages im Array an und elproc<br />
ist die Adresse des XDR-Filters zum Codieren bzw. Decodieren eines Elementes aus<br />
dem Array. Zeigt beim Decodieren der Zeiger *arrp auf NULL, dann legt xdr_array<br />
den erforderlichen Speicher selbständig an und trägt die Adresse des Speichers in<br />
*arrp ein. Damit muss die Applikation nicht pauschal Speicher für maxsize Elemente<br />
allokieren. Beim Codieren muss jedoch die Applikation selber den Speicher vor<br />
dem Aufruf von xdr_array anlegen. Ähnlich arbeiten auch die anderen Routinen.<br />
Der von diesen Routinen beim Decodieren allokierte Speicher nimmt die Daten<br />
auf. So kann anschließend die Applikation auf diese Daten zugreifen. Allerdings muss<br />
dieser Speicher später von der Applikation explizit freigegeben werden. Dazu dient<br />
die Funktion xdr_free:<br />
#include <br />
void xdr_free (proc, objp)
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 57<br />
xdrproc_t proc;<br />
char *objp;<br />
Sie verwendet das – zuvor zum Decodieren genutzte – XDR-Filter proc um den<br />
vom Zeiger objp referenzierten Speicherbereich freizugeben. Das Filter wird dabei<br />
mit einer dritten Bearbeitungsart (XDR_FREE) abgearbeitet. Dieses Filter kennt die<br />
Struktur von *objp und gibt die zuvor beim Decodieren darin allokierten Speicherbereiche<br />
frei.<br />
Records, also Wiederholungen von Feldern unterschiedlichen Datentyps werden<br />
durch eine Folge von Aufrufen an die obigen Routinen realisiert.<br />
Das Erstellen individueller Filter für eigene Datentypen läßt sich durch das Programm<br />
rpcgen weitgehend automatisieren. Es arbeitet stapelorientiert und benötigt<br />
eine Beschreibung der Parametertypen in XDR-Notation. Daraus erstellt rpcgen eine<br />
Übersetzung dieser Datentypen in C-Syntax, sowie ein Modul mit fertigen Konvertierungsfiltern<br />
für diese Datentypen.<br />
2.5.3 RPC-Programmierung<br />
Das XDR-Format legt zwar das Format der Parameter bei der Übertragung fest,<br />
offen bleibt jedoch noch, in welcher Form die Parameter übertragen werden. Auch<br />
das Identifizieren der gewünschten Funktion am Server und der Modus für den eigentlichen<br />
Aufruf der Funktion ist noch offen.<br />
Auffinden eines RPC-Servers<br />
Ein RPC-Server ist für die Prozeduren eines Programmes (einer Version) zuständig.<br />
Er gestattet den Zugriff auf diese Funktionen über das Netzwerk mit einem dynamisch<br />
allokierten Port. Den gewählten Port übermittelt er an einen Verwaltungsprozess,<br />
den Portmapper. Dieser ist über den well known port 111 erreichbar. Der<br />
Portmapper sammelt alle Registrierungen der lokalen RPC-Server und gibt darüber<br />
potentiellen RPC-Clients Auskunft (vgl. das Kommando rpcinfo -p).<br />
Prozeduraufruf<br />
Die Details der Kommunikation werden bei RPCs völlig vor der Anwendung verborgen.<br />
Eine Schale stellt dem Client-Teil eine sog. Stub-Prozedur zur Verfügung. Diese<br />
hat die gleiche Parameterliste, wie die Applikationsfunktion im Server. Allerdings<br />
transferiert sie die übergebenen Parameter zum Server und stellt das Ergebnis in<br />
Form der Ausgangsparameter bereit. Zum Serialisieren und Deserialisieren verwendet<br />
der Client-Stub ein XDR-Filter, das speziell auf die Eingangsparameter abgestimmt<br />
ist (Serialisierung), sowie ein weiteres XDR-Filter für die Ausgangsparameter
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 58<br />
(Deserialisierung). Am Server läuft als Hauptprozedur ein sog. Server-Wrapper. Er<br />
nimmt die vom Client-Stub übermittelten Eingangsparameter auf, und leitet sie an<br />
die gewünschte Server-Funktion weiter. Die Ausgangsparameter sendet er zurück an<br />
den Client-Stub. Auch hier werden zum Serialisieren und Deserialisieren die gleichen<br />
XDR-Filter verwendet. Allerdings betreibt sie der Server-Wrapper in umgekehrter<br />
Richtung wie der Client-Stub. Gemeinsam agieren der Client-Stub und der Server-<br />
Wrapper als Koppelglied zwischen den verteilten Anwendungskomponenten. Für<br />
den Transfer der Parameter verwenden sie XDR. Damit funktioniert diese Kopplung<br />
auch dann noch, wenn die beteiligten Rechner mit unterschiedlichen internen<br />
Darstellungsformen arbeiten.<br />
Bisher hat rpcgen lediglich die XDR-Filter und die Include-Datei für die Parameterstruktur<br />
erzeugt. rpcgen kann jedoch auch den Client-Stub und den Server-<br />
Wrapper erstellen. Dazu muss rpcgen noch die Adressdaten der Funktionen kennen.<br />
Diese Adressinformation besteht aus den IDs für Programm, Version und Funktion.<br />
2.5.4 Beispiel<br />
Die folgenden Listings zeigen den Umgang mit rpcgen für ein einfaches verteiltes<br />
Programm: Der Server bestimmt Primzahlen in einem wählbaren Bereich. Eingabeparameter<br />
ist hier der Bereich (von, bis). Der Ausgabeparameter ist ein array variabler<br />
Länge mit den in diesem Bereich vorhandenen Primzahlen. Die Datei primes.x<br />
enthält diese Parameter in XDR-Notation. Sie endet mit der Deklaration der IDs<br />
für Programm, Version und Funktion:<br />
/*<br />
* Max size of array of primes we can return<br />
*/<br />
const MAXPRIMES = 1000;<br />
/*<br />
* Input parameters: min, max range<br />
*/<br />
struct prime_request<br />
{<br />
int min;<br />
int max;<br />
};<br />
/*<br />
* Output parameter: an array of variable length<br />
*/<br />
struct prime_result<br />
{
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 59<br />
};<br />
int array < MAXPRIMES >;<br />
/*<br />
* Program definition<br />
*/<br />
program PRIMEPROG<br />
{<br />
version PRIMEVERS<br />
{<br />
prime_result FIND_PRIMES(prime_request) = 1;<br />
} = 1; /* Version 1 */<br />
} = 0x2000008a; /* program number */<br />
Das Übersetzen von primes.x mit rpcgen liefert insgesamt 4 neue Dateien:<br />
• primes.h enthält die Datenstrukturen aus primes.x in C-Notation. Sie bilden<br />
die zu den XDR-Filtern passenden Datentypen.<br />
• primes_xdr.c exportiert die XDR-Filter zum Bearbeiten der Ein- und Ausgangsparameter.<br />
• primes_clnt.c ist der Client-Stub. Er exportiert die Funktion<br />
find_primes_1. Sie ist die Version 1 von find_primes und übernimmt<br />
den Transfer der Parameter auf der Seite des Client.<br />
• primes_svc.c ist der Server-Wrapper. Er installiert in seiner main-Prozedur<br />
den Server. Weiters nimmt er die Requests von Clients entgegen, ruft die Applikationsfunktion(en)<br />
auf, und retourniert die Ergebnisse an die Clients.<br />
Der Applikationsprogrammierer muss zusätzlich zu primes.x lediglich die Applikationsfunktion(en)<br />
am Server und eine entsprechende Prozedur für den Client<br />
erstellen, welche die Server-Funktion(en) nutzt. Dazu müssen diese Programme<br />
natürlich die Struktur der Parameter in C-Notation kennen. Hier der Inhalt der<br />
von rpcgen erstellten Datei primes.h:<br />
#define MAXPRIMES 1000<br />
struct prime_request {<br />
int min;<br />
int max;<br />
};<br />
typedef struct prime_request prime_request;<br />
bool_t xdr_prime_request();
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 60<br />
struct prime_result {<br />
struct {<br />
u_int array_len;<br />
int *array_val;<br />
} array;<br />
};<br />
typedef struct prime_result prime_result;<br />
bool_t xdr_prime_result();<br />
#define PRIMEPROG ((u_long)0x2000008a)<br />
#define PRIMEVERS ((u_long)1)<br />
#define FIND_PRIMES ((u_long)1)<br />
extern prime_result *find_primes_1();<br />
Für den Server kommt vom Applikationsprogrammierer die Funktion<br />
find_primes_1. Sie bestimmt die Primzahlen im gewählten Bereich. Dazu verwendent<br />
diese Funktion (zumindest hier) das Unterprogramm is_prime:<br />
/*<br />
* Prime numbers: RPC - Server<br />
*/<br />
#include <br />
#include "primes.h" /* Headerfile generate with rpcgen */<br />
int<br />
isprime(int n){<br />
int i;<br />
};<br />
for (i = 2; i * i
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 61<br />
}<br />
static int prime_array[MAXPRIMES];<br />
int i,<br />
cnt = 0;<br />
for (i = request->min; i max; i++){<br />
if (isprime(i))<br />
prime_array[cnt++] = i;<br />
}<br />
/*<br />
* Assemble the reply packet. Note that the variable length<br />
* array we are returning is really a struct with the<br />
* element cnt, and a pointer to the first element.<br />
*/<br />
result.array.array_len = cnt;<br />
result.array.array_val = prime_array;<br />
return &result;<br />
Das Ergebnis wird als statische Variable retourniert. Dies ist notwendig, da die Lebensdauer<br />
lokaler Variablen mit dem Terminieren der definierenden Funktion endet.<br />
Der Client liest den gewünschten Bereich von der Kommandozeile ein, ruft die<br />
Serverfunktion auf und zeigt die gefundenen Primzahlen an:<br />
/*<br />
* primes_main.c: main procedure of primes client<br />
*/<br />
/*<br />
* accepts hostname of primes server and range of primes to<br />
* find from the command line, calls primes server, reports<br />
* primes found on console<br />
*/<br />
#include <br />
#include "primes.h"<br />
main(int argc, char *argv[]){<br />
int i;<br />
CLIENT *client;<br />
prime_result *result; /* return parameter */<br />
prime_request request; /* function parameter */<br />
if (argc != 4){<br />
printf("Usage: %s host min max \n", argv[0]);<br />
exit(1);<br />
}
KAPITEL 2. PROGRAMMIERSCHNITTSTELLEN UNTER UNIX 62<br />
}<br />
client = clnt_create(argv[1], PRIMEPROG, PRIMEVERS, "tcp");<br />
if (client == NULL)<br />
{<br />
clnt_pcreateerror(argv[1]);<br />
exit(1);<br />
}<br />
request.min = atoi(argv[2]);<br />
request.max = atoi(argv[3]);<br />
/*<br />
* Call Remote Procedure now<br />
*/<br />
result = find_primes_1(&request, client);<br />
if (result == NULL){<br />
clnt_perror(client, argv[1]);<br />
exit(1);<br />
}<br />
/*<br />
* print the results<br />
*/<br />
printf("count of primes found: %d\n", result->array.array_len);<br />
for (i = 0; i < result->array.array_len; i++){<br />
printf("%8d", result->array.array_val[i]);<br />
}<br />
printf("\n");<br />
/*<br />
* free memory allocated by the XDR-filter<br />
*/<br />
xdr_free(xdr_prime_result, result);<br />
Das XDR-Filter hat im Zuge des Deserialisierens den entsprechenden Speicher zum<br />
Aufnehmen der Ausgabeparameter allokiert. Der Speicher steht der Applikation für<br />
die weitere Nutzung zur Verfügung. Jedoch muss die Applikation diesen Speicher<br />
anschließend durch einen Aufruf von xdr_free explizit freigeben. Nur sie selber<br />
weiß, ab wann sie den vom Filter angeforderten Speicherbereich nicht mehr benötigt.<br />
Falls Client und Server mit unterschiedlichen internen Darstellungen arbeiten,<br />
muss das Filter primes_xdr.c und auch der Client-Stub bzw. der Server-Wrapper<br />
auf dem jeweiligen Zielrechner übersetzt werden. Diese Dateien können jedoch einfach<br />
aus primes.x erstellt werden. Deshalb liefern Anbieter eines RPC-Servers in<br />
der Regel auch die korrespondierende x-Datei aus.
Literaturverzeichnis<br />
Comer, D. E., [1995]: Internetworking with TCP/IP, Vol. I: Principles, Protocols,<br />
and Architecture, Prentice-Hall, dritte Auflage.<br />
Comer, D. E. und Stevens, D. L., [1996]: Internetworking with TCP/IP, Vol. III,<br />
Client-Server Programming and Applications, BSD Socket Version, Prentice-Hall,<br />
zweite Auflage.<br />
Comer, D. E. und Stevens, D. L., [1997]: Internetworking with TCP/IP, Vol. III,<br />
Client-Server Programming and Applications, Windows Sockets Version, Prentice-<br />
Hall.<br />
Douba, S., [1995]: Networking Unix, Sams Publishing, Indianapolis.<br />
Hafner, K. und Lyon, M., [1996]: ARPA KADABRA, Die Geschichte des INTER-<br />
NET, dpunkt.Verlag.<br />
Hall, B., [1996]: Beej’s Guide to Network Programming – Using Internet Sockets,<br />
last visited on APR/9/2002.<br />
http://www.ecst.csuchico.edu/~beej/guide/net/<br />
Halsall, F., [1996]: Data Communications, Computer Networks and Open Systems,<br />
Addison–Wesley, vierte Auflage.<br />
Hart, J. M. und Rosenberg, B., [1995]: Client/Server Computing for Technical Professionals,<br />
Addison–Wesley, Reading, Massachusetts.<br />
Krüger, G., [2000]: Go To Java 2 – Handbuch der Java-Programmierung, Addison–<br />
Wesely, zweite Auflage.<br />
Rago, S. A., [1993]: UNIX System V Network Programming, Addison Wesley, Reading<br />
Massachusetts.<br />
RFC-Editor, [2001]: last visited on FEB/27/2001.<br />
http://www.rfc-editor.org/<br />
Tanenbaum, A. S., [1998]: Computernetzwerke, Prentice-Hall, dritte Auflage.<br />
63