|
|
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 ?(Deuxième partie) par Thierry « Shaman » Mallard Traduction de l'article en Anglais Deuxièmes partie : communication entre clients et serveursDans le prédécent article, nous avons vu quelques uns des agents principaux d'ErlyStage : Thor et Mercury. Thor donne vie aux agents tels que Mercury, qui est responsable de la gestion des connexions TCP. Nous allons maintenant voir comment les clients et Mercury vont s'échanger des données d'informations. Réception des donnéesNous devons être capable de traiter les données arrivant de la connexion TCP gérée par Mercury. L'idée est ici de recevoir ces données, les formatter en ligne de textes, et de leur appliquer des fonctions. Celles-ci auront la particularité de renvoyer un pointeur vers une autre fonction, qui sera appliquer sur la ligne suivante, et ainsi de suite. Ceci permet d'écrire une machine à états finis, où les états sont modifiés par la réception des données, et où les fonctions sont utilisées pour passer d'un état à un autre. Hmm... en tout cas, c'est comme ça que je l'imagine, mes connaissances théoriques en matière de coding étant limitées ;-) La fonction getdata qui gère les données reçues ressemble à ceci :
%%
%% getdata : retrieve data from a socket
%%
getdata( CSocket, B ) ->
{ ok, Rest } = ?MODULE:dispatch( CSocket, B ),
case gen_tcp:recv( CSocket, 0) of
{ ok, Bs } = ?MODULE:getdata( CSocket, Rest ++ Bs ) ;
{ error, closed } -> ?MODULE:dispatch( Rest )
end.
Bon, le code est en fait un peu plus touffu que ça, mais nous en resterons là pour l'instant. Cette fonction recoit (gen_tcp:recv) les données et utilise la fonction dispatch pour traiter celles-ci. Le dispatcher renvoie les données non traitées dans Rest, qui seront ajoutées aux nouvelles données reçues (Bs). Puis, la fonction repart en boucle. Maintenant, voyons un peu comment faire le dispatcher. Nous voulons qu'il sépare les données en lignes de textes, et qu'il traite celles-ci une par une avec une fonction dynamique. Ceci signifie qu'à chaque fois qu'une fonction est appelée pour traiter une ligne, elle retourne un pointeur vers une autre (ou la même) fonction, qui traitera la ligne suivante. Voici son implémentation :
%%
%% The dispatcher : We parse the current data into lines and then we
%% process each line thought the current Module:Fun(Id, Arg), which
%% should return a possibly new processor
%%
dispatch( PegasusAgent, CSocket, ProcModule, ProcFun, ProcId, ProcArg, B)->
Pos = string:str(B, ?DELIMITER),
DelSize = string:len(?DELIMITER),
case Pos of
0 -> { ok, ProcModule, ProcFun, ProcId, ProcArg, B } ;
_ ->
Line = string:substr( B, 1, Pos-1 ),
Rest = string:substr( B, Pos+DelSize, string:len(B)
- (Pos-DelSize+1) ),
%% Apply the processor on the Line
%% notice the processor can return a different one (NewProc...)
%% this way, we make something like a Finite State Machine
{ok, NewProcModule, NewProcFun, NewProcId, NewProcArg } =
apply( ProcModule, ProcFun, [ ProcId, CSocket, Line, ProcArg ]),
%% And we loop back.
?MODULE:dispatch(
PegasusAgent, CSocket, NewProcModule, NewProcFun,
NewProcId, NewProcArg, Rest )
end.
Ne regardez pas de trop près l'argument PegasusAgent. Il sera revu dans un article prochain. Ici, nous analysons les données contenues dans B (pour Binaire) en cherchant un délimiteur. S'il y en a un (case _), nous coupons les données en une ligne de texte (Line) et un reste (Rest), qui sera traité au prochain cycle. Pour ceux qui débutent en Erlang, souvenez-vous (ou apprenez ;-) que "_" est une variable anonyme, cela signifie qu'on ne s'occupera pas de sa valeur. Le case Pos of retourne soit 0, soit une valeur différente de 0. C'est tout ce qui nous importe. Ceci aurait pu être écrit avec une instruction if, mais je préfère généralement, en Erlang, utiliser des case. S'il n'y a aucun délimiteur dans les données B, nous renvoyons les données à l'appelant pour qu'il concatène celles-ci avec les données à recevoir. Nous avons donc une ligne Line à traiter. Pour cela, nous appliquons (apply) le processeur courant, qui est un tuple { module, fonction, arguments }, sur celle-ci. La fonction renverra alors un autre tuple sur la fonction (possiblement la même) qui sera utilisée pour traiter la ligne suivante... De la sorte, nous pouvons implémenter un protocole très simplement. Et devinez quoi, c'est exactement ce que l'on va faire.. ;-) AtlasCe protocole a été écrit par Stephanus Du Toit il y a quelques années. Les objectifs principaux sont :
Cette transparence permet à Atlas de transporter des données dans différents codecs (codeur/décodeur). Ces codecs doivent être présentes dans toute implémentation d'Atlas. Les codecs obligatoires sont XML et Packed. La négociation dans AtlasLe protocole Atlas spécifie comment le client et le serveur initient leur communication, et déterminent ainsi le codec à utiliser. Cette première étape est appelée « négociation ».Cette négociation se déroule ainsi :
Ok, donc l'état initial, quand une connexion TCP arrive, est d'envoyer un message de bienvenue au client. Ceci est réalisée en placant le premier processeur sur la fonction atlas:negociate(init, CSocket,...) :
negociate( init, CSocket, _, _ ) ->
Message = ["ATLAS " , "server ", ?ATLAS_SERVER_NAME, ?DELIMITER],
ok = gen_tcp:send(CSocket, Message),
%% Next step is «getname» part ..
{ ok, ?MODULE, negociate, getname, [] } ;
Ici nous envoyons le message et retournons un pointeur (ou plus précisément un tuple) vers la fonction negociate, dans le même module (d'où la macro ?MODULE), avec comme premier argument getname. Le dispatcher de Mercury, que nous avons vu plus tôt, va utiliser cette fonction sur la ligne suivante. Nous avons également vu que la prochaine étape est de recevoir le message «ATLAS client Xxxxx» :
%% The syntax is: "ATLAS client " + Name of the server and eventualy version
%% number and + "\n"
negociate( getname, _CSocket, Line, _ ) ->
%% We don't care here of the client name...
%% Next step is get client possibilities
{ ok, ?MODULE, negociate, getcodecs, [] } ;
Le même schéma est utilisé ici. La fonction suivante est ?MODULE:negociate( getcodecs...), qui va écouter la liste des codecs que le clients connait. La fin de la liste est délimitée par une ligne vide. Nous utiliserons le «pattern matching» pour gérer cela :
%% When the client send an empty line it means there's no more codec to check
negociate( getcodecs, CSocket, "", CurrentCodecs ) ->
io:format("[Atlas] NEGOCIATION / CLIENT CODECS DONE ~n"),
Codec = answer( codec, CSocket, CurrentCodecs ),
%% We now switch to the filter negociation, such as it is
{ ok, ?MODULE, negociate, getfilter, Codec } ;
%% When the client sends "ICAN Xxx" it means it can use codec Xxx
%% We just store that in the var
%% .... if only I had a var ;-) (FIXME) => should be ok now ?
negociate( getcodecs, CSocket, "ICAN "++Codec, CurrentCodecs ) ->
%% We have a client possibility
io:format("[Atlas] NEGOCIATION / CLIENT CODEC : ~s ~n", [Codec]),
{ ok, ?MODULE, negociate, getcodecs, CurrentCodecs ++ [ Codec ] } ;
Si des «ICAN ...» arrivent du client, nous bouclons en renvoyant un pointeur vers la même fonction. Dans le cas contraire, quand le client termine la liste et envoie une ligne vide, nous allons vers ?MODULE:negociate(getfilter, Codec), le Codec étant déterminé par la fonction answer :
answer( codec, CSocket, CurrentCodecs ) ->
io:format("[Atlas] Client Available codecs : ~p ~n", [CurrentCodecs]),
ok = gen_tcp:send( CSocket, "IWILL Packed"++?DELIMITER++?DELIMITER),
?CODEC;
Nous trichons quelque peu pour le moment, en forcant la sélection du codec Packed, puisque c'est le seul codec implémenté en Erlang à ce jour. De même, les filtres ne sont pas écrits. La fonction getfilter est donc ... euh.. simple et directe ;-)
negociate( getfilter, CSocket, Line, Codec) ->
answer( filter, CSocket, [] ),
{ ok, ?MODULE, dialog, msg, Codec }.
answer( filter, CSocket, _CurrentFilters ) ->
ok = gen_tcp:send( CSocket, ?DELIMITER ).
Ok. Nous avons maintenant une implémentation d'Atlas qui est capable de négocier le codec Packed, sans filtres. Mais bon, ce n'est qu'un début. La fonction suivante est simplement ?MODULE:dialog(msg, ...) :
dialog( msg, CSocket, Line, Codec ) ->
{ [], [X] } =Codec:decode( Line ),
%% Now we pass the message to pegasus
global:send( pegasus_agent, X ),
{ ok, ?MODULE, dialog, msg, Codec }.
Souvenez-vous que celle-ci est appelée par le dispatcher de Mercury. Elle recoit une ligne de texte (Line), mais aussi quel Codec elle doit utiliser, lequel codec a été négocié juste avant. Cette fonction n'a donc qu'a appeler la fonction Codec:decode(Line) pour obtenir les véritables données au format Erlang (X). Actuellement, ces données sont alors simplement envoyées à l'agent Pegasus que Thor a créé au démarrage. La ligne de code que je trouve le plus étonnant est justement Codec:decode(Line). Ceci permet un lien dynamique vers un module et une fonction Erlang, que nous ne connaissons pas au moment de l'écriture du code. Le même principe sera utilisé plus tard avec Pegasus, permettant ainsi un passage dynamique des messages vers les modules adéquats. À suivre...Atlas est maintenant prêt à échanger des informations en utilisant un codec. Nous verrons dans le prochain article comment implémenter le codec Packed. D'ici là, bon code ! LiensMerci à Mickaël Rémond pour sa relecture ! :-) |
||||||||||||||||||||||||||||||||||||||||||||||||
Pour tout commentaire: erlang@erlang-fr.org
Dernière modification: 2005-11-11 18:48:11