Start > Algorithmik > Server in C

Server in C

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.

  1. Grundgerüst eines Servers
  2. Server mit Threads
  3. Server mit select()
  4. Klient
  5. Weitere Varianten
  6. Quellen
  7. Nutzung

Grundgerüst eines Servers

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 Verbindungs­abbau 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!

Schema 1 – Grundgerüst:
WSAStartup() // Lade Netzbibliothek
listSock = socket() // Erstelle Abhoerkanal
bind(listSock, addrInfo) // Binde Abhoerkanal an Netzadresse
listen(listSock, ...) // Starte Abhoeren
while ()
  cltSock = accept(listSock, ...) // Erstelle Klientenkanal
  while (recv(cltSock, ...) > 0) // Empfange bis Klient quittiert
    send(cltSock, ...) // Sende Daten zurueck
  shutdown(cltSock, ...) // Server quittiert
  closesocket(cltSock) // Schliesse Klientenkanal
closesocket(listSock) // Schliesse Abhoerkanal
WSACleanup() // Entlade Netzbibliothek

Server mit Threads

Die zweite Variante nutzt das Grundgerüst unseres Servers und lagert die klienten­bezogenen 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 System­ressourcen (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.

Schema 2 – Server mit Threads:
WSAStartup()
listSock = socket()
bind(listSock, addrInfo)
listen(listSock, ...)
while ()
  cltSock = accept(listSock, ...)
  cltThreadHandle = CreateThread(... cltSock, ...) // Erstelle Thread
  CloseHandle(cltThreadHandle) // Schliesse Thread-Handle
closesocket(listSock)
WSACleanup()

Ausführlicher Quelltext sowie Hinweise zur Kompilierung ohne C-Laufzeitbibliothek.

Server mit select()

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 Schreib­anforderungen 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 nichtb­lockierenden 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).

Schema 3 – Server mit select():
WSAStartup()
listSock = socket()
bind(listSock, addrInfo)
listen(listSock, ...)
while ()
  FD_ZERO(Alle Sets) // Select-Sets nullen
  FD_SET(listSock, &rSet) // Abhoerkanal immer auf neue Klienten abfragen
  FD_SET(cltSocks, &wSet oder &rSet) // Klientenkanaele auf Lesen/Schreiben
  cltSelCt = select(... &rSet, &wSet, ...)
  if FD_ISSET(listSock, &rSet) // A. Abhoerkanal lesbar?
    cltSock = accept(listSock, ...) // Nimm Klienten an
  for all cltSocks
    if FD_ISSET(cltSock, &rSet) // B. Klientenkanaele lesbar?
      if recv(cltSock, ...) == 0 // Dann lies; falls 0, quittiert Klient
        shutdown(cltSock, ...)
        closesocket(cltSock)
    if FD_ISSET(cltSock, &wSet) // C. Klientenkanaele schreibbar?
      send(cltSock, ...) // Dann schreib
closesocket(listSock)
WSACleanup()

Ausführlicher Quelltext.

Klient

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.

Schema 4 – Klient:
WSAStartup()
cltSock = socket();
connect(cltSock, ...);
while fgets(...)
  send(cltSock, ...)
  if recv(cltSock, ...) == 0
    break
shutdown(cltSock, ...)
closesocket(cltSock)
WSACleanup()

Ausführlicher Quelltext.

Weitere Varianten

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 nicht­blockierenden 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 nicht­blockierenden Kanälen (kqueue(), poll() und /dev/poll), asynchrone Kanäle (epoll(), AIO als Pendant zu Microsofts IOCP) mit nachrichten­orientierten Abfragen (Realtime Signals) erwähnenswert [2]. Zur Vertiefung sei auf die nachstehenden Quellen verwiesen.

Viel Spaß beim Ausprobieren und Erweitern!

Quellen

  1. IETF: RFC 862 (Echo-Server)
  2. Dan Kegel: The C10K Problem
  3. Warren Young: Which I/O Strategy Should I Use?
  4. Microsoft: Design Issues When Using IOCP in a Winsock Server

Nutzung

Hinweise zur Nutzung.

© 2015, 2015 asdala.de: Kon­takt & Daten­obhut