TCP Chat Server in C mit select()

Autor: Andre Adrian
Version 10.Juni.2007

Zusammenfassung

Beim Internet Relay Chat (IRC) schreibt ein Teilnehmer eine Textzeile welche alle anderen Teilnehmer lesen können. Die Chat Client Programme sind über TCP mit dem Chat Server Programm verbunden. Der Server verwaltet für jeden Client eine eigene TCP Verbindung.
Ein einfaches, kurzes Chat Server Programm in C für Linux und für MS-Windows wird vorgestellt. Am Beispiel werden die Themenbereiche Funktionale Programmierung, Fehlerbehandlung (Error Handling), Binär-Strings in C und natürlich die Kombination aus Socket Schnittstelle und select() besprochen.
Ein Streifzug durch die Themengebiete Zustand-Übergang-Diagramm (state transition diagram), ereignisgesteuertes Modell (event driven programming), Transaktion, wohl definierter Zustand (invariant) und ein bißchen System Analyse und Design bilden den theoretischen Rahmen.
Der Text ist geschrieben für Informatik Studenten (Vorlesung Betriebssysteme) und Junior System Programmierer.

Inhaltsverzeichnis

Funktionale Programmierung (FP)

Die Programmiersprache C gehört zu den imperativen Programmiersprachen. Die Programmiersprachen Erlang und Haskell sind funktionale Programmiersprachen. Hinter C steht als geistiges Modell die Turing-Maschine von Alan Turing. Typische Programmelemente sind die Prozedur mit Call-by-reference Parametern. Bei FP ist es das Lambda-Kalkül von Alonzo Church mit der Funktion. Beide Ansätze sind gleich mächtig - oder gleich beschränkt.
Bei der FP wird das Ergebnis einer reinen Funktion nur durch die Eingangsparameter der Funktion bestimmt. Bei der imperativen Programmierung bestimmen oft globale oder statische Variablen neben den Eingangsparametern das Ergebnis der Funktion.
Die Programmiersprache C erlaubt Funktionen. Der Chat Server ist - soweit es C zuläßt - im Sinne der funktionalen Programmierung geschrieben worden. Es werden keine globalen oder statischen Variablen benutzt. Die "State" Information liegt nur in lokalen Variablen der Funktionen. Das Programm ist nicht "state less", sondern "State minimiert". Der State ist die aufsummierte relevante Vergangenheit des Programms oder das Gedächtnis des Programmes.
Bemerkung: Das TCP Protokoll selbst hat "states". Nur das UDP Protokoll ist state less.
Die Vermeidung von globalen und statischen Variablen macht ein C Programm nicht zu einem Erlang oder Haskell Programm. Es macht das Programm aber "lokaler" oder "Schnittstellen minimierter". Und minimale Schnittstellen, am besten auf allen Abstraktionsebenen, führen zu robusten Programmen. Je kleiner die Abhängigkeit zwischen den Elementen (Funktionen, Prozeduren, Threads, Prozessen), umso einfacher ist das Debugging. Die kombinatorische Explosion der Testfälle ergibt sich aus zuviel "State" bzw. aus zuviel Wechselwirkung zwischen den Elementen.

Relevante Zustandswechsel, Invariante, Transaktion

Bei imperativen Programmiersprachen ändert die Ausführung jeder Quelltextzeile den State (die aufsummierte Vergangenheit, das Gedächtnis) des Programmes. Mit den Mitteln des State Transition Diagrams (siehe unten) ist es möglich den Quelltext so zu ordnen, daß nur wenige relevante Zustandswechsel auftreten. Das Program Pattern für "relevante Zustandwechsel" ist: Die Zustandsvariable gibt es einmal als "offizielle" Variable (die Zustandsvariable kann natürlich eine Struktur oder eine Klasse sein). Daneben gibt es eine (lokale) Arbeitskopie der Zustandsvariable. Mit dieser Arbeitskopie wird der neue Inhalt der Zustandsvariable nach einem Zustands-Wechsel vorbereitet. Sind alle Konsistenzchecks erfolgreich, wird zum Abschluß des Zustands-Wechsels der Inhalt der Arbeitskopie in die "offizielle" Zustands-Variable kopiert. Falls die Konsistenzchecks fehlschlagen behält die "offizielle" Zustands-Variable entweder ihren alten Inhalt, oder ein anderer, konsistenter Inhalt der Zustandsvariablen wird erzeugt (z.B. ein Fehlerzustand).
Durch die Anwendung dieses Program Pattern wandert das Programm von einem "wohldefinierten Zustand" zum nächsten "wohldefinierten Zustand". Die Schritte dazwischen sind nötig, können aber nicht mehr zu Inkonsistenzen der Zustandsvariable führen. Wenn die Konsistenzchecks versagen, können wieder Inkonsistenzen auftreten.
Das Program Pattern "relevanter Zustandswechsel" in der Multi-Tasking Version:
Eine einfache Variante des Zustands-Wechsels ohne kritischer Abschnitt und ohne Arbeitskopie ist:
Bjarne Stroustrup spricht im Buch Die C++ Programmiersprache von wohldefinierten Zustand (Invariante) und meint damit die Folge von konsistenten Inhalten der Zustandsvariable, d.h. der Kette der relevanten Zustandswechsel. Er schreibt in Kapitel 24.3.7.1: "Die Aufgabe der Initialisierung besteht somit darin, ein Objekt in einen Zustand zu bringen, in dem die Invariante gilt ... Jede Operation einer Klasse kann davon ausgehen, daß die Invariante zu Beginn des Aufrufs erfüllt ist, und muß sicherstellen, daß sie auch beim Verlassen der Operation gilt."
Die Datenbank-Programmierung hat mit der Transaktion ein ähnliches Programmiermittel. Eine Transaktion erfolgt "ganz oder garnicht". Entweder wird ein relevanter Zustandswechsel von einem wohldefinierten Zustand zum nächsten wohldefinierten Zustand ausgeführt oder per Rollback werden die Zwischenschritte rückgängig gemacht und der alte wohldefinierte Zustand bleibt weiter bestehen.

Funktionale Programmierung, Zustand und Ereignis

Für die funktionale Programmierung sind Funktionen mit Zustand (State) unreine Funktionen (impure functions). Für diese unreinen Funktionen bietet die FP keine direkte Hilfe. Für reine Funktionen bietet FP vieles. So können FP Compiler für die reinen Funktionen alle verfügbaren Prozessoren optimal ausnutzen, ohne das der Programmierer dem Compiler irgendwelche Tipps geben muß. Weiterhin ist für reine Funktionen die Program Verification möglich und manchmal sogar trivial. Bei dem TCP Chat Server sind alle Funktionen unreine Funktionen, hier bringt FP nichts.

Zustands-Übergangs-Diagramm, State Transition Diagram (STD), Ereignisgesteuertes Modell

Im Abschnitt "Relevante Zustandswechsel" wurde das Program Pattern für einen einzelnen Zustands-Übergang beschrieben. Alle Zustands-Übergänge zusammen bilden das Zustands-Übergangs-Diagramm (STD). Das STD ist ein Design Pattern, kein Program Pattern. Das STD beschreibt welche Zustände es gibt und welche Bedingungen für einen Zustands-Wechsel zu erfüllen sind. Die Begriffe ereignisgesteuertes Modell oder event-driven programming betonen den Zustandsübergang. Zustand und Ereignis sind nur zwei Seiten einer Münze: Der Zustand ist die Statik des Systems, das Ereignis (der Zustandswechsel) ist die Dynamik des Systems.
Das Werkzeug State Transition Diagram ist auch in der OO Welt sehr nützlich und hilfreich. Eine Klasse paßt ausgezeichnet zu STD. In einer Klasse lassen sich sauber und ordentlich die Zustandsvariable aufheben und durch Klassen-Funktionen die Zustandsübergänge mit Konsistenzchecks realisieren.
Übrigens zeigt sich am STD das alle Programmiersprachen gleich sind: Ein STD läßt sich mit einer FP Sprache, einer imperative Sprache mit Klassen oder ohne Klassen und sogar mit Assembler realisieren. Die Implementierungssprache spielt für das STD keine Rolle.

Zustands-Übergangs-Diagramm für TCP Chat Server

Jedes STD beginnt mit dem Start-Zustand. Aus der Aufgabenstellung (z.B. Anforderungsdokument) lassen sich die weiteren "Work" Zustände ermitteln. Oft ist es Aufgabe des System-Designers die "Error" Zustände zu finden. Wenn das System mit jedem, auch fehlerhaften, Input, mit dem Ausbleiben von Input und mit der Wiederholung von Input klarkommt weil ein entsprechender Zustand für alle diese Fälle besteht, dann ist das System robust.
Der System-Designer muß bei dem Entwurf des STD auch die Lücken in den Anforderungen aufdecken und diese Lücken schließen. Oft heißt es im Anforderungsdokument: "Nach A kann B, C, D folgen". Der System-Designer muß fragen: Wie soll sich das System verhalten wenn der Zustand A etwas anderes erhält als die Ereignisse welche zu den Zuständen B, C, D führen? Soll in diesem Fall das System abstürzen? Oder welches andere System-Verhalten wird gewünscht?
Der TCP Chat Server ist einfach. Die vagen Anforderungen an das System sind im Abschnitt "Zusammenfassung" oben schon angegeben. Die Wörter "Teilnehmer", "Server", "Client" und "TCP Verbindung" führen den System Designer zu folgenden Fragen, welche im Anforderungsdokument nicht ausdrücklich beantwortet werden:
 Damit ist das Mengengerüst geklärt. Für die Zustandsübergänge sind die Ereignisse, welche auftreten können, wichtig:
Die Fehler treten nicht direkt als Ereignisse auf. Die Fehler sind durchgefallene Konsistenzchecks. Die Fehlerbehandlung wird durch folgende Vereinbarung Teil des Zustands-Übergangs-Diagrammes: Fehlgeschlagende Konsistenzchecks führen entweder
Aus praktischen Gründen ist das absichtliche Beenden (termination) des Programmes ein Error Zustand.
Mit dieser Konsistenzcheck Erweiterung des Zustands-Übergangs-Diagrammes können die normale Verarbeitung, die Konsistenzchecks und die Fehlerbehandlung gemeinsam betrachtet werden.


Server (Daemon) Programmierung

Programmierer wurden früher in Anwendungsprogrammierer und Systemprogrammierer unterschieden. Die geistigen Fähigkeiten eines Anwendungsprogrammierers reichten aus zur Programmierung eines Client Programmes mit Character User Interface (CUI) Oberfläche. Ein Systemprogrammierer mußte auch die Tücken des Server Programmes meistern. Heute, zur Zeit der Peer-to-Peer Netzwerk Applikationen, verschwimmt die Trennlinie. Eine Applikation wie Skype ist Client für den lokalen Nutzer und Server für entfernte (remote) Nutzer. Skype benutzt für die Server-Funktionalität den Begriff Super-Node.
Der Anwendungsprogrammierer hat es leicht, weil er sich nur um einen Nutzer kümmern muß. Der Systemprogrammierer muß sich im Server Programm um viele Nutzer kümmern. Es ist der Schritt von 1 auf N welcher die Komplexität schafft und Rechtfertigung für das bessere Systemprogrammierer Gehalt ist.
Weiterhin sorgt der Absturz des Server Programmes für Ärger bei allen Nutzern. Der Absturz des Client Programmes verärgert nur einen Nutzer. Mehr verärgerte Nutzer erzeugen mehr Druck auf die Programmierer. Deshalb werden Server Programm Fehler eher repariert als Client Programm Fehler (so hofft man wenigstens).
Die N Nutzer des Server Programmes müssen irgendwie gleichzeitig bedient werden. Ein Server mit mehreren CPUs oder mehreren Cores kann verschiedene Tätigkeiten echt parallel ausführen. Ein Single Core Server kann verschiedene Tätigkeiten nur quasi parallel abarbeiten.
Für Multi Core Server sollte das Server Programm als Multi Prozess Programm realisiert werden. Der Apache Server ist ein gutes Beispiel. Er verwendet Multi Prozess und Multi Threading Technik für gute Skalierbarkeit.
Am anderen Ende der Leistungs- und Komplexitätsskala für das Server Programm liegen Software Interrupt Routinen. Die Software Interrupt Routinen benutzen nur einen Core und einen Thread. Alle Variablen liegen in einem Adressraum. Eine Synchronisation (Semaphore, Mutex, ...) für den Variablenzugriff ist nicht nötig. Im Betriebssystem UNIX (Linux) werden Software Interrupt Routinen mit dem Betriebsystem Aufruf (system call) select() verwaltet.

Callback Funktionen und select()

Die Programmierung von Graphischen Oberflächen (Graphical User Interface GUI) ist heute ohne Callback Funktionen nicht mehr vorstellbar. Das typische Programm eines Anwendungsprogrammierers besteht aus Hunderten von "register callback" Aufrufen zum Programmbeginn und aus massivem "message pumping" später. Wenn jedes graphische Element (Button, Entry, Form, ...) als eigener "Task" angesehen wird, dann ist der Anwendungsprogrammierer dank GUI  schon lange bei der Multi-Tasking Programmierung angekommen.
Der Betriebssystem Aufruf select() ist älter als das Callback Prinzip. Die Callback Funktionen des X Window System laufen in einem select() zusammen. Callback und select() lassen sich leicht zusammenbringen. Beim Callback ist die Callback Funktion das Wichtige. Bei select() ist der Filedeskriptor das Wichtige. Beide stehen für ein Programm-Objekt. Einmal ist der Objekt-Griff (object handle) eine Aufruf-Adresse, im anderen Fall ein Datei-Index. Das ist aber nicht weiter problematisch, weil dem eindeutigen Filedeskriptor leicht eine eindeutige Aufruf-Adresse zugeordnet werden kann.
Und nicht viel mehr geschieht bei dem register callback XtAddInput() eines neuen Eingabe Gerätes: Wird dem X Programm ein solches Gerät angemeldet, so wird in einer X Window System internen Tabelle die Callback Adresse mit dem entsprechenden Filedeskriptor verknüpft. Meldet nun die select() Funktion im Herzen des X Programmes neue Daten auf diesem Filedeskriptor (Details siehe unten), so wird über die Tabelle der passende Callback ausgesucht und aufgerufen.
Die Callback Funktionen für graphische Elemente im Fenster werden etwas anders verwaltet. Für das Betriebssystem ist jeder Mausklick gleich, d.h. egal ob der Benutzer einen Button anklickt oder eine Menüleiste, bei der select() Funktion kommt eine Eingabe von der Maus an.  Diese Eingabe wird nach der Mausposition ausgewertet. Mit der Mausposition wird das passende graphische Objekt gesucht, z.B. der Okay Button eines Popup Fensters. Diesem Objekt wird dann eine Nachricht gesendet.

Pivot Funktion select()

Die select() Funktion ist bei Programmen welche mehrere Aufgaben quasigleichzeitig per Software Interrupt Routinen ausführen das Nadelöhr. Es darf nur eine Endlosschleife im Programm geben. Und in dieser einzigen Endlosschleife liegt select(). Alle Filedeskriptoren, welche ja die Datenquellen des Servres darstellen, sind dem select() anzugeben. Der select() Aufruf legt die eigene Applikation im Betriebssystem schlafen. Wenn irgendwelche Daten an den per select() mitgeteilten Datenquellen oder Schnittstellen eintreffen, dann wird die Applikation wieder geweckt und arbeitet weiter.
Nach dem select() Aufruf wird üblicherweise die Liste der Filedescriptoren "abgeklappert". An select() wurden die offenen Filedescriptoren gemeldet. Diese fd_set Struktur wird von select() geändert. Nach dem Return der select() Funktion enthält die Struktur die Filedeskriptoren mit noch ungelesenen Eingangsdaten.
Mit read() können und sollen nun alle Eingangsdaten von allen aktiven Filedeskriptoren gelesen und verarbeitet werden. Die select() Endlosschleife durchläuft somit die Schritte
  1. fd_set Struktur mit offenen Filedeskriptoren zusammenstellen
  2. select() aufrufen
  3. Im Betriebssystem warten auf eingehende Daten
  4. select() beenden
  5. In der fd_set Struktur die Filedeskriptoren mit ungelesenen Daten suchen und finden
  6. Die entsprechenden Funktionen zum Daten lesen und Daten verarbeiten aufrufen
  7. Endlosschleife erneut durchlaufen

Fehlerbehandlung (Error Handling)

Jeder einzelne Betriebssystemaufruf kann scheitern. Eine einfache Reaktion auf solch ein negatives Ereignis ist Programm Beendigung (termination). Mit einem "stillen" Tod der Applikation ist dem Programmierer bei der Fehlersuche nicht geholfen. Es sollte schon ein bißchen "Warum" gemeldet werden. Mit der Zeit kann der Fehlerbehandlungs-Quelltext leicht den produktiven Quelltext an Quelltextzeilen übertreffen. Ein Verhältnis von 70% Quelltext für Eingangsdatenprüfungen, Konsistenzprüfungen, Fehlermeldungs Logging Verwaltung, Wiederanlaufmethoden nach Fehlern usw. zu 30% Quelltext für die reine Programmfunktion sind typisch für "mission critical" Server Programme.
Damit der "error source code" nicht den Blick auf den "work source code" verstellt können Makros eingesetzt werden. Im Beispielprogramm werden die Makros exit_if() und return_if() verwendet. Beide Makros führen eine Konsistenzprüfung zusammen mit einer Fehlermeldung aus. Wenn der Test fehlschlägt, beendet das Makro exit_if() das Programm mit Fehlermeldung. Das Makro return_if() beendet die Funktion mit Fehlermeldung. Beide Makros erzeugen den Fehlertext automatisch. Gemeldet wird Quelltextdatei, Quelltextzeile, Funktion und Fehlerursache.
Die Programmiersprache C kennt kein catch() und throw() für die Fehlerbehandlung. Für die Fehlerbehandlung in C wird goto und setjmp()/longjmp() eingesetzt. Das goto wird eingesetzt wenn Aufräumarbeiten in einer Funktion nötig sind um memory leaks zu vermeiden. Die setjmp()/longjmp() Funktionen werden eingesetzt wenn im Fehlerfall mehrere Funktionen gleichzeitig beendet werden sollen. Der Sprung erfolgt dann z.B. von der fünften Unterprogrammebene direkt zurück zur zweiten Unterprogrammebene. Im Abschnitt "Zustands-Übergangs-Diagramm für TCP Chat Server" oben wurde gezeigt wie Fehler als Ereignisse zu zusätzlichen Zustände führen. Diese Art der Fehlerbehandlung benötigt kein setjmp()/longjmp().
Besonders bei C sollte neben der Fehlerbehandlung innerhalb des C Programmes auch an eine Fehlerbehandlung ausserhalb des C Programmes gedacht werden. Es ist einfach und praktisch den Aufruf des Server Programmes in eine Shell Endlosschleife zu stellen. Wenn das C Programm "verstirbt", dann wird über die Shell Endlosschleife eine neue Inkarnation des C Programmes gestartet. Für die Nutzer ist dieser kleine Tod weniger ärgerlich als die "Server ist abgestürzt, wir warten auf den Techniker" Auskunft des Helldesks.

Binär-Strings in C

Die Null-terminierten C-Strings erlauben im String nicht das Zeichen '\0'. Für Binär-Strings kann eine Kodierung mit zwei Variablen verwendet werden. Die erste Variable ist die Adresse des Strings (string pointer), die zweite Variable ist die String Länge. Wird die String Länge mit einem call-by-reference Parameter übergeben läßt sich eine weitere "Schmutzecke" von C reinigen: Die call-by-reference String Länge kann beim Aufruf mit der maximalen String-Länge sizeof(string) geladen werden. Beim Return kann im gleichen Parameter die aktuelle String-Länge zurückgegeben werden.
Diese Binär-String Darstellung mit String pointer und call-by-reference String Länge wird bei der Berkeley Socket Schnittstelle seit 1983 verwendet. Im Beispiel wird die Socket Binär-String Lösung verwendet.

Compile und Ausführen

Der Quelltext wird unter Linux mit  cc -o chatd chatd.c  übersetzt. Die Programmdatei wird mit   ./chatd &   im Hintergrund gestartet. Für den ersten Test können zwei oder mehr telnet Programme mit  telnet 127.0.0.1 51234  auf dem gleichen Rechner gestartet werden. Eine Eingabe im telnet Programm wird per TCP Verbindung zum chatd Programm gesendet. Von dort wird die Eingabe an alle anderen Clients weitergesendet und ausgegeben.

Weitere Tests

Das Chat Server Programm chatd und das Chat Client Programm telnet können auch auf verschiedenen Servern laufen. Der Client muß die IP-Adresse des Servers kennen.
Mit dem Aufruf   netstat -antp   lassen sich die TCP Verbindungen ansehen. Laufen auf dem gleichen Rechner chatd und ein telnet gibt es drei Einträge: ein LISTEN auf dem Port 51234 durch das Programm chatd, ein CONNECT von chatd zu telnet und ein CONNECT von telnet nach chatd. Die letzten beiden Einträge beschreiben die gleiche TCP Verbindung, nur von den beiden unterschiedlichen Endpunkten aus.
Wird eine zweite Instanz (Inkarnation) von chatd gestartet, erscheint die Fehlermeldung   chatd: exit_if() chatd.c: 85: tcp_server_init: Error Address already in use.  Mit "Address" ist das 5er-Tupel "tcp 0.0.0.0:51234 0.0.0.0:*", d.h. der Listen Port des TCP Servers gemeint.
Mit   killall chatd   kann der Server "brutal" beendet werden.

Weitere Programmentwicklung

Mehr Error Handling ist immer möglich. Man kann versuchen die exit_if() Stellen durch Error Recovery im C-Quelltext zu ersetzen. Ziel ist jede Programm Beendigung zu vermeiden (Dieses Ziel ist in der Praxis in letzter Konsequenz nicht erreichbar, aber Studenten wollen ja irgendwie beschäftigt werden).
Das Programm kann mit pselect() oder mit poll() programmiert werden - beide system calls sind Varianten von select().
Das chatd Programm wird mit & als Hintergrund Prozess (background process) gestartet. Ein Prozess kann sich selbst in den Hintergrund stellen und kann sich von der Konsole abkoppelt.
Die einzige Verarbeitung von Signalen im Moment besteht aus dem Ignorieren von Signalen. Wer Frustration liebt kann das Thema Signale vertiefen. So können die Signale SIGINT oder SIGQUIT benutzt werden um das chatd Programm sauber "runterzufahren". Leider lassen sich einige Signale wie SIGKILL nicht fangen (catch) und machen das ganze Signal Handling zur vergeudeten Mühe.
Eine exotische TCP Eigenschaft sind "Out of band (OOB)" Daten. Kein normaler Mensch benutzt soetwas. Aber das OOB Thema zieht sich durch die komplette Socket Schnittstelle. Bei genügend viel Masochismus läßt sich die komplette chatd Kommunikation per OOB Daten ausführen. Der vierte select() Parameter exceptfds ist der Schlüssel zur OOB Hölle.
Etwas weniger sinnlos ist die Nutzung des dritten select() Parameters writefds. Üblicherweise vertraut man auf die Write Buffer des Betriebssystems und benutzt write() direkt. Aber man kann immer nach Betriebssystemfehlern suchen. Und wer select() benutzt um sein write() zu kontrollieren bewegt sich in so einer dunklen Betriebssystem-Ecke das kleine Blessuren nicht auszuschließen sind. Und selbst wenn es jetzt funktioniert - eine andere Hardware, eine neue Betriebssystemversion oder eine Portierung bereiten bestimmt Ärger.
Eine moderne Alternative zu select() ist die asynchrone IO. Ein aio_read() system call blockiert nicht. Mit aio_error() Aufrufen wird der Status der Leseoperation abgefragt. Zum Abschluß wird mit aio_return() der read() Returnwert geholt. Die asynchrone IO verlangt busy waiting auf aio_error() in der Applikation, aber eigentlich möchte der Programmierer vom Betriebssystem entlastet werden. Bei asynchroner IO bekommt der Programmierer eine Ladung "Verwaltungskram" obendrauf um die sich sonst das Betriebssystem kümmert. Mit apropos aio_ sieht man den asynchrone IO Zoo.
Zum Abschluß noch ein Tipp von Herzen: Wer select() so verwendet wie im Beispielprogramm unten der wird seines Lebens froh. Wer mehr Leistung braucht sollte sich den Apache Quelltext ansehen um "multi-processes/multi-threading workers" zu verstehen. Für den Autor gibt es nur zwei sinnvoll nutzbare select() Parameter: den zweiten Parameter readfds und den letzten Parameter timeout. Bei Echtzeit Systemen ist timeout nicht NULL.
Achtung: Timeout Verarbeitung ist oft nötig aber erschwert die Programmentwicklung deutlich - die Programm Dynamik aufgrund von Timeouts macht den Debugger nutzlos.

Der Quelltext

Der Quelltext besteht aus den Funktionen
Die genaue Funktion des Quelltextes erschließt sich durch aufmerksames Lesen des Quelltextes. Das   man   UNIX Kommando ist sehr hilfreich beim Verständnis der Betriebssystem Aufrufe. Besonders   man select_tut   für das select() Tutorial wird empfohlen. Falls Lesen nicht mehr weiterhilft, sollte man mit dem Programm "spielen": Quelltext ändern, printf() Kommandos einfügen, das Programm im Debugger ausführen, usw.
Falls auch Spielen nicht dem Verständnis hilft dann sollte man vielleicht der Systemprogrammierung Adieu sagen. Viel leichter als in diesem Beispiel wird Serverprogrammierung nicht. Für Fortgeschrittene gibt es hier eine Lösung mit Threads.

/* chatd.c
 * Chat Server (Daemon) fuer Linux
 * Autor: Andre Adrian
 * Version: 09jun2007
 *
 * Chat Server wartet auf TCP Port 51234 auf einen Verbindungsaufbau
 * von telnet Clients. Der Client sendet ASCII Meldungen an den Server.
 * Die Meldungen werden vom Server an alle anderen Clients gesendet.
 *
 * Aufruf Server:
 *  ./chatd &
 *
 * Aufruf Client:
 *  telnet IP_Adresse_Server 51234
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <syslog.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>

/* Die Makros exit_if() und return_if() realisieren das Error Handling
 * der Applikation. Wenn die exit_if() Bedingung wahr ist, wird
 * das Programm mit Fehlerhinweis Datei: Zeile: Funktion: errno beendet.
 * Wenn die return_if() Bedingung wahr ist, wird die aktuelle Funktion
 * beendet. Dabei wird der als Parameter 2 angegebene Returnwert benutzt.
 */

#define exit_if(expr) \
if(expr) { \
  syslog(LOG_WARNING, "exit_if() %s: %d: %s: Error %s\n", \
  __FILE__, __LINE__, __PRETTY_FUNCTION__, strerror(errno)); \
  exit(1); \
}

#define return_if(expr, retvalue) \
if(expr) { \
  syslog(LOG_WARNING, "return_if() %s: %d: %s: Error %s\n\n", \
  __FILE__, __LINE__, __PRETTY_FUNCTION__, strerror(errno)); \
  return(retvalue); \
}

#define MAXLEN 1024

#define OKAY 0
#define ERROR (-1)

int tcp_server_init(int port)
/* Server (listen) Port oeffnen - nur einmal ausfuehren
 * in port: TCP Server Portnummer
 * return: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  int listen_fd;
  int ret;
  struct sockaddr_in sock;
  int yes = 1;

  listen_fd = socket(PF_INET, SOCK_STREAM, 0);
  exit_if(listen_fd < 0);

  /* vermeide "Error Address already in use" Fehlermeldung */
  ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
  exit_if(ret < 0);

  memset((char *) &sock, 0, sizeof(sock));
  sock.sin_family = AF_INET;
  sock.sin_addr.s_addr = htonl(INADDR_ANY);
  sock.sin_port = htons(port);

  ret = bind(listen_fd, (struct sockaddr *) &sock, sizeof(sock));
  exit_if(ret != 0);

  ret = listen(listen_fd, 5);
  exit_if(ret < 0);

  return listen_fd;
}

int tcp_server_init2(int listen_fd)
/* communication (connection) oeffnen - fuer jeden neuen client ausfuehren
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 * return: wenn okay Socket Filedescriptor zum lesen vom Client, ERROR sonst
 */
{
  int fd;
  struct sockaddr_in sock;
  socklen_t socklen;

  socklen = sizeof(sock);
  fd = accept(listen_fd, (struct sockaddr *) &sock, &socklen);
  return_if(fd < 0, ERROR);

  return fd;
}

int tcp_server_write(int fd, char buf[], int buflen)
/* Schreibe auf die Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum Schreiben zum Client
 * in buf: Meldung zum Schreiben
 * in buflen: Meldungslaenge
 * return: OKAY wenn Schreiben vollstaendig, ERROR sonst
 */
{
  int ret;

  ret = write(fd, buf, buflen);
  return_if(ret != buflen, ERROR);
  return OKAY;
}

int tcp_server_read(int fd, char buf[], int *buflen)
/* Lese von der Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum lesen vom Client
 * out buf: Gelesene Meldung
 * inout buflen: in = maximale Meldungslaenge, out = gelesene Meldungslaenge
 * return: OKAY wenn Lesen okay, ERROR sonst
 */
{
  /* lese Meldung */
  *buflen = read(fd, buf, *buflen);
  if (*buflen <= 0) {
    /* End of TCP Connection */
    close(fd);
    return ERROR;               /* bedeutet fd ist nicht mehr gueltig */
  }
  return OKAY;
}

void loop(int listen_fd)
/* Server Endlosschleife
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  fd_set the_state;
  int maxfd;

  FD_ZERO(&the_state);
  FD_SET(listen_fd, &the_state);
  maxfd = listen_fd;

  for (;;) {                    /* Endlosschleife */
    fd_set readfds;
    int ret;
    int rfd;

    readfds = the_state;        /* select() aendert readfds */
    ret = select(maxfd + 1, &readfds, NULL, NULL, NULL);
    if ((ret == -1) && (errno == EINTR)) {
      /* Ein Signal ist aufgetreten. Ignorieren */
      continue;
    }
    exit_if(ret < 0);

    /* TCP Server LISTEN Port (Client connect) pruefen */
    if (FD_ISSET(listen_fd, &readfds)) {
      rfd = tcp_server_init2(listen_fd);
      if (rfd >= 0) {
        FD_SET(rfd, &the_state);        /* neuen Client fd dazu */
        if (rfd > maxfd) {
          maxfd = rfd;
        }
      }
    }

    /* TCP Server CONNECT Ports (Clients communication) pruefen */
    for (rfd = listen_fd + 1; rfd <= maxfd; ++rfd) {
      if (FD_ISSET(rfd, &readfds)) {
        char msgbuf[MAXLEN];
        int msgbuflen;

        /* Meldung vom Client lesen */
        msgbuflen = sizeof(msgbuf);
        ret = tcp_server_read(rfd, msgbuf, &msgbuflen);
        if (ERROR == ret) {
          FD_CLR(rfd, &the_state);      /* toten Client rfd entfernen */
        } else {
          /* Meldung an alle anderen Clients schreiben */
          int wfd;

          for (wfd = listen_fd + 1; wfd <= maxfd; ++wfd) {
            if (FD_ISSET(wfd, &the_state) && (rfd != wfd)) {
              tcp_server_write(wfd, msgbuf, msgbuflen);
            }
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  /* Fehler Logging einschalten */
  openlog(NULL, LOG_PERROR, LOG_WARNING);

  /* open Chat as TCP server */
  loop(tcp_server_init(51234));

  return OKAY;
}

MS-Windows TCP Chat Server Quelltext

Unter Microsoft Windows ist die Socket Schnittstelle ebenfalls verfügbar. Es gibt einige Unterschiede zwischen der Linux und der Microsoft Version:
Als Compiler wurde der kostenfreie Microsoft Visual C++ 2005 Express Edition verwendet. Der Linker benötigt die WS2_32.Lib unter "Property, Configuration Properties, Linker, Input, Additional Dependencies" welche im ebenfalls kostenlosen Microsoft Platform SDK enthalten ist.
Die Programme telnet und netstat gibt es auch unter MS-Windows. Anstelle von  netstat -antp  wird bei Microsoft  netstat -anp TCP  geschrieben. Die Optionen bei telnet sind gleich.

/* winchatd.c
 * Chat Server (Daemon) fuer MS-Windows
 * Autor: Andre Adrian
 * Version: 07jun2007
 *
 * Chat Server wartet auf TCP Port 51234 auf einen Verbindungsaufbau
 * von telnet Clients. Der Client sendet ASCII Meldungen an den Server.
 * Die Meldungen werden vom Server an alle anderen Clients gesendet.
 *
 * Aufruf Server:
 *  winchatd
 *
 * Aufruf Client:
 *  telnet IP_Adresse_Server 51234
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

/* Die Makros exit_if() und return_if() realisieren das Error Handling
 * der Applikation. Wenn die exit_if() Bedingung wahr ist, wird
 * das Programm mit Fehlerhinweis Datei: Zeile: Funktion: errno beendet.
 * Wenn die return_if() Bedingung wahr ist, wird die aktuelle Funktion
 * beendet. Dabei wird der als Parameter 2 angegebene Returnwert benutzt.
 */

#define exit_if(expr) \
if(expr) { \
  fprintf(stderr, "exit_if() %s: %d: %s: WinError %d\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  exit(1); \
}

#define return_if(expr, retvalue) \
if(expr) { \
  fprintf(stderr, "return_if() %s: %d: %s: WinError %d\n\n", \
  __FILE__, __LINE__, __FUNCTION__, WSAGetLastError()); \
  return(retvalue); \
}

#define MAXLEN 1024

#define OKAY 0

SOCKET tcp_server_init(int port)
/* Server (listen) Port oeffnen - nur einmal ausfuehren
 * in port: TCP Server Portnummer
 * return: Socket Filedescriptor zum Verbindungsaufbau vom Client
 */
{
  SOCKET listen_fd;
  int ret;
  struct sockaddr_in sock;

  listen_fd = socket(PF_INET, SOCK_STREAM, 0);
  exit_if(listen_fd < 0);

  memset((char *) &sock, 0, sizeof(sock));
  sock.sin_family = AF_INET;
  sock.sin_addr.s_addr = htonl(INADDR_ANY);
  sock.sin_port = htons(port);

  ret = bind(listen_fd, (struct sockaddr *) &sock, sizeof(sock));
  exit_if(ret != 0);

  ret = listen(listen_fd, 5);
  exit_if(ret < 0);

  return listen_fd;
}

SOCKET tcp_server_init2(SOCKET listen_fd)
/* communication (connection) oeffnen - fuer jeden neuen client ausfuehren
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 * return: wenn okay Socket Filedescriptor zum lesen vom Client, SOCKET_ERROR sonst
 */
{
  SOCKET fd;
  struct sockaddr_in sock;
  int socklen;

  socklen = sizeof(sock);
  fd = accept(listen_fd, (struct sockaddr *) &sock, &socklen);
  return_if(fd < 0, SOCKET_ERROR);

  return fd;
}

int tcp_server_write(SOCKET fd, char buf[], int buflen)
/* Schreibe auf die Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum Schreiben zum Client
 * in buf: Meldung zum Schreiben
 * in buflen: Meldungslaenge
 * return: OKAY wenn Schreiben vollstaendig, SOCKET_ERROR sonst
 */
{
  int ret;

  ret = send(fd, buf, buflen, 0);
  return_if(ret != buflen, SOCKET_ERROR);
  return OKAY;
}

int tcp_server_read(SOCKET fd, char buf[], int *buflen)
/* Lese von der Client Socket Schnittstelle
 * in fd: Socket Filedescriptor zum lesen vom Client
 * out buf: Gelesene Meldung
 * inout buflen: in = maximale Meldungslaenge, out = gelesene Meldungslaenge
 * return: OKAY wenn Lesen okay, SOCKET_ERROR sonst
 */
{
  /* lese Meldung */
  *buflen = recv(fd, buf, *buflen, 0);
  if (*buflen <= 0) {
    /* End of TCP Connection */
    closesocket(fd);
    return SOCKET_ERROR;               /* bedeutet fd ist nicht mehr gueltig */
  }
  return OKAY;
}

void loop(SOCKET listen_fd)
/* Server Endlosschleife
 * in listen_fd: Socket Filedescriptor zum Verbindungsaufbau vom Client
 * return: immer SOCKET_ERROR
 */
{
  fd_set the_state;
  SOCKET maxfd;

  FD_ZERO(&the_state);
  FD_SET(listen_fd, &the_state);
  maxfd = listen_fd;

  for (;;) {                    /* Endlosschleife */
    fd_set readfds;
    int ret;
    SOCKET rfd;

    readfds = the_state;        /* select() aendert readfds */
    ret = select((int)maxfd + 1, &readfds, NULL, NULL, NULL);
    exit_if(ret < 0);

    /* TCP Server LISTEN Port (Client connect) pruefen */
    if (FD_ISSET(listen_fd, &readfds)) {
      rfd = tcp_server_init2(listen_fd);
      if (rfd >= 0) {
        FD_SET(rfd, &the_state);        /* neuen Client fd dazu */
        if (rfd > maxfd) {
          maxfd = rfd;
        }
      }
    }

    /* TCP Server CONNECT Ports (Clients communication) pruefen */
    for (rfd = listen_fd + 1; rfd <= maxfd; ++rfd) {
      if (FD_ISSET(rfd, &readfds)) {
        char msgbuf[MAXLEN];
        int msgbuflen;

        /* Meldung vom Client lesen */
        msgbuflen = sizeof(msgbuf);
        ret = tcp_server_read(rfd, msgbuf, &msgbuflen);
        if (SOCKET_ERROR == ret) {
          FD_CLR(rfd, &the_state);      /* toten Client rfd entfernen */
        } else {
          /* Meldung an alle anderen Clients schreiben */
          SOCKET wfd;

          for (wfd = listen_fd + 1; wfd <= maxfd; ++wfd) {
            if (FD_ISSET(wfd, &the_state) && (rfd != wfd)) {
              tcp_server_write(wfd, msgbuf, msgbuflen);
            }
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  WSADATA wsaData;
  int ret;

  /* MS-Windows spezielle Socket Initialisierung */
  ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
  exit_if(ret != 0);
 
  /* open Chat as TCP server */
  loop(tcp_server_init(51234));

  /* MS-Windows spezielle Socket Terminierung */
  WSACleanup();

  return OKAY;
}


TCP Chat Server in anderen Programmiersprachen

Ein TCP Chat Server in Erlang findet sich hier.

Über den Autor

Der Autor hat 20 Jahre professionelle Programmiererfahrung in C und C++. Auch wenn die Firmen und Tätigkeitsbeschreibungen gewechselt haben, das Thema war 20 Jahre lang das gleiche: Echtzeit Systeme Programmierung.

Literatur

Von Brian "Beej Jorgensen" Hall gibt es eine gute Einführung in Netzwerk Programmierung mit Sockets unter Beej's Guide to Network Programming.

Die Programmierung von Servern in C mit der Socket Schnittstelle wird in den Büchern von Richard Stevens behandelt.
UNIX Network Programming, Prentice Hall, 1990, ISBN 0-13-949876-1
Programmieren von UNIX-Netzen, Hanser, Prentice Hall, 1992, ISBN 3-446-16318-2
UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI, Prentice Hall, 1998, ISBN 0-13-490012-X
UNIX Network Programming, Volume 2, Second Edition: Interprocess Communications, Prentice Hall, 1999, ISBN 0-13-081081-9

Die endgültige Literatur über die UNIX Betriebssystem Aufrufe sind die man Pages.

Über Funktionale Programmierung gibt es in Wikipedia eine Einleitung. Der englische Wikipedia Artikel functional programming  hat teilweise anderen Inhalt. Eine schöne Einleitung ist auch Functional Programming For The Rest of Us.

Die Begriffe Funktionale Programmierung und State führen zum Begriff Monad. Wie so oft erschließt sich das Thema über einen Wikipedia Artikel Monads in functional programming. Im Wikibook Artikel Haskell/Understanding monads wird die State monad eingeführt.

Es gibt einen Zusammenhang zwischen Funktionaler Programmierung und Program Verification. Grob vereinfacht ist Program Verification bei Programmen in FP Sprachen möglich und bei Programmen in imperativen Sprachen (praktisch) unmöglich. Für die Beweisbarkeit muß man leider leiden. Wikipedia stellt Program Verification in den grösseren Zusammenhang Formal verification.
Übrigens sorgt Program Verification nicht für fehlerfreie Systeme. Nach dem Beweis ist die Software fehlerfrei, die Hardware kann aber immer noch für Fehler sorgen. Gegen Hardware Fehler werden z.B. 2-aus-3 Entscheider (Voter) eingesetzt. Nun ist leider der Redundanz-Schalter oder der Mehrheits-Entscheider in einem redundanten System genau der Systemteil welcher selbst nicht redundant ausgeführt werden kann. Oder mathematisch/philosophisch betrachtet: Das System nähert sich asymptotisch der Perfektion, erreicht diese aber nie.

Zum Abschluß noch ein Link auf die Betriebssystemvorlesung von Prof. Helmut Weber, meinem Diplom Prof.