Client Memcached pour Objective Caml


  • Introduction :

Nous allons voir comment développer un client pour le logiciel serveur Memcached avec le langage Objective Caml. Le logiciel Memcached permet en résumé de stocker des données en mémoire vive ce qui offre des accès très rapides. Une valeur est simplement indexée par une clef unique qui est utilisée par le client pour écrire, modifier ou lire cette donnée. Memcached est typiquement utilisé pour stocker des valeurs temporaires afin de gérer un cache d’où son nom. Son protocole est relativement simple ce qui en fait un bon candidat pour une première approche du développement en Objective Caml.

Le code source complet est disponible dans cette archive.

  • Protocole Memcached :

La documentation disponible sur le protocole donne un premier aperçu des fonctions qui seront nécessaires :

– Les commandes simples comme la demande de version du serveur ou le « flush_all » nécessitent simplement l’envoi d’une seule ligne de texte au serveur qui enverra en réponse une ligne de texte.

– Les commandes plus complexes comme les statistiques ou l’écriture d’une donnée nécessitent de gérer l’envoi ou la réception de plusieurs lignes.

– Il sera nécessaire de pouvoir vérifier si la réponse du serveur à une commande du client ne correspond pas à une erreur.

– Nous aurons évidemment besoin d’une fonction pour ouvrir et fermer la connexion au serveur Memcached.

– Chaque commande prévue par le protocole sera implémentée par une fonction dédiée.

  • Typage :

Le protocole nous indique également les types à gérer. Il est par exemple souvent fait référence à des entiers non signés en 16, 32 ou 64 bits. Comme la notion d’entier non signé n’est pas disponible nativement en Objective Caml, nous allons gérer ces différents types par l’intermédiaire de modules dédiés.

Prenons l’exemple le plus simple qui est celui des entiers non signés 16 bits. La signature de ce module sera définie dans un fichier appelé « memcache_int16u.mli » :

	type t
	exception Overflow of t
	val zero : t
	val min : t
	val max : t
	val to_string : t -> string
	val of_string : string -> t
	val of_int : int -> t

Le type « t » correspondra au type Objective Caml natif pour stocker l’entier. En l’occurrence, pour les entiers non signés 16 bits, il s’agira du type « int ». L’exception « Overflow » sera levée si l’entier dépasse les limites. Ici ce sera le cas si le nombre est négatif ou bien dépasse la valeur 65535 (2^16 – 1).

On définie également les valeurs zéro (0), minimale (0) et maximale (65535). On aura également à notre disposition trois fonctions de conversions pour par exemple transformer une chaîne de caractères représentant un nombre vers le type entier non signé 16 bits.

Les trois modules dédiés à la gestion des entiers non signés auront cette même signature. Seule l’implémentation sera différente. Comme la signature de ce module reste très simple, il a été facile de la rédiger directement. Pour les signatures plus complexes, il est possible de générer automatiquement une première version à partir du fichier .ml d’implémentation grâce à l’option « -i » du compilateur Objective Caml.

Un autre type important correspond à la structure qui nous servira à stocker les informations pour la connexion au serveur :

	type connection = {
	  host: string; (** adresse IP du serveur *)
	  port: int; (** numéro de port *)
	  in_channel: in_channel; (** canal de communication entrant *)
	  out_channel: out_channel; (** canal de communication sortant *)
	};;

Un enregistrement de ce type sera passé en paramètre à toutes les fonctions qui auront besoin d’échanger des données avec le serveur Memcached.

  • Implémentation :

Passons maintenant au développement des fonctions proprement dites. Nous en détaillerons uniquement certaines pour donner un aperçu du langage Objective Caml.

Prenons par exemple la fonction chargée de recevoir plusieurs lignes en provenance du serveur :

(**
  Reçoit plusieurs lignes du serveur.
  @param connection Informations pour la connexion (déjà ouverte).
  @return Une FIFO contenant les lignes dans l'ordre de réception + une chaîne de caractères concaténation des lignes reçues et séparées par \n.
*)
let receive_lines_from_server ~connection =
  (* c'est par le canal de communicatio entrant que notre client reçoit les données du serveur *)
  let in_channel = connection.in_channel in
  (* on créé une structure FIFO pour stocker les lignes reçues du serveur *)
  let queue_lines = Queue.create () in
  (* on créé un tampon qui stockera lui aussi les lignes reçues du serveur mais sous forme de chaîne de caractères *)
  let text_lines = Buffer.create 1024 in
  (* on créé une variable "l" (une référence sur une chaîne vide) qui servira à stocker une ligne reçue *)
  let l = ref "" in
  (* on définie une fonction récursive "one_line" qui servira à traiter une ligne *)
  let rec one_line () =
    (* on récupère la ligne courante qui provient du serveur *)
    l := input_line in_channel;
    (* on supprime les fins de lignes *)
    l := Str.global_replace regexp_trail_eol "" !l;
    (* si ce n'est pas la dernière ligne *)
    if (!l <> end_line) then (
      (* on stocke la ligne courante dans la FIFO et dans le tampon *)
      Queue.add !l queue_lines;
      Buffer.add_string text_lines (!l ^ "\n");
      (* on rappelle la fonction pour traiter la prochaine ligne *)
      one_line ();
    )
    (* plus de ligne à traiter, on peut renvoyer le résultat *)
    else (
      (queue_lines, Buffer.contents text_lines)
    )
  in
    (* il faut appeler une première fois la fonction "one_line" pour commencer le traitement *)
    one_line ()
;;

Notez que la variable « l » est une référence sur une chaîne vide dont la valeur est modifiée dans la fonction « one_line ». Pour assigner une nouvelle valeur à une référence, il est nécessaire d’utiliser le signe « := ». Et pour obtenir le contenu (la valeur) de la référence, on utilise le signe « ! » devant le nom de la variable.

La fonction renvoie un tuple (une liste de valeurs) dont le premier élément est la suite des lignes reçues du serveur. Il s’agit de la variable « queue_lines » qui est une file FIFO. Le deuxième élément renvoyé est une chaîne de caractères qui contient la concaténation des lignes reçues du serveur. Les deux éléments contiennent donc au final les mêmes données. L’intérêt de renvoyer ces données sous deux formes différentes est de pouvoir selon les besoins traiter la réponse du serveur soit comme une suite de lignes soit comme une chaîne de caractères.

Voyons ensuite la fonction qui envoie la commande pour stocker une valeur auprès du serveur :

(**
  Commandes de stockage d'une clef/valeur.
  @param cmd Commande (voir la section "Storage commands" du protocole).
  @param key Clef de la valeur.
  @param flag Valeur "flag" d'une commande de stockage (voir la section "Storage commands" du protocole).
  @param time Expiration.
  @param bytes Nb d'octets des données à stocker.
  @param data Les données à stocker.
  @param connection Informations pour la connexion (déjà ouverte).
  @raise Bad_answer_from_server Réponse inattendue du serveur.
  @return Indique si la valeur a été stockée ou pas.
*)
let storage ~cmd ~key ~flag ~time ~bytes ~data ~connection =
  (* on construit la commande de stockage à partir des paramètres *)
  let s = (cmd ^ " " ^ key ^ " " ^ (Int16u.to_string flag) ^ " " ^ (string_of_int time) ^ " " ^ (string_of_int bytes)) in
  (* on envoie la ligne au serveur *)
  let () = send_one_line_to_server ~connection ~s in
  (* et ensuite on envoie au serveur la donnée à stocker *)
  let s = Buffer.contents data in
  let () = send_one_line_to_server ~connection:connection ~s:s in
  (* le serveur doit nous retourner une réponse *)
  let answer = receive_one_line_from_server ~connection:connection in
  (* on vérifie que cette réponse ne correspond pas à une erreur *)
  let () = check_error_answer_from_server ~answer:answer in
  (* on examine ensuite cette réponse qui doit correspondre à une des réponses possibles pour la commande de stockage *)
  let new_value =
    match answer with
      | "STORED" -> Stored
      | "NOT_STORED" -> Not_stored
      | _ -> raise (Bad_answer_from_server answer)
  in
    (* la fonction retourne cette réponse sous forme d'un type "storage_answer" que nous avons défini *)
    new_value
;;

La fonction prend en paramètres toutes les données nécessaires au traitement qu’elle effectue. Notez que le caractère « ~ » au début de chaque paramètre indique que ce sont des paramètres nommés. C’est à dire que lorsque la fonction sera appelée, il sera possible d’indiquer ce nom pour chaque valeur de paramètre ce qui permet ainsi de s’affranchir du respect de l’ordre de définition des paramètres. La fonction peut ainsi être appelée de cette manière :

	storage ~connection:connexion ~flag:(Int16u.of_int 32) ~time:1219422874 ~bytes:1024 ~cmd:"commande" ~key:"clef" ~data:tampon

On peut voir que l’on rappelle pour chaque paramètre son nom défini lors de la déclaration de la fonction. Il n’est donc pas obligatoire de respecter le même ordre dans les paramètres. Si le nom des paramètres est suffisamment explicite, c’est également une manière de rendre plus clair le code.

Notez que le caractère « ^ » est utilisé pour la concaténation des chaînes de caractères. C’est l’équivalent du caractère « . » dans le langage PHP.

Pour finir, remarquez une construction très utilisée dans le langage Objective Caml qui est le « match ». Ici, cela permet de comparer la valeur de la variable « answer » avec les chaînes de caractères « STORED » et « NOT_STORED » et selon le cas de stocker dans « new_value » soit « Stored » soit « Not_stored ». Si la comparaison échoue, le défaut indiqué par le caractère « _ » est de lever l’exception « Bad_answer_from_server ». La construction « match » ne s’applique pas uniquement à des chaînes de caractères. Elle peut par exemple s’appliquer à des listes ou à un type défini dans le programme. Vous êtes invité à consulter la documentation du langage Objective Caml pour plus de détails.

Pour finir, voyons la fonction qui permet d’obtenir les statistiques globales du serveur :

(**
  Statistiques globales du serveur.
  @param connection Informations pour la connexion (déjà ouverte).
  @raise Bad_answer_from_server Si réponse inattendue ou si une des réponses n'est pas dans la liste "names_stats".
  @return Une liste de paires (nom, valeur) correspondant aux statistiques.
*)
let stats ~connection =
  (* on envoie la commande "stats" au serveur *)
  let s = "stats" in
  let () = send_one_line_to_server ~connection:connection ~s:s in
  (* on reçoit la réponse en plusieurs lignes de la part du serveur *)
  let (lines, answer) = receive_lines_from_server ~connection:connection in
  (* on doit au moins recevoir une ligne *)
  (* si ce n'est pas le cas, on lève une exception *)
  let () =
    if (Queue.length lines <= 0) then (raise (Bad_answer_from_server answer))
  in
  (* fonction qui traite une seule ligne des statistiques reçues *)
  let one_line acc l =
    (* les lignes sont de la forme "STAT <nom> <valeur>" *)
    let r = Str.regexp ("^STAT \\([^ ]+\\) \\(.+\\)") in
      (* si la ligne est au bon format *)
      if (Str.string_match r l 0) then (
        try
          (* on récupère la partie "nom" *)
          let name = Str.matched_group 1 l in
          (* on récupère la partie "valeur" *)
          let value = Str.matched_group 2 l in
          (* on vérifie que les clefs/valeurs reçues sont bien autorisées *)
          let () =
            if (not (List.mem_assoc name names_stats)) then (raise (Bad_answer_from_server answer))
          in
          (* conversion des valeurs en entiers non signés *)
          let new_value =
          let type_value = List.assoc name names_stats in
            match type_value with
              | Ts_32u _ -> Ts_32u (Int32u.of_string value)
              | Ts_64u _ -> Ts_64u (Int64u.of_string value)
              | Ts_string _ -> Ts_string value
              | Ts_2x_32u _ -> Ts_2x_32u value
            in
            (* on ajoute le tuple (nom, valeur) à la liste qui sera renvoyée par la fonction *)
              (name, new_value) :: acc
        with
          | Not_found -> raise (Bad_answer_from_server answer) (* une des lignes n'est pas au bon format *)
      )
      else (
        raise (Bad_answer_from_server answer) (* une des lignes n'est pas au bon format *)
      )
  in
    (* pour chaque ligne reçue du serveur, on appelle la fonction "one_line" *)
    Queue.fold (one_line) [] lines
;;

On peut voir que la fonction « one_line » a notamment comme paramètre une variable appelée « acc » (pour « accumulateur »). Ce type de paramètre est souvent présent en Objective Caml. Il permet en effet d’accumuler le résultat d’appels successifs à une fonction donnée. Ici, la fonction « one_line » est appelée autant de fois qu’il y a de lignes à traiter grâce à l’utilisation de la fonction « Queue.fold ». On peut également voir dans l’appel à cette fonction que l’accumulateur a comme valeur de départ une liste vide.

On retrouve aussi la construction « match…with » dont nous avons déjà parlé. Cette fois-ci, la comparaison porte sur les types d’entiers non signés que nous avions définis.

  • Compilation « bytecode » :

Objective Caml offre deux modes de compilation :

– Un mode qui permet d’obtenir un exécutable natif, de la même manière que l’on obtiendrait un exécutable depuis des sources en langage C. L’avantage est de produire un programme avec de très bonnes performances. En contre-parti, ce programme fonctionnera uniquement sur la plate-forme (par exemple un système FreeBSD sur PC) pour lequel il a été compilé.

– Un mode « bytecode » qui produira un programme destiné à être exécuté par la machine virtuelle Objective Caml. C’est un mécanisme similaire à celui utilisé par le langage Java. L’exécution sera plus lente qu’avec un programme natif mais il ne sera plus dépendant de la plate-forme.

Vous trouverez dans l’archive contenant les sources un script shell indiquant les commandes pour compiler le code en mode « bytecode » et ainsi obtenir une librairie et un exécutable de démonstration.

Par exemple, pour compiler le fichier memcache_int16u.ml qui contient l’implémentation des entiers 16 bits non signés, la commande est la suivante :

	ocamlc -c memcache_int16u.ml

Ce qui produira un fichier objet « memcache_int16u.cmo ».

  • Références :

Protocole Memcached : http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt

Site officiel Objective Caml : http://www.ocaml.org/

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s