 
 
 
 Boîte à outils client-serveur
Nous présentons un ensemble de modules pour la construction 
de client-serveur entre programmes Objective CAML. Cette boîte à outils
est ensuite utilisée dans les deux applications suivantes.
Une application se distingue d'une autre par le protocole qu'elle
utilise et par les traitements qu'elle y associe. Pour le reste, à
savoir les mécanismes d'attente de connexion, de détachement du
traitement de la connexion sur un autre processus, les lectures et
écritures sur une socket, les applications sont très
semblables les unes aux autres.
Profitant de la possibilité de mélanger la généricité modulaire et
l'extension des objets que nous offre Objective CAML, nous
allons réaliser un ensemble de foncteurs prenant comme argument un
protocole de communication et engendrant des classes génériques
implantant les mécanismes des clients et des serveurs. Il ne nous restera
ensuite qu'à les sous-classer pour obtenir des traitements
particuliers.
 Protocoles
Un protocole de communication est un type de données qu'il est
possible de traduire sous forme de chaînes de caractères afin de 
faire transiter d'une machine à une autre des données par une socket. 
Ceci peut se traduire sous la forme d'une signature.
# module type PROTOCOL = 
   sig 
     type t
     val to_string : t -> string 
     val of_string : string -> t
   end ;;
La signature impose que le type de données soit monomorphe, mais hormis
cette restriction, du moment qu'il est possible de le traduire en
chaîne de caractères et inversement, nous pouvons choisir comme structure
de données des valeurs aussi complexes que l'on souhaite. En
particulier, rien ne nous interdit d'avoir comme donnée un objet.
# module Integer = 
   struct 
     class integer x = 
       object 
         val v = x 
         method x = v
         method str = string_of_int v
       end
     type t = integer
     let to_string o = o#str
     let of_string s = new integer (int_of_string s)
   end ;;
En faisant quelques restrictions sur les types de données
manipulables, nous pouvons utiliser le module Marshal, décrit page 
??, pour
définir les fonctions de traduction.
# module Make_Protocole = functor ( T : sig type t end ) -> 
   struct 
     type t = T.t
     let to_string (x:t) = Marshal.to_string x [Marshal.Closures]
     let of_string s = (Marshal.from_string s 0 : t)
   end ;;
 Communication
Puisqu'un protocole est une donnée qu'il est possible de traduire sous
la forme d'une chaîne de caractères, nous pouvons en faire un
persistant et le stocker dans un fichier.
La seule difficulté pour lire une valeur depuis un fichier quand on ne
connaît pas son type est qu'a priori nous ne connaissons pas la taille
de la donnée en question. Et puisque le fichier en question sera en
fait une socket, nous ne pouvons pas nous fier au marqueur de fin de
fichier. Pour résoudre ce problème, nous faisons précéder la donnée
par la taille en nombre de caractères à lire. Les douze premiers
caractères contiennent sa taille et des espaces.
Le foncteur Com prend en paramètre un module de signature PROTOCOL
et définit les fonctions d'émission et de réception des valeurs codées dans le protocole.
# module Com = functor (P : PROTOCOL) ->
   struct
     let send fd m = 
       let mes = P.to_string m in
       let l = (string_of_int (String.length mes)) in
       let buffer = String.make 12 ' ' in 
         for i=0 to (String.length l)-1 do buffer.[i] <- l.[i] done ;
         ignore (ThreadUnix.write fd buffer 0 12) ;
         ignore (ThreadUnix.write fd mes 0 (String.length mes))
 
     let receive fd = 
       let buffer = String.make 12 ' ' 
       in
         ignore (ThreadUnix.read fd buffer 0 12) ;
         let l = let i = ref 0 
         in while (buffer.[!i]<>' ') do incr i done ;
            int_of_string (String.sub buffer 0 !i)
       in 
         let buffer = String.create l 
         in ignore (ThreadUnix.read fd buffer 0 l) ;
            P.of_string buffer 
   end ;;
module Com :
  functor(P : PROTOCOL) ->
    sig
      val send : Unix.file_descr -> P.t -> unit
      val receive : Unix.file_descr -> P.t
    end
Notons que nous utilisons les fonctions read et
write du module ThreadUnix et non celles du module
Unix; cela nous permettra d'utiliser les fonctions du module
dans un thread sans bloquer l'exécution des autres processus.
 Serveur
Un serveur est réalisé comme une classe abstraite paramétrée par le 
type de données du protocole. Son constructeur prend comme argument
le numéro du port et le nombre de connexions simultanées
acceptables. La méthode de traitement d'une requête est abstraite;
elle doit être implantée dans une sous-classe de server pour
obtenir une classe concrète.
# module Server = functor (P : PROTOCOL) ->
   struct
     module Com = Com (P)
 
     class virtual ['a] server p np =
       object (s)
         constraint 'a = P.t
         val port_num = p
         val nb_pending = np
         val sock = ThreadUnix.socket Unix.PF_INET Unix.SOCK_STREAM 0
 
         method start =
           let host = Unix.gethostbyname (Unix.gethostname()) in 
           let h_addr = host.Unix.h_addr_list.(0) in
           let sock_addr = Unix.ADDR_INET(h_addr, port_num) in
             Unix.bind sock sock_addr ;
             Unix.listen sock nb_pending ;
             while true do 
               let (service_sock, client_sock_addr) = ThreadUnix.accept sock 
               in ignore (Thread.create s#treat service_sock) 
             done
         method send = Com.send 
         method receive = Com.receive 
         method virtual treat : Unix.file_descr -> unit
       end 
   end ;;
Afin de fixer les idées, nous reprenons le service majuscule comme
illustration mais en donnant la possibilité d'envoyer des listes de
mots. 
# type message = Str of string | LStr of string list ;;
# module Maj_Protocol = Make_Protocole (struct type t=message end) ;;
# module Maj_Server = Server (Maj_Protocol) ;;
# class maj_server p np = 
   object (self)
     inherit [message] Maj_Server.server p np 
     method treat fd = 
       match self#receive fd with 
           Str s  -> self#send fd (Str (String.uppercase s)) ; 
                     Unix.close fd  
         | LStr l -> self#send fd (LStr (List.map String.uppercase l)) ;
                     Unix.close fd  
   end ;;
class maj_server :
  int ->
  int ->
  object
    val nb_pending : int
    val port_num : int
    val sock : Unix.file_descr
    method receive : Unix.file_descr -> Maj_Protocol.t
    method send : Unix.file_descr -> Maj_Protocol.t -> unit
    method start : unit
    method treat : Unix.file_descr -> unit
  end
Le traitement se décompose en la réception de la requête, son
filtrage, son traitement et l'émission du résultat. Le foncteur permet
de se concentrer sur le service pour réaliser le serveur, le reste est
générique. Cependant, si on souhaite avoir un mécanisme différent,
comme par exemple gérer des acquittements, rien n'interdit de
redéfinir les méthodes de communication héritées.
 Client
Pour réaliser des clients utilisant un protocole donné, nous
définissons trois fonctions généralistes :
- 
 connect : établit une connexion avec un serveur, elle
prend son adresse (adresse IP et numéro de port) et rend un
descripteur de fichier correspondant à une socket connectée au
serveur. 
-  emit_simple : ouvre une connexion, envoie un message
et referme la connexion.
-  emit_answer : idem que la précédente, mais attend la
réponse du serveur avant de refermer la connexion.
# module Client = functor (P : PROTOCOL) ->
   struct 
     module Com = Com (P)
                   
     let connect addr port = 
       let sock = ThreadUnix.socket Unix.PF_INET Unix.SOCK_STREAM 0 
       and in_addr = (Unix.gethostbyname addr).Unix.h_addr_list.(0)
       in ThreadUnix.connect sock (Unix.ADDR_INET(in_addr, port)) ;
          sock 
 
     let emit_simple addr port mes =
       let sock = connect addr port 
       in Com.send sock mes ;  Unix.close sock 
 
     let emit_answer addr port mes = 
       let sock = connect addr port 
       in Com.send sock mes ;  
          let res = Com.receive sock 
          in Unix.close sock ; res
   end ;;
module Client :
  functor(P : PROTOCOL) ->
    sig
      module Com :
        sig
          val send : Unix.file_descr -> P.t -> unit
          val receive : Unix.file_descr -> P.t
        end
      val connect : string -> int -> Unix.file_descr
      val emit_simple : string -> int -> P.t -> unit
      val emit_answer : string -> int -> P.t -> P.t
    end
Les deux dernières fonctions sont de plus haut niveau que la première.
Le vecteur de la liaison entre le client et le serveur n'apparaît pas.
L'utilisateur de emit_answer n'a même pas besoin de savoir
que le calcul qu'il demande est effectué sur une machine distante. Pour
lui, il invoque une fonction qui est représentée par une adresse et un
port avec un argument qui est le message envoyé, et une valeur lui est
retournée. Le côté distribué peut lui paraître anecdotique.
Un client du service majuscule est excessivement aisé à réaliser.
En supposant que la machine boulmich 
héberge ce service sur le port 12345; la fonction list_uppercase peut
se définir par un appel au service.
# let list_uppercase l = 
   let module Maj_client = Client (Maj_Protocol) 
   in match Maj_client.emit_answer "boulmich" 12345 (LStr l) 
      with Str x -> [x]
        | LStr x -> x ;;
val list_uppercase : string list -> string list = <fun>
 Pour en faire plus
La première amélioration à apporter à notre boîte à outils est une
gestion des erreurs qui ici est totalement absente. Une récupération
des exceptions qui surviennent lors de la rupture d'une connexion et
un mécanisme de <<nouvel essai>> seraient les bienvenus. 
Dans la même veine, le client et le serveur gagneraient à être munis
d'un mécanisme de timeout permettant de borner le temps
d'attente d'une réponse. 
Le fait d'avoir réalisé le serveur générique comme une classe, qui de
surcroît est paramétrée par le type de données qui transitent sur le
réseau, permet de l'étendre facilement pour augmenter ou modifier
son comportement afin de réaliser les améliorations souhaitées. 
Une autre approche est d'enrichir les protocoles de communication.
On peut par exemple ajouter des requêtes d'acquittement au protocole,
ou encore accompagner chaque requête d'un checksum permettant
de vérifier que le réseau n'a pas corrompu les données.
 
 
