Le but de ce TP est de programmer un mini-serveur WEB en C.

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 (en anglais, les threads). On utilisera les processus légers POSIX (aussi nommées pthreads) qui sont portables.

Il s'agit du TP proposé en 2007 par Antoine Miné. La correction est disponible sur la page d'Antoine.

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 processus légers 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 processus légers.

Une source de documentation sur les processus légers 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 processus léger 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:

  • netcat et telnet permettent d'interagir avec le serveur;
  • netstat permet d'afficher la liste des connections ouvertes sur le système; l'option -t permet de se limiter aux connections TCP et -l permet d'obtenir les sockets d'écoute;
  • un navigateur WEB tel que mozilla, firefox ou links; pour se connecter au serveur, on demandera simplement une URL de la forme http://localhost:8080/truc/bidule.
  • top

Exercice

Serveur HTTP

Écrire un serveur en 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 le processus léger principal 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).

Journal d'accès

Modifier 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 tous les processus légers, 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.

Ensemble de processus légers

Modifier votre serveur pour qu'il crée au démarrage un ensemble de N processus légers pour gérer les connections. Celles-ci sont initialement en attente. Quand une nouvelle connection est établie, un processus légers en attente est choisi pour la traiter. Quand la connection se termine, le processus léger se place à nouveau en attente. Si les N processus légers sont occupés, le processus léger principal (qui s'occupe de recevoir les demandes de connection) se place en attente jusqu'à ce qu'un processus léger 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 processus légers.

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

  • un verrou d'exclusion mutuelle (pour éviter que les signaux ne se perdent),
  • une boucle qui replace le processus léger en attente tant que la condition souhaitée n'est pas réalisée (pour éviter les réveils injustifiés).

top