Wie sieht's aus?
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
FD_SET(fd, &fdset);
FD_CLR(fd, &fdset);
FD_ISSET(fd, &fdset);
FD_ZERO(&fdset);
Was tut's?
select() ist ein Monster. Es erspart einem so viel krankes Pfuschen mit nicht-blockierenden Sockets, daß man es auf jeden Fall in- und auswendig kennen sollte. Mit select() kann man, vereinfacht gesagt, schauen ob man von einem Filedeskriptor lesen kann, bevor man es versucht. Das gilt auch für's Schreiben, wobei das wesentlich seltener sinnvoll scheint (ich selbst habe es bisher eigentlich nie dafür benutzt). Praktisch gesehen ermöglicht das auch von mehreren Sockets zu lesen.
Das Konzept ist recht simpel: man legt ein Set aus Filedeskriptoren an, die man überwachen möchte. Das ist ein Ding vom Typ fd_set, wie es als zweiter, dritter und vierter Parameter vorkommt. Abhängig davon als welchen Parameter man das Set angibt, werden verschiedene Aktionen überwacht. Man kann also drei getrennte Sets anlegen, mit unterschiedlichen oder gleichen Deskriptoren drin.
Das ganze wird noch praktischer durch die Möglichkeit des Timeouts. Möglich ist sofortiges Zurückkehren ohne Warten (also ohne Blockieren), Zurückkehren nach Ablauf eines Timeouts (damit kann man auch portabel Timer mit genauerer Auflösung als mit sleep() basteln, siehe auch dreckiges Insiderwissen) oder Blockieren, also Zurückkehren sobald wirklich was passiert ist.
Wenn man nun also zu nicht-blockierenden Sockets greifen will, weil man beispielsweise von mehreren Sockets quasi-gleichzeitig lesen will, kann man auch alle normalen Sockets in ein Set kloppen und mit select() darauf warten lassen, wo was passiert, und dann darauf reagieren, als hätte man die ganze Zeit nur darauf gewartet. Der Vorteil liegt darin, daß das System arg geschont wird (es blockiert in select() und verbraucht keine CPU-Zeit, während man bei nicht-blockierenden Sockets ständig nachsehen muß und damit die CPU zur Weißglut bringt).
select() kann übrigens nicht nur verwendet werden, um herauszufinden ob von einem Socket gelesen werden kann ohne zu blockieren. Im Zusammenhang mit connect() ergibt sich noch ein Szenario, nämlich zu warten, bis ein Socket schreibbar wird. Damit kann man "unendlich" langes Warten beim Aufbauen einer Verbindung vermeiden. Man macht dazu einen Socket nicht-blockierend, löst ein connect() aus, wartet danach mit select() darauf, daß dieser Socket schreibbar wird, und kann danach damit weiter arbeiten, als hätte man ein normales connect() gemacht (man muß ihn natürlich wieder blockierend machen, wenn man ihn normal verwenden möchte). Zu diesem Vorgehen gibt es ebenfalls ein Beispiel im Abschnitt Beispielcode.
select() arbeitet übrigens, wie viele der anderen "Socket"-Funktionen auch, ebenfalls auf Deskriptoren wie sie von pipe() oder open() geliefert werden. Damit ist es z.B. sehr simpel einen Konsolen-Client zu schreiben, der auf die Tastatur und auf eine Netzwerkverbindung achten muß.
Meistens ist ein fd_set ein Bitfeld oder Array, in dem der Deskriptor als Index verwendet wird, aber selbstverständlich kann man sich auf sowas nicht verlassen. Damit das nicht nötig ist, gibt es die Makros FD_SET, FD_CLR, FD_ISSET und FD_ZERO, die einen Eintrag setzen, entfernen, abfragen oder das ganze Set leeren.
Der Rückgabewert ist die Anzahl der Sockets auf denen was passiert ist, oder -1 im Fehlerfall. Nach der Rückkehr von select() sind in den Sets die Deskriptoren gesetzt, auf die das entsprechende Ereignis stattgefunden hat. Sie können dann mit FD_ISSET erkannt werden. Achtung: die Sets sind nach dem Aufruf nicht mehr identisch, und auch die Struktur mit dem Timeout ist nicht mehr konsistent. Manche Plattformen lassen hier soviel "übrig", wie der Aufruf vor dem Ablauf des Timeouts zurückkam. Um auf der sicheren Seite zu bleiben muß man also vor jedem Aufruf von select() die Sets und den Timeout neu setzen. Bitte daran denken FD_ZERO aufzurufen, bevor mit FD_SET neue Sockets gesetzt werden, sonst gibt's einen hässlichen Mischmasch aus neuen und alten Einträgen.
Inzwischen habe ich festgestellt, bzw. wurde darauf hingewiesen, daß select() das Komplizierteste an der ganzen Socketprogrammiererei zu sein scheint. Daher habe ich mich dazu breitschlagen lassen noch eine Extra-Seite zu select zu schreiben, auf der es hoffentlich erschöpfend behandelt wird.
Was machen die Parameter?
Der erste Parameter ist der höchste Deskriptor PLUS EINS.
Die drei folgenden Parameter sind Zeiger auf fd_sets, die zum Lesen, Schreiben oder auf Ausnahmen getestet werden sollen. Hier darf NULL eingesetzt werden.
Der letzte Parameter ist entweder ein Zeiger auf eine struct timeval{} mit einem Timeout (oder 0 in tv_sec und tv_usec für sofortiges Rückkehren), oder aber NULL um in select() zu blockieren.
Wie verwende ich es?
Siehe Abschnitt Beispielcode.