Logo
  Erlang-fr.org  

Présentation du site
Cours Erlang
Articles
Projets
Autour d'Erlang-fr
Liens
 
English Area
Miroir: Documentation Erlang
Miroir: Archives Erlang/OTP
 
Recherche

Logo Process-one
Erlang

ErlyStage: Erlang est-il adapté pour le développement d'un jeu de rôle massivement multi-joueurs ?

(Troisième partie)

par Thierry « Shaman » Mallard

Traduction de l'article en Anglais

Troisième partie : Implémentation du codec 'Packed'

Les deux articles précédents présentaient la structure générale d'ErlyStage, et comment clients et serveurs allaient communiquer entre eux. Cette tâche spécifique est faite en utilisant un codec (codeur/décodeur). Deux codecs sont spécifiés dans le protocole Atlas. Ici, nous allons voir et implémenter le codec « Packed ».

Decodage

Les spécifications d'Atlas montrent que les éléments sont encodés comme suit :

[type][nom=][donnees][|fintype]

Les listes peuvent être les suivants :

  • ( ) pour une liste
  • [ ] pour un hashage
  • $ pour une chaîne de caractères
  • @ pour un entier
  • # pour un réel (float)

Par exemple, les données suivantes :

 [@id=17$nom=Fred le grand#poids=1.5(args=@1@2@3)] 

signifient, en Erlang : [ { "id", 17 }, { "nom", "Fred le grand" }, { "poids", 1.5 }, { "args", [1,2,3] } ]

Notre but ici est donc de traduire les données encodées en une variable Erlang. La première fonction utilisée est codec_packed:decode(Data), qui renvoie le tuple { atlas, Resultat } :

decode( Data ) ->
    % Nous devons avoir le format {atlas, [...]} 
    { atlas, [Result] } = ?MODULE:packed_parse( Data, []) ,
    % Ok, on renvoie le tuple prevu
    { atlas, Result }.

La principale fonction de décodage est codec_packed:packed_parse( Data, []). Le premier argument représente les données codées, tandis que la seconde variable accumule les données qui ont sont décodées (donc [] au début).

La fonction packed_codec:packed_parse(...)

Nous allons utiliser le pattern matching pour deviner le type du premier élément. Le match se fera sur le premier caractère des données (Data) ; par exemple, pour tester s'il s'agit d'un entier :

%% Un entier
packed_parse( "@" ++ Rest, Data ) ->
    { Rest2, Name } = get_name( Rest ),
    { Rest3, Data2 } = packed_parse( integer, Rest2, []),

    %% Nous devrions tester ici si Name existe
    packed_parse( Rest3, Data ++ [ make_hash( Name, Data2 ) ] );

S'il y a bien correspondance, nous appelons la fonction spécifique packed_parse ( integer, ... ), après avoir récupéré le nom de la variable.

packed_parse( integer, Data, List ) ->
     get_integer( Data ) ;

%% args : 1er les donnees a traiter
%%        2nd les donnees qui ont ete traitees
%%        3rd valeur actuelle de l'entier
%% l'algorithme est simple : on prend le premier caractere,
%% si c'est un entier : Current*10+Char
%% sinon on renvoie la valeur Current (le parse est fini)

get_integer( [], Done, CurrentI ) ->
    { [], CurrentI } ;

get_integer( [ H | Tail ], Done, CurrentI ) ->
    case lists:member(H, ?INTEGER_TOKENS) of
        true ->  get_integer( Tail, Done ++ [H], CurrentI * 10 + (H - $0)) ;
        false -> { [H] ++ Tail, CurrentI }
    end.

Les commentaires devraient suffire à comprendre la fonction. Le module codec_packed implémente la même fonction pour tous les types nécessaires. Les seuls cas particuliers sont les listes et les hashages. Nous utilisons la récursion pour traiter cela, la balise de fin (par exemple : ")" pour finir une liste), terminant le processus.

Voyons comment ces fonctions agissent sur ce simple exemple :

[@age=20$nom=Shaman]

La première correspondance se fait sur le premier "[" :

%% Cherche un hashage
packed_parse( "[" ++ Rest, Data ) ->
    { Rest2, Name } = get_name( Rest ),
    { Rest3, Data2 } = packed_parse( map, Rest2, []),
    packed_parse( Rest3, Data ++ [ make_hash( Name, Data2) ] );

On isole un nom (Name), qu'ici il n'y a pas pour la liste, et on appelle alors

%% MAPS -----
packed_parse( map, "]" ++ Data, List ) ->
    { Data, List } ;
packed_parse( map, Data, List) ->
    { Rest, Data2 } = packed_parse( Data, []),
    packed_parse( map, Rest, List ++ Data2);

La première fois que cette fonction est appelée, le premier argument est @age=20$nom=Shaman]. Ce n'est pas la fin du hashage, nous continuons donc avec l'appel récursif packed_parse sur ces données (Data).

La fonction correspondante est celle traitant des entiers, que nous avons vu plus tôt. Elle renverra { "age", 20 } and $nom=Shaman] comme Rest (données restant à traiter). La boucle suivante détectera une chaîne :

packed_parse( "$" ++ Rest, Data ) ->
    %% Nous cherchons d'abord un nom pour cette valeur
    { Rest2, Name } = get_name( Rest ),
    { Rest3, Data2 } = packed_parse( string, Rest2, [] ),
    packed_parse( Rest3, Data ++ [ make_hash( Name, Data2 ) ] );

Le packed_parse(string, ...) correspondant renverra {"nom", <<"Shaman">> }, et ] comme «données à traiter». Ceci correspondra alors à packed_parse( map, "]" ... ) qui retournera à l'appelant, ayant détecté la fin du hashage. Nous avons donc bien ici la liste [ { "age", 20 }, {"nom", <<"Shaman">> } ]. Ok, j'ai triché, mon age n'est pas de 20 ans.... ;-)

Une précision ici : il n'y a pas de type «chaine» en Erlang, nous avons donc décider d'utiliser le type binaire à la place. Cela ne présente aucun problème pour afficher la chaine, si nécessaire : il suffit d'utiliser le BIF «binary_to_list».

Voilà. Nous avons le partie décodage. Occupons-nous maintenant de l'encodage...

Encodage

Pour coder les données, nous allons utiliser l'une des caractéristiques intéressantes du langage Erlang : les gardiens. Ceci permet d'utiliser la même fonction, mais avec différentes implémentation en fonction du type de données qui lui est passé en argument. Vous devinez comment cela va nous aider...

La fonction d'encodage est codec_packed:encode(Data, Acc, Status), où Data représente les données à encoder, et Acc les données l'ayant été.

Voici comment les gardiens nous aident :

encode(Data, Acc, Status) when list(Data) ->
    lists:map(fun(X)-> encode(X, Acc, Status) end, Data);

Ceci n'arrive que si Data est une liste. Dans ce cas, nous appliquons l'encodage sur chaque élément de la liste en utilisant la fonction lists:map. Si les données avaient été un tuple, l'implémentation suivante serait passée :

encode(Data, Acc, Status) when tuple(Data) ->
    {atlas, Object_desc} = Data,
    case is_map(Object_desc) of
        true ->
            {ok, "[" ++ compose_map(Object_desc) ++ "]"};
        false ->
            {error, 'Le premier niveau devrait etre un hashage'}
    end.

Cette fonction passe le relais à compose_map, qui s'occupe itérativement de l'Object_desc et analyse les éléments de données :

compose_map([H|T], Acc) ->
    {Name, Data} = H,
    {Type, Data_as_string} = analyze(Data),
    compose_map(T, Acc ++ pack(Type, Name, Data_as_string)).

Cette analyse est faite en utilisant là encore les gardiens, comme nous l'avons vu plus tot. Ceci permet de traduire les données en un type { Type, Data_as_string } qui sera utilisée pour encoder les données, en fonction des spécifications du codec.

Voici deux exemples d'analyses :

analyze(Data) when integer(Data) ->
    {integer, integer_to_list(Data)};

analyze(Data) when float(Data) ->
    [Number] = io_lib:format("~f", [Data]),
    {float, Number};

C'est assez simple. La fonction pack(Type, Name, Data_as_string) pour les mêmes types de données sont listées ici :

pack(integer, Name, Data_as_string) ->
    "@" ++ naming(Name) ++ Data_as_string;

pack(float, Name, Data_as_string) ->
    "#" ++ naming(Name) ++ Data_as_string;

Par exemple, pour encoder le tuple { atlas, [ { "age", 20 }, { "nom", <<"Shaman">>} ] } :

  1. la fonction encode appelle compose_map( [ { "age", .... ])
  2. compose_map enregistre les noms et appelle les fonctions
    • analyze( 20 ), qui renverra { integer, 20 }
    • analyze( <<"Shaman">> ), qui renverra elle { string, "Shaman" }
  3. maintenant, compose_map peut empaqueter les données :
    • pack( integer, "age", "20" ) renvoie @age=20
    • pack( string, "nom", "Shaman") renvoie $nom=Shaman

La fonction encode peut alors renvoyer la valeur [@age=20$name=Shaman].

Voici donc comment le codec Packed a été implémenté. En fait, il ne devrait pas être très difficile de réaliser le codec XML en utilisant la même logique. Ceci pourrait faire un bon exercice ;-)

Fin de la première trilogie

J'espère que ces trois articles vous ont donné une bonne idée de ce qui se passe dans Worldforge/STAGE, et comment Erlang peut aider à implémenter un serveur dans ce cadre-là. Ma tâche est dorénavant de revenir au développement d'ErlyStage. J'écrirais de nouveaux articles au fur et à mesure que ce code avance... Donc, à bientôt !

D'ici là, vous pouvez nous rejoindre sur irc.worldforge.org, canal #coders :-)

Liens

Merci à Mickaël Rémond pour sa relecture ! :-)

Pour tout commentaire: erlang@erlang-fr.org

Dernière modification: 2005-11-11 18:48:13