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