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 ?

(Deuxième partie)

par Thierry « Shaman » Mallard

Traduction de l'article en Anglais

Deuxièmes partie : communication entre clients et serveurs

Dans 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ées

Nous 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.. ;-)

Atlas

Ce protocole a été écrit par Stephanus Du Toit il y a quelques années. Les objectifs principaux sont :

  • pouvoir transporter des données ayant les types classiques tels que «entiers», «flottant», «chaine», «listes»
  • rester transparent pour les clients et les serveurs

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 Atlas

Le 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 :
Client lien Serveur
(Connexion TCP ) --->
<--- ATLAS server erly-stage
ATLAS client XXXXXX --->
Maintenant, le client va dire quels codecs il comprend
par exemple ici, XML et Packed
ICAN XML --->
ICAN Packed --->
On finit la liste avec une ligne vide
(ligne vide) --->
Le serveur répond alors quel codec il souhaite utiliser
Ici, nous proclamons Packed
<--- IWILL Packed
<--- (ligne vide)
Le client confirme avec une ligne vide
(ligne vide) --->
Le serveur fait de même
<--- (ligne vide)

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 !

Liens

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

Pour tout commentaire: erlang@erlang-fr.org

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