|
|
Présentation du site Cours Erlang Articles Projets Autour d'Erlang-fr Liens English Area Miroir: Documentation Erlang Miroir: Archives Erlang/OTP Recherche ![]()
|
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 ». DecodageLes spécifications d'Atlas montrent que les éléments sont encodés comme suit :[type][nom=][donnees][|fintype] Les listes peuvent être les suivants :
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... EncodagePour 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">>} ] } :
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 trilogieJ'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 :-) LiensMerci à Mickaël Rémond pour sa relecture ! :-) |
Pour tout commentaire: erlang@erlang-fr.org
Dernière modification: 2005-11-11 18:48:13