Un serveur WEB en OpenScheme
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'entres systèmes Scheme.
Nous poursuivons la réalisation du serveur WEB en Open-Scheme. Le mois dernier, nous avions utilisé les ports d'entrée sortie standard pour réaliser lire les requêtes et écrire les résultats. Ce mois-ci, nous allons utiliser les ports Unix pour effectuer ces entrées/sorties.
Description
Le mois dernier, notre serveur WEB commençait à voir le jour. Il restait très simple et il utilisait les ports d'entrées/sorties standard pour effectuer les transferts de données. Cette fois-ci, nous allons utiliser de ``vrais'' ports Unix pour les transferts. Pour cela, nous utiliserons la bibliothèque NET d'OpenScheme qui propose quelques fonctions pour les manipuler. Attention, les versions antérieures à 1.3.6 contiennent des bugs qui rendent ce code inutilisable. Vous pourrez donc télécharger l'environnement depuis le WEB (www.open-scheme.com) ou le trouver sur le CDROM de Linux Magazine. L'environnement light suffit.
Le fichier source de ce serveur est disponible sur le site http://www.erian-concept.com/pub/ows.tgz.
Les sockets
Les sockets sont des dispositifs d'entrées / sorties de bas niveau proposés dans les systèmes Unix pour les communications entre machines. Quasiment tout l'Internet repose sur eux. D'un point de vue programmation en C, une fois qu'un socket est ouvert, on le manipule comme un descripteur de fichier pas niveau (un entier) avec les fonction classiques read() et write().
On distingue deux types de transferts par socket : le mode stream ou les données sont acheminées bit à bit et dans l'ordre d'émission et le mode datagram ou les informations sont transmises par blocs, potentiellement dans le désordre et avec possibilité de pertes de paquets. En général, le mode stream est le plus utilisé.
Il existe deux sortes de sockets : les clients et les serveurs. Les sockets clients se créent en spécifiant un nom de machine distante ou son adresse IP et un numéro de port. La création et la connexion du socket passe par plusieurs étapes (fonctions C socket() et connect()). Une fois le socket créé et connecté, il est utilisable pour effectuer des transferts bi-directionnels.
La création d'un serveur passe aussi par plusieurs étapes : création (socket()), assignation d'un port (bind()), écoute des demandes de connexion (listen()). Une fois le socket serveur créé, il n'est pas encore utilisable pour les transferts. La seule chose que l'on puisse faire avec est d'attendre qu'un client se connecte à son port (accept()). Lorsqu'un client se connecte, le socket serveur se dédouble. La première partie reste utilisable pour scruter d'autres demandes de connexions alors que la seconde partie est prête pour effectuer des transferts bi-directionnels avec le client qui s'est connecté.
Seul le super utilisateur a le droit de créer des sockets serveurs pour les ports de 0 à 1024. Les autres utilisateurs doivent se contenter des numéros au-dessus de 1024.
Toute cette mécanique est gérée par la bibliothèque NET d'OpenScheme qui gomme les difficultés pour offrir une interface simple et disponible quelque soit le système.
Voici les fonctions de cette bibliothèque relatives aux sockets :
Gestion des sockets
(net:socket? objet)
Cette fonction retourne vrai si l'objet passé en argument est un socket, et faux sinon.
(net:socket:make-client hôte port)
Cette fonction retourne un socket client en mode stream sur l'hôte et le port passés en arguments, ou faux en cas d'erreur. L'hôte peut être spécifié soit par son adresse IP soit par son nom Internet.
(net:socket:make-server port)
Cette fonction retourne un socket serveur en mode stream sur le port passé en argument. Le socket est prêt à accepter des connexions de clients.
(net:socket:accept socket)
Cette fonction retourne un socket serveur pour le transfert d'informations en mode stream à partir du socket serveur principal passé en argument. L'appel à cette fonction est bloquant tant qu'un client n'est pas connecté. On pourra utiliser les timers ou les threads pour effectuer des tâches en arrière plan.
(net:socket:shutdown socket)
Cette fonction termine tout les sockets.
Transfert d'informations
(net:socket:read socket tampon nombre)
Cette fonction lit un certain nombre d'octets à partir du socket et les place dans le tampon passé en argument. La fonction retourne le nombre d'octets effectivement lus.
(net:socket:ready? socket . délai)
Cette fonction permet de savoir si des octets sont disponibles en lecture sur le socket passé en argument. Un délai maximum en mili-secondes peut être spécifié. Elle retourne vrai si au moins un octet est prêt, et faux sinon.
(net:socket:write socket tampon nombre)
Cette fonction écrit un nombre d'octets à partir du tampon sur le socket passé en argument. Elle retourne le nombre d'octets effectivement écrits.
(net:socket:getc socket)
Cette fonction retourne un caractère dont la valeur est la valeur de l'octet lu à partir du socket passé en argument. En cas d'erreur, elle retourne faux.
(net:socket:putc socket caractère)
Cette fonction écrit un caractère dans le socket passé en argument. Elle retourne vrai si l'octet est écrit correctement, et faux en cas d'erreur.
Informations sur les socket
(net:socket:port socket)
Cette fonction retourne le numéro du port sur lequel est connecté le socket.
(net:socket:address socket)
Cette fonction retourne l'adresse distante sur laquelle est connecté le socket passé en argument.
(net:socket:local-adresse socket)
Enfin, cette fonction retourne l'adresse locale sur laquelle est connectée le socket.
Résolution nom-adresse
On pourra aussi utiliser des fonctions complémentaires de conversion entre noms Internet et adresses IP :
(net:host:name . adresse)
Cette fonction retourne le nom du serveur dont l'adresse est passée en argument dans une chaîne de caractères en notation classique avec points. Si l'adresse n'est pas spécifiée, le nom usuel du serveur local est retourné.
(net:host:address . nom)
Cette fonction retourne l'adresse IP ou une liste d'adresses IPs du serveur dont le nom est passé en argument. Si le nom n'est pas spécifié, le nom du serveur local est utilisé.
Ports virtuels
OpenScheme dispose d'un système évolué pour gérer des ports virtuels. Un port virtuel est un objet que rien ne distingue des ports d'entrées/sorties traditionnels généralement associés aux fichiers ou à la console. Un port virtuel est construit en invoquant la fonction (open-custom-port ...) avec en argument quatorze fonctions permettant de réaliser les entrées / sorties. Ce système est très pratique pour utiliser sur n'importe quoi les fonctions standards d'entrées/sortie. Dans notre cas, le projet à initialement été conçu pour fonctionner avec les entrées/sorties standard. Qu'à cela ne tienne ! Nous allons transformer les sockets en ports et continuer à utiliser le code existant !
Nous définissons la fonction socket->port dont l'argument est le socket que l'on souhaite convertir :
(define (socket->port socket)
 (let ([dummy (lambda args 'ok)])
   (open-custom-port 
     ; custom-stat
     dummy
     ; custom-close
     (lambda args
      (net:socket:shutdown socket))
     ; custom-seek
     dummy
     ; custom-tell
     dummy
     ; custom-length
     dummy
     ; custom-ready
     (lambda (n)
      (net:socket:ready? socket n))
     ; custom-read
     (lambda (n)
      (let ([string (make-string n)])
        (net:socket:read socket string n)
        string))
     ; custom-write
     (lambda (string n)
      (let ([n (net:socket:write socket string n)])
        (if (= n -1) 0 n)))
     ; custom-getc
     (lambda ()
      (let ([c (net:socket:getc socket)])
        (if c c *eof*)))
     ; custom-ungetc
     dummy
     ; custom-putc
     (lambda (char)
      (net:socket:putc socket char))
     ; custom-flush
     dummy
     ; custom-get
     dummy
     ; custom-set
     dummy)))
Comme on peut le voir, un certain nombre de fonctionnalités ne peuvent pas être réalisées sur les sockets, aussi les fonctions sont remplacées par dummy, une fonction ``qui ne fait rien''. Les fonctions réalisables appellent les fonctions sur les sockets équivalentes vues ci-dessus.
Maintenant, nous sommes prêts à modifier légèrement le code de la dernière fois !
Le serveur
La boucle principale de traitement doit être légèrement modifiée pour prendre en compte les sockets :
; Programme principal
(let ([server
       ; Construction de l'objet serveur
       (build 
        <server>
       :name            "http://www.dummy.com"
       :address         "1.1.1.1"
       :port            8080
       :root-directory  "/home/gdw/erian/doc/html"
       :script-alias    '("/cgi-bin")
       :script-suffix   '(".cgi"
                           ".bat"
                           ".sh"
                           ".osm"
                           ".cgi")
       :default-indexes '("index.html"
                           "index.htm"
                           "index.cgi")
       :browse          #t
       :access-log      "ows.log"
       :error-log       "ows.err")])
     ; Fabrication du socket serveur
     (let ([socket (net:socket:make-server
                    (<server>:port server))]
          [accept #f])
      (if (not socket)
          (error "unable to create a server socket"))
       ; Continuation sur erreur
       (if (call/cc 
           (lambda (return)
             (<server>:error! server return)
             (return #f)))
          (close-input-output-port accept))
       ; Boucle de traitement
       (let loop ([n 1])
         ; Conversion socket vers port
         (set! accept
               (socket->port 
                 (net:socket:accept socket)))
         ; Modification des ports standards
         (set! *current-input-port*  accept)
        (set! *current-output-port* accept)
         ; Lecture et traitement de la requête
         (let ([query (read-request server)])
          (process-query server query))
         ; Fermeture du port = fermeture du socket
         (close-input-output-port accept)
         ; Connexion suivante
         (loop (+ n 1)))))
Le socket serveur est créé ; dès lors, on fabrique la continuation sur erreur et on entre dans la boucle de traitement. On accepte la connexion d'un client, et on place le socket résultant, après l'avoir transformé en port, dans la variable accept. On modifie alors la valeur des ports d'entrées / sortie standards pour qu'ils valent le port virtuel lié au socket. Dès lors, les lectures et les écritures sur les ports standards se feront avec le socket. Comme le mois dernier, on lit et on traite la requête, puis on ferme le port et on continue l'itération.
Lecture des requêtes
La lecture des requêtes est effectuée par la fonction read-query. Cette fonction a été définie la fois précédente. Elle faisait appel à la fonction get-line qui était simplement définie comme équivalente à read-line. Avec les sockets, il est nécessaire d'être plus prudent et tenir compte d'éventuels délais pendant les transmissions. Nous redéfinissons donc get-line par :
(define (get-line)
 (let* (; caractère retour-chariot
         [cr   (string-ref "\r" 0)]
         ; List des caractère lus
         [list (let loop ()
               (let ([c (read-char)])
                 (cond [(or (eq? c cr)
                            (eq? c #\newline))
                        '()]
                        ; Si fin de fichier, essayer encore
                        [(eof-object? c)
                        (if (char-ready? (current-input-port))
                            (loop)
                            c)]
                       [else
                        (let ([tail (loop)])
                          (if (eof-object? tail)
                              tail
                              (cons c tail)))])))])
   (if (eof-object? list)
       list
       (list->string list))))
Voilà maintenant la prise en compte des sockets pour la maquette de serveur WEB terminée. La prochaine fois, nous ajouterons le traitement des scripts CGIs.
L'auteur

Guilhem de Wailly, directeur de la société Erian Concept : support, formations, configurations, administration, développements Linux. Environnement Open-Scheme.
http://www.erian-concept.com
Références
WEB
¨	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.