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
=
2
5
5
5
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
4
0
0
3
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
1
2
3
4
5
else
try
int_of_string
Sys.argv.
(1
)
with
_
->
1
2
3
4
5
;;
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é.