dreckiges Insiderwissen

In diesem Kapitel geht's in die Tiefen des Systems. Einiges davon wird sich auf UNIX beziehen (falls nicht anders angegeben), manches auf Windows, und manches ist sogar portabel. Auf jeden Fall sollte man es nicht blind glauben, sondern erstmal testen. :-)

socklen_t, size_t, ssize_t, ...

In den Deklarationen der Funktionen stecken manchmal solche Datentypen, bei denen erstmal unklar ist, was sich dahinter verbirgt. Das ist gewissermaßen der Sinn davon, man soll nicht auf bestimmte Implementierungsdetails eingehen, sondern davon unabhängig sein. Aber als Programmierer muß man zwangsläufig manchmal wissen, was sich hinter den Typen verbirgt.

Die ersten Anlaufstellen sind /usr/include/ und /usr/include/sys/. Hier sollte man mit grep etwas wühlen. Wenn man Entwicklungsumgebungen nutzt, können die vielleicht die passende Deklaration finden (vim kann sowas z.B.).

Bei size_t und socklen_t handelt es sich um vorzeichenlose Grössen, das erste s in ssize_t steht für signed (denn die Syscalls geben ja -1 im Fehlerfall zurück). Hier ist man im Allgemeinen mit (unsigned) int gut beraten (FreeBSD 4.9), aber letzendlich muß man das natürlich nachsehen oder ausprobieren. Insbesondere in den derzeitigen Wirren der Zeit zur Umstellung auf 64 Bit gibt's da manchmal was neues (man denke an den Ärger mit Dateien > 2 Gigabyte).

Sockets und High-Level-I/O

Der Grundsatz ist: don't mix low-level-I/O and high-level-I/O. Das weiß jeder, der zwischen Aufrufen von fgets() mal mit dem eigentlichen Deskriptor gearbeitet hat. Die High-Level-I/O ist aber sehr bequem, gerade fgets() kann einem einiges erleichtern. Es gibt das hübsche Bindeglied fdopen(), das aus einem Filedeskriptor eine FILE-Struktur zaubert, die man dann einsetzen kann. Wer möchte kann das mal ausprobieren, auch bei fprintf() macht das Laune. Das fclose() nicht vergessen.

Sleep mit select()

Portabel kann man meistens nur in Sekundenauflösung schlafen. select() bietet mit dem Timeout-Parameter eine portable Möglichkeit für genauere Sleeps an. Das wäre z.B. so machbar:

struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 35000;     /* 35000 Mikrosekunden = 35 Millisekunden */
select(0, NULL, NULL, NULL, &tv);
    

Dabei darf man sich aber nicht durch die Auflösung im Mikrosekundenbereich täuschen lassen. In Wirklichkeit ist die Genauigkeit eher so im zweistelligen Millisekundenbereich. Wer genaue Sleeps oder Timer braucht, sollte sich mit den Möglichkeiten seines Betriebssystems vertraut machen.

Nicht-blockierende Sockets

Für alle, die es dennoch tun wollen, hier Code der's tut:

#ifdef WIN32
    unsigned long ul = 1;
    ioctlsocket(s, FIONBIO, &ul);
#else
    fcntl(s, F_SETFL, O_NONBLOCK);
#endif
    

Unter Windows reicht hierbei die winsock.h, unter UNIX wird die fcntl.h zusätzlich benötigt.

recv()/read(), send()/write()

Unter UNIX sind Sockets fast das gleiche wie Dateien. Man kann sie ebenfalls mit read() lesen und mit write() beschreiben. Da der einzige Unterschied zu recv() und send() im letzten Parameter besteht, der in unseren Fällen immer 0 ist, kann man auch getrost read() und write() nehmen.

Unter Windows ist das natürlich anders, da gibt es read() und write() erstmal nicht. Wer portabel bleiben will, sollte also trotzdem recv() und send() verwenden, da dies auf beiden Plattformen klappt.

IP-Adressen hochzählen

Wer mit Sockets programmiert wird früher oder später einen Portscanner schreiben, der einfach alle Ports mit connect() abklappert. Bald wird auch der Wunsch auftauchen, einen Bereich von IP-Adressen zu scannen.

Der Tipp: wenn man die IP-Adressen in Host Byte Order umgewandelt hat, dann sind es ganz normale Zahlen, und 192.168.1.2 folgt tatsächlich auf 192.168.1.1.

Erfahrungsgemäß schauen die Leute sich nämlich die Ausgabe in Network Byte Order an, und halten es für etwas sehr mystisches... :-)

Beispielcode findet sich unter Beispielcode.

Timeouts von connect()

Es ist plattformabhängig, wie lange ein connect() versucht eine Verbindung aufzubauen. Unter Windows werden defaultmäßig vier Versuche unternommen (der erste beim Aufruf, dann drei Wiederholungen nach 3, 6 und 12 Sekunden, was zu einem Rückkehren nach 21 Sekunden führt). Die UNIX-Varianten die ich bisher benutzt habe geben schneller auf, sodaß hier selten der Wunsch besteht etwas am Normalverhalten zu ändern. Im Abschnitt Beispielcode gibt es ein kleines Programm, das unter Windows ein "schnelleres connect()" implementiert. Prinzipiell kann das so auch auf UNIX übertragen werden.

Lesen von der Konsole

Für konsolenbasierte Clients bietet sich unter UNIX select() und STDIN_FILENO an. Unter Windows klappt das nicht. Der richtige Ansatz ist hier einen Thread zu erzeugen (CreateThread(), siehe win32.hlp), der mit fgets() auf stdin blockiert, und aktiv wird sobald eine Zeile gelesen wurde. Siehe auch Beispielcode in der Rubrik Beispielcode.