Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

 << zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch für Linux, Unix und Windows
– 2., aktualisierte und erweiterte Auflage 2006
Buch: C von A bis Z

C von A bis Z
1.116 S., mit CD, Referenzkarte, 39,90 Euro
Galileo Computing
ISBN 3-89842-643-2
gp Kapitel 28 Netzwerkprogrammierung und Cross-Plattform-Entwicklung
  gp 28.1 Begriffe zur Netzwerktechnik
    gp 28.1.1 IP-Nummern
    gp 28.1.2 Portnummer
    gp 28.1.3 Host- und Domainname
    gp 28.1.4 Nameserver
    gp 28.1.5 Das IP-Protokoll
    gp 28.1.6 TCP und UDP
    gp 28.1.7 Was sind Sockets?
  gp 28.2 Headerdateien zur Socketprogrammierung
    gp 28.2.1 Linux/UNIX
    gp 28.2.2 Windows
  gp 28.3 Client-/Server-Prinzip
    gp 28.3.1 Loopback-Interface
  gp 28.4 Erstellen einer Client-Anwendung
    gp 28.4.1 socket() – Erzeugen eines Kommunikationsendpunktes
    gp 28.4.2 connect() – Client stellt Verbindung zum Server her
    gp 28.4.3 Senden und Empfangen von Daten
    gp 28.4.4 close(), closesocket()
  gp 28.5 Erstellen einer Server-Anwendung
    gp 28.5.1 bind() – Festlegen einer Adresse aus dem Namensraum
    gp 28.5.2 listen() – Warteschlange für eingehende Verbindungen einrichten
    gp 28.5.3 accept() und die Serverhauptschleife
  gp 28.6 (Cross-Plattform)TCP-Echo-Server
    gp 28.6.1 Der Client
    gp 28.6.2 Der Server
  gp 28.7 Cross-Plattform-Development
    gp 28.7.1 Abstraktion Layer
    gp 28.7.2 Headerdatei Linux/UNIX
    gp 28.7.3 Linux/UNIX-Quelldatei
    gp 28.7.4 Headerdatei MS-Windows
    gp 28.7.5 Windows-Quelldatei
    gp 28.7.6 All together – die main-Funktionen
    gp 28.7.7 Ein UDP-Beispiel
    gp 28.7.8 Mehrere Clients gleichzeitig behandeln
  gp 28.8 Weitere Anmerkungen zur Netzwerkprogrammierung
    gp 28.8.1 Das Datenformat
    gp 28.8.2 Der Puffer
    gp 28.8.3 Portabilität
    gp 28.8.4 Von IPv4 nach IPv6
    gp 28.8.5 RFC-Dokumente (Request for Comments)
    gp 28.8.6 Sicherheit


Galileo Computing - Zum Seitenanfang

28.7 Cross-Plattform-Development  downtop

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.


Galileo Computing - Zum Seitenanfang

28.7.1 Abstraktion Layer  downtop

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.


Galileo Computing - Zum Seitenanfang

28.7.2 Headerdatei Linux/UNIX  downtop

/* 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

Galileo Computing - Zum Seitenanfang

28.7.3 Linux/UNIX-Quelldatei  downtop

/* 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.



Galileo Computing - Zum Seitenanfang

28.7.4 Headerdatei MS-Windows  downtop

/* 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

Galileo Computing - Zum Seitenanfang

28.7.5 Windows-Quelldatei  downtop

/* 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");
}

Galileo Computing - Zum Seitenanfang

28.7.6 All together – die main-Funktionen  downtop

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:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.6   Client-Anwendung unter MS-Windows bei der Ausführung

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.7   Server-Anwendung unter MS-Windows bei der Ausführung

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.8   Client-Anwendung unter Linux bei der Ausführung

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.9   Server-Anwendung unter Linux bei der Ausführung


Galileo Computing - Zum Seitenanfang

28.7.7 Ein UDP-Beispiel  downtop

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:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.10   Der UDP-Server im Einsatz unter Linux

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.11   Die (UDP)Client-Anwendung unter Linux


Galileo Computing - Zum Seitenanfang

28.7.8 Mehrere Clients gleichzeitig behandeln  toptop

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:

gp  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.
gp  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:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.12   Der Server kann jetzt mehrere Anfragen (Clients) bearbeiten

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.13   Einer von zwei gerade aktiven Clients

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 28.14   Der andere der beiden aktiven Clients zur selben Zeit

 << zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z
C von A bis Z
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Shell-Programmierung






 Shell-Programmierung


Zum Katalog: Linux-UNIX-Programmierung






 Linux-UNIX-Programmierung


Zum Katalog: C/C++






 C/C++


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Reguläre Ausdrücke






 Reguläre Ausdrücke


Zum Katalog: Linux






 Linux


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de