Author: Christian.Queinnec@lip6.fr

## 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.