28.7 Cross-Plattform-Development
 
Das kleine Beispiel des TCP-Echo-Servers zeigte Ihnen einen einfachen Weg, wie Sie mit einfachen #ifdef Präprozessor-Direktiven eine Cross-Plattform-Anwendung schreiben können. Das Problem dabei allerdings (am Quellcode) war, dass alles für jede Plattform in eine Datei geschrieben wurde. Für das kleine Programm ist das zwar nicht die Rede wert, aber bei umfangreicheren und komplizierten Programmen ist es sinnvoller, einen abstrakten Layer (Abstraktion Layer) zu schreiben. Nicht anders wird dies übrigens bei solchen Mammut-Projekten wie MySQL und dem Apache realisiert – auch hier finden Sie für alle gängigen Plattformen eine extra Version.
28.7.1 Abstraktion Layer
 
Hinter diesem Begriff verbirgt sich nichts Kompliziertes. Der Abstraktion Layer isoliert Plattform-spezifische Funktionen und Datentypen in separate Module für portablen Code. Die Plattform-spezifischen Module werden dann speziell für jede Plattform geschrieben. Des Weiteren erstellen Sie eine neue Headerdatei, in der sich eventuell die Plattform-spezifischen typedef’s und #define’s mitsamt den Funktionsprototypen der Module befinden. Bei der Anwendung selbst binden Sie nur noch diese Headerdatei ein. Auf den folgenden Seiten finden Sie nun die einzelnen Quellcodes für unseren abstrakten Layer – ich werde diesen einfach SOCKETPRX nennen.
28.7.2 Headerdatei Linux/UNIX
 
/* socketprx.h für Linux/UNIX */
#ifndef SOCKETPRX_H_
#define SOCKETPRX_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
/* Ein eigener primitver Datentyp für den Socket-Deskriptor */
#define socket_t int
/* Funktionsprototypen */
void error_exit(char *error_message);
int create_socket( int af, int type, int protocol );
void bind_socket(socket_t
*sock, unsigned long adress, unsigned short port);
void listen_socket( socket_t *sock );
void accept_socket( socket_t *new_socket, socket_t *socket );
void connect_socket(socket_t *sock, char *serv_addr,
unsigned short port);
void TCP_send( socket_t *sock, char *data, size_t size);
void TCP_recv( socket_t *sock, char *data, size_t size);
void UDP_send ( socket_t *sock, char *data, size_t size,
char *addr, unsigned short port);
void UDP_recv( socket_t *sock, char *data, size_t size);
void close_socket( socket_t *sock );
void cleanup(void);
#endif
 28.7.3 Linux/UNIX-Quelldatei
 
/* socketlayer.c – für Linux/UNIX */
#include "socketprx.h"
/* Funktion gibt aufgetretene Fehler aus und
* beendet die Anwendung */
void error_exit(char *error_message) {
fprintf(stderr, "%s: %s\n", error_message, strerror(errno));
exit(EXIT_FAILURE);
}
int create_socket( int af, int type, int protocol ) {
socket_t sock;
const int y = 1;
/* Erzeuge das Socket */
sock = socket(af, type, protocol);
if (sock < 0)
error_exit("Fehler beim Anlegen eines Socket");
/* Mehr dazu siehe Anmerkung am Ende des Listings ... */
setsockopt( sock, SOL_SOCKET,
SO_REUSEADDR, &y, sizeof(int));
return sock;
}
/* Erzeugt die Bindung an die Serveradresse
* (genauer an einen bestimmten Port) */
void bind_socket(socket_t *sock, unsigned long adress,
unsigned short port) {
struct sockaddr_in server;
memset( &server, 0, sizeof (server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(adress);
server.sin_port = htons(port);
if (bind(*sock, (struct sockaddr*)&server,sizeof(server)) < 0)
error_exit("Kann das Socket nicht \"binden\"");
}
/* Teile dem Socket mit, dass Verbindungswünsche
* von Clients entgegengenommen werden */
void listen_socket( socket_t *sock ) {
if(listen(*sock, 5) == –1 )
error_exit("Fehler bei listen");
}
/* Bearbeite die Verbindungswünsche von Clients
* Der Aufruf von accept() blockiert solange,
* bis ein Client Verbindung aufnimmt */
void accept_socket( socket_t *socket, socket_t *new_socket ){
struct sockaddr_in client;
int len;
len = sizeof(client);
*new_socket=accept(*socket,(struct sockaddr *)&client, &len);
if (*new_socket == –1)
error_exit("Fehler bei accept");
}
/* Baut die Verbindung zum Server auf */
void connect_socket(socket_t *sock, char *serv_addr,
unsigned short port) {
struct sockaddr_in server;
struct hostent *host_info;
unsigned long addr;
memset( &server, 0, sizeof (server));
if ((addr = inet_addr( serv_addr )) != INADDR_NONE) {
/* argv[1] ist eine numerische IP-Adresse */
memcpy( (char *)&server.sin_addr, &addr, sizeof(addr));
}
else {
/* Für den Fall der Fälle: Wandle den
* Servernamen bspw. "localhost" in eine IP-Adresse um */
host_info = gethostbyname( serv_addr );
if (NULL == host_info)
error_exit("Unbekannter Server");
memcpy( (char *)&server.sin_addr, host_info->h_addr,
host_info->h_length);
}
server.sin_family = AF_INET;
server.sin_port = htons( port );
/* Baue die Verbindung zum Server auf */
if (connect(
*sock, (struct sockaddr *)&server, sizeof( server)) < 0)
error_exit( "Kann keine Verbindung zum Server herstellen");
}
/* Daten versenden via TCP */
void TCP_send( socket_t *sock, char *data, size_t size) {
if(send( *sock, data, size, 0) == –1 )
error_exit("Fehler bei send()");
}
/* Daten empfangen via TCP */
void TCP_recv( socket_t *sock, char *data, size_t size) {
int len;
len = recv (*sock, data, size, 0);
if( len > 0 || len != –1 )
data[len] = '\0';
else
error_exit("Fehler bei recv()");
}
/* Daten senden via UDP */
void UDP_send ( socket_t *sock, char *data, size_t size,
char *addr, unsigned short port){
struct sockaddr_in addr_sento;
struct hostent *h;
int rc;
/* IP-Adresse vom Server überprüfen */
h = gethostbyname(addr);
if (h == NULL)
error_exit("Unbekannter Host?");
addr_sento.sin_family = h->h_addrtype;
memcpy ( (char *) &addr_sento.sin_addr.s_addr,
h->h_addr_list[0], h->h_length);
addr_sento.sin_port = htons (port);
rc = sendto(*sock, data, size, 0,
(struct sockaddr *) &addr_sento,
sizeof (addr_sento));
if (rc < 0)
error_exit("Konnte Daten nicht senden – sendto()");
}
/* Daten empfangen via UDP */
void UDP_recv( socket_t *sock, char *data, size_t size){
struct sockaddr_in addr_recvfrom;
int len;
int n;
len = sizeof (addr_recvfrom);
n = recvfrom ( *sock, data, size, 0,
(struct sockaddr *) &addr_recvfrom, &len );
if (n < 0) {
printf ("Keine Daten empfangen ...\n");
return;
}
}
/* Socket schließen */
void close_socket( socket_t *sock ){
close(*sock);
}
/* Unter Linux/UNIX nichts zu tun ... */
void cleanup(void){
printf("Aufraeumarbeiten erledigt ...\n");
return;
}
Hinweis In diesem Beispiel zu Linux/UNIX wurde die Funktion setsockopt() verwendet. Durch die Verwendung von der symbolischen Konstante SO_REUSEADDR stellen Sie das Socket so ein, damit es erlaubt ist, dass mehrere Prozesse (Clients) denselben Port teilen – sprich: Mehrere Clients können innerhalb kürzester Zeit mit dem Server in Verbindung treten. Außerdem lösen Sie damit auch das Problem, dass der Server beim Neustart seinen lokalen Port erst nach zwei Minuten Wartezeit wieder benutzen kann.
|
28.7.4 Headerdatei MS-Windows
 
/* socketprx.h für MS-Windows */
#ifndef SOCKETPRX_H_
#define SOCKETPRX_H_
#include <stdio.h>
#include <stdlib.h>
#include <winsock.h>
#include <io.h>
#define socket_t SOCKET
void error_exit(char *error_message);
int create_socket( int af, int type, int protocol );
void bind_socket(socket_t *sock, unsigned long adress,
unsigned short port);
void listen_socket( socket_t *sock );
void accept_socket( socket_t *new_socket, socket_t *socket );
void connect_socket(socket_t *sock, char *serv_addr,
unsigned short port);
void TCP_send( socket_t *sock, char *data, size_t size);
void TCP_recv( socket_t *sock, char *data, size_t size);
void UDP_send (socket_t *sock, char *data, size_t size);
void UDP_recv( socket_t *sock, char *data, size_t size,
char *addr, unsigned short port);
void close_socket( socket_t *sock );
void cleanup(void);
#endif
28.7.5 Windows-Quelldatei
 
/* socketlayer.c – für MS-Windows */
#include <stdio.h>
#include <stdlib.h>
#include <winsock.h>
#include <io.h>
#define socket_t SOCKET
/* Funktion gibt aufgetretene Fehler aus und
* beendet die Anwendung */
void error_exit(char *error_message) {
fprintf(stderr,"%s: %d\n", error_message, WSAGetLastError());
exit(EXIT_FAILURE);
}
/* Initialisiere TCP für Windows ("winsock"),
* legt ein Socket an,
* und gibt das Socket als Rückgabewert zurück */
int create_socket( int af, int type, int protocol ) {
socket_t sock;
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD (1, 1);
if (WSAStartup (wVersionRequested, &wsaData) != 0)
error_exit( "Fehler beim Initialisieren von Winsock");
else
printf("Winsock initialisiert\n");
/* Erzeuge das Socket */
sock = socket(af, type, protocol);
if (sock < 0)
error_exit("Fehler beim Anlegen eines Socket");
return sock;
}
/* Erzeugt die Bindung an die Serveradresse
* (genauer an einen bestimmten Port) */
void bind_socket(socket_t *sock, unsigned long adress,
unsigned short port) {
struct sockaddr_in server;
memset( &server, 0, sizeof (server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(adress);
server.sin_port = htons(port);
if (bind(*sock, (struct sockaddr*) &server,
sizeof( server)) == SOCKET_ERROR)
error_exit("Kann das Socket nicht \"binden\"");
}
/* Teile dem Socket mit, dass Verbindungswünsche
* von Clients entgegengenommen werden */
void listen_socket( socket_t *sock ) {
if(listen(*sock, 5) == –1 )
error_exit("Fehler bei listen");
}
/* Bearbeite die Verbindungswünsche von Clients
* Der Aufruf von accept() blockiert solange,
* bis ein Client Verbindung aufnimmt */
void accept_socket( socket_t *socket, socket_t *new_socket ){
struct sockaddr_in client;
int len;
len = sizeof(client);
*new_socket=accept(*socket, (struct sockaddr *)&client, &len);
if (*new_socket == INVALID_SOCKET)
error_exit("Fehler bei accept");
}
/* Baut die Verbindung zum Server auf */
void connect_socket( socket_t *sock, char *serv_addr,
unsigned short port) {
struct sockaddr_in server;
struct hostent *host_info;
unsigned long addr;
memset( &server, 0, sizeof (server));
if ((addr = inet_addr( serv_addr )) != INADDR_NONE) {
/* argv[1] ist eine numerische IP-Adresse */
memcpy( (char *)&server.sin_addr, &addr, sizeof(addr));
}
else {
/* Für den Fall der Fälle: Wandle den
* Servernamen bspw. "localhost" in eine IP-Adresse um */
host_info = gethostbyname( serv_addr );
if (NULL == host_info)
error_exit("Unbekannter Server");
memcpy( (char *)&server.sin_addr, host_info->h_addr,
host_info->h_length);
}
server.sin_family = AF_INET;
server.sin_port = htons( port );
/* Baue die Verbindung zum Server auf */
if (connect(
*sock, (struct sockaddr*)&server, sizeof( server)) < 0)
error_exit( "Kann keine Verbindung zum Server herstellen");
}
/* Daten versenden via TCP */
void TCP_send( socket_t *sock, char *data, size_t size ) {
if( send (*sock, data, size, 0) == SOCKET_ERROR )
error_exit("Fehler bei send()");
}
/* Daten empfangen via TCP */
void TCP_recv( socket_t *sock, char *data, size_t size) {
int len;
len = recv (*sock, data, size, 0);
if( len > 0 || len != SOCKET_ERROR )
data[len] = '\0';
else
error_exit("Fehler bei recv()");
}
/* Daten senden via UDP */
void UDP_send ( socket_t *sock, char *data, size_t size,
char *addr, unsigned short port){
struct sockaddr_in addr_sento;
struct hostent *h;
int rc;
/* IP-Adresse vom Server überprüfen */
h = gethostbyname(addr);
if (h == NULL)
error_exit("Unbekannter Host?");
addr_sento.sin_family = h->h_addrtype;
memcpy ( (char *) &addr_sento.sin_addr.s_addr,
h->h_addr_list[0], h->h_length);
addr_sento.sin_port = htons (port);
rc = sendto(*sock, data, size, 0,
(struct sockaddr *) &addr_sento,
sizeof (addr_sento));
if (rc == SOCKET_ERROR)
error_exit("Konnte Daten nicht senden – sendto()");
}
/* Daten empfangen via UDP */
void UDP_recv( socket_t *sock, char *data, size_t size){
struct sockaddr_in addr_recvfrom;
int len;
int n;
len = sizeof (addr_recvfrom);
n = recvfrom ( *sock, data, size, 0,
(struct sockaddr *) &addr_recvfrom, &len );
if (n == SOCKET_ERROR)
error_exit("Fehler bei recvfrom()");
}
/* Socket schließen und Winsock freigeben */
void close_socket( socket_t *sock ){
closesocket(*sock);
}
void cleanup(void){
/* Cleanup Winsock */
WSACleanup();
printf("Aufräumarbeiten erledigt ...\n");
}
 28.7.6 All together – die main-Funktionen
 
Nachdem Ihnen nun zwei Versionen von SOCKETPRX zur Verfügung stehen, können Sie die Module jetzt auf dem System Ihrer Wahl übersetzen und ausführen. Der Vorteil, Sie benötigen nur noch eine Hauptfunktion – alle Plattform-spezifischen Eigenheiten verstecken sich ja nun hinter dem Layer. Und noch ein weiterer Vorteil, bei einer guten Planung des Layers gestaltet sich die Erstellung der main()-Funktion erheblich leichter und kürzer – da Sie die Fehlerüberprüfungen nun auch dem Layer überlassen können. Besonders bezahlt macht sich ein solcher Layer, wenn Sie einzelne Routinen immer wieder benötigen. Somit können Sie eine tolle und simple Cross-Plattform-Bibliothek anbieten.
Der Server
Das Beispiel des TCP-Echo-Servers wurde hier erweitert. Daraus ist nun eine Art 1:1-Chat zwischen dem Server und Client geworden (wie Sie mehr als einen Client bearbeiten können, erfahren Sie noch). Der Server »lauscht« am Port 15000 und wartet, bis ein Client mit diesem in Verbindung tritt. Sobald ein Client eine Verbindung zum Server hergestellt hat, können Sie (der Server) dem Client eine Zeichenkette als Nachricht senden. Anschließend wartet der Server auf eine Antwort vom Client. Sendet der Client dem Server die Textfolge »quit«, so bedeutet dies für den Server, dass der Client »aufgelegt« hat und der Server wartet wieder (mittels accept()) auf eine Verbindungsanfrage eines Clients. Hier der Quellcode des Servers:
/* server.c */
#include <string.h>
#include "socketprx.h"
#define BUF 1024
int main (void) {
socket_t sock1, sock2;
int addrlen;
char *buffer = (char*) malloc (BUF);
sock1 = create_socket(AF_INET, SOCK_STREAM, 0);
atexit(cleanup);
bind_socket( &sock1, INADDR_ANY, 15000 );
listen_socket (&sock1);
addrlen = sizeof (struct sockaddr_in);
while (1) {
accept_socket( &sock1, &sock2 );
do {
printf ("Nachricht zum Versenden: ");
fgets (buffer, BUF, stdin);
TCP_send (&sock2, buffer, strlen (buffer));
TCP_recv (&sock2, buffer, BUF-1);
printf ("Nachricht empfangen: %s\n", buffer);
} while (strcmp (buffer, "quit\n") != 0);
close_socket (&sock2);
}
close_socket (&sock1);
return EXIT_SUCCESS;
}
Der Client
Der Quellcode des Clients ist ähnlich simpel aufgebaut. Dieser versucht zunächst eine Verbindung zum Server aufzubauen. Ist dies geglückt, wartet dieser zunächst auf eine Antwort vom Server. Schickt der Server dem Client eine Antwort, so wird diese auf die Standardausgabe ausgegeben. Jetzt ist der Client an der Reihe, dem Server eine Zeichenkette zu senden. Geben Sie hierfür »quit« an, beendet sich die Client-Anwendung und nimmt alle Aufräumarbeiten vor. Dass die Aufräumarbeiten (die Funktion cleanup()) durchgeführt werden, haben Sie mit Standard-Funktion atexit() sichergestellt, welche beim Beenden des Prozesses die Funktion cleanup() aufruft (was unter Linux/UNIX unbedeutend ist). Solch ein Cleanup wird generell gerne in dieser Form verwendet. Selbiges Cleanup wird übrigens auch beim Server eingerichtet und durchgeführt, sofern sich dieser beendet.
Ansonsten findet ein reger Kommunikationsaustausch zwischen Server und Client statt. Hier der Quellcode für den Client.
/* client.c */
#include <string.h>
#include "socketprx.h"
#define BUF 1024
int main (int argc, char **argv) {
socket_t sock;
char *buffer = (char *)malloc (BUF);
if( argc < 2 ){
printf("Usage: %s ServerAdresse\n", *argv);
exit(EXIT_FAILURE);
}
sock = create_socket(AF_INET, SOCK_STREAM, 0);
atexit(cleanup);
connect_socket(&sock, argv[1], 15000);
do {
buffer[0] = '\0';
TCP_recv (&sock, buffer, BUF-1);
printf ("Nachricht erhalten: %s\n", buffer);
printf ("Nachricht zum Versenden: ");
fgets (buffer, BUF, stdin);
TCP_send (&sock, buffer, strlen (buffer));
} while (strcmp (buffer, "quit\n") != 0);
close_socket (&sock);
return EXIT_SUCCESS;
}
Hinweis Vergessen Sie beim Übersetzen nicht die Datei socketlayer.c hinzuzulinken!
|
Das Programm bei der Ausführung:
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.6
Client-Anwendung unter MS-Windows bei der Ausführung
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.7
Server-Anwendung unter MS-Windows bei der Ausführung
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.8
Client-Anwendung unter Linux bei der Ausführung
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.9
Server-Anwendung unter Linux bei der Ausführung
28.7.7 Ein UDP-Beispiel
 
Bei unserem Layer wurden ja auch Funktionen zum Datenaustausch via UDP geschrieben. Außerdem wurde hier auch einiges zu UDP erwähnt, so dass ich Ihnen hier ein kleines Client-/Server-Beispiel nicht vorenthalten will.
Der Server
Der Server wartet auf die Verbindung irgendeines Clients, der einen einfachen String als zweites Argument in der Kommandozeile versendet. Der Server gibt diese Zeichen mitsamt der lokalen Server-Uhrzeit auf die Standardausgabe aus und wartet anschließend erneut wieder auf Daten am Port 1234 von irgendeinem Client.
/* udp_server.c */
#include <string.h>
#include <time.h>
#include "socketprx.h"
#define LOCAL_SERVER_PORT 1234
#define BUF 255
int main (int argc, char **argv) {
socket_t sock;
char puffer[BUF];
time_t time1;
char loctime[BUF];
char *ptr;
/* Socket erzeugen */
sock = create_socket( AF_INET, SOCK_DGRAM, 0);
atexit(cleanup);
bind_socket(&sock, INADDR_ANY, LOCAL_SERVER_PORT);
printf ("Warte auf Daten am Port (UDP) %u\n",
LOCAL_SERVER_PORT);
/* Server-Schleife */
while (1) {
memset (puffer, 0, BUF);
UDP_recv( &sock, puffer, BUF );
/* Zeitangaben präparieren */
time(&time1);
strncpy(loctime, ctime(&time1), BUF);
ptr = strchr(loctime, '\n' );
*ptr = '\0';
/* Erhaltene Nachricht ausgeben */
printf ("%s: Daten erhalten: %s\n",
loctime, puffer);
}
return EXIT_SUCCESS;
}
Der Client
/* udp_client.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "socketprx.h"
#define BUF 1024
#define SERVER_PORT 1234
int main (int argc, char **argv) {
socket_t sock;
/* Kommandozeile auswerten */
if (argc < 3) {
printf ("Usage: %s <server> <string>\n",argv[0]);
exit (EXIT_FAILURE);
}
/* Socket erzeugen */
sock = create_socket( AF_INET, SOCK_DGRAM, 0);
atexit(cleanup);
bind_socket(&sock, INADDR_ANY, 0);
UDP_send(&sock,argv[2],strlen(argv[2]),argv[1], SERVER_PORT);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.10
Der UDP-Server im Einsatz unter Linux
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.11
Die (UDP)Client-Anwendung unter Linux
28.7.8 Mehrere Clients gleichzeitig behandeln
 
Einen gravierenden Nachteil allerdings hatten alle Server-Beispiele, die Sie bisher geschrieben haben. Alle Server sind nur für eine Client-Anfrage ausgelegt – sprich die Server konnten nur einen Client gleichzeitig bearbeiten. Alle anderen Clients wurden in die Warteschlange gesteckt und mussten warten, bis der Server wieder frei für weitere Verbindungswünsche ist. Für Anwendungen wie Webserver, Chat-Programme, Spiele-Server etc. ist dieser Zustand unbrauchbar.
Um diesen Umstand zu verbessern, gibt es mehrere Möglichkeiten. Wobei sich hier die verschiedenen Varianten erheblich auf den Plattformen unterscheiden. Sinnvolle und mögliche Varianten wären:
|
Die Verwendung von (Multi-)Threads. Dabei wird für jeden Client ein neuer Thread gestartet. Der »Nachteil« von den Threads ist, dass es auf den verschiedenen Plattformen die verschiedensten Thread-Bibliotheken gibt und somit nur bedingt portabel sind. |
|
Die Verwendung von Prozessen. Hierbei wird für jeden Client ein neuer (Server-)Prozess gestartet – jeder Client bekommt hierbei praktisch seinen eigenen Server. Vorraussetzung hierfür ist allerdings, dass Sie sich mit der Systemprogrammierung der entsprechenden Plattform auskennen. Schließlich müssen die einzelnen Prozesse kontrolliert werden. |
Hinweis Sofern Sie mehr zur Natur Linux/UNIX tendieren, kann ich Ihnen mein Buch »Linux-UNIX-Programmierung« ans Herz legen. Hier wird das Thema »Netzwerkprogrammierung« weit umfassender – auch mit den Threads und Prozessen – behandelt.
|
Neben diesen Möglichkeiten gibt es selbstverständlich eine Reihe weiterer Möglichkeiten, um mehrere Clients zu behandeln. Unter MS-Windows bspw. könnten Sie hierfür die WSA-Routinen WSAAsyncSelect() oder WSAEventSelect() verwenden. Bei Linux/UNIX hingegen würden sich hierbei auch asynchrone E/A-Routinen nach »POSIX«-Erweiterungen eignen.
select() – Eine portablere Altervative
Neben den eben beschriebenen Möglichkeiten, die Sie verwenden können, um mehrere Clients zu behandeln, soll hier auf die Möglichkeit mit der Funktion select() etwas genauer eingegangen werden. Diese Funktion ist sowohl auf MS- als auch auf Linux/UNIX-Systemen vorhanden – und somit ein geeigneter Kandidat für eine portablere Lösung.
Das Problem bei einem Server, wie Sie ihn bisher verwendet haben, ist, dass dieser immer nur auf einen Socket-Deskriptor gewartet hat und auch immer über einen Socket-Deskriptor Daten empfangen bzw. versendet wurden. Wurde beim Server bspw. revc() aufgerufen, blockierte dieser Aufruf den Socket-Deskriptor solange, bis der Client wirklich Daten an diesen gesendet hat. Klar, man kann das Blockieren auch damit umgehen, indem man den Socket-Deskriptor als nicht-blockierend einrichtet (bspw. mit fcntl()). Allerdings sollte man bedenken, dass hierbei ständig überprüft wird, ob an einem Socket Daten vorliegen – das heißt, es wird in einer Schleife dauerhaft gepollt – was die CPU unnötig belastet. Mit der Funktion select() können Sie den Socket-Deskriptor so einrichten, dass nur dann CPU-Zeit benötigt wird, wenn auch wirklich Daten an einem Socket-Deskriptor vorliegen.
Hinweis Dieser Abschnitt sollte nicht den Eindruck erwecken, die Funktion select() sei eine Routine, die sich nur zur Netzwerkprogrammierung eignet. select() kann überall dort eingesetzt werden, wo auch Deskriptoren verwendet werden bzw. synchrones Multiplexing verwendet werden soll. Des Weiteren lassen sich mit select() auch hervorragend so genannte Timeouts einrichten.
|
Hierzu die Syntax zur Funktion select() unter Linux/UNIX:
// entsprechend nach POSIX 1003.1–2001
#include <sys/select.h>
// entsprechend nach früheren Standards
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select( int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout );
Und hier die ähnliche Syntax unter MS-Windows:
int select( int n, fd_set FAR * readfds, fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout );
Mit dem ersten Parameter n geben Sie die Größe der folgenden Menge an. Hierfür wird gewöhnlich der Wert des höchsten (Socket-)Deskriptor plus eins angegeben. Sie sollten sich allerdings nicht darauf verlassen, dass hier automatisch eine aufsteigende und lückenlose Reihenfolge für die (Socket-)Deskriptoren vergeben werden. Welche Nummer der nächste (Socket-)Deskriptor verwendet, entscheidet immer noch das System. Daher empfiehlt es sich, jeden gesetzten (Socket-)Deskriptor mit dem zu vergleichen, welcher rein theoretisch der Höchste ist.
Die nächsten drei Parameter sind Zeiger auf die fd_sets, welche zum Lesen, Schreiben oder auf Ausnahmen getestet werden. Sofern Sie einen der Parameter nicht verwenden wollen, können Sie hierfür NULL angeben. Drei getrennte Sets sind nötig, da man ja nicht alle (Socket-)Deskriptoren auf Lesen oder Schreiben testen möchte.
Der am häufigsten verwendete Parameter (wie es auch im anschließenden Beispiel der Fall ist) ist readfds. Mit diesem Parameter wird überprüft, ob auf den (Socket-)Deskriptoren Daten zum Lesen vorhanden sind. Das Gegenstück dazu ist der Parameter writefds – hiermit können Sie die Beschreibbarkeit von (Socket-)Deskriptoren überprüfen – sprich, ob ein Deskriptor bereit ist, eine Ausgabe anzunehmen (diese wird bspw. gerne bei Pipes verwendet). Der dritte fd_set-Parameter exceptfds wird weitaus seltener verwendet. Dieser kann verwendet werden, um zu überprüfen, ob bei einen (Socket-)Deskriptor irgendwelche besonderen Zustände (Ausnahmen) vorliegen. Dies wird bspw. bei Out-of-band-Daten (MSG_OOB) verwendet (siehe Manual-Page zu send() und/oder recv()).
Nach dem Aufruf von select() wird diese Menge in Teilmengen der Filedeskriptoren verteilt, welche die Bedingungen erfüllen.
Mit dem letzten Parameter können Sie ein Timeout, eine Zeit im Format von Sekunden (tv_sec) und Mikrosekunden (tv_usec), einrichten. Diese Zeit wird dann abgewartet, bis eine bestimmte Bedingung eintritt. Sind Sie daran nicht interessiert, können Sie auch hier NULL angeben. Es gibt aber auch einen Nachteil, wenn sich select() vorzeitig verabschiedet (vor Ablauf der festgelegten Zeit). select() gibt keine Auskunft darüber, wie lange denn tatsächlich gewartet wurde. Dazu muss extra eine Funktion wie bspw. gettimeofday() aufgerufen werden.
Die Funktion gibt die Anzahl der Filedeskriptoren zurück, die Ihre Bedingung erfüllt haben (einfach, die Anzahl der (Socket-)Deskriptoren, die bereit sind). Wenn die Zeit abgelaufen ist (Timeout) wird 0 und bei einem Fehler des Funktionsaufrufs select()-1 zurückgegeben.
Ein Problem bei select() ist, dass es mit Bitfeldern arbeitet – was somit abhängig vom Betriebssystem ist. Die Bitfeldgröße bei BSD bspw. beträgt 256 und unter Linux 1024. Somit können auf BSD nur die ersten 256 und unter Linux 1024 Deskriptoren angesprochen werden. Unter MS-Windows kann dieser Wert gar nur bis zu 64 Deskriptoren betragen. Wie viele Deskriptoren Sie denn nun tatsächlich pro Prozess verwenden können, ist mit der symbolischen Konstante FD_SETSIZE definiert. Natürlich macht es jetzt wenig Sinn, alle (Socket–) Deskriptoren zu überwachen. Zum Glück müssen Sie sich eigentlich recht wenig um diese Menge kümmern, da Ihnen der Datentyp fd_set die Arbeit zum Speichern der (Socket-)Deskriptoren abnimmt und einige Makros den Zugriff darauf erleichtern. Hier die Makros, um die Mengen zu bearbeiten:
FD_ZERO(fd_set *set);
FD_SET(int element, fd_set *set);
FD_CLR(int element, fd_set *set);
FD_ISSET(int element, fd_set *set);
Die Makros lassen sich recht schnell erklären. FD_ZERO() macht aus der Menge set eine leere Menge, FD_SET() fügt element der Menge set hinzu und FD_CLR() entfernt element aus der Menge set. Mit FD_ISSET() können Sie überprüfen, ob element in der Menge set vorkommt (genauer: gesetzt ist).
Das folgende Beispiel, ein einfacher TCP-Echo-Server, soll Ihnen die Funktion select() demonstrieren. Das Beispiel ist dem 1:1-Chat zwischen dem Server und dem Client recht ähnlich, welches Sie in diesem Kapitel bereits geschrieben haben. Nur, begnügen wir uns beim Server jetzt damit, dass dieser nur die Zeichenketten auf dem Bildschirm ausgibt und dem Client nicht antwortet. Allerdings mit dem gravierenden Unterschied, dass der Server nun mehrere Clients »gleichzeitig« behandeln kann. Genauer bis zu FD_SETSIZE Clients. Sobald auch hier ein Client die Zeichenfolge »quit« sendet, entfernt der Server den Client (genauer den (Socket-)Deskriptor) aus der Menge.
Im Beispiel wurde aus Übersichtlichkeitsgründen darauf verzichtet, die select()-Abhandlung in unseren Layer SOCKETPRX zu implementieren. In der Praxis wäre dies allerdings sehr sinnvoll, da die Verwendung von select() doch zu einem der etwas komplizierteren Teile der Programmierung gehört. Dank dem Layer SOCKETPRX, fällt allerdings die Übersicht der Abhandlung von select() erheblich leichter als ohne. Hier der gut dokumentierte Source-Code zum Server, welcher nun mehrere Clients auf einmal abarbeiten kann:
/* multi_server.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "socketprx.h"
#define BUF 1024
int main (void) {
socket_t sock1, sock2, sock3;
int i, ready, sock_max, max=-1;
int client_sock[FD_SETSIZE];
fd_set gesamt_sock, lese_sock;
char *buffer = (char*) malloc (BUF);
sock_max = sock1 = create_socket(AF_INET, SOCK_STREAM, 0);
atexit(cleanup);
bind_socket( &sock1, INADDR_ANY, 15000 );
listen_socket (&sock1);
for( i=0; i<FD_SETSIZE; i++)
client_sock[i] = –1;
FD_ZERO(&gesamt_sock);
FD_SET(sock1, &gesamt_sock);
for (;;) {
/* Immer Aktualisieren */
lese_sock = gesamt_sock;
/* Hier wird auf die Ankunft von Daten oder
* neuer Verbindungen von Clients gewartet */
ready = select( sock_max+1, &lese_sock, NULL, NULL, NULL );
/* Eine neue Client-Verbindung ... ? */
if( FD_ISSET(sock1, &lese_sock)) {
accept_socket( &sock1, &sock2 );
/* Freien Platz für (Socket-)Deskriptor
* in client_sock suchen und vergeben */
for( i=0; i< FD_SETSIZE; i++)
if(client_sock[i] < 0) {
client_sock[i] = sock2;
break;
}
/* Mehr als FD_SETSIZE Clients sind nicht möglich */
if( i == FD_SETSIZE )
error_exit("Server überlastet – zuviele Clients");
/* Den neuen (Socket-)Deskriptor zur
* (Gesamt)Menge hinzufügen */
FD_SET(sock2, &gesamt_sock);
/* select() benötigt die höchste
* (Socket-)Deskriptor-Nummer */
if( sock2 > sock_max )
sock_max = sock2;
/* höchster Index für client_sock
* für die anschließende Schleife benötigt */
if( i > max )
max = i;
/* ... weitere (Lese-)Deskriptoren bereit ? */
if( --ready <= 0 )
continue; //Nein ...
} //if(FD_ISSET ...
/* Ab hier werden alle Verbindungen von Clients auf
* die Ankunft von neuen Daten überprüft */
for(i=0; i<=max; i++) {
if((sock3 = client_sock[i]) < 0)
continue;
/* (Socket-)Deskriptor gesetzt ... */
if(FD_ISSET(sock3, &lese_sock)){
/* ... dann die Daten lesen */
TCP_recv (&sock3, buffer, BUF-1);
printf ("Nachricht empfangen: %s\n", buffer);
/* Wenn quit erhalten wurde ... */
if (strcmp (buffer, "quit\n") == 0) {
/* ... hat sich der Client beendet */
close_socket (&sock3); //Socket schließen
FD_CLR(sock3, &gesamt_sock); //aus Menge löschen
client_sock[i] = –1; //auf –1 setzen
printf("Ein Client hat sich beendet\n");
}
/* Noch lesbare Deskriptoren vorhanden ... ? */
if( --ready <= 0 )
break; //Nein ...
}
}
} // for(;;)
return EXIT_SUCCESS;
}
Und jetzt noch der Quellcode zur entsprechenden Client-Anwendung:
/* multi_client.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "socketprx.h"
#define BUF 1024
int main (int argc, char **argv) {
socket_t sock;
char *buffer = (char *)malloc (BUF);
if( argc < 2 ){
printf("Usage: %s ServerAdresse\n", *argv);
exit(EXIT_FAILURE);
}
sock = create_socket(AF_INET, SOCK_STREAM, 0);
atexit(cleanup);
connect_socket(&sock, argv[1], 15000);
do {
buffer[0] = '\0';
printf ("Nachricht zum Versenden: ");
fgets (buffer, BUF, stdin);
TCP_send (&sock, buffer, strlen (buffer));
} while (strcmp (buffer, "quit\n") != 0);
close_socket (&sock);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.12
Der Server kann jetzt mehrere Anfragen (Clients) bearbeiten
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.13
Einer von zwei gerade aktiven Clients
 Hier klicken, um das Bild zu Vergrößern
Abbildung 28.14
Der andere der beiden aktiven Clients zur selben Zeit
|