Netzwerkprogrammierung - Server

Martin Kompf

Die Entwicklung von Serverprogrammen gestaltet sich prinzipiell nicht wesentlich komplizierter als die Clientprogrammierung. In diesem Artikel wird der vollständige Quellcode eines einfachen Webservers gezeigt.

Im Artikel «Netzwerkprogrammierung» wurde ein einfaches Clientprogramm beschrieben, welches per HTTP Anfragen an einen Webserver senden kann. Es stellt sich die Frage, ob ein Serverprogramm ähnlich einfach realisierbar ist. Wenn zur Programmierung wiederum die Socketschnittstelle verwendet wird, so zeigt sich, dass tatsächlich nur wenig mehr getan werden muss, damit ein C-Programm als Server im Netz agieren kann:

Nach der Erzeugung des Sockets via socket() sollte die Bindung an eine bestimmte, «gut bekannte» Portnummer (im Falle von HTTP die 80) vorgenommen werden. Dies erfolgt durch den Aufruf von bind(). Dann teilt man dem Betriebssystem per listen() mit, dass man an diesem Port Verbindungswünsche von Clients anzunehmen gedenkt. Das entsprechende C-Programm (Download httpserv.c) sieht bis zu diesem Punkt also ganz ähnlich aus wie der Client aus dem vorherigen Artikel:

/* httpserv.c
 * Demoprogramm zur Programmierung von Netzwerkservern
 * Es wird ein simpler http Server implementiert,
 * der ausschließlich GET requests bearbeiten kann */
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#ifdef _WIN32
/* Headerfiles für Windows */
#include <winsock.h>
#include <io.h>

#else
/* Headerfiles für Unix/Linux */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define closesocket(s) close(s)
#endif

/* http requests werden normalerweise auf Port 80 
 * vom Server beantwortet */
#define HTTP_PORT 80

static void serv_request( int in, int out, char* rootpath);

/****************** MAIN *********************/
int main( int argc, char **argv)
{
    struct sockaddr_in server, client;
    int sock, fd;
    int len;

#ifdef _WIN32  
    /* Initialisiere TCP für Windows ("winsock") */
    short wVersionRequested;
    WSADATA wsaData;
    wVersionRequested = MAKEWORD (1, 1);
    if (WSAStartup (wVersionRequested, &wsaData) != 0) {
        fprintf( stderr, "Failed to init windows sockets\n");
        exit(1);
    }
#endif

    /* Teste auf Kommadozeilenargument "documentroot" */
    if (2 != argc) {
        fprintf( stderr, "usage: httpserv documentroot\n");
        exit(1);
    }
    
    /* Erzeuge das Socket */
    sock = socket( PF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror( "failed to create socket");
        exit(1);
    }

    /* Erzeuge die Socketadresse des Servers 
     * Sie besteht aus Typ und Portnummer */
    memset( &server, 0, sizeof (server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl( INADDR_ANY);
    server.sin_port = htons( HTTP_PORT);

    /* Erzeuge die Bindung an die Serveradresse 
     * (d.h. an einen bestimmten Port) */
    if (bind( sock, (struct sockaddr*)&server, sizeof( server)) < 0) {
        perror( "can't bind socket");
        exit(1);
    }

    /* Teile dem Socket mit, dass Verbindungswünsche
     * von Clients entgegengenommen werden */
    listen( sock, 5);

Nun kommt der interessante Teil: Die Serverhauptschleife. Hier wird die Funktion accept() aufgerufen. Diese blockiert solange, bis ein Client seinerseits ein connect() mit Adresse und Portnummer unseres Servers aufruft. Dann liefert accept() einen neuen Socket zurück, über den der Datentransfer mit dem Client in beiden Richtungen (Daten vom Client, Daten zum Client) ablaufen kann. Dieser Datenaustausch muss in unserem Beispiel natürlich den Regeln von HTTP folgen und wurde komplett in die Funktion serv_request() ausgelagert. Nach Beendigung der Verbindung mit dem Client ruft der Server wiederum accept() auf, um weitere Verbindungswünsche abarbeiten zu können:


    /* Bearbeite die Verbindungswünsche von Clients 
     * in einer Endlosschleife
     * Der Aufruf von accept() blockiert solange, 
     * bis ein Client Verbindung aufnimmt */
    for (;;) {
        len = sizeof( client);
        fd = accept( sock, (struct sockaddr*)&client, &len);
        if (fd < 0) {
            perror( "accept failed");
            exit(1);
        }

        /* Bearbeite den http Request */
        serv_request( fd, fd, argv[1]);
        /* Schließe die Verbindung */
        closesocket( fd);
    }
}

Die Funktion serv_request() zur serverseitigen Implementierung von HTTP wurde aus Platzgründen auf ein Minimum beschränkt. Sie kann ausschließlich GET Requests bearbeiten, kennt nur HTML-Dateien (kann also weder GIF- noch JPEG-Images übertragen) und hat nur eine rudimentäre Fehlerbehandlung:

/*
 * serv_request
 * Bearbeite den auf in ankommenden http request
 * Die zu sendenden Daten werden auf out ausgegeben
 */
static void serv_request( int in, int out, char* rootpath)
{
    char buffer[8192];
    char *b, *l, *le;
    int count, totalcount;
    char url[256];
    char path[256];
    int fd;
    int eoh = 0;

    b = buffer;
    l = buffer;
    totalcount = 0;
    *url = 0;
    while ( (count = recv( in, b, sizeof(buffer) - totalcount, 0)) > 0) {
        totalcount += count;
        b += count;
        while (l < b) {
            le = l;
            while (le < b && *le != '\n' && *le != '\r') ++le;
            if ('\n' == *le || '\r' == *le) {
                *le = 0;
                printf ("Header line = "%s"\n", l);
                sscanf( l, "GET %255s HTTP/", url);
                if (strlen(l)) eoh = 1;
                l = le + 1;
            }
        }
        if (eoh) break;
    }

    if ( strlen(url)) {
        printf( "got request: GET %s\n", url);
        sprintf(path, "%s/%s", rootpath, url);
        fd = open( path, O_RDONLY);
        if (fd > 0) {
            sprintf( buffer, "HTTP/1.0 200 OK\nContent-Type: text/html\n\n");
            send( out, buffer, strlen(buffer), 0);
            do {
                count = read( fd, buffer, sizeof(buffer));
                send( out, buffer, count, 0);
                printf(".");
                fflush(stdout);
            } while (count > 0);
            close( fd);
            printf("finished request: GET %s\n", url);
        }
        else {
            sprintf( buffer, "HTTP/1.0 404 Not Found\n\n");
            send( out, buffer, strlen(buffer), 0);
        }
    }
    else {
        sprintf( buffer, "HTTP/1.0 501 Method Not Implemented\n\n");
        send( out, buffer, strlen(buffer), 0);
    }
}

Das Programm kann wiederum mittels des freien Borland Compilers per

bcc32 httpget.c

oder dem MinGW Compiler durch


gcc -o httpserv.exe httpserv.c -lwsock32

und unter Linux mit dem GNU-Compiler per

gcc -o httpserv httpserv.c

übersetzt werden. Dann startet man den Server mit

httpserv p:/cplus/artikel

(p:/cplus/artikel ist ein Verzeichnis auf meinem Computer, in dem sich einige HTML-Dateien befinden. Sie sollten es natürlich durch ein entsprechendes Verzeichnis auf Ihrem Rechner ersetzen!) und kann jetzt zum Beispiel durch

httpget 127.0.0.1 /gmp.html

die Datei gmp.html (diese muss selbstverständlich in p:/cplus/artikel existieren) über das Netzwerk übertragen. Oder man startet seinen Webbrowser und gibt http://127.0.0.1/gmp.html in das Adressfeld ein. Das Netzwerk besteht in diesen Fällen allerdings nur aus dem Loopbackadapter mit der Adresse 127.0.0.1. Diese Adresse zeigt immer auf den eigenen Computer, so lassen sich Netzwerkprogramme testen, falls nur ein einziger Rechner vorhanden ist. Wer in der glücklichen Lage ist, mehrere per TCP/IP verbundene Rechner zu besitzen, sollte ruhig einmal httpserv und httpget auf verschiedenen Computern laufen lassen.

Mit den in den letzten beiden Artikeln vorgestellten Programmen httpget und httpserv besteht die Möglichkeit, Dateien zwischen verschiedenen Rechnern per HTTP zu übertragen. Natürlich müssen für die Entwicklung eines «richtigen» Webservers noch viele Erweiterungen vorgenommen werden, insbesondere was die gleichzeitige Behandlung paralleler Clientzugriffe, die Implementierung des vollständigen HTTP-Protokolls und die Übertragung anderer Dateiformate als HTML betrifft. Einige Tips dazu folgen in einem der nächsten Artikel.