Le temps réel

A la conquête du jeu vidéo partie 4

L'heure est grave

J’ai eu 4 jours de congé et j’ai tapé dans le projet comme un gros cochon. Je dois dire que je me suis vraiment éclaté sur cette partie et j’ai découvert plein de trucs intéressants. Par contre, j’ai énormément de retard sur la rédaction de l’article, donc c’est parti !

Pour cette partie, je vais te parler exclusivement de l’aspect communication et échange entre le client et le serveur.

Plan de combat

Grâce au site https://www.draw.io/ j’ai pu faire un schéma de tout ce que je dois développer et il y a blindé de trucs à faire alors que ce n’est que la partie que « hors combat ».

Techno

Concernant le choix des technologies, j’ai décidé de partir sur du node.js pour le côté serveur, couplé avec socket.io pour la gestion du temps réel. Ça me semble être une bonne combinaison, surtout que j’ai déjà fait quelques expérimentations avec ces deux-là et ça fonctionne bien ! Et pour ce qui concerne les bases de données, j’ai choisi MongoDB que je n’ai jamais utilisé, ce sera un bonne raison de le prendre en main et de découvrir cette technologie.

Je ne vais pas faire un long discours sur ces technos mais si tu ne connais pas, node.js c’est du JavaScript côté serveur exécuté par le moteur V8 de Google (en gros c’est très très rapide). socket.io, lui, c’est plutôt une bibliothèque qui va s’occuper de la communication entre le client et le serveur, avec en autres des websockets. Pour ce qui est de MongoDB, bah… C’est un système de gestion de base de données.

Environnement

Configurer l’environnement de travail, ce n’est pas franchement ce qu’il y a de plus marrant. J’ai installé Apache, Compass, NPM, etc… J’ai créé un répertoire wars.io, parce que je trouvais le nom sexy, et j’ai ajouté un virtualHost. J’ai changé aussi mes DNS internes pour que le domaine wars.io pointe directement sur le nouveau répertoire et bien sûr créé un nouveau dépôt git.

Comme je vais utiliser NPM pour installer node.js et socket.io, je vais initialiser le projet avec celui-ci. il va automatiquement créer le fichier package.json de départ.

npm init

J’ouvre le Terminal et j’installe node.js et socket.io. Pour ce projet j’ai décidé de travailler avec la norme ES6, j’ai donc installé Babel qui est un transpileur JavaScript et dans la foulée j’ai aussi installé jQuery, évidement…

#installation de node js
sudo npm install -g --save

#installation de socket.io
sudo npm install socket.io --save

#installation de MongoDB
sudo npm install mongodb --save

J’utilise l’option –save pour ajouter les dépendances de ces bibliothèques dans le fichier package.json. Jete un coup d’œil dedans pour voir si les dépendances ont bien été ajoutées.

Au niveau de mon répertoire, je crée un fichier apps.php, ça sera l’application en tant que telle. Je crée aussi deux sous répertoires, client et server, pour différencier les fichiers côté client et côté serveur.

Le plus important maintenant est de créer le fichier server.js (dans le répertoire server) et dedans je lance un serveur http (la base de node.js quoi).

let lib_http 		= require('http');
let lib_socket_io 	= require('socket.io');

let http_server = lib_http.createServer(function(req, res){

}).listen(3333);

Et comme tu peux le voir, j’utilise let qui vient de l’ES6. Je donne donc un coup de Babel et je sors un fichier compilé qui se nomme server_compiled.js. Ensuite je lance le serveur en console.

#compilation du fichier server
node_modules/.bin/babel --presets "es2015" server/server.js --watch --out-file server/server_compiled.js

#lance nodejs
node server/server_compiled.js

Avant de tester le serveur, je crée un fichier config.js qui est aussi placé dans le dossier serveur et qui sera compilé par Babel à chaque modification. Il me permettra de centraliser toute la config serveur. Je crée aussi un répertoire style avec deux sous répertoires, css et sass, et j’ajoute un fichier apps.scss qui sera compilé par Compass à chaque modification.

J’ouvre plusieurs Terminal et je lance les commandes qui vont bien.

#compilation sass -> voir la création du fichier config.rb
compass watch

#compilation de la config server
node_modules/.bin/babel --presets "es2015" server/config.js --watch --out-file server/config_compiled.js

#compilation du client.js
node_modules/.bin/babel --presets "es2015" client/client.js --watch --out-file client/client_compiled.js

#démarrage du serveur MongoDB
mongodb/bin/mongod --dbpath mongodb/data/

#je lance un shell mongoDB
mongodb/bin/mongo

Voilà, j’ai terminé la préparation de mon environnement de travail et tu pourrais presque croire que je suis un trader, c’est magnifique n’est ce pas ?

L'application

Dans le fichier apps.php, je charge mon fichier de style et une version de jQuery depuis le dépôt local. Le fait d’avoir lancé le serveur et d’avoir require la bibliothèque socket.io me donne la possibilité de récupérer les fichiers client de socket.io depuis le serveur. Enfin, j’appelle le fichier client_compiled.js en prenant soin d’ajouter un numéro de version qui change tout le temps pour éviter le cache de ce fichier.

<?php
	$url = "wars.io";
?><!doctype html>
<html class="no-js" lang="fr">
<head>
		<meta charset="UTF-8">
		<title></title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />

		<link rel="stylesheet" type="text/css" href="styles/css/apps.css" />
</head>
<body>
		
	<script src="http://<?php echo $url; ?>/node_modules/jquery/dist/jquery.min.js"></script>
	<script src="http://<?php echo $url; ?>:3333/socket.io/socket.io.js"></script>
	<script src="http://<?php echo $url; ?>/client/client_compiled.js?v=<?php uniqid(); ?>"></script>
	
</body>
</html>

Grâce à la commande suivante (il faut installer nodemon via npm), mon serveur node.js va se relancer automatiquement dès la modification d’un fichier du répertoire.

#ne plus utiliser node mais nodemon
nodemon server/server_compiled.js

emit et on

Avant d’aller plus loin, il faut absolument comprendre deux fonctions de socket.io, emit() qui sert à envoyer et on() à recevoir ! Ils sont disponibles aussi bien du côté client que du côté serveur.

Dans l’exemple suivant, le client envoie un message « Salut » au serveur via la fonction emit(). Le serveur, via la fonction on(), peut réceptionner le message, et en callback répondre à ce client et lui dire « Bien reçu ! ». De son côté, le client peut réceptionner la réponse avec la fonction on().

//côté client
socket.emit("message","salut");

socket.on("reponse", function(reponse){
	//message reçu du serveur
	alert(reponse);
});
//côté serveur
socket.on("message", function(message){
	//message reçu du client, envoyer une réponse
	socket.emit("reponse","Bien reçu !");
});

Le serveur

Je charge les bibliothèques qui sont nécessaires, ensuite, je code deux classes, l’une pour le jeu et l’autre pour le réseau. J’instancie la classe warsio_server, je lance le serveur HTTP et j’écoute les connexions sur le serveur avec socket.io.

//import de la config server
import {server_config} from "./config_compiled.js";

let e = console.log;

let lib_http 		= require('http');
let lib_socket_io 	= require('socket.io');
let lib_mongodb 		= require('mongodb');

class link{
	constructor(){

	}
	
	init_io(){
		//ecoute
		this.io = lib_socket_io.listen(this.http_server);
	}
}

class warsio_server extends link{
	
	constructor(server_config){
		//INIT SERVER
		e("/**************************************\n*\n*            Server started\n*            ==============\n*\n**************************************/");
		super();
		this.server_config = server_config;

		//initialisation http
		this.init();	
	}
	
	init(){
		//création du serveur
		this.http_server = lib_http.createServer(function(req, res){

		}).listen(this.server_config.port);
		
		this.init_io();
	}	
}

new warsio_server(server_config);

Au niveau de la classe link, je détecte les connexions via la méthode on() sur l’objet this.io.sockets. Dans la fonction de callback, j’ai un argument que j’ai nommé « user_socket » qui est la socket du client courant (celui qui vient de se connecter). Grâce à elle, je peux directement communiquer avec lui. Ensuite je vais détecter la réception de message et de déconnexion et afficher un message en console pour chaque fonction de rappel.

Attention, les « mots-clé » connection et disconnect ne peuvent pas être changés, cela fait partie de socket.io, mais « message », lui, peut être changé.

init_io(){
	//ecoute
	this.io = lib_socket_io.listen(this.http_server);
	
	//detection de connection
	this.io.sockets.on('connection', (user_socket) => {
		e("> Un client vient de se connecter");

		user_socket.on('message', (params) => {
			e("> Message reçu depuis le client");
		});
		
		user_socket.on('disconnect', (params) => {
			e("> Déconnection");
		});
	});
}

Client

Je reprends la même logique au niveau du client. Sauf qu’au lieu de détecter les connexions, là ce que je veux, c’est me connecter. Pour cela, j’utilise la méthode connect qui va faire le boulot tout seul.

function e(a){ console.log(a); }

class link{
	
	constructor(){
		//e("init constructor link");
	}
	
	socket_connexion( ){
		if (typeof io != "undefined" && io != null){
			this.socket = io.connect( 'http://wars.io:3333' );

			//quand la connexion est établie
			this.socket.on('connect', () => {
				e(this.socket.id);
			});
			
			//réception message serveur
			this.socket.on('message', (params) => {
				e("message");
			});
			
			//le serveur à deconnecter
			this.socket.on('disconnect', () => {
				e("Le serveur a été déconnecté");
			});
		}else{
			this.error("Le serveur n'est pas disponible.");
		}
	}
}

class warsio extends link{
	
	constructor(){
		super();	

		//init de connexion
		this.connected = false;
		super.socket_connexion();
	}	
}

new warsio();

Et là c’est cool car si je vais sur l’URL wars.io, je peux voir deux choses. Dans la console du serveur node.js, j’ai un message « >. Un client vient de se connecter » et sur mon navigateur dans Firebug, l’ID de la socket est affiché, donc la connexion est bien établie !

Principe

Maintenant le but, c’est que le client envoie des messages au serveur et que le serveur réponde à ce client ou à tous les autres, ça va être le principe du temps réel (le push serveur).

Donc pour cela, j’ajoute les méthodes qui vont permettre de répondre, les fameux emit().

Il y a 4 manières de répondre, soit le serveur répond au client courant, soit à un client spécifique, soit à tous les autres ou enfin, à tous les clients confondus.

//envoie un message à l’utilisateur courant.
send_to_client(user_socket, action, params){
	user_socket.emit(action , params);
}

//envoie un message à un utilisateur en particulier.
send_to_specific_client(user_socket_id, action, params){
	this.io.sockets.to(user_socket_id).emit(action , params);
}

//envoie un message à tous les clients connectés au serveur.
send_to_all_clients( action , params ){
	this.io.sockets.emit( action , params );
}

//envoie un message à tous les autres clients.
send_to_all_other_clients(user_socket, action, params){
	user_socket.broadcast.emit( action , params );
}

Et bien sûr, je fais pareil au niveau du client, mais c’est plus simple, car le client peut envoyer uniquement un message au serveur (il n’y a pas de communication client à client).

//envoie un message au serveur
send_to_server(action, params){
	this.socket.emit(action,params);
}

Allez c’est parti pour un petit exemple ! Voilà un peu comment j’imagine la demande pour rejoindre une partie.

//quand la connexion est établie
this.socket.on('connect', () => {
	this.send_to_server( "message" , { action : "rejoindre_partie" , params : { "id_partie" : 33 } });
});

Mon serveur va juste afficher « > Message reçu depuis le client ». J’appelle la méthode de manière dynamique, sorte de call_user_func_array(), donc mon action va correspondre à une méthode de l’objet et params sera l’argument de cette méthode.

class link{

	init_io(){
		//ecoute
		this.io = lib_socket_io.listen(this.http_server);
		
		//detection de connection
		this.io.sockets.on('connection', (user_socket) => {
			
			user_socket.on('message', (params) => {
				this.receive_from_client(user_socket, params);
			});
			
		});
	}
		
	//réception
	receive_from_client(user_socket, response = null){
		this[response.action](user_socket, response.params);
	}
}

Alors évidement je dois ajouter la nouvelle méthode sous peine d’avoir un joli message d’erreur.

rejoindre_partie(user_socket, params){
	//traitement
}

Une fois que j’ai inscrit le joueur dans la partie, je dois dire au serveur de renvoyer une réponse (c’est le minimum quand on est poli). J’ai choisi de mettre le mot-clé « response_ » suivi du nom de l’action, histoire d’avoir une certaine structure dans le nommage des fonctions.

rejoindre_partie(user_socket, params){
	//traitement
	
	//réponse au client
	this.send_to_client( user_socket, "message" , { 
		action : "response_rejoindre_partie", 
		params : {
			result 		: true,
			list_joueur 	: get_joueur_partie()
		}
	});	
}

Et pour le client, je reprends exactement le même principe d’appel dynamique. Sans oublier d’ajouter la méthode « response_rejoindre_partie » (désolé pour le franglais).

class link{

	socket_connexion( ){
		if (typeof io != "undefined" && io != null){
			this.socket = io.connect( 'http://wars.io:3333' );

			//quand la connexion est établie
			this.socket.on('connect', () => {
				this.send_to_server( "message" , { action : "rejoindre_partie" , params : { "id_partie" : 33 } });
			});
			
			//réception message serveur
			this.socket.on('message', (params) => {
				this.receive_from_server(params);		
			});
		}
	}
	
	//réception
	receive_from_server(response){
		this[response.action](response.params);
	}
	
}

class warsio extends link{
	response_rejoindre_partie(params){
		//traitement
	}
}

Maintenant j’ai un système qui me permet d’envoyer des messages au serveur et inversement un serveur qui est capable d’envoyer des messages aux clients.

14/05/2016

Yann Vangampelaere - nouslesdevs -

Sinon jete un coup d’oeil aux autres catégories

Ma boîte aux lettres

Tu veux me demander quelque chose ?