08.10.2013 Aufrufe

Skriptum

Skriptum

Skriptum

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!