next up previous contents index
Next: E Index général & Up: D Sockets BSD et Previous: XV Éléments de serveurs   Contents   Index

Subsections

XVI Anatomie d'un serveur Web


1 Le protocole HTTP

ATTENTION CE CHAPITRE N'A PAS FAIT L'OBJET D'UNE REVISION DEPUIS DE NOMBREUSES ANNÉES. LES INFORMATIONS CONTENUES Y SONT JUSTES MAIS PASSABLEMENT OBSOLÈTES ! Ce document est une présentation succincte du protocole HTTP 1.0 et du serveur Apache qui l'utilise.

Le protocole HTTPXVI1 est le protocole d'application utilisé pour véhiculer, entres autres, de l'HTMLXVI2sur l'Internet.

C'est le protocole le plus utilisé en volume depuis 1995, devant FTP, NNTP et SMTP ; il accompagne l'explosion de l'utilisation du système global d'information ``World-Wide Web''.

Depuis 1990, date d'apparition du ``Web'', le protocole HTTP évolue doucement mais surement. Il est longtemps resté sous forme de ``draft''. La première version déployée largement a été la 1.0, définie dans la RFC 1945 de mai 1996. Depuis le début du mois de janvier 1997 est apparue la version 1.1, deux fois plus volumineuse pour tenir compte des nouvelles orientations de l'usage du service.

Aujourd'hui ce protocole est tellement répandu que pour bon nombre de nouveaux utilisateurs du réseau, l'Internet c'est le ``web'' !

Techniquement, l'usage de ce protocole se conçoit comme une relation entre un client et un serveur. Le client, appelé génériquement un ``browser'', un ``User Agent'', ou encore butineur de toile, interroge un serveur connu par son ``urlXVI3'' dont la syntaxe est bien décrite dans la RFC 1738.

Par exemple la chaîne de caractères http://www.sio.ecp.fr/ est une url ; il suffit de la transmettre en tant qu'argument à un quelconque outil d'exploration et celui-ci vous renverra (si tout se passe comme prévu !) ce qui est prévu sur le serveur en question pour répondre à cette demande (car il s'agit bien d'une requête comme nous le verrons plus loin dans ce chapitre).

Le serveur, supposé à l'écoute du réseau au moment où la partie cliente l'interroge, utilise un port connu à l'avance. Le port 80 est dédié officiellement au protocole httpXVI4, mais ce n'est pas une obligation (cette décision est prise à la configuration du serveur). L'url qui désigne un serveur peut contenir dans sa syntaxe le numéro de port sur lequel il faut l'interroger, comme dans :

1.1 Exemple d'échange avec http

Le transport des octets est assuré par TCPXVI5 et le protocole est ``human readable'', ce qui nous autorise des essais de connexion avec le client tcp à tout faire : telnet ! Bien entendu on pourrait utiliser un ``browser'' plus classique, mais celui-ci gérant pour nous le détail des échanges il ne serait plus possible de les examiner.

$ telnet localhost 80
Trying...
Connected to localhost.
Escape character is '^]'.


Ce qui est tapé par l'utilisateur et la réponse du serveur.

GET / HTTP/1.0

La requête, suivie d'une ligne vide.

HTTP/1.1 200 OK
Date: Fri, 01 Mar 2002 10:59:06 GMT
Server: Apache/1.3.23 (Unix)
Last-Modified: Sat, 10 Nov 2001 16:13:02 GMT
ETag: "1381-8b-3bed520e"
Accept-Ranges: bytes
Content-Length: 79
Connection: close
Content-Type: text/html

<HTML>
<HEAD>
<TITLE>Ceci est un titre</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
Connection closed by foreign host.

Enfin la réponse du serveur, que l'on peut décomposer en trois parties : mm
  1. Un code de retour (HTTP)
  2. Un en-tête MIME
  3. Des octets, ici ceux d'une page écrite en HTML.
Notons également la deconnexion à l'initiative du serveur, en fin d'envoi de la page HTML.

1.2 Structure d'un échange

L'exemple qui précède est typique d'un échange entre le client et le serveur : une question du client génère une réponse du serveur, le tout lors d'une connexion TCP qui se termine lors de l'envoi du dernier octet de la réponse (clôture à l'initiative du serveur).

Le serveur ne conserve pas la mémoire des échanges passés, on dit aussi qu'il est sans état, ou ``stateless''.

La question et la réponse sont bâties sur un modèle voisin : le message HTTP.

\includegraphics{fig.web.01.ps} cm figure XVI.01

Les parties A, B et C forment l'en-tête du message, et D le corps.

A
La première ligne du message, est soit la question posée (``request-line''), soit le statut de la réponse (``status-line'').

  • La question est une ligne terminée par CRLF, elle se compose de trois champs :

    Une méthode
    à prendre dans GET, HEAD, ou POST.

    GET
    Plus de 99% des requêtes ont cette méthode, elle retourne l'information demandée dans l'URL (ci-dessous).

    HEAD
    La même chose que GET, mais seul l'en-tête du serveur est envoyé. Utile pour faire des tests d'accessibilité sans surcharger la bande passante. Utile également pour vérifier de la date de fraicheur d'un document (information contenue dans l'en-tête).

    POST
    Cette méthode permet d'envoyer de l'information au serveur, c'est typiquement le contenu d'un formulaire rempli par l'utilisateur.

    Une ressource
    que l'on désigne par une URLXVI6.

    Par exemple http://www.site.org/.

    La version du protocole
    , sous forme HTTP-Numéro de version. Par exemple HTTP/1.1 !

  • La réponse. Cette première ligne n'est que le statut de la réponse, les octets qui la détaillent se trouvent plus loin, dans le corps du message. Trois champs la composent, elle se termine par CRLF :

    La version du protocole
    , sous forme HTTP-Numéro de version, comme pour la question.

    Statut
    C'est une valeur numérique qui décrit le statut de la réponse. Le premier des trois digits donne le sens général :

    1xx N'est pas utilisé ``Futur Use''
    2xx
    Succès, l'action demandée a été comprise et exécutée correctement.
    3xx
    Redirection. La partie cliente doit reprendre l'interrogation, avec une autre formulation.
    4xx
    Erreur coté client. La question comporte une erreur de syntaxe ou ne peut être acceptée.
    5xx
    Erreur coté serveur. Il peut s'agir d'une erreur interne, due à l'OS ou à la ressource devenue non accessible.

    Phrase
    C'est un petit commentaire (``Reason- Phrase'') qui accompagne le statut, par exemple le statut 200 est suivi généralement du commentaire ``OK'' !

B
C'est une partie optionnelle, qui contient des informations à propos du corps du message. Sa syntaxe est proche de celle employée dans le courrier électronique, et pour cause, elle repecte aussi le standard MIMEXVI7.

Un en-tête de ce type est constitué d'une suite d'une ou plusieurs lignes (la fin d'une ligne est le marqueur CRLF) construite sur le modèle :

Nom_de_champ : Valeur_du_champ CRLF

Éventuellement le marqueur de fin de ligne peut être omis pour le séparateur ``;''.

Exemple d'en-tête MIME :

        Date: Fri, 01 Mar 2002 10:59:06 GMT
        Server: Apache/1.3.23 (Unix)
        Last-Modified: Sat, 10 Nov 2001 16:13:02 GMT
        ETag: "1381-8b-3bed520e"
        Accept-Ranges: bytes
        Content-Length: 79
        Connection: close
        Content-Type: text/html

Date:
C'est la date à laquelle le message a été envoyé. Bien sûr il s'agit de la date du serveur, il peut exister un décalage incohérent si les machines ne sont pas synchronisées (par exemple avec XNTP).

Server:
Contient une information relative au serveur qui a fabriqué la réponse. En générale la liste des outils logiciels et leur version.

Content-type:
Ce champ permet d'identifier les octets du corps du message.

Content-length:
Désigne la taille (en octets) du corps du message, c'est à dire la partie D de la figure XVI.1.

Last-modified:
Il s'agit de la date de dernière modification du fichier demandé, comme l'illustre le résultat de la commande ll (voir aussi la coïncidence de la taille du fichier et la valeur du champ précédent).

-rw-r--r--  1 web   doc    139 Nov 10 17:13 index.html

ETag:
C'est un identificateur du serveur, constant lors des échanges. C'est un moyen de maintenir le dialogue avec un serveur en particulier, par exemple quand ceux-ci sont en grappe pour équilibrer la charge et assurer la redondance.

C
Une ligne vide (CRLF) qui est le marqueur de fin d'en-tête. Il est donc absolument obligatoire qu'elle figure dans le message. Son absence entraine une incapacité de traitement du message, par le serveur ou par le client.

D
Le corps du message. Il est omis dans certains cas, comme une requête avec la méthode GET ou une réponse à une requête avec la méthode HEAD.

C'est dans cette partie du message que l'on trouve par exemple les octets de l'HTML, ou encore ceux d'une image...

Le type des octets est intimement lié à celui annoncé dans l'en-tête, plus précisement dans le champ Content-Type.

Par exemple :

Content-Type: text/html $\Longrightarrow$ Le corps du message contient des octets à interpréter comme ceux d'une page écrite en HTML.

Content-Type: image/jpg $\Longrightarrow$ Le corps du message contient des octets à interpréter comme ceux d'une image au format jpeg


2 URIs et URLs

Le succès du ``web'' s'appuie largement sur un système de nommage des objets accessibles, qui en uniformise l'accès, qu'ils appartiennent à la machine sur laquelle on travaille ou distants sur une machine en un point quelconque du réseau (mais supposé accessible). Ce système de nommage universel est l'url (``Uniform Resource Locator'' -- RFC 1738) dérivé d'un système de nommage plus général nommé uri (``Universal Resource Identifier'' -- RFC 1630).

La syntaxe générale d'un(e) url est de la forme :

<scheme>:<scheme-specific-part>

Succintement la ``scheme'' est une méthode que l'on sépare à l'aide du caractère ``:'' d'une chaîne de caractères ascii 7 bits dont la structure est essentiellement fonction de la ``scheme'' qui précède et que l'on peut imaginer comme un argument.

Une ``scheme'' est une séquence de caractères 7bits. Les lettres ``a'' à ``z'', les chiffres de ``0'' à ``9'', le signe ``+'', le ``.'' et le ``-'' sont admis. Majuscules et minuscules sont indifférenciés.

Exemples de ``schemes'' : http, ftp, file, mailto, ...Il en existe d'autres (cf la RFC) non indispensables pour la compréhension de cet exposé.

Globalement une url doit être encodée en ascii 7 bitsXVI8 sans caractère de contrôle (c'est à dire entre les caractères 20 et 7F), ce qui a comme conséquence que tous les autres caractères doivent être encodés.

La méthode d'encodage transforme tout caractère non utilisable directement en un triplet formé du caractère ``%'' et de deux caractères qui en représente la valeur hexadécimale. Par exemple l'espace (20 hex) doit être codé %20.

Un certain nombre de caractères, bien que théoriquement représentables, sont considérés comme non sûrs (``unsafe'') et devraient être également encodés de la même manière que ci-dessus, ce sont :

                % < > " # { } | \ ^ ~ [ ] `

Pour un certain nombre de ``schemes'' (http...) certains caractères sont réservés car ils ont une signification particulière. Ce sont :

                     ; / ? : @ = &
Ainsi, s'ils apparaissent dans l'url sans faire partie de sa syntaxe, ils doivent être encodés.

2.1 Scheme http

Une url avec la ``scheme'' http bien formée doit être de la forme :

``path'' et ``searchpath'' sont optionnels.

host
C'est un nom de machine ou une adresse IP.

port
Le numéro de port. S'il n'est pas précisé, la valeur 80 est prise par défaut.

path
C'est un sélecteur au sens du protocole http.

searchpath
C'est ce que l'on appelle la ``query string'', autrement dit la chaîne d'interrogation.

À l'intérieur de ces deux composantes, les caractères / ; ? sont réservés, ce qui signifie que s'ils doivent être employés, ils doivent être encodés pour éviter les ambiguïtés.

Le ? marque la limite entre l'objet interrogeable et la ``query string''. À l'intérieur de cette chaîne d'interrogation le caractère + est admis comme raccourci pour l'espace (ascii 20 hex). Il doit donc être encodé s'il doit être utilisé en tant que tel.

De même, à l'intérieur de la ``query string'' le caractère = marque la séparation entre variable et valeur, le & marque la séparation entre les couples variable = valeur.

Exemple récapitulatif :

Notez le ``é'' codé %E9, c'est à dire le caractère de rang 14 x 16 + 9 = 233. On peut également observer quatre variables q, hl, start et sa dont la signification peut être partiellement devinée, mais dont le remplissage reste à la charge du serveur en question.

Le rôle de la chaîne ``search'' est celui de ce que l'on appelle une CGI ou ``Common Gateway Interface'', c'est à dire un programme qui effectue le lien entre le serveur interrogé et des programmes d'application, ici une recherche dans une base de données. Nous examinons succinctement le principe de fonctionnement de tels programmes page [*].


3 Architecture interne du serveur Apache

Cette partie se consacre à l'étude du fonctionnement du serveur ApacheXVI9.

Pour comprendre les mécanismes internes d'un tel serveur il est absolument nécessaire de pouvoir en lire le code source, cette contrainte exclu les produits commerciaux.

Au mois de mars 2002, d'après le ``Netcraft Web Server SurveyXVI10'' le serveur le plus utilisé (55%) est très majoritairement celui du projet Apache.

D'après ses auteurs, le serveur Apache est une solution de continuité au serveur du NCSAXVI11. Il corrige des bugs et ajoute de nombreuses fonctionnalités, particulèrement un mécanisme d'API pour permettre aux administrateurs de sites de développer de nouveaux modules adaptés à leurs besoins propres.

Plus généralement, tout ce qui n'est pas strictement dans les attributions du serveur (gestion des processus, gestion mémoire, gestion du réseau) est traité comme un module d'extension. Le fichier apache_1.3.23/htdocs/manual/misc/API.html de la distribution standard apporte des précisions, le serveur http://www.apacheweek.com/ pointe également sur grand nombre de documents très utiles.

Le serveur Apache introduit également la possibilité de serveurs multi-domaines (domaines virtuels), ce qui est fort apprécié des hébergeurs de sites.

3.1 Environnement d'utilisation

La figure XVI.2 qui suit, synthétise l'environnement d'utilisation.

\includegraphics{fig.web.02.ps}

Le serveur se met en \oeuvre simplement. La compilation fournit un exécutable, httpd, qui, pour s'exécuter correctement, a besoin des trois fichiers ASCII de configuration : srm.conf, access.conf, httpd.conf. C'est en fait dans celui-ci que sont effectués l'essentiel des ajustements locaux à la configuration standard.

Lors de l'exécution, trois fichiers sont modifiésXVI12 :

httpd.pid
(PidFile) contient le ``process ID'' du ``leader'' de groupe ; à utiliser avec le signal SIGHUP (cf figure XVI.2) pour provoquer la re-lecture et le re-démarrage ``à chaud'' du serveur, ou avec SIGTERM pour mettre fin à son activité.

access_log
(CustomLog) Qui contient le détail des accès clients. Ce fichier peut devenir très volumineux.

error_log
(ErrorLog) Qui contient le détail des accès infructueux et des éventuels problèmes de fonctionnement du serveur.

Le ``daemon'' httpd est soit du type (ServerType) ``standalone'' ou si son invocation est occasionnelle, à la demande piloté par inetd (cf page 4).

Il démarre son activité par ouvrir le port (Port) désigné dans la configuration, puis s'exécute avec les droits de l'utilisateur User et du groupe Group. Sa configuration se trouve dans le répertoire conf sous-répertoire de ServerRoot. Les fichiers accessibles par les connexions clientes, eux, se situent dans le répertoire DocumentRoot qui en est la racine, c'est à dire vue comme "/" par les browsers clients.

Le fichier httpd.pid contient un numéro de processus ``leader'' de groupe car en fait le serveur Apache, dès son initialisation, démarre un certain nombre de processus, autant de serveurs capables de comprendre les requêtes des clients. En voici un exemple :

MinSpareServers 5
C'est le nombre minimum d'instances du serveur (non compris le processus maître) en attente d'une connexion. S'il en manque, elles sont créées.
MaxSpareServers 10
C'est le nombre maximum d'instances du serveur (non compris le processus maître) en attente d'une connexion. S'il y en a de trop elles sont supprimées.
StartServers 5
C'est le nombre minimum d'instances du serveur (non compris le processus maître) au démarrage.
MaxClients 150
C'est le nombre maximum de clients (requêtes HTTP) simultanés. Cette constante peut être augmentée en fonction de la puissance de la machine.

Un processus joue le rôle de régulateur, du point de vue Unix c'est un processus chef de groupe (``leader''). La commande ps permet de visualiser une situation opérationnelle :

     web 17361  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
    root  2794     1  0  Feb 23  ? 0:06 /usr/local/bin/httpd -d /users/web
     web 17363  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
     web 17270  2794  0  Mar 13  ? 0:01 /usr/local/bin/httpd -d /users/web
     web 17362  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
     web 17401  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
     web 17313  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
     web 17312  2794  0  Mar 13  ? 0:01 /usr/local/bin/httpd -d /users/web
     web 17355  2794  0  Mar 13  ? 0:00 /usr/local/bin/httpd -d /users/web
     web 17314  2794  0  Mar 13  ? 0:01 /usr/local/bin/httpd -d /users/web

Ici il y a 9 instances du serveurs et le processus maître, qu'il est aisé de reconnaitre (2794) car il est le père des autres processus (ce qui n'implique pas qu'il en est le chef de groupe, mais la suite de l'analyse nous le confirmera).

La commande netstat -f inet -a | grep http ne donne qu'une ligne :

tcp        0      0  *.http                 *.*                    LISTEN
Cela signifie qu'aucune connexion n'est en cours sur ce serveur, et qu'une seule ``socket'' est en apparence à l'écoute du réseau. C'est une situation qui peut sembler paradoxale eu égard au nombre de processus ci-dessus, le code nous fournira une explication au paragraphe suivant.

La commande tail -1 logs/access.log fournit la trace de la dernière requête :

www.chezmoi.tld - - [01/Mar/2002:17:13:28 +0100] "GET / HTTP/1.0" 200 79

Il s'agit de la trace de notre exemple d'interrogation du début de ce chapitre !

3.2 Architecture interne

Attention, ce paragraphe concerne la version 1.1.1 du logiciel. Le fonctionnement de la version courante, 1.3.23, reste similaire mais le code ayant beaucoup changé, les numéros de lignes sont devenus de fait complètement faux.

Ce qui nous intéresse plus particulièrement pour expliquer le fonctionnement du serveur se trouve dans le répertoire src/, sous répertoire du répertoire principal de la distribution :

$ ll apache_1.1.1/
total 19
-rw-------  1 fla  users  3738 Mar 12  1996 CHANGES
-rw-------  1 fla  users  2604 Feb 22  1996 LICENSE
-rw-------  1 fla  users  3059 Jul  3  1996 README
drwxr-x---  2 fla  users   512 Feb  7 22:14 cgi-bin
drwxr-x---  2 fla  users   512 Feb  7 22:14 conf
drwxr-x---  2 fla  users   512 Feb  7 22:14 htdocs
drwxr-x---  2 fla  users  2048 Feb  7 22:14 icons
drwxr-x---  2 fla  users   512 Jul  8  1996 logs
drwxr-x---  2 fla  users  2048 Mar 12 10:42 src
drwxr-x---  2 fla  users   512 Feb  7 22:15 support

Dans ce répertoire qui compte au moins 75 fichier (wc -l *.[ch] $\Rightarrow $ 27441 lignes) nous nous restreignons aux fichiers essentiels décrits dans le ``README'' soit encore une petite dizaine d'entres eux (wc -l $\Rightarrow $ 6821 lignes) : mod_cgi.c, http_protocol.c, http_request.c, http_core.c, http_config.c, http_log.c, http_main.c, alloc.c

Dans un premier temps nous allons examiner le fonctionnement de la gestion des processus, du mode de relation entre le père et ses fils.

Puis, dans un autre paragraphe, nous examinerons plus particulièrement ce qui se passe lors d'une connexion, avec le cas particulier de l'exécution d'une CGIXVI13 qui comme son nom l'indique est le programme qui effectue l'interface entre le serveur HTTP et un programme d'application quelconque. Dans la suite de ce document le terme ``cgi'' employé seul désigne ce type d'application.

3.2.1 Gestion des processus

Au commencement est le main, et celui-ci se trouve dans le fichier http_main.c, comme il se doit !

    ... 	        ...
   1035 pool *pconf;                    /* Pool for config stuff */
   1036 pool *ptrans;                   /* Pool for per-transaction stuff */  
    ... 	        ...
   1472 int       
   1473 main(int argc, char *argv[])
   1474 {       
    ... 	        ...
   1491     init_alloc();
   1492     pconf = permanent_pool;
   1493     ptrans = make_sub_pool(pconf);

La fonction init_alloc appelle make_sub_pool qui initialise un intéressant mécanisme de buffers chaînés, utilisé tout au long du code dès lors qu'il y a besoin d'allouer de la mémoire.

    ... 	        ...
   1523     setup_prelinked_modules();

Les différents modules du serveurs sont ajoutés dans une liste chaînée.

   1524
   1525     server_conf = read_config (pconf, ptrans, server_confname);
   1526  
   1527     if(standalone) {
   1528         clear_pool (pconf);     /* standalone_main rereads... */
   1529         standalone_main(argc, argv);
   1530     } 
   1531     else {
    ... 	        ...
   1580     }
   1581     exit (0);
   1582 }

``Standalone'' est à 0 si le serveur est invoqué depuis inetd XVI14. Son mode de fonctionnement le plus efficace reste avec ce paramètre à 1 (voir le cours sur inetd), que nous supposons ici.

La fonction standalone_main (ligne 1362) prépare le processus à son futur rôle de serveur. Pour bien comprendre le cette fonction, il faut imaginer qu'elle est invoquée au démarrage, et ``à chaud'', pour lire et relire la configuration.

Ligne 1369
, la variable one_process = 0 (sinon le processus est en mode debug) occasionne l'appel à la fonction detach (ligne 876). Celle-ci transforme le processus en ``leader'' de groupe avec la succession bien connue fork + setsid (pour plus de détails, voir le cours sur les daemons).

Ligne 1374
, la fonction sigsetjmp enregistre la position de la pile et l'état du masque d'interruption (deuxième argument non nul) dans restart_buffer. Tout appel ultérieur à siglongjmp forcera la reprise de l'exécution en cette ligne.

Ligne 1377
On ignore tout signal de type SIGHUP, cela bloque toute tentative cumulative de relecture de la configuration.

Ligne 1382
(one_process = 0) on envoie un signal SIGHUP à tous les processus du groupe. Cette disposition n'est utile que dans le cas d'un re-démarrage ``à chaud''. La variable pgrp est initialisée par la fonction detach (ligne 876), elle contient le PID du chef de groupe.

L'intéret d'avoir un groupe de processus est que le signal envoyé à son ``leader'' est automatiquement envoyé à tous les processus y appartenant.

Chaque processus qui reçoit ce signal, s'il est en mesure de le traiter, appelle la fonction just_die qui exécute un exit(0) (ligne 943). Donc tous les fils meurent, sauf le chef de groupe.

Ligne 1390
, l'appel à la fonction reclaim_child_processes() effectue autant de wait qu'il y avait de processus fils, pour éviter les zombis.

Ligne 1398
Relecture des fichiers de configuration.

Ligne 1400
set_group_privs (ligne 960) change le ``user id'' et le ``group id'' si ceux-ci ont été modifiés.

Ligne 1401
accept_mutex_init (ligne 166) fabrique un fichier temporaire (/usr/tmp/htlock.XXXXXX), l'ouvre, réduit son nombre de lien à 0, de telle sorte qu'il sera supprimé dès la fin du processus. Ce fichier est le verrou d'accès à la socket principale, comme nous l'examinerons un peu plus loin.

Ligne 1402
reinit_scoreboard (ligne 596) Cette fonction remet à zéro, ou crée (setup_shared_mem ligne 432) la zone de mémoire commune entre le chef de groupe et ses processus fils. Cette zone mémoire est, soit un segment de mémoire partagée (IPC du système V), soit de la mémoire rendue commune par un appel à la primitive mmap (Le choix est effectué par configure qui analyse les possibilités du système, avant compilation du serveur).

La taille de cette zone est de ${\tt HARD\_SERVER\_LIMIT} \times
{\tt sizeof(short\_score)}$ octets, la structure short_score, définie dans scoreboard.h, recueille les statistiques d'utilisation de chaque serveur et son statut (par exemple SERVER_READY, SERVER_DEAD...). C'est par ce moyen que le serveur maître contrôle les serveurs fils.

HARD_SERVER_LIMIT définit le nombre maximum de connexions actives, donc d'instances de serveur, à un instant donné. En standard cette valeur est 150, elle peut être augmentée dans le fichier de configuration httpd.conf (voir ci-dessus au paragraphe II.1)

Enfin la figure XVI.4 montre son rôle stratégique.

Ligne 1413
(on suppose listeners = NULL), après avoir initialisé une structure d'adresse, appel à la fonction make_sock (ligne 1298). Celle-ci crée et initialise une socket, et, détail important, appelle par deux fois setsockopt, ce qui mérite un commentaire :

...      ...
1312     if((setsockopt(s, SOL_SOCKET,SO_REUSEADDR,(char *)&one,sizeof(one)))
1313        == -1) {
...      ...
1318     if((setsockopt(s, SOL_SOCKET,SO_KEEPALIVE,(char *)&keepalive_value,
1319         sizeof(keepalive_value))) == -1) {
...      ...
SO_REUSEADDR
Indique que la règle d'exclusivité suivie par bind(2) pour l'attribution d'un port ne s'applique plus : un processus peut se servir d'un même port pour plusieurs usages différents (comme par exemple le client ftp qui attaque les port 21 et 20 du serveur avec le même port local), voire même des processus différents (c'est le cas ici) peuvent faire un bind avec le même port sans rencontrer la fatidique erreur 48 (``Address already in use'') !

Vue des clients HTTP, le serveur est accessible uniquement sur le port 80, ce que nous avons remarqué au paragraphe II.1 (netstat) sans l'expliquer, voila qui est fait !

SO_KEEPALIVE
Indique à la couche de transport, ici TCP, qu'elle doit émettre à interval régulier (non configurable) un message à destination de la socket distante. Que celle-ci n'y réponde pas et la prochaine tentative d'écriture sur la socket se solde par la réception d'un SIGPIPE, indiquant la disparition de la socket distante (voir plus loin la gestion de ce signal dans la fonction child_main, ligne 1139).

Ligne 1430
set_signals (1007) prévoit le comportement lors de la réception des signaux :

SIGHUP Appel de restart
SIGTERM Appel de sig_term

Ligne 1438
et la suivante, création d'autant de processus fils qu'il en est demandé dans le fichier de configuration (StartServers). La fonction make_child (1275) appelle fork, puis dans le fils modifie le comportement face aux signaux SIGHUP et SIGTERM (just_die appelle exit) avant d'exécuter child_main.

\includegraphics{fig.web.03.ps} cm figure XVI.03

Arrivés à ce stade, il nous faut analyser l'attitude des deux types de processus.

3.2.1.1 Le processus maître

Ligne 1444
démarre une boucle infinie de surveillance. Seule la réception et le traitement d'un signal peut l'interrompre.

Ligne 1458
Ici, si le nombre de serveurs disponibles est inférieur au nombre minimal requis, il y regénération de ceux qui manquent avec la fonction make_child

3.2.1.2 Les processus esclaves

Ligne 1139
La gestion de ces processus, autant de serveurs Web opérationnels, débute avec l'appel de la fonction child_main.

Ligne 1167
Début de la boucle infinie qui gère cette instance du serveur. Au cours d'une boucle le serveur gère l'acceptation d'une requête et son traitement.

Ligne 1174
Gestion du SIGPIPE donc des clients qui déconnectent avant l'heure !

Ligne 1180
Si le nombre de serveurs libres (count_idle_servers) est supérieur à la limite configurée, ou si

Ligne 1182
le nombre de requêtes traitées par ce processus a atteint la limite max_requests_per_child, le processus s'arrête de lui-même. C'est l'auto-régulation pour libérer des ressources occupées par un trop grand nombre de processus serveurs inutiles.

Ligne 1190
L'appel à la fonction accept_mutex_on vérouille l'accès à la ressource définie précédement avec accept_mutex_init (ligne 166). Ce vérouillage est bloquant et exclusif. C'est à dire qu'il faut qu'un autre processus en dévérouille l'accès pour que le premier processus sur la liste d'attente (gérée par le noyau Unix) y accède.

Ce fonctionnement est assuré suivant la version d'Unix par la primitive flock ou par la primitive fcntl.

Les sémaphore du Système V (semget, semctl, semop...) assurent la même fonctionnalité, en plus complet, ils sont aussi plus complexes à mettre en \oeuvre.

Cette opération est à rapprocher de l'option SO_REUSEADDR prise ligne 1312. Il faut éviter que plusieurs processus serveurs disponibles ne répondent à la requête. Il n'y a qu'un seul processus prêt à répondre à la fois et dès que le accept (ligne 1217) retourne dans le code utilisateur la ressource est dévérouillée (on suppose toujours listeners = 0).

Ligne 1221
La fonction accept_mutex_off dévérouille l'accès à la socket.

Ligne 1245
read_request lit la requête du client, donc un message HTTP.

Ligne 1247
process_request fabrique la réponse au client, donc un autre message HTTP.

\includegraphics{fig.web.04.ps} cm figure XVI.04

3.2.2 Prise en main des requêtes

Le fichier concerné par la lecture de la requête est http_protocol.c.

La lecture de la première ligne du message HTTP est assurée par la fonction read_request_line, ligne 329XVI15.

La lecture de l'en-tête MIME est assurée par la fonction get_mime_headers, ligne 356. Attention, seule l'en-tête lue le corps du message dans le cas de a méthode POST est lu plus tard, lors du traitement du message, par le programme d'application (CGI).

Le fichier concerné par la formulation de la réponse est http_request.c et la fonction principale process_request, ligne 772. Celle-ci appelle en cascade process_request_internal, ligne 684.

Cette dernière effectue un certain nombre de tests avant de traiter effectivement la requête. Parmi ceux-ci on peut relever,

Ligne 716
La fonction unescape_url (util.c, ligne 593) assure le décodage des caractères réservés et transcodés comme il est spécifié par la RFC 1738.

Ligne 723
La fonction getparents filtre les chemins (``pathname'') qui prêtent à confusion.

Ligne 768
La fonction invoke_handler est le point d'entrée dans le traitement de la requête. Cette fonction (http_config.c, ligne 267) invoque le programme (module) qui se charge de fabriquer la réponse, au vue du contenu du champ content_type de la requête. Si celui est inexistant, comme dans notre exemple du paragraphe I, il est positionné par défaut à la valeur html/text.

3.2.3 Deux types de CGIs

Pour la suite nous supposons que la prise en main de la requête est faite par le module ``cgi'', défini dans le fichier mod_cgi.c. Le point d'entrée est la fonction cgi_handler (ligne 207), c'est à dire celle qui appellée par invoke_handler, vue au paragraphe ci-dessus.

cm

La lecture du code permet de déduire qu'il y a deux types de CGI, la distinction est faite parle nom de la cgi elle-même.

La figure XVI.5 résume les 2 situations possibles d'exécution d'une CGI.

\includegraphics{fig.web.05.ps} cm figure XVI.05

Si le nom de l'exécutable commence par nph-XVI16 le comportement du serveur http change. Dans le cas ordinaire (nom quelconque) les données transmises depuis le client vers la cgi et réciproquement, passent par le processus httpd et via un tube non nommé (``pipe'').

Dans le cas d'une cgi ``nph'', seules les données lues depuis le client (par exemple avec la méthode POST) transitent par le processus httpd, la réponse, elle, est émise directement, ce qui améliore les performances en évitant une séquence lecture/écriture inutile. Ce comportement est justifié dès que de gros volumes d'octets sont à transmettre au client (de grandes pages HTML, des images...).

Attention, dans ce dernier cas, c'est à la CGI de fabriquer l'intégralité du message HTTP, y compris l'en-tête MIME. A elle également de gérer la déconnexion prématurée du client (SIGPIPE).

Ces deux modes de fonctionnement ne sont pas clairement documentés, en fait il s'agit d'une caractéristique du serveur du CERN, maintenue pour assurer sans doute la compatibilité avec les applicatifs déjà écrits. Il n'est pas assuré que cette possibilité existera dans les futures versions du serveur Apache, notamment celles qui utiliseront la version 1.1 d'HTTP.

Examinons le code du fichier mod_cgi.c :

    207 int cgi_handler (request_rec *r)
    208 {
    209     int nph;
    ...     ...
    222     nph = !(strncmp(argv0,"nph-",4));
    ...     ...

La variable nph vaut 1 si la cgi est de ce type.

    ...     ...
    251     add_common_vars (r);
    ...     ...

Ici on commence à fabriquer l'environnement d'exécution de la cgi Cette fonction (fichier util_script.c, ligne 126) complète les variables qui ne dépendent pas du contenu de la requête, par exemple#35688#>SERVER_SOFTWARE, REMOTE_HOST,...

    ...     ...
    277      if (!spawn_child (r->connection->pool, cgi_child, (void *)&cld,
    278                        nph ? just_wait : kill_after_timeout,
    279                        &script_out, nph ? NULL : &script_in)) {
    280         log_reason ("couldn't spawn child process", r->filename, r);
    281         return SERVER_ERROR;
    282     }
    ...     ...

L'appel de cette fonction provoque la création d'un processus fils, celui qui finalement va exécuter la cgi. Il faut remarquer le deuxième argument qui est un pointeur de fonction (cgi_child), et le sixième qui est nul dans le cas d'une cgi du type ``nph''.

script_in et script_out sont respectivement les canaux d'entrée et sortie des données depuis et vers le processus qui exécute la cgi. Il parait donc logique que dans le cas d'une cgi de type ``nph'' script_in soit nul. Un mécanisme non encore analysé duplique la sortie de ce processus vers le client plutôt que vers le processus serveur.

Nous continuons la description du point de vue du processus père, donc httpd.

Ligne 295 et les suivantes, jusqu'à la ligne 332, le processus lit ce que le client lui envoie, si la méthode choisie est du type POST. Le contenu est renvoyé vers le processus fils, sans transformation :

    311             if (fwrite (argsbuffer, 1, len_read, script_out) == 0)
    312                 break;
    ...     ...
    335     pfclose (r->connection->pool, script_out);

Il est donc clair que c'est à celui-ci d'exploiter ce qu'envoie le client, par exemple le résultat d'une forme de saisie.

    337     /* Handle script return... */
    338     if (script_in && !nph) {
    ...         ...
    373         send_http_header(r);
    374         if(!r->header_only) send_fd (script_in, r);
    375         kill_timeout (r);
    376         pfclose (r->connection->pool, script_in);
    377     }

Ce test traite le cas d'une cgi normale, dont la sortie est lue par le serveur, puis renvoyée au client (ligne 374).

Examinons maintenant comment se prépare et s'exécute le processus fils :

    101 void cgi_child (void *child_stuff)
    102 {    
    ...         ...
    126     add_cgi_vars (r);
    127     env = create_environment (r->pool, r->subprocess_env);

Ces deux dernières lignes préparent l'environnement du futur processus fils. Il s'agit du tableau de variables accessibles depuis la variable externe environ, pour tout processus. La fonction add_cgi_vars (fichier util_script.c, ligne 192) ajoute, entres autres, les variables REQUEST_METHOD et QUERY_STRING à l'environnement.

Cette dernière variable joue un rôle majeur dans la transmission des arguments à la cgi quand la méthode choisie est GET. En effet, dans ce cas, le seul moyen pour le client d'envoyer des arguments à la cgi est d'utiliser l'URL, comme par exemple dans :

La syntaxe de l'URL prévoit le caractère ``?'' comme séparateur entre le nom et ses arguments. Chaque argument est ensuite écrit sous la forme :

nom = valeur

Les arguments sont séparés les uns des autres par le caractère ``SPMamp;''.

    135     error_log2stderr (r->server);
    136     
    ...         ...
    138     if (nph) client_to_stdout (r->connection);

Ligne 135, la sortie d'erreur est redirigée vers le fichier error_log, et ligne 138, la sortie standard du processus, c'est la socket, donc envoyée directement vers le client !

    184     if((!r->args) || (!r->args[0]) || (ind(r->args,'=') >= 0))
    185         execle(r->filename, argv0, NULL, env);
    186     else
    187         execve(r->filename, create_argv(r->pool, argv0, r->args), env);

Puis finalement on exécute la cgi ! Bien sûr, si le programme va au delà de la ligne 187, il s'agit d'une erreur...


4 Principe de fonctionnement des CGIs


4.1 CGI -- Méthode GET, sans argument

Dans ce paragraphe nous examinons ce qui se passe lors de l'exécution d'une CGI très simple (shell script, le source suit) que l'on suppose placée dans le répertoire idoine, pour être exécutée comme lors de la requête suivante :

$ telnet localhost 80
Trying ...
Connected to localhost.
Escape character is '^]'.
GET /cgi-bin/nph-test-cgi HTTP/1.0


La requête tapée par l'utilisateur.

HTTP/1.0 200 OK
Content-type: text/plain
Server: Apache/1.1.1


L'en-tête du message HTTP renvoyé par le serveur. La ligne de statut est générée par la cgi car il s'agit d'une cgi de type nph-

CGI/1.0 test script report:

argc is 0. argv is .

SERVER_SOFTWARE = Apache/1.1.1
SERVER_NAME = www.chezmoi.tld
GATEWAY_INTERFACE = CGI/1.1
SERVER_PROTOCOL = HTTP/1.0
SERVER_PORT = 80
REQUEST_METHOD = GET
SCRIPT_NAME = /cgi-bin/nph-test-cgi
QUERY_STRING = 
REMOTE_HOST = labas.tresloin.tld
REMOTE_ADDR = 192.168.0.1
REMOTE_USER =
CONTENT_TYPE =
CONTENT_LENGTH =
Connection closed by foreign host.


Le corps du message. Ces octets sont générés dynamiquement par le programme nph-test-cgi.

Et voici le source de cet exemple (une modification du script de test livré avec Apache) :

#!/bin/sh

echo HTTP/1.0 200 OK
echo Content-type: text/plain
echo Server: $SERVER_SOFTWARE
echo

echo CGI/1.0 test script report:
echo

echo argc is $#. argv is "$*".
echo


Remarquez la fabrication de l'en-tête MIME, réduite mais suffisante. Le echo seul génère une ligne vide, celle qui marque la limite avec le corps du message.

echo SERVER_SOFTWARE = $SERVER_SOFTWARE
echo SERVER_NAME = $SERVER_NAME
echo GATEWAY_INTERFACE = $GATEWAY_INTERFACE
echo SERVER_PROTOCOL = $SERVER_PROTOCOL
echo SERVER_PORT = $SERVER_PORT
echo REQUEST_METHOD = $REQUEST_METHOD
echo QUERY_STRING = $QUERY_STRING
echo REMOTE_HOST = $REMOTE_HOST
echo REMOTE_ADDR = $REMOTE_ADDR
echo REMOTE_USER = $REMOTE_USER
echo CONTENT_TYPE = $CONTENT_TYPE
echo CONTENT_LENGTH = $CONTENT_LENGTH


Toutes ces variables sont celles de l'environnement généré par le module cgi_handler avant d'effectuer le exec.

4.2 CGI -- Méthode GET, avec arguments

Examinons maintenant un cas plus compliqué, avec des arguments transmis, ou ``query string''. Celle-ci est transmise à la cgi via la variable d'environnement QUERY_STRING. C'est à la cgi de l'analyser puisque son contenu relève de l'applicatif et non du serveur lui-même. Essayons de coder la cgi de l'exemple :

Première version :

#!/bin/sh
echo Content-type: text/plain
echo
echo "QUERY_STRING=>"$QUERY_STRING "<="
exit 0

L'interrogation avec un telnet produit la sortie :

QUERY_STRING=>base=datas&mot=acacia&champ=.MC<=

Se trouve très facilement sur les sites ftp le source d'un outil nommé cgiparseXVI17, parfaitement adapté à l'analyse de la chaîne transmise. D'où la deuxième version :

#!/bin/sh

CGIBIN=~web/cgi-bin
BASE=`$CGIBIN/cgiparse -value base`  
MOT=`$CGIBIN/cgiparse -value mot`  
CHAMP=`$CGIBIN/cgiparse -value champ`


Cette partie du code montre l'analyse du contenu de la variable QUERY_STRING avec l'outil cgiparse.

echo Content-type: text/plain
echo

echo "BASE="$BASE
echo "MOT="$MOT
echo "CHAMP="$CHAMP

exit 0


Et là, la fabrication du message renvoyé. La cgi renvoie ses octets via un tube au serveur, c'est donc celui-ci qui se charge de fabriquer un en-tête MIME.

Puis le résultat de l'exécution :

BASE=datas
MOT=acacia
CHAMP=.MC

4.3 CGI -- Méthode POST

La méthode POST autorise un client à envoyer au serveur une liste de couples variable=valeur. Chaque couple est séparé des autres par une fin de ligne, c'est à dire (CR,LF).

Cette méthode est bien adaptée à la transmission d'informations collectées coté client dans une formeXVI18de saisie, et dont le volume est variable.

La liste des couples est écrite dans le corps du message, par le programme client, et ne fait pas partie de l'URL, comme c'est le cas pour la méthode GET. Il y a évidement une limite au nombre maximum d'octets envoyé par le client, c'est le serveur qui en fixe la valeurXVI19. Du coté du client, il faut prévenir le serveur, dans l'en-tête du message HTTP, de la taille du corps du message. Cela s'effectue avec le champ Content-length:.

L'exemple suivant ne fait que renvoyer la liste des couples lus, vers le client. Attention il ne fonctionne qu'avec telnet.

$ telnet www.chezmoi.tld 80
Trying 192.168.0.2...
Connected to www.chezmoi.tld.
Escape character is '^]'.
POST /cgi-bin/test-post HTTP/1.0
Content-length:14

areuh=tagada


Le corps du message fait bien 14 caractères si on compte la fin de ligne (CR+LF).

HTTP/1.0 200 OK
Date: Mon, 24 Mar 1997 14:41:26 GMT
Server: Apache/1.1.1
Content-type: text/plain

REQUEST_METHOD = POST
CONTENT_LENGTH = 14
Couple lu : areuh=tagada
Connection closed by foreign host.


La réponse du serveur liste les couples lus, ici un seul ! La variable globale REQUEST_METHOD pourrait être utilisée pour adapter le comportement de la cgi en fonction de la méthode demandée.

Et voici le source de la cgi :

#!/bin/sh
#
# Ce script teste la methode POST
#
echo Content-type: text/plain
echo
echo REQUEST_METHOD = $REQUEST_METHOD 
echo CONTENT_LENGTH = $CONTENT_LENGTH 
while read l
do
    echo "Couple lu :" $l   
done 
exit 0

4.4 Ecriture d'une CGI en Perl

Voir cours de Jean-Jacques Dhenin...


5 Conclusion - Bibliographie

Rien n'est constant, tout change...Et il faut être constamment à l'affut de la nouveauté dans ce domaine très réactif qu'est celui du ``World Wide Web''.

Les quelques références bibliographique qui suivent illustrent ce cours, mais il est évident qu'elles sont bien insuffisantes pour couvrir tous les aspects que le terme ``web'' sous-entend !

RFC 1521
N. Borenstein, N. Freed, ``MIME (Multipurpose Internet Mail Extensions) Part One: Mechanisms for Specifying and Describing the Format of Internet Message Bodies'', 09/23/1993. (Pages=81) (Format=.txt, .ps) (Obsoletes RFC1341) (Updated by RFC1590)

RFC 1590
J. Postel, ``Media Type Registration Procedure'', 03/02/1994. (Pages=7) (Format=.txt) (Updates RFC1521)

RFC 1630
T. Berners-Lee, ``Universal Resource Identifiers in WWW : A Unifying Syntax for the Expression of Names and Addresses of Objects on the Network as used in the World-Wide Web'', 06/09/1994. (Pages=28) (Format=.txt)

RFC 1738
T. Berners-Lee, L. Masinter, M. McCahill, ``Uniform Resource Locators (URL)'', 12/20/1994. (Pages=25) (Format=.txt)

RFC 1945
T. Berners-Lee, R. Fielding, H. Nielsen, ``Hypertext Transfer Protocol - HTTP/1.0'', 05/17/1996. (Pages=60) (Format=.txt)

RFC 2068
R. Fielding, J. Gettys, J. Mogul, H. Frystyk, T. Berners-Lee,``Hypertext Transfer Protocol - HTTP/1.1'', 01/03/1997. (Pages=162) (Format=.txt)

TCP/IP Illustrated Volume 3
W. Richard Stevens - Addison-Wesley - Janvier 1996.


next up previous contents index
Next: E Index général & Up: D Sockets BSD et Previous: XV Éléments de serveurs   Contents   Index
Fran├žois Laissus 2009-02-27