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

 << zurück
Linux-UNIX-Programmierung von Jürgen Wolf
Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006
Buch: Linux-UNIX-Programmierung

Linux-UNIX-Programmierung
1216 S., mit CD, 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-749-8
gp Kapitel 11 Netzwerkprogrammierung
  gp 11.1 Einführung
  gp 11.2 Aufbau von Netzwerken
    gp 11.2.1 ISO/OSI und TCP/IP – Referenzmodell
    gp 11.2.2 Das World Wide Web (Internet)
  gp 11.3 TCP/IP – Aufbau und Struktur
    gp 11.3.1 Netzwerkschicht (Datenübertragung)
    gp 11.3.2 Internetschicht
    gp 11.3.3 Transportschicht (TCP, UDP)
    gp 11.3.4 Anwendungsschicht
  gp 11.4 TCP Socket
  gp 11.5 Kommunikationsmodell
  gp 11.6 Grundlegende Funktionen zum Zugriff auf die Socket-Schnittstelle
    gp 11.6.1 Ein Socket anlegen – socket()
    gp 11.6.2 Verbindungsaufbau – connect()
    gp 11.6.3 Socket mit einer Adresse verknüpfen – bind()
    gp 11.6.4 Auf Verbindungen warten – listen() und accept()
    gp 11.6.5 Senden und Empfangen von Daten (1) – write() und read()
    gp 11.6.6 Senden und Empfangen von Daten (2) – send() und recv()
    gp 11.6.7 Verbindung schließen – close()
  gp 11.7 Aufbau eines Clientprogramms
    gp 11.7.1 Zusammenfassung: Clientanwendung und Quellcode
  gp 11.8 Aufbau des Serverprogramms
    gp 11.8.1 Zusammenfassung: Serveranwendung und Quellcode
  gp 11.9 IP-Adressen konvertieren, manipulieren und extrahieren
    gp 11.9.1 inet_aton(), inet_pton() und inet_addr()
    gp 11.9.2 inet_ntoa() und inet_ntop()
    gp 11.9.3 inet_network()
    gp 11.9.4 inet_netof()
    gp 11.9.5 inet_lnaof()
    gp 11.9.6 inet_makeaddr()
  gp 11.10 Namen und IP-Adressen umwandeln
    gp 11.10.1 Name-Server
    gp 11.10.2 Informationen zum Rechner im Netz – gethostbyname und gethostbyaddr
    gp 11.10.3 Service-Informationen – getservbyname() und getservbyport()
  gp 11.11 Der Puffer
  gp 11.12 Standard-E/A-Funktionen verwenden
    gp 11.12.1 Pufferung von Standard-E/A-Funktionen
  gp 11.13 Parallele Server
  gp 11.14 Syncrones Multiplexing – select()
  gp 11.15 POSIX-Threads und Netzwerkprogrammierung
  gp 11.16 Optionen für Sockets setzen bzw. erfragen
    gp 11.16.1 setsockopt()
    gp 11.16.2 getsockopt()
    gp 11.16.3 Socket-Optionen
  gp 11.17 UDP
    gp 11.17.1 Clientanwendung
    gp 11.17.2 Serveranwendung
    gp 11.17.3 recvfrom() und sendto()
    gp 11.17.4 bind() verwenden oder weglassen
  gp 11.18 UNIX-Domain-Sockets (IPC)
    gp 11.18.1 Die Adressstruktur von UNIX-Domain-Sockets
    gp 11.18.2 Lokale Sockets erzeugen – socketpair()
  gp 11.19 Multicast-Socket
    gp 11.19.1 Anwendungsgebiete von Multicast-Verbindungen
  gp 11.20 Nicht blockierende I/O-Sockets
  gp 11.21 Etwas zu Streams und TLI, Raw Socket, XTI
    gp 11.21.1 Raw Socket
    gp 11.21.2 TLI und XTI
    gp 11.21.3 RPC (Remote Procedure Call)
  gp 11.22 IPv4 und IPv6
    gp 11.22.1 IPv6 – ein wenig genauer
  gp 11.23 Netzwerksoftware nach IPv6 portieren
    gp 11.23.1 Konstanten
    gp 11.23.2 Strukturen
    gp 11.23.3 Funktionen
  gp 11.24 Sicherheit und Verschlüsselung


Rheinwerk Computing

11.14 Syncrones Multiplexing – select()  toptop

Bisher kennen Sie drei Möglichkeiten, Clients zu bearbeiten. Die »normale« Server-Client-Lösung, die für nur eine Clientanfrage ausgelegt ist, die Verwendung von mehreren Prozessen und die Lösung mit der Thread-Programmierung. Welche davon eignet sich allerdings am besten, wenn man mehrere Clients gleichzeitig bearbeiten will, wie dies z. B. bei einem Webserver, Chat-Programmen, einem Spieleserver usw. benötigt wird.

Die einfache Server-Client-Lösung, die nur für einen Client ausgelegt war, fällt schon mal flach, da alle anderen Clients, die eine Anfrage an den Server stellen, in eine Warteschlange gesteckt werden und warten müssen, bis der Server wieder für Verbindungswünsche frei ist.

Die Threads sind eine weitere interessante Lösung, allerdings, wenn hier das Thema »Portabilität« ins Spiel kommt, schneiden auch die Threads relativ schlecht ab. Zwar gibt es für alle Plattformen Thread-Bibliotheken, nur sind diese leider nur bedingt portabel. Und in der Netzwerkprogrammierung ist das Thema »Portabilität« schon ein groß geschriebenes Thema.

Also wäre die Verwendung von mehreren Prozessen (mittels fork()) eine interessante Lösung. Hierbei wird für jeden Client ein neuer (Server-)Prozess erzeugt und gestartet, jeder Client bekommt praktisch einen eigenen Server. Voraussetzung hierfür ist allerdings, dass Sie sich mit der Systemprogrammierung der entsprechenden Plattform auskennen. Schließlich müssen die einzelnen Prozesse auch kontrolliert werden.

Natürlich gibt es hier nebenbei noch weitere Möglichkeiten, mehrere Clients zu behandeln. Allerdings sind dies immer wieder Dinge, die stark von der Plattform der Entwicklung abhängen.

Und trotzdem gibt es eine Lösung, womit man auch mehrere Plattformen zufrieden stellen kann, nämlich die Funktion select(). Mit select() lassen sich recht komfortabel mehrere Clients behandeln - und das auch auf verschiedenen Plattformen wie MS Windows und Linux-/UNIX-Systemen. Also ein sehr geeigneter Kandidat für portable Lösungen.

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 z. B. revc() aufgerufen, blockierte dieser Aufruf den Socket-Deskriptor so lange, 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 (z. 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.


Hier also die Syntax zur entsprechenden select()-Funktion:

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

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 wird z. B. 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 z. B. 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 z. B. 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 z. B. beträgt 256 und unter Linux 1024. Somit können auf BSD nur die ersten 256 und unter Linux 1024 Deskriptoren angesprochen werden. 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. Nach dem Starten des Servers dürfen Sie gerne mehrere Clients gleichzeitig starten und dem Server Nachrichten zukommen lassen. Sie werden feststellen, dass der Server keinen Client blockiert und somit ohne Problem mehrere Clients »gleichzeitig« handeln kann (echtes Multiplexing eben) – genauer FD_SETSIZE Clients. Sobald auch hier ein Client die Zeichenfolge »quit« sendet, entfernt der Server den Client (genauer: den (Socket-)Deskriptor) aus der Menge. Die Netzwerkfunktionen um select() herum wurden in extra Funktionen »ausgelagert«, damit Sie den Überblick zu den eigentlichen Funktionen rund um select() behalten können. Hierzu der Server »multi_server.c«:

/* multi_server.c */
#include <stdlib.h>
#include <stdio.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.>
#define BUF 1024
#define socket_t int
/* 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");
    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 so lange, 
 * 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");
}
/* 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()");
}
/* Socket schließen */
void close_socket( socket_t *sock ){
    close(*sock);
}
 
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);
  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 Clientverbindung ... ? */
    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 */
             //Socket schließen
             close_socket (&sock3);   
             //aus Menge löschen
             FD_CLR(sock3, &gesamt_sock);  
             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;
}

Jetzt noch der Quellcode zum Client:

/* client.c */ 
#include <stdlib.h>
#include <stdio.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.>
#define BUF 1024
#define socket_t int
/* 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");
    setsockopt( sock, SOL_SOCKET,
                SO_REUSEADDR, &y, sizeof(int));
    return sock;
}
/* 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 nummerische IP-Adresse */
       memcpy( (char *)&server.sin_addr, 
               &addr, sizeof(addr));
   }
   else {
      /* Für den Fall der Fälle: Wandle den Servernamen
       * z. B. "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()");
}
/* Socket schließen */
void close_socket( socket_t *sock ){
    close(*sock);
}
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);
  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;
}

Die Programme bei der Ausführung:

[ tty1 ]
$ gcc -o multi_server multi_server.c
$ gcc -o client client.c
$ ./multi_server
[ tty2 ]
$ ./client localhost
Nachricht zum Versenden: Hallo Server von tty2
Nachricht zum Versenden:  
[ tty3 ]
$ ./client 127.0.0.1
Nachricht zum Versenden: Hallo Server von tty3
Nachricht zum Versenden:
[ tty1 ]
$ ./multi_server
Nachricht empfangen: Hallo Server von tty2
Nachricht empfangen: Hallo Server von tty3
[ tty2 ]
$ ./client localhost
Nachricht zum Versenden: Hallo Server von tty2
Nachricht zum Versenden: Ich beende mich jetzt dann
Nachricht zum Versenden: quit
$
[ tty3 ]
$ ./client 127.0.0.1
Nachricht zum Versenden: Hallo Server von tty3
Nachricht zum Versenden: Ich auch bye!
Nachricht zum Versenden: quit
$
[ tty1 ]
$ ./multi_server
Nachricht empfangen: Hallo Server von tty2
Nachricht empfangen: Hallo Server von tty3
Nachricht empfangen: Ich beende mich jetzt dann
Nachricht empfangen: quit
Ein Client hat sich beendet
Nachricht empfangen: Ich auch, bye!
Nachricht empfangen: quit
Ein Client hat sich beendet
...
...

Zugegeben, das Beispiel war noch sehr theoretisch, daher ein Fall aus dem echten Leben der Netzwerkprogrammierung. Das beste Beispiel in diesem Fall dürfte wohl ein HTTP-Webserver sein. Das folgende Listing zeigt Ihnen das Grundgerüst eines solchen Webservers. Im Großen und Ganzen entspricht dieses auf den ersten Blick etwas komplexere Beispiel einem typischen Aufbau eines parallelen Servers, wie Sie diesen bereits in diesem Kapitel gesehen haben. Sie finden in diesem Beispiel viele netzwerktypische Techniken, weshalb es sich lohnen kann, sich umfassend damit zu beschäftigen. Der Quellcode ist, sofern dies nötig ist, gut dokumentiert.


Hinweis   Sofern Sie mit Begriffen eines Webservers wie Request-Header oder Response-Header überhaupt nichts anzufangen wissen, empfehle ich Ihnen das Kapitel zur CGI-Programmierung in C zu lesen, das Sie in dem Buch »C von A bis Z« finden (auch zu finden auf der Buch-CD).


/* http_server.c */
#include <unistd. 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 <arpa/inet.h>
#include <signal.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#define bool int
#define true  1
#define false 0
#define SERVER_PORT     2001
#define BUFFER_SIZE     2048
typedef void (*sighandler_t)(int);
static sighandler_t
my_signal(int sig_nr, sighandler_t signalhandler) {
   struct sigaction neu_sig, alt_sig;
   neu_sig.sa_handler = signalhandler;
   sigemptyset (&neu_sig.sa_mask);
   neu_sig.sa_flags = SA_RESTART;
   if (sigaction (sig_nr, &neu_sig, &alt_sig) < 0)
      return SIG_ERR;
   return alt_sig.sa_handler;
}
/* String anhand bestimmter Tokens zerlegen */
static int
GetToken(char *buf, int n, char *token, int toklen, char delim) {
  int len;
  int i = 0;
  int k;
  for( k=1; k<=n;k++) {
    token[0] = 0;
    len = 0;
    while(buf[i] == delim || buf[i] == ' ' || buf[i] == 0 ||
          buf[i] == '\t' || buf[i] == '\r' || 
          buf[i] == '\n') {
      i++;
    }
    while(buf[i]!=delim && buf[i]!=' ' && buf[i]!='\0' &&
          buf[i]!='\t' && buf[i]!=10 && buf[i]!=13) {
      token[len++]  =  buf[i++];
      if(len > toklen - 1) {
        return -1;
      }
    }
    token[len] = 0;
  }
  return len;
}
/* Ganze Blöcke von size Bytes Größe an den */
/* Socket (Client) senden                   */
static int SendBlock(int soc,unsigned char *buf, int size) {
  unsigned char *r;
  int len = 0;
  r = buf;
  while( size > 0 ) {
    if((len = send(soc, r, size, 0)) == -1) {
      return -1;
    }
    size -= len;
    r += len;
  }
  return 0;
}
/* Variable Anzahl Bytes an Socket (den Client) schicken */
static int SendBuf(int soc, char *buf) {
  char *r = NULL;
  int len = 0;
  int rlen = strlen(buf);
  r = buf;
  while( rlen > 0 ) {
    if((len = send(soc, r, strlen(r), 0)) == -1) {
      return -1;
    }
    rlen -= len;
    r += len;
  }
  return 0;
}
/* Den Response-Header an den Client senden; z. B.:
   HTTP/1.1 200 OK
   Date: Wed, 29 Jun 2004 00:38:10 GMT
   Server: Apache/2.0.12 (UNIX) PHP/5.0.3 mod_ssl/2.8.3 ...
   Last-Modified: Wed, 15 Jun 2004 12:53:23 GMT
   Content-Length: 3752
   Connection: close
   Content-Type: text/html
   ...
*/
static void
SendHTTPHeader( int soc, int code, const char *phase,
                int length, time_t *pftime ) {
  char msg[255] = {0};
  struct tm *ptm, *pftm;
  time_t stime;
  sprintf(msg, "HTTP/1.1 %d %s\r\n", code, phase);
  SendBuf(soc, msg);
  time(&stime);
  ptm = gmtime(&stime);
  strftime( msg, 255,
            "Date: %a, %d %b %Y %H:%M:%S %Z\r\n", ptm );
  SendBuf(soc, msg);
  SendBuf(soc, "Server: http_server 0.1\r\n");
  if(pftime != NULL) {
    pftm = gmtime(pftime);
    strftime( msg, 255, "Last-Modified: "
              "%a, %d %b %Y %H:%M:%S %Z\r\n", pftm);
    SendBuf(soc, msg);
  }
  SendBuf(soc, "Accept-Ranges: none\r\n");
  sprintf(msg, "Content-Length: %d\r\n", length);
  SendBuf(soc, msg);
  SendBuf(soc, "Connection: Keep-Alive\r\n");
  SendBuf(soc, "Content-Type: text/html\r\n");
  SendBuf(soc, "\r\n");
}
static int httpd(int soc) {
  int returnval = 0;
  char buf[512] ={0};
  char method[255] = {0};
  char uri[512] = {0};
  char headline[1024] = {0};
  char request_header[BUFFER_SIZE] = {0};
  char *r = NULL;
  char hTitle[32] = {0};
  char hValue[128] = {0};
  FILE *fd;
  bool isFirst = true;
  /* Aus Deskriptor einen FILE-Stream machen */
  fd = fdopen(soc, "r");
  printf("-----------Request-Header Client-------------\n");
  /* Request-Header des Clients auslesen */
  while(1) {    /* Request-Header zeilenweise einlesen */
    r = fgets(buf, BUFFER_SIZE, fd);
    /* ... Request-Header des Clients fertig, wenn ... */
    if( *r == '\r' && *(r+1) == '\n') {
      break;
    }
    printf("ClientRequest: %s", r);
/* Als Erstes wird immer die Methode der Anfrage gesendet */
/* meistens ist die Methode GET oder POST                 */
/* Diese Methode wollen wir entsprechend mit GetToken()   */
/* präpariert in dem char-Array method speichern.         */
    if(isFirst) {
      strcpy(headline, r);
      GetToken(headline, 1, method, 255, 0);
      isFirst = false;
    }
    /*  Keep-Alive? */
    else {
      strcat(request_header, r);
      GetToken(r, 1, hTitle, 32, ':');
      if(strcmp(hTitle, "Connection") == 0 ) {
        GetToken(r, 2, hValue, 128, ' ');
        if(strcmp(hValue, "Keep-Alive") == 0 ) {
          printf("Client benötigt keep alive\r\n");
          returnval = 1;
        } else {
          printf("Client benötigt kein keep alive\r\n");
          returnval = -1;
        }
      }
    }
  }
  printf("Fertig mit dem Lesen des Request-Headers...\r\n");
  printf("--------------------------------------------\n");
  fflush(stdout);
  /* Wurde die Methode GET gesendet - */
  /* Beispiel unterstützt nur diese   */
  if((strcmp(method,"GET")==0)||(strcmp(method,"get")==0)) {
    char dir[255];
    char path[512] = {0};
    FILE *file;
    struct stat fst;
    GetToken(headline, 2, uri, 1024, 0);
    GetToken(uri, 1, dir, 255, '/');
    printf("Folgende Datei wurde vom Client"
           " angefordert %s\n",uri);
    sprintf(path, ".%s", uri);
    file = fopen(path, "r");
    if(file == NULL) {
    /* Ausgabe an den Browser, falls Datei nicht vorhanden */
      char error404[] =
        "<HTML><HEAD><TITLE>404 Not Found</TITLE>"
        "</HEAD><BODY><H1>Nicht gefunden</H1>"
        "Die angeforderte URL konnte nicht auf"
        " diesem Server gefunden werden."
        "<P><HR><I>http_server0.1<I></BODY>"
        "</HTML>\r\n\r\n";
      printf("404 Nachricht\r\n");
      SendHTTPHeader( soc, 404, "FILE NOT FOUND.",
                      strlen(error404)+1, NULL );
      SendBlock( soc, error404, strlen(error404)+1 );
      fflush(stdout);
      SendBuf(soc, "\r\n\r\n");
      return -1;
    } else {
      /* Server-Response erzeugen-Die Antwort des Servers */
      unsigned char *blob = NULL;
      stat(path, &fst);
      printf("Datei existiert ...\r\n");
      fflush(stdout);
      /* Den Response-Header an den Client schicken */
      SendHTTPHeader( soc, 200, "OK", fst.st_size,
                      &(fst.st_mtime) );
      /* Speicher für d. angeforderte Dokument anfordern */
      blob = (unsigned char *)malloc(fst.st_size);
      /* Komplettes Dokument in blob einlesen */
      fread(blob, fst.st_size, 1, file);
      /* Komplettes Dokument an den Client senden */
      SendBlock(soc, blob, fst.st_size);
      /* ...fertig - aufräumen */
      fclose(file);
      printf("Alle Daten gesendet ...\r\n");
      fflush(stdout);
      SendBuf(soc, "\r\n\r\n");
      free(blob);
    }
  }
  return returnval;
}
int main(int argc, char *argv[]) {
  int soc, soc_cli;
  socklen_t srv_len, cli_len;
  int ret = -1;
  struct sockaddr_in srv_addr, cli_addr;
  const int y = 1;
  /* 1. Typischer Vorgang einer Serveranwendung:    */
  /*    +   Einen Socket erzeugen - socket()        */
  /*    + Den eigenen Port festlegen - bind()       */
  /*    + Auf Verbindungswünsche warten - listen()  */
  /*    + Verbindung annehmen - accept()            */
  soc = socket(AF_INET, SOCK_STREAM, 0);
  srv_addr.sin_family = AF_INET;
  srv_addr.sin_addr.s_addr = htons(INADDR_ANY);
  srv_addr.sin_port = htons(SERVER_PORT);
  srv_len = sizeof(srv_addr);
  setsockopt( soc, SOL_SOCKET, SO_REUSEADDR,
              &y, sizeof(int));
  ret = bind(soc, (struct sockaddr *)&srv_addr, srv_len);
  if(ret != 0) {
    printf("Fehler: binding server socket\r\n");
    return EXIT_FAILURE;
  }
  ret = listen(soc, 5);
  if(ret != 0) {
    printf("Fehler: listening server socket\r\n");
    return EXIT_FAILURE;
  }
  /* SIGCHLD ignorieren */
  my_signal(SIGCHLD, SIG_IGN);
  while(1) {
    int pid;
    fd_set fdlist, testfd;
    int result = 0;
    int rlen = 0;
    printf("Warte auf Clientverbindung...\r\n");
    cli_len = sizeof(cli_addr);
    soc_cli = accept( soc,
                      (struct sockaddr *)&cli_addr,
                      (socklen_t*)&cli_len );
    /* Parallelen Server erzeugen */
    pid = fork();
    switch(pid) {
      /* Kindprozess kümmert sich um den neuen Client */
      /* (z. B. Browser)                              */
    case 0:
      if(soc_cli != -1) {
        printf("Client verbunden mit socket %d\r\n",
           soc_cli);
        /* Menge in fdlist auf null setzen */
        FD_ZERO(&fdlist);
        /* Den Client-Socket der Menge hinzufügen */
        FD_SET(soc_cli, &fdlist);
        while(1) {
          testfd = fdlist;
          /* Sind auf der Deskriptor(en)-Menge */
          /* testfd Daten zum Lesen vorhanden? */
          result = select( FD_SETSIZE, &testfd,
                           NULL, NULL, NULL );
          /* Fehler bei select() */
          if(result <= 0) {
            break;
          }
          /* Prüfen, ob das Bit für soc_cli in */
          /* testfd gesetzt ist ...           */
          if(FD_ISSET(soc_cli, &testfd)) {
            ioctl(soc_cli, FIONREAD, &rlen);
            if(rlen == 0) {
              /* Bit für soc_cli aus der Menge */
              /* fdlist wieder löschen         */
              FD_CLR(soc_cli, &fdlist);
              break;
            }
            /* Die Anforderung des Clients */
            /* bearbeiten                  */
            result = httpd(soc_cli);
            if(result < 0) {
              break;
            }
          }
        }
        printf("Client socket %d beendet\r\n", soc_cli);
        fflush(stdout);
        close(soc_cli);
      } else {
        printf("Fehler:accepting client's connection.\r\n");
      }
      exit(EXIT_SUCCESS);
      break;
    case -1:
      printf("Fehler bei fork() (%s)\r\n", strerror(errno));
      break;
    default:
      /* Elternprozess benötigt den verbundenen Socket */
      /* nicht mehr, da sich der Kindprozess damit    */
      /* beschäftigt                                 */
      close(soc_cli);
      break;
    }
  }
  return EXIT_SUCCESS;
}

Das Programm bei der Ausführung:

$ gcc -o http_server http_server.c

Erstellen Sie nun eine HTML-Datei. Speichern Sie diese Datei im selben Verzeichnis, in dem Ihr Webserver ausgeführt wird. Hier wird die Datei mit dem Namen index.html gespeichert.

$ ./http_server
Waiting for client connecting...

Öffnen Sie jetzt einen Webclient (Ihren Lieblings-Webbrowser, wenn Sie wollen), und geben Sie folgende Adresse ein:

http://localhost:2001/index.html

Jetzt müssten Sie den Inhalt der Datei index.html in Ihrem Browser lesen können.


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

Abbildung 11.10    Erfolgreich mit dem http_server verbunden


Für den Fall, dass Sie eine nicht vorhandene Datei anfordern, wird die servertypische 404-Nachricht (File not found) zurückgegeben.


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

Abbildung 11.11    Ein nicht vorhandenes Dokument wurde angefordert.


Außerdem können Sie die Ausgabe Ihres Webservers bei der Arbeit in der Konsole betrachten:

$ ./http_server
Warte auf Clientverbindung...
Client verbunden mit socket 4
-------------Request-Header Client---------------
ClientRequest: GET /index.html HTTP/1.1
ClientRequest: Host: localhost:2001
ClientRequest: User-Agent: Mozilla/5.0 (X11; U; Linux i686;
ClientRequest: Accept:text/xml,application/xml,appl ...
ClientRequest: Accept-Encoding: gzip,deflate, compress ...
ClientRequest: Accept-Charset: ISO-8859–1, utf-8;q=0.66, ...
ClientRequest: Keep-Alive: 300
ClientRequest: Connection: keep-alive
Client benötigt kein keep alive
ClientRequest: If-Modified-Since: Sat, 08 May 2004 23:23:26 
ClientRequest: Cache-Control: max-age=0
Fertig mit dem Lesen des Request-Headers...
------------------------------------------------
Folgende Datei wurde vom Client angefordert /index.html
Datei existiert ...
Alle Daten gesendet ...
Client-socket-4-Verbindung beendet
Warte auf Client-Verbindung ...
Client verbunden mit socket 4
Warte auf Clientverbindung ...
-------------Request-Header Client---------------
ClientRequest: GET /icnzgtfhjkldex.html HTTP/1.1
ClientRequest: Connection: Keep-Alive
Client benötigt keep alive
ClientRequest: Pragma: no-cache
ClientRequest: Cache-control: no-cache
ClientRequest: Accept: text/*, image/jpeg, image/png, image/...
ClientRequest: Accept-Encoding: x-gzip, x-deflate, gzip, defla...
ClientRequest: Accept-Charset: iso-8859–15, utf-8;q=0.5, *;q=0.5
ClientRequest: Accept-Language: de, DE, en
ClientRequest: Host: localhost:2001
Fertig mit dem Lesen des Request-Headers ...
------------------------------------------------
Folgende Datei wurde vom Client angefordert /icnzgtfhjkldex.html
404 Nachricht
Client-socket-4-Verbindung beendet

Keep Alive   Bei HTTP/1.0 waren alle Verbindungen nicht persistent, d. h., der Server hat die Verbindung zum Client (z. B. nach dem Liefern einer HTML-Datei) abgebrochen. Wenn der Client eine persistente Verbindung aufbauen wollte, musste dieser den Connection-Header mit Keep Alive bestücken. In HTTP/1.1 ist dies jetzt umgekehrt der Fall. Die Verbindungen sind hierbei immer persistent, außer der Client schickt einen Connection-Header mit close.


Eine Zeile, in der Sie sicherlich hängen geblieben sind, war wohl die folgende Zeile:

ioctl(soc_cli, FIONREAD, &rlen);

Damit lässt sich ganz bequem herausfinden, wie viele ungelesene Bytes in einem Filedeskriptor (hier Socket) noch verfügbar sind. Die Anzahl befindet sich anschließend in rlen. Im Beispiel wird, wenn keine Bytes mehr verfügbar sind, der Filedeskriptor (hier Socket) aus der Menge fdlist mit dem Makro FD_CLR() wieder entfernt.


Hinweis zu \r\n   Sicherlich ist Ihnen im Listing bei den Ausgaben des Servers auch das \r\n am Ende aufgefallen. Im Prinzip ist es allerdings egal, ob Sie hierbei \n oder \r\n verwenden – vorausgesetzt natürlich, die Clientsoftware spricht dasselbe Protokoll wie der Server. Gewöhnlich werden Serverdämonen so programmiert, dass diese \r\n und \n kennen.


 << zurück
  
  Zum Rheinwerk-Shop
Neuauflage: Linux-UNIX-Programmierung
Neuauflage:
Linux-UNIX-
Programmierung

bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Linux-Server






 Linux-Server


Zum Rheinwerk-Shop: Das Komplettpaket LPIC-1 & LPIC-2






 Das Komplettpaket
 LPIC-1 & LPIC-2


Zum Rheinwerk-Shop: Linux-Hochverfügbarkeit






 Linux-
 Hochverfügbarkeit


Zum Rheinwerk-Shop: Shell-Programmierung






 Shell-
 Programmierung


Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
Info





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


Nutzungsbestimmungen | Datenschutz | Impressum

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

Cookie-Einstellungen ändern