Die folgenden Beispiele beschreiben den Aufbau eines Servers in C, einmal mit Threads und einmal mit select(). Verwendet wird ein Echoserver [1], der Anfragen auf Port 7 unverändert an den Klienten zurücksendet.
Wir beginnen mit einem allgemeinen Schema zur Verwendung der Netzbibliothek, welches uns später als Basis für die Thread-Variante dient, und stellen ihre wichtigsten Funktionen vor. WSAStartup()
und WSACleanup()
bilden den formalen Rahmen für Auf- und Abbau der Windows-Netzbibliothek. Mit socket()
und accept()
werden Abhörkanal und Klientenkanäle (Ports) erstellt. bind()
und listen()
dienen der Vorbereitung des Abhörens. Die eigentliche Kommunikation findet mit send()
und recv()
statt. Die Funktionen shutdown()
und closesocket()
dienen dem korrekten Verbindungsabbau und der Kanalschließung.
Dieses Modell hat den Vorteil der Simplizität und den Nachteil, immer nur einen Klienten bedienen zu können. Alle anderen Klienten müssen sich hinten anstellen!
Die zweite Variante nutzt das Grundgerüst unseres Servers und lagert die klientenbezogenen Netzfunktionen – recv()
, send()
, shutdown()
, closesocket()
– über CreateThread()
in einen nebenläufigen Programmstrang aus, dem als Parameter der jeweilige Klientenkanal mitgegeben wird. Beendet wird der Thread automatisch nach Ablauf seines Programmkodes; CloseHandle()
gibt lediglich den Bezeichner (Handle) frei.
Der Vorteil gegenüber unserem Grundgerüst liegt in der Möglichkeit, nun parallel mehrere Klienten bedienen zu können. Auch läßt sich diese Variante relativ gut auf POSIX-Systeme portieren. Ein gewisser Nachteil liegt darin, daß multiple Threads mehr an Systemressourcen (Speicher und Zeit) benötigen als ein einläufiger Programmstrang. Erschwerend kommt dazu, daß alle Threads jeweils auf eine Netzfunktion warten: jeder Kliententhread auf recv()
und der Hauptthread auf accept()
. So ist das Programm zumeist blockiert wie schon in unserem ersten Schema – zumindest, wenn wir blockierende Netzfunktionen nutzen wie hier.
Ausführlicher Quelltext sowie Hinweise zur Kompilierung ohne C-Laufzeitbibliothek.
Um das o. g. Warten zu umgehen, kann man entweder die Kanäle entblockieren oder die Funktion select()
verwenden, die über die Makros FD_SET()
Anweisungen bekommt, welche Kanäle auf Lese- oder Schreibanforderungen abzufragen sind und diese Information in den übergebenen Listen (hier rSet
zum Lesen und wSet
zum Schreiben) übergibt. Diese Listen können mit FD_ISSET()
dann zumeist ohne Blockade ausgelesen werden. In den meisten praktischen Implementierungen wird select()
jedoch mit nichtblockierenden Kanälen kombiniert, um die Gefahr einer Blockade (z. B. durch abstürzende Klienten) ganz auszuschließen.
Der Vorteil von select()
ist die Möglichkeit der parallelen Bedienung von Klienten ohne Threads. Zudem ist die Funktion auch auf POSIX-Systeme portierbar. Nachteilig ist der Umstand, daß select()
über die Konstante FD_SETSIZE
zumeist auf maximal 64 parallele Verbindungen beschränkt ist, auch wenn bei manchen Systemen dieser Wert noch erhöht werden kann. Bedingt durch die sequentielle Natur der Listen-Abfrage wären allerdings hohe Klientenzahlen auch nicht performant. Auch müssen alle Kanäle wiederholt mit select()
abgefragt werden (Polling).
Ausführlicher Quelltext.
Das Grundgerüst eines Echo-Klienten sieht hingegen einfach aus. Lediglich die Funktion connect()
ist neu, die das Gegenstück zu accept()
bildet. Die Server-Funktionen wie bind()
und listen()
entfallen hingegen.
Ausführlicher Quelltext.
Wie schon erwähnt, sind Threads und select()
größtenteils sowohl unter UNIX als auch unter Windows einsetzbar, gleichfalls eigene Prozesse (was aber noch mehr Aufwand für das System bedeuten würde als der Einsatz von Threads). Neben den schon angesprochenen nichtblockierenden Kanälen, die wie select()
den Nachteil einer wiederholten Abfrage mit sich bringen, gibt es unter Windows leistungsstärkere, wenngleich weniger portable Möglichkeiten: Die Nutzung asynchroner Kanäle, die statt Polling anlaßbezogene Nachrichten verwenden, select-ähnliche Ereignisobjekte wie WSAEventSelect()
sowie überlappende Kanäle (Overlapping ports, IOCP, I/O completion ports) [3, 4], letztgenannte mit der höchsten Performanz. Unter UNIX sind neben dem klassischen Polling von nichtblockierenden Kanälen (kqueue(), poll() und /dev/poll), asynchrone Kanäle (epoll(), AIO als Pendant zu Microsofts IOCP) mit nachrichtenorientierten Abfragen (Realtime Signals) erwähnenswert [2]. Zur Vertiefung sei auf die nachstehenden Quellen verwiesen.
Viel Spaß beim Ausprobieren und Erweitern!