25.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 (am Quellcode) war allerdings, 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 (Abstraction 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.
25.7.1 Abstraction Layer 

Hinter dem Begriff Abstraction Layer verbirgt sich nichts Kompliziertes. Der Abstraction Layer isoliert plattformspezifische Funktionen und Datentypen in separate Module für portablen Code. Die plattformspezifischen Module werden dann speziell für jede Plattform geschrieben. Des Weiteren erstellen Sie eine neue Headerdatei, in der sich eventuell die plattformspezifischen typedef und #define 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.
25.7.2 Headerdatei für 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
25.7.3 Linux/UNIX-Quellcodedatei 

/* socketlayer.c - für Linux/UNIX */ #include "socketprx.h" /* Die 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 Sockets"); /* 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 gesagt 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 so lange, * bis ein Client Verbindung aufnimmt. */ void accept_socket( socket_t *socket, socket_t *new_socket ){ struct sockaddr_in client; unsigned 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) { unsigned 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 des Servers ü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; unsigned 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 ist 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 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. |
25.7.4 Headerdatei für 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
25.7.5 Windows-Quellcodedatei 

/* socketlayer.c - für MS-Windows */ #include <stdio.h> #include <stdlib.h> #include <winsock.h> #include <io.h> #define socket_t SOCKET /* Die 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 Sockets"); return sock; } /* Erzeugt die Bindung an die Serveradresse * (genauer gesagt 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 so lange, * bis ein Client Verbindung aufnimmt. */ void accept_socket( socket_t *socket, socket_t *new_socket ){ struct sockaddr_in client; unsigned 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; unsigned 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("Aufraeuumarbeiten erledigt ...\n"); }
25.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 ist, dass Sie nur noch eine Hauptfunktion benötigen – alle plattformspezifischen Eigenheiten verstecken sich ja nun hinter dem Layer. Und es gibt noch einen weiteren 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. Der Quellcode des Servers sieht so aus:
/* 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 er 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 der Standard-Funktion atexit() sichergestellt, die beim Beenden des Prozesses die Funktion cleanup() aufruft (was unter Linux/UNIX unbedeutend ist). Solch ein Cleanup wird generell gern in dieser Form verwendet. Dasselbe 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 sehen Sie den 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! |
Abbildung 25.6 zeigt das Programm bei der Ausführung.
Abbildung 25.6 Die Client-Anwendung unter MS-Windows bei der Ausführung
Abbildung 25.7 Die Server-Anwendung unter MS-Windows bei der Ausführung
Abbildung 25.8 Die Client-Anwendung unter Linux bei der Ausführung
Abbildung 25.9 Die Server-Anwendung unter Linux bei der Ausführung
25.7.7 Ein UDP-Beispiel 

Bei unserem Layer wurden ja auch Funktionen zum Datenaustausch via UDP geschrieben. Außerdem wurde auch einiges zu UDP erwähnt, sodass 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; }
Abbildung 25.10 zeigt das Programm bei der Ausführung.
Abbildung 25.10 Der UDP-Server im Einsatz unter Linux
Abbildung 25.11 Die (UDP)Client-Anwendung unter Linux
25.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 für weitere Verbindungswünsche frei ist. Für Anwendungen wie Webserver, Chat-Programme, Spiele-Server etc. ist dieser Zustand unbrauchbar.
Um diesen Zustand zu verbessern, gibt es mehrere Möglichkeiten, wobei sich hier die Varianten auf den verschiedenen Plattformen erheblich 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 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. Voraussetzung 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 zu Linux/UNIX tendieren, möchte ich Ihnen mein Buch »Linux-UNIX-Programmierung« ans Herz legen. In ihm wird das Thema »Netzwerkprogrammierung« weit umfassender – auch mit den Threads und Prozessen – behandelt. |
Neben diesen Möglichkeiten gibt es selbstverständlich eine Reihe weiterer Verfahren, um mehrere Clients zu behandeln. Unter MS-Windows beispielsweise könnten Sie hierfür die WSA-Routinen WSAAsyncSelect() oder WSAEventSelect() verwenden. Bei Linux/UNIX hingegen würden sich hierfür auch asynchrone E/A-Routinen nach »POSIX«-Erweiterungen eignen.
»select()« – Eine portablere Alternative
Neben den eben beschriebenen Möglichkeiten, die Sie verwenden können, um mehrere Clients zu bedienen, 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 hat. Wurde beim Server beispielsweise revc() aufgerufen, blockierte dieser Aufruf den Socket-Deskriptor so lange, bis der Client wirklich Daten an diesen gesendet hat. Natürlich kann man das Blockieren auch dadurch umgehen, dass man den Socket-Deskriptor als nicht-blockierend einrichtet (beispielsweise mit fcntl()). Allerdings sollten Sie 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 sogenannte Timeouts einrichten. |
Hier sehen Sie 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 ist 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-)Deskriptors 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 wird. 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, der rein theoretisch der höchste ist.
Die nächsten drei Parameter sind Zeiger auf die fd_sets, die 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 Überprüfung wird beispielsweise gerne bei Pipes verwendet). Der dritte fd_set-Parameter, exceptfds, wird weitaus seltener verwendet. Mit ihm überprüfen Sie, ob bei einem (Socket-)Deskriptor irgendwelche besonderen Zustände (Ausnahmen) vorliegen. Dies wird beispielsweise 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, die 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 dem Ablauf der festgelegten Zeit). select() gibt keine Auskunft darüber, wie lange denn tatsächlich gewartet wurde. Dazu muss extra eine Funktion wie beispielsweise 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 beispielsweise 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 sogar nur bis zu 64 Deskriptoren betragen. Wie viele Deskriptoren Sie 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 sehen Sie 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 gesagt: 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, den 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 gibt es den gravierenden Unterschied, dass der Server nun mehrere Clients »gleichzeitig« behandeln kann – genauer gesagt 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 des Layers SOCKETPRX kann ich select() erheblich leichter abhandeln als ohne. Hier folgt der gut dokumentierte Source-Code zum Server, der nun die Abfragen mehrerer 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 - zu viele 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"); } /* Sind noch lesbare Deskriptoren vorhanden ...? */ if( --ready <= 0 ) break; //Nein ... } } } // for(;;) return EXIT_SUCCESS; }
Und jetzt folgt 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; }
Abbildung 25.12 zeigt das Programm bei der Ausführung.
Abbildung 25.12 Der Server kann jetzt mehrere Anfragen (Clients) bearbeiten.
Abbildung 25.13 Einer von zwei gerade aktiven Clients
Abbildung 25.14 Der andere der beiden aktiven Clients zur selben Zeit
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.