Précédent Index Suivant

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 :
# 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 :
  1. Les requêtes transitent entre un navigateur WEB et notre serveur d'application selon le format des requêtes HTTP.
  2. Les données constituant les requêtes obéissent au format de codage des applications CGI.
  3. Le contenu des réponses est donné selon le format des pages HTML.
  4. 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 &eacute;lectroniques
</A>
<LI> &Eacute;tat des cotisations<BR>
<FORM 
 method="GET" 
 action="http://freres-gras.ufr-info-p6.jussieu.fr:12345/etat_cotis">
Date de d&eacute;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 &eacute;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 "&Eacute;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é.






Précédent Index Suivant