Serveur WEB multi-thread en C

Le but de ce TP est de programmer un mini-serveur WEB. On utilisera le C pour changer du Perl.

L'objet de ce TP n'est pas d'apprendre le protocole HTTP (vous devez commencer à le connaître) mais de réaliser un serveur capable de traiter plusieurs connections en parallèle grâce à des processus légers: les threads. On utilisera les threads POSIX (aussi nommées pthreads) qui sont portables.

Aide

HTTP

On rappelle que le protocole HTTP 1.1 est décrit dans la RFC 2616.

Voir également les explications du TP sur les clients HTTP.

pthreads

Les fonctions liées aux threads POSIX sont déclarées dans l'en-tête pthread.h. Elles commencent toutes par le préfixe pthread_. Lors de la compilation, il faudra utiliser l'option -lpthread afin de lier la bibliothèque de threads.

Une source de documentation sur les threads POSIX est la spécification Single Unix 2.

Note: contrairement aux fonctions systèmes Unix classiques qui renvoient -1 ou NULL en cas d'erreur et mettent à jour la variable globale errno, les fonctions de thread renvoient directement un code d'erreur (compatible avec strerror et perror) mais ne changent pas errno.

Outils

Les outils suivants seront utiles pour tester votre serveur:

Exercices

1) Écrivez un serveur C qui écoute sur le port TCP 8080 (socket, bind, listen); on utilisera l'option SO_REUSEADDR (setsockopt) pour éviter le délai imposé entre deux bind successifs du même port. À chaque nouvelle connection d'un client (accept), le serveur crée un nouveau processus léger (pthread_create) pour la traiter tandis que la thread principale continue, en parallèle, à attendre d'autres connections. Une requête de la forme GET /chemin HTTP/1.1 sera interprétée comme une demande de téléchargement du fichier se trouvant à la position chemin par rapport au répertoire où est lancé le serveur. Elle générera une réponse de la forme:

HTTP/1.1 200 OK
Content-length: nnn

contenu du fichier

qui précise la taille nnn et le contenu du fichier. Vous prendrez les précautions d'usage pour éviter les problèmes de sécurité (n'envoyer que des fichiers normaux, en lecture public, etc.) Vous pouvez également adjoindre des en-têtes additionnelles telles que Date, Last-Modified ou Content-Type.

Si le fichier n'existe pas ou est inaccessible, le serveur donnera une réponse de la forme:

HTTP/1.1 404 Not found
Content-type: text/html

<html><head><title>Not Found</title></head><body>
Sorry, the object you requested was not found.
</body><html>

Après avoir répondu à la requête (mode close) ou quand le client clos la socket (mode keep-alive), le processus léger ferme la connection et quitte (pthread_exit, voir aussi pthread_detach).

Correction) serveur1.c.

Journal d'accès

2) Modifiez votre serveur pour qu'il garde en mémoire vive un journal (log) de chaque requête traitée: URL demandée, IP du client, date, statu d'erreur (les fonctions getpeername, inet_ntop, gettimeofday et ctime_r pourront être utiles). Quand un client demande l'URL spéciale /log, le serveur envoie ce journal sous forme de page HTML.

Le journal étant partagé entre toutes les threads, tout accès doit être protégé par un verrou d'exclusion mutuelle (pthread_mutex_t, pthread_mutex_init, pthread_mutex_lock, pthread_mutex_unlock). (L'utilisation d'un verrou de type pthread_rwlock_t est également possible.)

La page /log étant dynamique, c'est une bonne idée d'interdire sa mise en cache par le client ou un éventuel proxy grâce à l'en-tête Cache-control: no-cache. Il est également possible d'inclure une balise de la forme <META HTTP-EQUIV="Refresh" CONTENT="5"> dans l'en-tête de la page HTML générée pour inciter le client WEB à actualiser la page toutes les 5 secondes.

Correction) serveur2.c.

Pool de threads

3) Modifiez votre serveur pour qu'il crée au démarrage un pool de N threads pour gérer les connections. Celles-ci sont initialement en attente. Quand une nouvelle connection est établie, une thread en attente est choisie pour la traiter. Quand la connection se termine, la thread se place à nouveau en attente. Si les N threads sont occupées, la thread principale (qui s'occupe de recevoir les demandes de connection) se place en attente jusqu'à ce qu'une thread se libère. Contrairement au serveur des questions précédentes, il n'y a donc pas de création ni de destruction dynamique de threads.

On utilisera des variables de condition (pthread_cond_t) pour placer une thread en attente d'un évènement (pthread_cond_wait) et pour signaler un évènement à une thread en attente (pthread_cond_signal). Attention à bien protéger chaque attente d'évènement par:

Correction) serveur3.c.


Antoine Miné