Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Créer une application multi-utilisateurs avec AS3 et node.js

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par frangois (frangois), le 07 août 2010

Le tutoriel qui suit va consister à construire une application multi-utilisateurs avec Flash et node.js.

Descriptif

Une application multijoueur typique comporte toujours 2 couches applicatives bien distinctes :

le client : développé en AS3, qui est affiché sur une page Web classique, au travers d’un serveur Web. C’est la partie visible pour l’utilisateur. Le client envoie les entrées utilisateur au serveur. Il y a autant d’instances du client que d’utilisateurs. Chaque instance se connecte en TCP à l’unique serveur identifié par une unique adresse IP, et envoie ses données sur un socket précis, identifié par un port.

le serveur : il n’y a qu’un seul serveur. Ce serveur écoute l’ensemble des clients sur un socket particulier identifié par son numéro de port. Un serveur de chat va par exemple redispatcher l’ensemble des messages reçus à l’ensemble des clients connectés. Traditionnellement, les serveurs utilisés sont soit des solutions commerciales (SmartFox, ElectroServer, Flash Media Server, Red5, etc), soit des développements propriétaires, réalisés dans des langages susceptibles de supporter des charges importantes (Java, C++, Python Stackless pour Eve Online, Erlang pour le chat Facebook). Les serveurs sont généralement multi-threadés et synchrones : un thread est alloué par client connecté, avec un segment de pile dédié, le tout reste en mémoire pendant la durée de connexion du client au serveur.

Pourquoi node.js ?

Node.js est un projet open-source basé sur l’interpréteur Javascript V8 de Google. C'est un interpréteur Javascript côté serveur, auquel a été ajoutée une API réseau de bas niveau dotée par exemple des fonctionnalités HTTP, sockets, UDP. C'est donc un toolkit destiné à concevoir des serveurs. Il utilise Javascript comme langage de script, un langage simple, proche de l’AS3, et donc rapide à assimiler pour le développeur Flash de base. Tout le monde connait quelques bouts de Javascript.

Node.js possède deux caractèristiques supplémentaires qui font tout son intérêt :

la programmation évènementielle : 100% basé sur les gestionnaires d’évènements, en node.js “tout est évènement”. Hors de la durée de vie d'un évènement, le serveur “dort”.

asynchrone : comme le serveur Web nginx, il n’utilise pas une thread par client, mais écoute directement le kernel via epoll, poll, detect, et affecte de l'espace en pile à la volée au sein de son unique thread d'exécution. Donc il gère très bien les problèmes de concurrence, ce qui en fait l'outil idéal pour les problématiques de serveurs multi-utilisateurs temps-réel. Chose à savoir, Nginx s'est hissé en un an au 3ème rang des serveurs Web les plus déployés. Le micro-threading, c'est l'avenir.

Ce dernier paragraphe est totalement imbitable, et il n’est pas nécessaire de le comprendre, la seule chose à retenir : “ça envoie du pâté, et visiblement ça tourne sous Linux”.

Quelques liens complémentaires :

• Le site officiel comporte un Hello World assez simple : Hello World.

• Un excellent article de vulgarisation en français : le blog d'Inovia.

• Les sources d'un serveur de chat complet par l'auteur de node.js, qui m'ont servies de base de travail : node_chat sur GitHub.

• La documentation complète de l'API comporte d'autres exemples de serveurs.

Installation de node.js

Les pré-requis pour pouvoir installer node.js sont les suivants :

• node.js ne tourne que sous Linux,

• gcc et make doivent être installés

sudo apt-get install gcc

• pas de proxy sur le(s) port(s) utilisé(s),

• le(s) port(s) utilisés doivent être ouverts côté routeur/firewall et redirigés vers le serveur (pour les usagers de Free c'est ici ).

• si vous ne disposez pas encore d'un serveur : un PC sous Ubuntu suffit. Il n'est PAS nécessaire d'installer Apache/MySQL/PHP, seul les packages git-core, open-ssh, make et gcc sont nécessaires.

Commençons par récupérer les sources via Git :

 
$ mkdir ~/temp
$ git clone git://github.com/ry/node.git
$ cd node

Nous voilà munis des dernières sources. On compile ces sources via la manière traditionnelle :

$./configure

L’absence d’OpenSSL provoque un warning, mais n’est pas bloquante.

$ make

La compilation dure 5-10 minutes sur un Core Quad, puisque V8 est inclus dans les sources.

$ sudo make install

Nous pouvons maintenant vérifier que tout fonctionne correctement :

$ node -v
v0.1.103

Node et ses dépendances sont installés dans /usr/local/bin.

Prise en main de node.js : servir proprement le policy file

Depuis Flash Player 9, toutes les applications requérant par la suite un accès vers un socket distant initie l’échange client-serveur en interrogeant le serveur sur le port 843, en attendant en retour un fichier de sécurité autorisant ou non l’accès.

Typiquement, ce genre de chose est intégré aux serveurs commerciaux, ou alors il est requis d’installer le serveur de policy file proposé par Adobe (http://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html). Commençons par développer notre propre serveur de Policy File, histoire de tâter un peu le terrain.

Voici la source Javascript du serveur :

// importation des modules nécessaires
var net = require('net'),
    path = require('path'),
    fs = require('fs');
 
// création du serveur
net.createServer( function (socket) {
 
  // réglage de l'encodage sur UTF-8
  socket.setEncoding("utf8");
 
  // on forge le path complet de notre fichier .xml
  var securityFileName = path.join( process.cwd(), 'policy.xml' );
 
  // s’il existe
  path.exists( securityFileName, function(exists) {
    fs.readFile( securityFileName, "binary", function( err, file ) {
 
      // on l’envoie au client par le socket sur le port 843
      socket.write( file, "binary" );
 
      // on ferme le socket
      socket.end();
    });
  });
}).listen( 843 );

On sauve ça dans un fichier .js, par exemple node-security.js. On crée ensuite dans le même dossier, le fichier policy.xml qui contient notre policy d’accès, ici on autorise uniquement le port 8888, sur lequel tournera notre serveur de jeu :

<?xml version="1.0"?>
<cross-domain-policy>
   <allow-access-from domain="*" to-ports="8888" />
</cross-domain-policy>

Puis on lance le serveur en tâche de fond :

$ sudo node node-security.js &
[1] 2740

On peut maintenant interroger le serveur depuis une autre machine à travers Internet, en utilisant l’utilitaire de bas-niveau Telnet, par exemple sous Windows, Démarrer > Exécuter > telnet :

Microsoft Telnet> o 88.12.xx.xx 843

On voit s’afficher notre policy file, puis le socket se fermer : notre .swf est maintenant autorisé à continuer.

Pour stopper le serveur, on tue le process par son PID :

$ sudo kill 2740

Ce serveur de policy file devra tourner en tâche de fond durant tout le reste du tutoriel.

Dans la pratique : une application de chat

côté serveur

Nous voilà prêts désormais pour développer un serveur de chat basique. Pour l'instant, nous pouvons ouvrir un socket par client, puis y envoyer des données :

net.createServer( function (socket) {
  socket.write('foo');
});

Nous allons avoir besoin de recevoir des données : le principe d'un serveur de chat est le suivant :

• le serveur reçoit une donnée d'un client,

• il renvoie cette donnée vers tous les clients connectés.

net.createServer( function (socket) {
  socket.write('foo');
  socket.on( "data", function (data) {
    // traitement des données lues
  });
});

On constate ici la similitude avec l'Actionscript : on traite un évenement par un handler on( <constante chaine>, <handler> ).

Nous aurons ici 2 types de messages : /join <pseudo> à la connexion, /msg <message> lorsque c'est un message à afficher. Le tout est véhiculé sous forme de chaîne UTF-8.

net.createServer( function (socket) {
  socket.setEncoding('utf8');
  socket.on( "data", function (data) {
    if( data.substring( 0, 5 ) == '/join' ) {
       // connexion d'un nouvel utilisateur
       // ...
    }
 
    if( data.substring( 0, 5 ) == '/msg' ) {
       // message à renvoyer à tous les utilisateurs
       // ...
    }
  });
});

Il nous reste à garder une liste des sockets ouverts, afin de pouvoir leur dispatcher les messages reçus. On utilisera un simple Object {…}, qui fonctionne de la même manière en JS et en AS.

la source complète du serveur

Voici le code complet du serveur, sauvons-le sous node-chat-server.js

// importation des modules necessaires
var net = require('net');
var sessions = {};
 
// creation du serveur
var server = net.createServer( function (socket) {
 
  socket.setEncoding('utf8');
 
  // handler de données entrantes
  socket.on( "data", function (data) {
 
	// l'utilisateur rejoint pour la 1ère fois
	if( data.substring( 0, 5 ) == '/join' ) {
 
		// on loggue côté serveur
		console.log('[join] ' + data.substring( 6, data.length ) );
 
		// on stocke une référence vers le socket dans l'objet <sessions>
		var session = { socket: socket,
						nick: data.substring( 6, data.length ) };
		sessions[ Math.floor( Math.random() * 99999999999 ).toString() ] = session;
		console.log(sessions);
	}
 
	// l'utilisateur envoie un message
	if( data.substring( 0, 4 ) == '/msg' ) {
 
		// on loggue côté serveur
		console.log('[msg] ' + data.substring( 5, data.length ) );
 
		// on parcourt l'objet <sessions> à la recherche du nickname correspondant
		for ( var i in sessions ) {
			var session = sessions[i];
			if ( session.socket === socket ) {
				var nick = session.nick;
			}
		}
 
		// on parcourt l'objet <sessions> et envoie le msg à tous les sockets
		for ( var i in sessions ) {
			sessions[i].socket.write( nick + ' : ' + data.substring( 5, data.length ) );
		}		
	}	
  });
 
  // handler de déconnexion
  socket.on("end", function () {
 
	// on supprime la référence vers le socket dans l'objet <sessions>
	for ( var i in sessions ) {
		var session = sessions[i];
		if ( session.socket === socket ) {
			socket.end();
			delete sessions[i];
		}
	}
 
  });  
 
}).listen( 8888 );

côté client

Il nous reste à développer un petit client de chat en AS3, qui aura un rôle très limité :

• Envoyer /join <pseudo> dans le socket à la connexion de l'utilisateur,

• Envoyer /msg <message> dans le socket au clic sur le bouton <envoyer>,

• Afficher dans une zone de texte les messages reçus du serveur.

Commençons par instancier un Socket :

// creation du socket
_socket = new Socket();
 
_socket.addEventListener( Event.CONNECT, onConnect );
_socket.addEventListener( ProgressEvent.SOCKET_DATA, onData );
...

Nous écoutons principalement 2 évenements :

onConnect : émis lorsque la connexion au serveur a été initiée sans encombre.

onData : émis à la réception d'une donnée depuis le serveur.

l'envoi de données

_socket.writeUTFBytes( '/msg ' + _msgInput.text );
_socket.flush();

La première méthode est assez claire d'elle-même, on écrit dans le socket une chaîne UTF-8. Attention à bien appeler la méthode Socket.flush(), qui force l'envoi effectif des données (sinon, il ne se passera rien).

la réception de données

var read : String = _socket.readUTFBytes(_socket.bytesAvailable)

C'est cette méthode qui permet de lire <n> bytes depuis un Socket, et les converti en String à la volée

la source compléte du client Flash

package 
{
	import com.carlcalderon.arthropod.Debug;
 
        import com.bit101.components.TextArea;
	import com.bit101.components.InputText;
	import com.bit101.components.Text;
	import com.bit101.components.PushButton;
	import com.bit101.components.Label;
 
	import flash.display.Sprite;
	import flash.net.Socket;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.MouseEvent;
 
	/**
	 * ...
	 * @author 
	 */
	public class Application extends Sprite 
	{
		private var _loginInput : InputText;
		private var _loginButton : PushButton;
 
		private var _sendButton : PushButton;
		private var _msgInput : InputText;
		private var _msgWindow : TextArea;
 
		private var _login : String;
 
		private var _socket : Socket;
 
		public function Application() : void 
		{
			Debug.clear();
 
			// creation de l'U.I.
			_loginInput = new InputText( this, 150, 250, "guest" + Math.floor( Math.random() * 10000 ) );
			_loginButton = new PushButton( this, 350, 250, "connexion", onLogin );
 
			// creation du socket
			_socket = new Socket();
 
			_socket.addEventListener( Event.CLOSE, onClose );
			_socket.addEventListener( Event.CONNECT, onConnect );
			_socket.addEventListener( IOErrorEvent.IO_ERROR, onError );
			_socket.addEventListener( SecurityErrorEvent.SECURITY_ERROR, onSecurityError );
			_socket.addEventListener( ProgressEvent.SOCKET_DATA, onData );
		}
 
		private function onLogin( e : MouseEvent ) : void
		{
			// suppression de l'U.I. de login
			removeChild(_loginInput);
			removeChild(_loginButton);
 
			_login = _loginInput.text;
 
			_loginInput = null;
			_loginButton = null;
 
			// creation de l'U.I. du chat
			_msgWindow = new TextArea( this, 5, 5 );
			_msgWindow.width = 780;
			_msgWindow.height = 550;
			_msgInput = new InputText( this, 5, 570, "taper un message ici" );
			_msgInput.width = 670;
			_sendButton = new PushButton( this, 680, 570, "envoyer", onSend );			
 
			// connexion au serveur
			_socket.connect( 'xxx.xxx.109.181', 8888 );
		}
 
                // envoi d'un message suite à un clic utilisateur
		private function onSend( e : MouseEvent) : void
		{
			_socket.writeUTFBytes( '/msg ' + _msgInput.text );
			_socket.flush();
			_msgInput.text = '';
		}
 
		private function onClose( e : Event ) : void 
		{
			Debug.log( 'onClose' );
		}
 
                // connexion au socket effectuée
		private function onConnect( e : Event ) : void 
		{
			Debug.log( 'onConnect' );
 
			_socket.writeUTFBytes( '/join ' + _login );
			_socket.flush();			
		}
 
		private function onError( e : IOErrorEvent ) : void 
		{
			Debug.error( 'onError' );
		}
 
		private function onSecurityError( e : SecurityErrorEvent ) : void 
		{
			Debug.error( 'onSecurityError' );
		}
 
                // réception de données depuis le socket
		private function onData( e : ProgressEvent ) : void 
		{
			Debug.log( 'onData' );
			_msgWindow.text += _socket.readUTFBytes(_socket.bytesAvailable) + "\n";
		}
 
	}
 
}

Conclusion

On lance maintenant le serveur via les commandes habituelles :

$ node node-chat-server.js &

On lance ensuite le client Flash depuis une autre machine, et voilà, ça marche.

En à peine quelques lignes de code Javascript simple et basique, voilà un serveur de chat rapide et efficace. A partir de cette base, voici quelques améliorations possibles et pistes à creuser :

• Les sources d'un serveur de chat complet par l'auteur de node.js sont une mine d'or : node_chat sur GitHub.

• les messages sont encodés sous forme de chaînes UTF-8. C'est lourd et lent. Pour un jeu online, il serait préférable d'utiliser un format binaire. En supprimant la ligne socket.setEncoding('utf8');, l'argument data reçu ne sera plus une chaîne mais un objet Buffer, particulier à node.js.

• un serveur de jeu online doit pouvoir communiquer avec une base de données. Sachant que MySQL tourne sur socket (mysql.sock), il est relativement simple de communiquer avec.