Hintergrundwissen

Sockets und Dateideskriptoren

Dateideskriptoren sind unter UNIX kleine ganze Zahlen, die einer mit open() geöffneten Datei zugeordnet sind. Diese dürfen nicht mit den FILE-Zeigern der stdio-Funktionen verwechselt werden. Auf Dateideskriptoren können Funktionen wie read(), write(), close(), aber auch ioctl() und dup() angewendet werden.

Unter UNIX gibt es eine Reihe weiterer Objekte, die sich in erster Näherung wie Dateien verhalten und ebenfalls über kleine ganze Zahlen identifizieren lassen. Zu diesen gehören Sockets, aber auch Pipes. Dies ist natürlich kein Zufall, sondern so eine Art objektorientiertes Design. Wollte man dies als Klassendiagramm darstellen (was natürlich gewagt ist, da es sich ja nicht wirklich um Klassen handelt), dann ergäbe sich dieser Anblick:

Klassendiagramm für File, Socket und Pipe

Das heißt man kann alles, was hier als Methoden von Abstract File dargestellt ist, sowohl mit Dateien als auch mit Sockets anstellen. Außerdem können Funktionen der Standardbibliothek genutzt werden, die mit Dateideskriptoren arbeiten. Es ist also z.B. durchaus möglich, einen Socket per fdopen() zu einem FILE-Zeiger zu machen und diesen dann mit fgets() zum zeilenweisen Lesen zu nutzen. Man sollte aber Aufrufe von High-Level und Low-Level-Funktionen nicht beliebig mischen, weil Daten in Puffern der Standardbibliothek stecken bleiben könnten. Wenn man z.B. direkt nach einem fgets() ein read() aufruft, dann kann es sein, dass jene Teile fehlen, die fgets() zum Fertigstellen einer weiteren Zeile "aufspart".

EADDRINUSE

Manchmal schlägt das Binden eines Sockets mit dem Fehlercode EADDRINUSE fehl. Um das genau verstehen zu können, muss man sich das Zustandsdiagramm von TCP ansehen. Für die Praxis reicht es zu wissen, dass man dieses Problem bekommt, wenn eine Socket-Verbindung nicht ordentlich abgebaut wird (was gerade in der Entwicklung oft passiert, wenn man einen Server ständig "abwürgt", schnell etwas ändert, neu compiliert und dann wieder starten will). Der Listen-Socket verbleibt dann in einem Wartezustand, der erst nach einem Timeout von 1-2 Minuten verlassen wird. Zuvor ist das Binden eines neuen Sockets an die gleiche lokale Adresse nicht möglich.

Die Socket-Option SO_REUSEADDR erlaubt es, auch in diesen Situationen einen Socket zu binden. Weiterhin nicht möglich ist es einen Socket an eine Adresse zu binden, an der bereits ein aktiver Server-Socket lauscht.

Out-of-band-Daten

Die Funktionen send() und recv() erlauben die Angabe des Flags MSG_OOB. Mit diesem Flag werden Out-of-band-Daten gesendet bzw. empfangen. Das ist gewissermaßen eine Überholspur des normalen Datenstroms und wird in TCP durch das Urgent-Flag und den Urgent-Pointer implementiert. Der ursprüngliche Gedanke war z.B. Steuersequenzen wie Ctrl+C vorrangig an eine Remote-Shell zu übertragen. In der Praxis haben Out-of-band-Daten eine geringe Bedeutung und sollten besser nicht benutzt werden, weil die Gefahr groß ist, dass man auf Implementierungslücken stösst oder andere Teilnehmer "verwirrt".

Byte Order

Unter Byte Order versteht man die Reihenfolge der Bytes in einem größeren Ganzzahltypen, wie z.B. einem 32-Bit-Wert. Hier gibt es die beiden Varianten Little Endian und Big Endian. Beide kommen als natürliche Darstellungsweisen von Mikroprozessoren vor, z.B. Little Endian bei allem was vom Intel 8086 abstammt und Big Endian bei Motorola 68xxx.

Die Bytes eines mehrbytigen Ganzzahltypen haben unterschiedliche Wertigkeiten. Das niedrigwertigste Byte zählt einfach, das nächste mit Faktor 256, das darauf mit 65536 und das darauf mit 16777216 (also 20, 28, 216 und 224). Betrachtet man nun, an welchen Speicheradressen die einzelnen Bytes stehen, dann gilt:

Anhand eines Beispiels mit dem Zahlenwert 0xdeadbeef:

Adresse Little Endian Big Endian
x + 0x00 0xef 0xde
x + 0x01 0xbe 0xad
x + 0x02 0xad 0xbe
x + 0x03 0xde 0xef

Weil man bei Netzwerkprotokollen aber keine Plattformabhängigkeit haben möchte, hat man sich auf eine Byte Order geeinigt: Big Endian. Diese wird auch als Network Byte Order bezeichnet. Die Byte Order auf dem lokalen System wird als Host Byte Order bezeichnet. Dementsprechend ist auf Systemen mit Big-Endian-Darstellung Network Byte Order und Host Byte Order identisch. Die Funktionen zur Umwandlung stehen auf beiden Arten von Systemen zur Verfügung, sodass Programmcode plattformunabhängig gestaltet werden kann: man ruft sie einfach immer auf und die jeweilige Bibliotheks-Implementierung sorgt dafür, dass das Richtige passiert.

Nicht-blockierende Sockets

Unter UNIX gibt es für Sockets die Möglichkeit, mittels fcntl() das Flag O_NONBLOCK zu setzen. Damit blockieren Systemaufrufe wie recv() nicht mehr, sondern kehren sofort zurück, ggf. mit dem Fehlercode EWOULDBLOCK wenn es nichts zu tun gibt. Dies scheint eine gute Lösung für Situationen zu sein, bei denen nicht bekannt ist, ob auf einem Socket Daten verfügbar sind, und ein Blockieren das Abarbeiten anderer Programmteile verhindern würde. Sie bringt aber auch andere Probleme mit sich, nämlich wenn tatsächlich daruf gewartet werden muss, dass Daten verfügbar werden, ohne dass in der Zwischenzeit andere Arbeit verrichtet werden kann. Dann gibt es nämlich im wesentlichen zwei Lösungen:

Hier sind blockierende Systemaufrufe viel effizienter. Bevor man also zu nicht-blockierenden Aufrufen greift sollte man zunächst über das Motiv nachdenken und ggf. prüfen, ob sich die Aufgabenstellung auch mit select lösen lässt.

Asynchrone Ein-/Ausgabe

Ein weiteres Feature unter UNIX ist die asynchrone Ein-/Ausgabe, die ebenfalls via fcntl() aktiviert werden kann. Dabei wird einerseits das Flag O_ASYNC gesetzt, andererseits ein Besitzer festgelegt. Der Besitzer wird über die Prozess-ID definiert und erhält ein Signal, wenn der Socket lesbar bzw. beschreibbar geworden ist. Soweit nicht anders festgelegt ist dies SIGIO, kann aber auf ein beliebiges Signal konfiguriert werden (z.B. um mehrere Sockets auseinander zu halten). Um mit dem Signal etwas anfangen zu können muss ein Signal-Handler installiert werden (sigaction(), siehe auch Beispiel-Programm zu Tour 6, Prozesse). Dieser wird asynchron zum normalen Programmfluss ausgeführt, d.h. man handelt sich ähnliche Probleme ein wie mit Threads.

Meistens setzt man im Signal-Handler ein Flag, um im Hauptprogrammfluss darauf reagieren zu können. Hat man aber eine solche zentrale Stelle im Hauptprogramm, an der man die Flags prüfen kann, so lässt sich hier meistens auch select an Stelle asynchroner Sockets einsetzen. Dies bietet außerdem den Vorteil portabel zu sein.


Zurück zur Hauptseite