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

Inhaltsverzeichnis
Vorwort
Vorwort des Gutachters
1 Einstieg in C
2 Das erste Programm
3 Grundlagen
4 Formatierte Ein-/Ausgabe mit »scanf()« und »printf()«
5 Basisdatentypen
6 Operatoren
7 Typumwandlung
8 Kontrollstrukturen
9 Funktionen
10 Präprozessor-Direktiven
11 Arrays
12 Zeiger (Pointer)
13 Kommandozeilenargumente
14 Dynamische Speicherverwaltung
15 Strukturen
16 Ein-/Ausgabe-Funktionen
17 Attribute von Dateien und das Arbeiten mit Verzeichnissen (nicht ANSI C)
18 Arbeiten mit variabel langen Argumentlisten – <stdarg.h>
19 Zeitroutinen
20 Weitere Headerdateien und ihre Funktionen (ANSI C)
21 Dynamische Datenstrukturen
22 Algorithmen
23 CGI mit C
24 MySQL und C
25 Netzwerkprogrammierung und Cross–Plattform-Entwicklung
26 Paralleles Rechnen
27 Sicheres Programmieren
28 Wie geht’s jetzt weiter?
A Operatoren
B Die C-Standard-Bibliothek
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch
Buch: C von A bis Z

C von A bis Z
3., aktualisierte und erweiterte Auflage, geb., mit CD und Referenzkarte
1.190 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1411-7
Pfeil 25 Netzwerkprogrammierung und Cross–Plattform-Entwicklung
Pfeil 25.1 Begriffe zur Netzwerktechnik
Pfeil 25.1.1 IP-Nummern
Pfeil 25.1.2 Portnummer
Pfeil 25.1.3 Host- und Domainname
Pfeil 25.1.4 Nameserver
Pfeil 25.1.5 Das IP-Protokoll
Pfeil 25.1.6 TCP und UDP
Pfeil 25.1.7 Was sind Sockets?
Pfeil 25.2 Headerdateien zur Socketprogrammierung
Pfeil 25.2.1 Linux/UNIX
Pfeil 25.2.2 Windows
Pfeil 25.3 Client/Server-Prinzip
Pfeil 25.3.1 Loopback-Interface
Pfeil 25.4 Erstellen einer Client-Anwendung
Pfeil 25.4.1 »socket()« – Erzeugen eines Kommunikationsendpunktes
Pfeil 25.4.2 »connect()« – ein Client stellt eine Verbindung zum Server her
Pfeil 25.4.3 Senden und Empfangen von Daten
Pfeil 25.4.4 »close()« und »closesocket()«
Pfeil 25.5 Erstellen einer Server-Anwendung
Pfeil 25.5.1 »bind()« – Festlegen einer Adresse aus dem Namensraum
Pfeil 25.5.2 »listen()« – Warteschlange für eingehende Verbindungen einrichten
Pfeil 25.5.3 »accept()« und die Serverhauptschleife
Pfeil 25.6 (Cross-Plattform-)TCP-Echo-Server
Pfeil 25.6.1 Der Client
Pfeil 25.6.2 Der Server
Pfeil 25.7 Cross-Plattform-Development
Pfeil 25.7.1 Abstraction Layer
Pfeil 25.7.2 Headerdatei für Linux/UNIX
Pfeil 25.7.3 Linux/UNIX-Quellcodedatei
Pfeil 25.7.4 Headerdatei für MS-Windows
Pfeil 25.7.5 Windows-Quellcodedatei
Pfeil 25.7.6 All together – die »main«-Funktionen
Pfeil 25.7.7 Ein UDP-Beispiel
Pfeil 25.7.8 Mehrere Clients gleichzeitig behandeln
Pfeil 25.8 Weitere Anmerkungen zur Netzwerkprogrammierung
Pfeil 25.8.1 Das Datenformat
Pfeil 25.8.2 Der Puffer
Pfeil 25.8.3 Portabilität
Pfeil 25.8.4 Von IPv4 nach IPv6
Pfeil 25.8.5 RFC-Dokumente (Request for Comments)
Pfeil 25.8.6 Sicherheit


Rheinwerk Computing - Zum Seitenanfang

25.7 Cross-Plattform-Development Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

25.7.1 Abstraction Layer Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

25.7.2 Headerdatei für Linux/UNIX Zur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

25.7.3 Linux/UNIX-Quellcodedatei Zur nächsten ÜberschriftZur vorigen Überschrift

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



Rheinwerk Computing - Zum Seitenanfang

25.7.4 Headerdatei für MS-Windows Zur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

25.7.5 Windows-Quellcodedatei Zur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

25.7.6 All together – die »main«-Funktionen Zur nächsten ÜberschriftZur vorigen Überschrift

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


Rheinwerk Computing - Zum Seitenanfang

25.7.7 Ein UDP-Beispiel Zur nächsten ÜberschriftZur vorigen Überschrift

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


Rheinwerk Computing - Zum Seitenanfang

25.7.8 Mehrere Clients gleichzeitig behandeln topZur vorigen Überschrift

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



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z

 C von A bis Z
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: C/C++






 C/C++


Zum Katalog: Einstieg in C






 Einstieg in C


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
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.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de