Un serveur WEB en OpenScheme
Multi-threading
Par :
Guilhem de Wailly (gdw@erian-concept.com)

Résumé
Dans cet article, nous continuons la programmation d'un serveur WEB élémentaire en Scheme. Nous utilisons l'environnement Open-Scheme disponible librement sur le site www.open-scheme.com. Certaines fonctionnalités utilisées sont des extensions à la norme du langage qui peuvent être assez facilement portées vers d'autres systèmes Scheme.
Cette étude sera le prétexte pour mieux comprendre par la pratique comment fonctionnent les serveurs WEB. 
Le mois dernier, nous avons ajouté au serveur la possibilité de gérer des serveurs virtuels ce qui permet d'héberger plusieurs sites WEB sur le même serveur.
Ce mois-ci, nous allons modifier le serveur pour qu'il puisse traiter plusieurs requêtes simultanément. Pour cela, nous allons utiliser les threads d'OpenScheme.
L'environnement Open-Scheme est amélioré en fonction de ce projet. Aussi, il est nécessaire de se procurer la version la plus récente. Nous faisons notre maximum pour fournir l'environnement en temps et en heure, et librement, sur le serveur WEB d'Open-Scheme (www.open-scheme.com), sur le CDROM de Linux Magazine ou dans l'archive http://www.erian-concept.com/pub/ows.tgz.
Correction
L'écriture de données sur les ports d'entrées / sorties présentait une erreur dans la version précédente d'OpenScheme. Cela se manifestait lorsque de grosses données étaient écrites.
De plus, les sorties du serveur se font maintenant préalablement en mémoire de manière à pouvoir en calculer la taille. Cette taille sera placée dans l'entête Content-lenght: avant l'écriture du résultat.
Réalisation
La possibilité de créer des serveurs WEB virtuels introduite le mois dernier a significativement étendu les possibilités de notre petit serveur WEB. Cependant, le fait qu'il ne traite les requêtes qu'une à une limite sérieusement ses possibilités.
Ce mois-ci, nous allons donc ajouter la possibilité à notre mini-serveur de traiter plusieurs requêtes de manière entrelacée ou concurrente, en utilisant la bibliothèque des Threads.
Bibliothèque de Thread
Un thread est un objet Scheme qui représente une portion d'exécution. Le système exécute le programme principal et tous les threads créés pendant des laps de temps assez courts (100 mili-secondes), en passant de l'un à l'autre. Le système restaure le contexte d'exécution du thread (pile, valeur des registres). Les threads partagent l'espace des variables globales.
Dans OpenScheme, les threads sont préemptifs, c'est à dire que le système interrompt autoritairement le thread en court pour passer au suivant. D'autres système possèdent des threads coopératifs ; dans ce cas, chaque threads doit rendre la main au système. Les threads préemptifs amènent plus de fluidité dans l'exécution, mais ils sont beaucoup plus difficiles à réaliser.
La particularité d'OpenScheme est de permettre un thread d'invoquer toute continuation à tout moment.
(thread:new proc)
Cette fonction invoque la fonction proc en lui passant comme argument le nouveau thread créé. L'exécution de cette fonction sera morcelée de manière complètement transparente par le système.
Le thread nouvellement créé est démarré immédiatement.
(thread? object)
Cette fonction retourne vrai si l'objet object est un thread, et faux sinon.
(thread:start! thread)
Cette fonction permet de démarrer un thread qui est arrêté.
(thread:stop! thread)
Cette fonction stoppe le thread passé en argument.
(thread:kill thread)
Cette fonction termine le thread passé en argument.
(thread:running? thread)
Cette fonction retourne vrai si thread est en cours d'exécution et faux s'il est arrêté.
(thread:alive? thread)
Cette fonction retourne vrai si thread est toujours valide. Un thread est invalidé lorsque la fonction qui a servi à le démarré se termine ou lorsque thread:kill est invoquée.
(thread:switch thread)
Cette fonction écourte le temps imparti à thread en autorisant le système à passer au thread suivant, en avance.
(thread:lock!) / (thread:unlock!)
Les expressions situées entre ces deux appels sont exécutées sans interruption. Attention, leur usage peut être dangereux car il bloque les autres threads. On veillera à ne pas invoquer de continuation qui empêcheraient l'exécution de thread:unlock!, directe ou indirecte, comme lors d'une erreur (on utilisera alors la fonction trap).
*thread:first*
Cette variable globale contient la liste de tous les threads du système. Cette liste peut être vide.
Ajout du multi-threading
Le support du multi-threading va être ajouté en apportant de simples modifications au serveur. La principale modification va toucher la boucle de traitement des requêtes qui sera confiée à un thread au lieu d'être traitée par le programme principal. Ce traitement interviendra dès qu'un client aura effectué une demande de connexion.
Modifions tout d'abord le fichier d'initialisation du serveur :
Initialisation ows.ini
Nous ajoutons simplement à chaque serveur le nombre maximum de connexions qu'il peut recevoir simultanément. Cette option permettra de limiter la bande passante globalement ou individuellement, pour chaque serveur :
[ows]
address         = 127.0.0.1
...
max connexions  = 1
...
error logs      = ows.err                        

[www.erian-concept.local]
address         = 127.0.0.1
...
max connexions  = 10
...
error logs      = /tmp/ows.err
Par convention, lorsque le serveur principal ows possède son option max connexions à un, le multi-threading n'est pas supporté. Sinon, ce nombre indique le nombre maximum de requêtes pouvant être traitées par l'ensemble des serveurs.
Le nombre max connexions de chaque serveur virtuel donne le nombre de connexions maximum, serveur par serveur.
Classe <server>
La classe des serveurs doit être aussi modifiée :
(define-class <server> #f
 [name            :initform ""]
 ...
 [error           :initform #f]
  [max-connexions  :initform 1]
 [connexions      :initform 0])
L'attribut max-connexions contient le nombre maximum de connexions pour un serveur, et l'attribut connexions indique le nombre de connexions en cours pour le serveur.
Boucle principale
La boucle principale doit être modifiée pour confier le traitement des requêtes à une fonction spécialisée :
(let ([servers (hash:new)])
  ...
 (hash:put!
  servers
  (string->symbol server)
  (build
   <server>
   :name            server
    ...
   :error-log       (get-str "error logs"      "ows.err")
    :max-connexions  (get-nb  "max connexions"  1)))))
   (profile:get ows.ini #f #f #f))
 (let ([ows (hash:get servers 'ows)])
    ...
   (let loop ([n 1])
     (format *current-error-port* 
             "~a: accept connection (~a)\n"
             (uptime) n)
     (let* ([child (net:socket:accept socket)]
            [port  (net:socket->port  child)])
        (schedule-query ows servers port)
        (loop (+ n 1)))))))
La fonction schedule-query sera chargée de traiter les requêtes ; elle pourra le faire dans des threads ou dans le programme principal.
Exécution des requêtes
L'exécution des requêtes est maintenant confiée à une fonction dont le rôle est de créer un thread lorsque le serveur est en mode multi-threads:
; Exécution des requêtes
(define (schedule-query ows servers io-port)
 ; procédure de traitement de la requête
 (let ([proc (lambda (thread)
              ; information de mise au point
              (format *current-error-port* 
                     "~a: read the request\n"
                     (uptime))
              ; lecture de la requête
              (let ([query (read-request ows io-port)])
              (if query 
                ; identification du serveur virtuel
                 (let ([server (witch-server ows servers query)])
                  (if server
                    (begin
                     ; Attendre que le serveur
                     ; virtuel puisse accepter une
                     ; nouvelle requête.
                     (wait-for-connexion 
                     server
                     (lambda ()
                       (<server>:connexions server)))
                     ; incrément du nombre de connexions
                     (server-connexions! server +1)
                    (server-connexions! ows    +1)
                     ; production du résultat
                     (process-query server query io-port)
                     ; décrément du nombre de connexions
                     (server-connexions! server -1)
                    (server-connexions! ows    -1))))))
              ; Information de mise au point
              (format *current-error-port* 
                     "~a: shutdown the socket\n\n"
                     (uptime))
              ; Fermeture du port
              (close-input-output-port io-port))])
   ; Si le serveur principal est en mode multi-thread...
  (if (> (<server>:max-connexions ows) 1)
    (begin
      ; Attendre que le serveur puisse accepter 
      ; un nouveau thread...
      (wait-for-connexion ows
                         (lambda ()
                           (length *thread:first*)))
      ; Information de mise au point
      (format *current-error-port*
             "~a: create a new thread\n"
             (uptime))
      ; créer un thread pour le traitement de la requête
      (thread:new proc))
    ; sinon, traiter la requête dans le 
    ; programme principal.
    (proc #f))))
Cette fonction ne présente pas de difficulté et repose complètement sur l'ancienne version.
Acceptation d'une connexion
Cette fonction est chargée de vérifier qu'un serveur peut accepter une nouvelle connexion :
(define (wait-for-connexion server how-many)
  ; nombre maximum de connexion pour le serveur
 (let ([max-connexions (<server>:max-connexions server)])
    ; boucle d'attente. Le nombre de connexions en
    ; cours est obtenu par la fonction how-many
    (let loop ([connexions (how-many)])
      ; si le nombre de connexion est supérieur
      ; au nombre maximum...
      (if (> connexions max-connexions)
         (begin
            ; Information de mise au point
           (format *current-error-port*
                   "~a: too many connexions ~a ; max=~a\n"
                   (uptime)
                   connexions
                   max-connexions)
            ; passer au thread suivant
            (thread:switch)
            ; de retour, essayer à nouveau...
           (loop (how-many)))))))
Cette fonction compare le nombre actuel de connexions au nombre maximum de connexions acceptable par le serveur. S'il est supérieur, la fonction laisse le système passer au thread suivant, puis essaye à nouveau la comparaison.
Modification du nombre de connexions
La modification du nombre de connexions du serveur doit être protégée des autres threads pour éviter des erreurs de mise à jour. Pour cela, on bloque le thread courant durant toute la modification :
(define (server-connexions! server offset)
 (thread:lock!)
 (<server>:connexions! server
                       (+ (<server>:connexions server)
                          offset))
 (thread:unlock!))
Autres modifications
L'ensemble des autres fonctions a été modifié de manière à ce que les entrées / sorties ne soient plus effectuées dans les ports standards, mais dans le port passé en argument de ces fonctions. En effet, l'utilisation des threads interdit la personnalisation thread par thread des variables globales et l'utilisation d'arguments devient obligatoire.
Conclusion
L'utilisation de la bibliothèque des threads d'OpenScheme a permit de modifier le serveur pour lui donner des possibilités de traitements concurrents des requêtes. On constate que le langage Scheme permet, encore une fois, d'exprimer les choses complexes très simplement et efficacement.
L'auteur

Guilhem de Wailly, gérant de la société Erian Concept (www.erian-concept.com): support, formations, configurations, administration, développements Linux. Environnement Open-Scheme et vice-président de la R&D de Tornado Technology (www.aecviz.com).
Références
WEB
¨	PHP
http://www.php.net
¨	TCP/IP, Architecture, protocoles, applications
Douglas Comer
InterEdition
·	W3C
http://www.w3c.org
·	CGI
http://www.jmarshall.com/easy/cgi/
·	CGI
http://hoohoo.ncsa.uiuc.edu/cgi/
·	CGI
http://www.tsden.org/ryutaroh/fileupload-e.shtml
·	CGI en Français
http://www.scripts-fr.com/
·	CGI+Scheme
http://www.lh.com/~oleg/ftp/Scheme/web.html
·	FastCGI
http://www.fastcgi.com/
·	HTTP en Français
http://webbo.enst-bretagne.fr/ActiveWebFr/eg-uk-tut.book_29.fr.html
·	HTTP 1.0
http://www.ietf.org/rfc/rfc1945.txt
·	HTTP 1.1
http://www.ietf.org/rfc/rfc2616.txt
Scheme
¨	Structure et Interprétation des programmes informatiques
H. Abelson, GJ. Sussman
InterEdition
¨	The Scheme Programming Languages - Ansi Scheme 
R. Kent Dybvig
Prentice Hall
¨	Les langages Lisps - Christian Queinnec
InterEdition
¨	Programmer avec Scheme - J. Chazarin
Thomson Publishing
¨	Revised4 Report on the Algorithmic Language Scheme
W. Clinger, J. Rees
ftp://ftp.nj.nec.com/pub/kelsey
Environnements Scheme Free
¨	Bigloo - Manuel Serrano
http://kaolin.unice.fr
Environnement de programmation Scheme.
¨	DrScheme - Rice University
http://www.cs.rice.edu/CS/PLT/
Environnement Scheme libre très avancé.
¨	PCScheme - Texas Instrument
ftp://cui.unige.ch/public/pcs/pcscheme.exe
Un très bon environnement de programmation Scheme pour DOS, avec éditeur intégré.
¨	Scm - A. Jaffer
http://www-swiss.ai.mit.edu/~jaffer
La référence des interprètes Scheme. Très petit, rapide, pour beaucoup de plates-formes, extensible.
¨	Stk - Erik Gallesio
http://kaolin.unice.fr
Interprète Scheme avec la bibliothèque TK.
D'autres liens sont visibles sur le site www.schemers.org.
Environnements Scheme commerciaux
¨	ChezScheme - Cadence, Inc
http://www.scheme.com/
Environnement Scheme très performant.
¨	EdScheme, 3Dscheme - Scheme, Inc
http://www.schemers.com/
Environnement de programmation Scheme pour Windows.
¨	Inlab Scheme - Inlab Software GmbH
http://www.munich.net/inlab/scheme/
Environnement commercial
¨	Open-Scheme - Erian Concept
http://www.open-scheme.com
Environnement professionnel de programmation Scheme comprenant un interprète, un compilateur et un débogueur symbolique. 
Existe en version libre et commerciale, pour Linux, FreeBSD, Solaris, Windows et BeOS, sur systèmes Intel et Sun.