Internet-Software arbeitet nach dem Client-Server-Konzept. Die Client-Anwendung wird vom Anwender genutzt, um einen bestimmten Dienst in Anspruch zu nehmen.
Die Server-Software stellt die entsprechende Funktionalität über das Internet zur Verfügung.
Für die Initiierung der Kommunikation ist die Client-Anwendung verantwortlich. Die Server-Software wartet dagegen passiv auf den Aufbau einer Verbindung. Wie Sie sehen werden, spiegelt sich dies auch in den verwendeten Funktionen des Socket-Interface wider. Die Trennung zwischen aktivem und passivem Verbindungsaufbau gilt für viele der klassischen Internet-Anwendungen wie eMail (POP und SMTP), News (NNTP) und WWW (HTTP). Dennoch muß aber bereits ein noch relativ einfach anmutender FTP-Client in der Lage sein, eingehende Verbindungen entgegenzunehmen.
Auch bei der Implementierung eines Servers hat man die Wahl zwischen TCP und UDP. Analog zu den im Teil 3 dieser Reihe besprochenen Client-Anwendungen unterscheidet sich auch die Implementierung eines Servers je nach verwendetem Protokoll.
Schauen wir uns diesmal zunächst UDP an. Einen Server dieses Typs können Sie bereits mit Ihren bisher erworbenen Kenntnissen realisieren, da Ihre Server-Software vergleichbar mit dem Echo-Client aus dem letzten Teil schlicht auf das Eintreffen von Daten wartet; prinzipiell entfällt bei UDP der passive Verbindungsaufbau.
Nach dem obligatorischen Erzeugen eines Sockets mit der Funktion socketO, legen Sie mit bind() den Port fest, an dem Ihre Server-Software auf Daten von einer Client-Anwendung warten soll. Dazu rufen Sie eine erweiterte Form von recv() auf, die Ihnen erlaubt, die IP-Adresse und den Port des Absenders zu ermitteln:
long recvfrom(int s. void *buf, long len, int flags, void *from, int *fromlen);
Der Übergabe des Socket-Handle folgt der Zeiger auf den Empfangspuffer und dessen Länge. Danach kommt ein Zeiger auf eine vom Aufrufer zur Verfügung gestellte sockaddr in-Struktur und ein Zei-ger(!) auf deren Länge.
In die sockaddr_in-Struktur schreibt die Funktion die IP-Adresse und den Port des Absenders der Nachricht. So weiß der Server, woher die eingegangene Nachricht stammt und kann die entsprechende Antwort an den richtigen Client mit sendto() schicken.
Einen auf TCP basierenden Server muß die auf den Client bezogene Kombination aus IP-Adresse und Port nicht weiter interessieren, da zwischen den beiden Kommu-nikationspartnem eine feste Verbindung besteht. Nachdem Sie ein Socket erzeugt und den lokalen Port mit bind() festgelegt haben, richten Sie für das Socket eine Warteschlange für eingehende Verbindungswünsche ein, die dann von Ihrem Programm nach und nach abgearbeitet werden. Ohne diese Warteschlange müßte Ihr Server immer unverzüglich auf einen Client reagieren, was nicht ohne weiteres möglich ist, da der Server immer nur mit einem Client gleichzeitig kommuniziert. Die Funktion
int listen(int s, int backlog);
richtet diese Warteschlange ein. Sie erwartet als ersten Parameter das Socket-Handle und als zweiten die Anzahl der maximal in die Warteschlange zu stellenden Verbindungswünsche. Auf die eigentliche Verbindung wartet dann erst die folgende Funktion:
int accept(int s, const void *addr, int *addrlen);
Neben dem Socket-Handle wird eine vom Aufrufer bereitgestellte sockaddr_in-Struktur und deren Länge als Zeiger(!) übergeben. Die sockaddr_in-Strukur dient beim Zustandekommen einer Verbindung dazu, die IP-Adresse und den Port des Clients aufzunehmen. Ähnlich wie connect() kehrt auch diese Funktion erst zurück, nachdem eine Verbindung zustande gekommen oder aber ein Fehler aufgetreten ist. Das Funktionsergebnis ist in diesem Fall das Socket-Handle eines neuen Socket, der die eben eingerichtete Verbindung repräsentiert, d.h. über diesen neuen Socket tauschen Ihre Server-Software und die Client-Anwendung Daten aus. Der beim Aufruf von accept() angegebene Socket bleibt erhalten und kann wieder für nachfolgende accept()-Aufrufe eingesetzt werden.
Als Beispiel für die Implementierung eines TCP-Servers habe ich mir den Dienst "Quote of the Day" (qotd) herausgesucht. Dieser Dienst wartet schlicht auf die Verbindungsaufnahme durch einen Client und sendet anschließend eine mehr oder minder sinnige Nachricht zurück. Nach der Initialisierung des Socket-Interface wird die Portnummer für den qotd-Dienst mittels getservbyname() ermittelt. Anschließend wird das Socket mit socket() angelegt und durch bind() an den mit getservbynameO ermittelten lokalen Port gebunden. Mit listen() wird das Socket auf die Entgegennahme von Verbindungen vorbereitet. In einer Endlosschleife wartet unser Server mit accept() schließlich immer wieder darauf, daß ein Client eine Verbindung aufbaut. Ist eine Verbindung zustande gekommen, sendet das Programm einen Text zum Client und schließt das Socket wieder, um auf eine neue Verbindung zu warten.
Die längste Zeit wird der qotd-Server daher auf die Rückkehr von accept() warten. Auch die meisten anderen bisher kennen-gelemten Funktionen warten mit der Rückkehr zum Aufrufer, bis tatsächlich die Daten mit der Gegenstelle ausgetauscht sind, hemmen also den weiteren Programmablauf. Das kann etwa im Falle von connect() nur einige Sekunden dauern, bei accept() dagegen unerträglich lang sein. Die Funktion recv() wartet dann auch noch solange auf eingehende Daten, bis ein Timer vom Socket-Interface abgelaufen ist. Dieses Blocking genannte Verhalten führt in einer GEM-Anwendung dazu, daß auf Benutzereingaben nicht mehr reagiert werden kann oder andere Anwendungen munter über die eigenen Fenster malen. Die Socket-Schnittstelle sieht aber mit
long sfcntKint s, long cmd, long arg);
eine Möglichkeit vor, das Blocking für das Socket mit dem Handle s abzuschalten Dazu wird der Parameter cmd auf F_SETFL und der Parameter arg auf O NONBLOCK gesetzt.
Von da an kehrt connect() sofort mit EINPROGRESS zurück und baut im Hintergrund die Verbindung auf. In einer Programmschleife, die beispielsweise auch die Nachrichten der Benutzeroberfläche verarbeitet, rufen Sie immer wieder connect() auf und werten die Rückgabe aus. Mit der Rückgabe EISCONN ist die Verbindung aufgebaut und Sie können mit der Kommunikation beginnen. Die Funktion recv() auf einen non-Blocking-Socket angewendet, liefert bei einem leerem Empfangspuffer EWOULDBLOCK und hemmt damit die Abarbeitung anderer Ereignisse ebenfalls nicht mehr. Da sich sfcntl() immer auf einen von Ihnen erzeugten Socket bezieht, können Sie etwa bei DNS-Anfragen via gethostbyname() das Warten auf das Funktionsergebnis leider nicht verhindern.
Mit Ende diesen Teils haben Sie alle Funktionen des Socket-Interface kennengelernt, die Sie benötigen, um Internet-Anwendungen in einer Client- oder Server-Variante basierend auf TCP oder UDP zu erstellen. Im nächsten und letzten Teil der Einführung werde ich Ihnen einen weiteren Weg zeigen,- das blockierende Verhalten der Socket-Funktionen zu umgehen sowie auf einige besondere Möglichkeiten von IConnect eingehen.