Servlets HTTP
Un servlet est un << module >> à intégrer dans une application
serveur pour répondre aux requêtes des clients. Bien qu'un servlet ne
soit pas spécifique à un protocole, on utilisera le protocole HTTP
pour la communication (voir la figure 21.1).
Dans la pratique le terme servlet correspond à
un servlet HTTP.
Le moyen classique de construire des pages HTML dynamiques sur un
serveur HTTP est d'utiliser des commandes CGI
(Common Gateway Interface). Celles-ci prennent en
argument une URL pouvant contenir des
données provenant d'un formulaire. L'exécution
produit alors une page HTML qui est envoyée au client. On trouvera
aux liens suivants la description du protocole HTTP et des CGI.
Lien
http://www.eisti.fr/eistiweb/docs/normes/rfc1945/1945tm.htm
Lien
http://hoohoo.ncsa.uiuc.edu/docs/cgi/overview.html
C'est un mécanisme un peu lourd car il lance à chaque requête un
nouveau programme.
Les servlets HTTP sont lancés une fois pour toutes, et peuvent
décoder les arguments du format CGI pour exécuter une
requête. Les servlets permettent de profiter des possibilités des
navigateurs WEB pour construire l'interface graphique d'une
application.
Figure 21.1 : communication entre un navigateur et un serveur Objective CAML
Nous définissons dans la suite un serveur pour le protocole
HTTP. Nous ne traiterons pas l'ensemble des spécifications de ce
protocole, mais nous nous limiterons aux quelques fonctions nécessaires
à l'implantation d'un serveur mimant le comportement d'une
application CGI.
Dans un premier temps, nous définissons un module générique de
serveur Gsd. Puis nous donnons les fonctions utiles à la
définition d'une application de ce serveur générique pour
traiter une partie du protocole HTTP.
Formats HTTP et CGI
Nous voulons obtenir un serveur qui imite le comportement d'une
application CGI. Une des premières tâches à réaliser est de décoder
le format des requêtes HTTP avec les extensions CGI pour le passage
des arguments.
Les clients de ce serveur pourront donc être des navigateurs tels
Netscape ou Windows Explorer.
Acquisition des requêtes
Les requêtes qui transitent selon le protocole HTTP ont
essentiellement trois composantes : une méthode, une URL et des
données. Les données répondent à un format particulier.
Nous réalisons dans ce paragraphe un ensemble de fonctions permettant la
lecture, le découpage et le décodage des composantes d'une requête.
Ces fonctions pourront déclencher l'exception :
# exception Http_error of string ;;
exception Http_error of string
Décodage
La fonction decode, qui utilise l'auxiliaire
rep_xcode, a pour but de rétablir les caractères qui ont
été encodés par le client HTTP : les espaces (qui ont été
remplacés par +) et certains caractères réservés qui ont
été remplacés par leur code hexadécimal.
# let rec rep_xcode s i =
let xs = "0x00" in
String.blit s (i+1) xs 2 2;
String.set s i (char_of_int (int_of_string xs));
String.blit s (i+3) s (i+1) ((String.length s)-(i+3));
String.set s ((String.length s)-2) '\000';
Printf.printf"rep_xcode1(%s)\n" s ;;
val rep_xcode : string -> int -> unit = <fun>
# exception End_of_decode of string ;;
exception End_of_decode of string
# let decode s =
try
for i=0 to pred(String.length s) do
match s.[i] with
'+' -> s.[i] <- ' '
| '%' -> rep_xcode s i
| '\000' -> raise (End_of_decode (String.sub s 0 i))
| _ -> ()
done;
s
with
End_of_decode s -> s ;;
val decode : string -> string = <fun>
Fonctions de manipulation de chaînes
On définit dans le module String_plus les
fonctions de découpage de chaînes de caractères :
-
prefix et suffix qui extraient une sous-chaîne à partir d'un index;
- split qui retourne la sous-chaîne jusqu'au caractère séparateur;
- unsplit qui concatène deux chaînes en ajoutant un caractère séparateur entre elles.
# module String_plus =
struct
let prefix s n =
try String.sub s 0 n
with Invalid_argument("String.sub") -> s
let suffix s i =
try String.sub s i ((String.length s)-i)
with Invalid_argument("String.sub") -> ""
let rec split c s =
try
let i = String.index s c in
let s1, s2 = prefix s i, suffix s (i+1) in
s1::(split c s2)
with
Not_found -> [s]
let unsplit c ss =
let f s1 s2 = match s2 with "" -> s1 | _ -> s1^(Char.escaped c)^s2 in
List.fold_right f ss ""
end ;;
Découpage des données d'un formulaire
Les requêtes sont souvent émises depuis une page HTML contenant
un formulaire. Le contenu d'un formulaire est transmis comme une
chaîne de caractères contenant les noms et les valeurs associées
aux champs du formulaire. La fonction get_field_pair
transforme cette chaîne en une liste
d'association.
# let get_field_pair s =
match String_plus.split '=' s with
[n;v] -> n,v
| _ -> raise (Http_error ("Bad field format : "^s)) ;;
val get_field_pair : string -> string * string = <fun>
# let get_form_content s =
let ss = String_plus.split '&' s in
List.map get_field_pair ss ;;
val get_form_content : string -> (string * string) list = <fun>
Lecture et découpage
La fonction get_query extrait de la requête la
méthode désignée ainsi que l'URL associée qu'elle stocke dans
un tableau de chaînes de caractères. On peut ainsi utiliser une
application CGI qui récupère ces arguments dans le tableau des
arguments de la ligne de commande. La fonction
get_query utilise l'auxiliaire get. La
taille maximale d'une requête est arbitrairement limitée, par
nous, à 2555 caractères.
# let get =
let buff_size = 2555 in
let buff = String.create buff_size in
(fun ic -> String.sub buff 0 (input ic buff 0 buff_size)) ;;
val get : in_channel -> string = <fun>
# let query_string http_frame =
try
let i0 = String.index http_frame ' ' in
let q0 = String_plus.prefix http_frame i0 in
match q0 with
"GET"
-> begin
let i1 = succ i0 in
let i2 = String.index_from http_frame i1 ' ' in
let q = String.sub http_frame i1 (i2-i1) in
try
let i = String.index q '?' in
let q1 = String_plus.prefix q i in
let q = String_plus.suffix q (succ i) in
Array.of_list (q0::q1::(String_plus.split ' ' (decode q)))
with
Not_found -> [|q0;q|]
end
| _ -> raise (Http_error ("Non supported method: "^q0))
with e -> raise (Http_error ("Unknown request: "^http_frame)) ;;
val query_string : string -> string array = <fun>
# let get_query_string ic =
let http_frame = get ic in
query_string http_frame;;
val get_query_string : in_channel -> string array = <fun>
Le serveur
Pour obtenir un pseudo-serveur CGI, qui ne sait en l'état traiter
que la méthode GET, on écrit la fonction
http_service dont l'argument fun_serv est une
fonction de traitement des requêtes HTTP telle qu'elle aurait pu
être écrite pour une application CGI.
# module Text_Server = Server (struct type t = string
let to_string x = x
let of_string x = x
end);;
# module P_Text_Server (P : PROTOCOL) =
struct
module Internal_Server = Server (P)
class http_servlet n np fun_serv =
object(self)
inherit [P.t] Internal_Server.server n np
method receive_h fd =
let ic = Unix.in_channel_of_descr fd in
input_line ic
method treat fd =
let oc = Unix.out_channel_of_descr fd in (
try
let request = self#receive_h fd in
let args = query_string request in
fun_serv oc args;
with
Http_error s -> Printf.fprintf oc "HTTP error : %s <BR>" s
| _ -> Printf.fprintf oc "Unknown error <BR>" );
flush oc;
Unix.shutdown fd Unix.SHUTDOWN_ALL
end
end;;
Comme il n'est pas prévu de faire communiquer, via le servlet, de valeurs internes
Objective CAML spéciales, on choisit le type string comme type
du protocole. Les fonctions of_string et to_string
ne font rien.
# module Simple_http_server =
P_Text_Server (struct type t = string
let of_string x = x
let to_string x = x
end);;
On construit alors la fonction principale de lancement du service
en construisant une instance de la classe http_servlet.
# let cgi_like_server port_num fun_serv =
let sv = new Simple_http_server.http_servlet port_num 3 fun_serv
in sv#start;;
val cgi_like_server : int -> (out_channel -> string array -> unit) -> unit =
<fun>
Test du servlet
Il est toujours utile en cours de développement de pouvoir tester les parties déjà réalisées.
Pour cela on réalise un petit serveur HTTP qui envoie tel quel le fichier
inscrit dans la requête HTTP qui lui a été adressée. La fonction
simple_serv envoie le fichier dont le nom suit la requête GET (deuxième
élément du tableau des arguments). La fonction simple_serv trace
les différents arguments passés dans la requête.
# let send_file oc f =
let ic = open_in_bin f in
try
while true do
output_byte oc (input_byte ic)
done
with End_of_file -> close_in ic;;
val send_file : out_channel -> string -> unit = <fun>
# let simple_serv oc args =
try
Array.iter (fun x -> print_string (x^" ")) args;
print_newline();
send_file oc args.(1)
with _ -> Printf.printf "erreur\n";;
val simple_serv : out_channel -> string array -> unit = <fun>
# let run n = cgi_like_server n simple_serv;;
val run : int -> unit = <fun>
L'appel run 4003 lance ce servlet sur le port 4003.
Par ailleurs, on lance un navigateur effectuant
la requête de chargement de la page baro.html
sur le port 4003. La figure 21.2 montre l'affichage
du contenu de cette page dans le navigateur.
Figure 21.2 : requête HTTP sur un servlet Objective CAML
Le navigateur a envoyé la requête
GET /baro.html
pour le chargement de la page, et ensuite la requête de chargement de
l'image GET /canard.gif.
Interface HTML pour un servlet
Nous utilisons le serveur à la CGI
pour construire une interface HTML pour la base de données du
chapitre 6 (voir page
??).
Le menu de la fonction main est ici affiché sous forme
d'une page HTML proposant les mêmes choix. Les réponses aux
requêtes sont aussi des pages HTML dynamiquement
construites par le servlet.
La construction dynamique de pages fait appel à l'utilitaire que nous
définissons ci-dessous.
Protocole de l'application
Nous utilisons dans notre application plusieurs éléments de
plusieurs protocoles :
-
Les requêtes transitent entre un navigateur WEB et notre
serveur d'application selon le format des requêtes HTTP.
- Les données constituant les requêtes obéissent au format
de codage des applications CGI.
- Le contenu des réponses est donné selon le format des pages
HTML.
- Enfin, la nature des requêtes est donnée selon un format
spécifique à cette application.
Nous voulons répondre à trois requêtes : demande de la liste des adresses
postales, demande de la liste des adresses électroniques et demande de
l'état des cotisations entre deux dates données. À chacune d'elles,
nous associons respectivement les noms :
postal_addr,
email_addr et etat_cotis. Dans ce dernier cas, nous
transmettrons également deux chaînes de caractères contenant les
dates souhaitées. Ces deux dates correspondent aux valeurs des champs
debut et fin d'un formulaire HTML.
À la première connexion d'un client la page de garde suivante est
envoyée. Les noms des requêtes y sont codés sous forme d'ancres HTML.
<HTML>
<TITLE> association </TITLE>
<BODY>
<HR>
<H1 ALIGN=CENTER> L'association</H1>
<P>
<HR>
<UL>
<LI> Liste des
<A HREF="http://freres-gras.ufr-info-p6.jussieu.fr:12345/postal_addr">
adresses postales
</A>
<LI> Liste des
<A HREF="http://freres-gras.ufr-info-p6.jussieu.fr:12345/email_addr">
adresses électroniques
</A>
<LI> État des cotisations<BR>
<FORM
method="GET"
action="http://freres-gras.ufr-info-p6.jussieu.fr:12345/etat_cotis">
Date de début : <INPUT type="text" name="debut" value="">
Date de fin : <INPUT type="text" name="fin" value="">
<INPUT name="action" type="submit" value="Envoyer">
</FORM>
</UL>
<HR>
</BODY>
</HTML>
Nous supposerons que cette page est contenue dans le fichier
assoc.html.
Primitives pour HTML
Les fonctionnalités de l'utilitaire HTML sont réunies au sein
de la seule classe print. Elle possède un
champ indiquant le canal de sortie. Elle peut donc aussi bien être
utilisée dans le cadre d'une application CGI (où le canal de
sortie est la sortie standard) que d'une application utilisant le
serveur HTTP défini au paragraphe précédent (où le canal de
sortie est une socket de service).
Les méthodes proposées permettent essentiellement d'encapsuler du
texte dans des balises HTML. Ce texte est, soit passé directement en
argument aux méthodes sous forme de chaîne de caractères, soit
produit par une fonction. Par exemple, la méthode principale
page prend en premier argument une chaîne correspondant à
l'en-tête de la page1, et en second argument une fonction affichant le contenu
de la page. La méthode page produit les balises
correspondantes du protocole HTML.
Le nom des méthodes reprend le nom des balises HTML
correspondantes en y ajoutant parfois quelques options.
# class print (oc0:out_channel) =
object(self)
val oc = oc0
method flush () = flush oc
method str =
Printf.fprintf oc "%s"
method page header (body:unit -> unit) =
Printf.fprintf oc "<HTML><HEAD><TITLE>%s</TITLE></HEAD>\n<BODY>" header;
body();
Printf.fprintf oc "</BODY>\n</HTML>\n"
method p () =
Printf.fprintf oc "\n<P>\n"
method br () =
Printf.fprintf oc "<BR>\n"
method hr () =
Printf.fprintf oc "<HR>\n"
method hr () =
Printf.fprintf oc "\n<HR>\n"
method h i s =
Printf.fprintf oc "<H%d>%s</H%d>" i s i
method h_center i s =
Printf.fprintf oc "<H%d ALIGN=\"CENTER\">%s</H%d>" i s i
method form url (form_content:unit -> unit) =
Printf.fprintf oc "<FORM method=\"post\" action=\"%s\">\n" url;
form_content ();
Printf.fprintf oc "</FORM>"
method input_text =
Printf.fprintf oc
"<INPUT type=\"text\" name=\"%s\" size=\"%d\" value=\"%s\">\n"
method input_hidden_text =
Printf.fprintf oc "<INPUT type=\"hidden\" name=\"%s\" value=\"%s\">\n"
method input_submit =
Printf.fprintf oc "<INPUT name=\"%s\" type=\"submit\" value=\"%s\">"
method input_radio =
Printf.fprintf oc "<INPUT type=\"radio\" name=\"%s\" value=\"%s\">\n"
method input_radio_checked =
Printf.fprintf oc
"<INPUT type=\"radio\" name=\"%s\" value=\"%s\" CHECKED>\n"
method option =
Printf.fprintf oc "<OPTION> %s\n"
method option_selected opt =
Printf.fprintf oc "<OPTION SELECTED> %s" opt
method select name options selected =
Printf.fprintf oc "<SELECT name=\"%s\">\n" name;
List.iter
(fun s -> if s=selected then self#option_selected s else self#option s)
options;
Printf.fprintf oc "</SELECT>\n"
method options selected =
List.iter
(fun s -> if s=selected then self#option_selected s else self#option s)
end ;;
Nous supposerons que cet utilitaire est fourni par le module
Html_frame.
Pages dynamiques pour la gestion d'associations
Pour chacune des trois requêtes de l'application, il faut
construire une page en réponse. Nous utilisons pour cela
l'utilitaire Html_frame donné ci-dessus. Ce qui signifie
que les pages ne sont pas réellement construites, mais que leurs
différents composants sont émis séquentiellement sur le canal de
sortie.
Nous ajoutons une page (virtuelle) supplémentaire qui est
retournée en réponse à une requête erronée ou incomprise.
Page d'erreur
La fonction print_error prend en argument une fonction
d'émission de page HTML (i.e. : une instance de la classe
print) et une chaîne de caractères contenant le message
d'erreur.
# let print_error (print:Html_frame.print) s =
let print_body() =
print#str s; print#br()
in
print#page "Erreur" print_body ;;
val print_error : Html_frame.print -> string -> unit = <fun>
Toutes nos fonctions d'émission d'une réponse à une requête
auront en paramètre un premier argument contenant la fonction
d'émission d'une page HTML.
Liste des adresses postales
La page composant la réponse à la demande de la liste des adresses
postales est obtenue en formatant la liste des chaînes de
caractères obtenue par la fonction adresses_postales
définie pour la base d'adhérents (voir page
??). Nous supposons que cette fonction, et
toute autre concernant directement les requêtes sur la base de
données, ont été définies dans un module nommé
Assoc.
Pour émettre cette liste, nous utilisons une fonction
de sortie de lignes simples :
# let print_lines (print:Html_frame.print) ls =
let print_line l = print#str l; print#br() in
List.iter print_line ls ;;
val print_lines : Html_frame.print -> string list -> unit = <fun>
La fonction de réponse à la demande des adresses postales est :
# let print_adresses_postales print db =
print#page "Adresses postales"
(fun () -> print_lines print (Assoc.adresses_postales db))
;;
val print_adresses_postales : Html_frame.print -> Assoc.data_base -> unit =
<fun>
Outre la fonction d'émission de page, la fonction
print_adresses_postales prend en second paramètre la base
de données.
Liste des adresses électroniques
Cette fonction est construite sur le même principe que celle donnant
la liste des adresses postales sauf qu'elle fait appel à la
fonction adresses_electroniques du module Assoc :
# let print_adresses_electroniques print db =
print#page "Adresses électroniques"
(fun () -> print_lines print (Assoc.adresses_electroniques db)) ;;
val print_adresses_electroniques :
Html_frame.print -> Assoc.data_base -> unit = <fun>
État des cotisations
C'est encore le même principe qui gouverne la
définition de cette fonction : récupérer les données
correspondant à la requête (qui ici est un couple), puis
émettre les chaînes de caractères correspondantes.
# let print_etat_cotisations print db d1 d2 =
let ls, t = Assoc.etat_cotisations db d1 d2 in
let page_body() =
print_lines print ls;
print#str ("Total : "^(string_of_float t));
print#br()
in
print#page "État des cotisations" page_body ;;
val print_etat_cotisations :
Html_frame.print -> Assoc.data_base -> string -> string -> unit = <fun>
Analyse des requêtes et réponse
Nous définissons deux fonctions de production de réponse en
fonction d'une requête HTTP. La première
(print_get_answer) répond à une requête supposée
formulée par une méthode GET du protocole HTTP. La seconde
aiguille la production de la réponse selon la méthode de requête
utilisée.
Ces deux fonctions prennent en second argument un tableau de chaînes
de caractères contenant les éléments de la requête HTTP tels
qu'ils ont été analysés par la fonction
get_query_string (voir page
??). Le premier élément du tableau
contient la méthode et le second le nom de la requête sur la
base.
Dans le cas d'une demande d'état des cotisations, les dates de
début et de fin composant la requête sont contenues dans les deux
champs du formulaire associé à cette demande. Les données du
formulaire sont contenues dans le troisième champ du tableau qui
doit être décomposé par la fonction get_form_content
(voir page ??).
# let print_get_answer print q db =
match q.(1) with
| "/postal_addr" -> print_adresses_postales print db
| "/email_addr" -> print_adresses_electroniques print db
| "/etat_cotis"
-> let nvs = get_form_content q.(2) in
let d1 = List.assoc "debut" nvs
and d2 = List.assoc "fin" nvs in
print_etat_cotisations print db d1 d2
| _ -> print_error print ("Unknown request: "^q.(1)) ;;
val print_get_answer :
Html_frame.print -> string array -> Assoc.data_base -> unit = <fun>
# let print_answer print q db =
try
match q.(0) with
"GET" -> print_get_answer print q db
| _ -> print_error print ("Unsupported method : "^q.(0))
with
e
-> let s = Array.fold_right (^) q "" in
print_error print ("Some thing wrong with request: "^s) ;;
val print_answer :
Html_frame.print -> string array -> Assoc.data_base -> unit = <fun>
Programme principal et application
Le programme principal de l'application est un exécutable autonome
paramétré par le
numéro de port du service.
La base de données est lue avant le lancement du serveur. La
fonction de service est obtenue à partir de la fonction
print_answer définie ci-dessus et de la fonction
générique de serveur HTTP cgi_like_server définie au
paragraphe précédent (voir page ??). Cette
dernière est donnée par le module Servlet.
# let get_port_num() =
if (Array.length Sys.argv) < 2 then 12345
else
try int_of_string Sys.argv.(1)
with _ -> 12345 ;;
val get_port_num : unit -> int = <fun>
# let main() =
let db = Assoc.read_base "assoc.dat" in
let assoc_answer oc q = print_answer (new Html_frame.print oc) q db in
Servlet.cgi_like_server (get_port_num()) assoc_answer ;;
val main : unit -> unit = <fun>
Pour obtenir l'application complète, nous rassemblons dans un
fichier httpassoc.ml les définitions des fonctions d'affichage.
Ce fichier se termine par un appel à la fonction main :
main() ;;
Nous pouvons alors produire un exécutable nommé assocd par
la commande de compilation :
ocamlc -thread -custom -o assocd unix.cma threads.cma \
gsd.cmo servlet.cmo html_frame.cmo string_plus.cmo assoc.cmo \
httpassoc.ml -cclib -lunix -cclib -lthreads
Ne reste plus alors qu'à lancer le serveur, charger la page
HTML2 contenue dans le fichier assoc.html
donné au début de ce paragraphe (page ??) et
cliquer.
La figure 21.3 montre un exemple d'utilisation.
Figure 21.3 : requête HTTP sur un servlet Objective CAML
Le navigateur effectue une première connexion sur le servlet qui lui
envoie la page de menu. Une fois les champs de saisie remplis,
l'utilisateur envoie une
nouvelle requête qui contient les champs saisis. Ceux-ci sont décodés, et
le serveur fait un accès à la base de données de l'association pour récupérer l'information
demandée qui est traduite en HTML, envoyée au client qui affiche cette nouvelle page.
Pour en faire plus
Cette application a de nombreux prolongements.
Tout d'abord le protocole HTTP utilisé est trop simple par rapport
aux nouvelles versions qui ajoutent un en-tête informatif sur le
type, le contenu et la longueur de la page envoyée. De même
la méthode POST, qui autorise une modification sur le serveur,
n'est pas intégrée3
Pour pouvoir décrire le type et le contenu des pages renvoyées, il est
nécessaire d'intégrer dans les servlets la convention MIME utilisée
pour la description des documents comme pour les documents attachés dans les courriers
électroniques.
La transmission d'images, comme à la figure 21.2, permet
aussi de construire des interfaces pour les jeux à 2 joueurs (voir
chapitre 17), où l'on associe des liens aux
dessins des cases à jouer. Comme l'arbitre du jeu connaît les coups
légaux, seules les cases valides sont associées à un lien.
L'extension MIME autorise aussi à définir de nouveaux types de
données. On intègre alors le protocole interne des valeurs Objective CAML
comme un nouveau type MIME. Ces valeurs ne sont réellement
compréhensibles que par un programme Objective CAML utilisant le même
protocole interne. Ainsi une requête d'un client sur une valeur
Objective CAML distante est effectuée via une requête HTTP. On peut ainsi
passer dans la page HTML une fermeture sérialisée qui sera passée en
tant qu'argument de la requête. Celle-ci, une fois reconstruite du
côté du serveur s'exécute pour fournir le résultat escompté.