Skriptum

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