|
TP 4 M2 Miage, web sockets
De $1
Table des matières- 1.1. Introduction
- 2.
Dans ce TP nous allons étudier les WebSockets faisant partie de ce qu'on appelle WebRTC (Real Time Communication), c'est "à côté de HTML5" et c'est un nouveau protocole (et une API JavaScript que doivent implémenter les navigateurs) synchrone, à faible latence, bi-directionnel.
Pour faire des WebSockets il faut un serveur supportant ce protocole. Aujourd'hui presque tous les serveurs (apache, glassfish, tomcat, jetty, etc.) supportent ce protocole nativement ou à l'aide d'extensions. Comme l'application que nous développons est un jeu multi-participants, nous allons utiliser un micro serveur très à la mode en ce moment: NodeJS. Ce serveur intègre l'interpréteur JavaScript V8 de Google et permet de coder la couche applicative côté serveur en JavaScript. On pourra ainsi échanger entre le serveur et les clients des objets JavaScript, sans devoir systématiquement les transformer dans un autre langage.
Nous commencerons par étudier deux applications de chat synchrones basés sur la librairie socket.io très populaire dans le monde des WebSockets (il existe des clients pour Java, C#, etc. il est par exemple possible d'écrire une application Android qui parle avec un serveur nodeJS via des WebSockets).
Livre gratuit en français, sur NodeJS, écrit par d'anciens élèves de la Miage de Nice : http://nodejs.xomis.com/fr/
- 2.1. Installation de NodeJS et des modules express et socket.io
- 3.
Quel que soit votre système d'exploitation, allez sur le site http://www.nodejs.org et suivez la procédure d'installation (click sur le bouton "install"). Installez la dernière version.
Une fois nodeJS installé, vérifiez qu'il fonctionne en tapant dans une fenêtre dos/shell la commande "node --version", cela devrait afficher quelque chose comme: "node 0.10.x". Ok, tout va bien.
- 3.1.1. Test d'une première application NodeJS
- 4.
Créez un fichier test.js file avec ce contenu :
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
Sauvez le et exécutez la commande suivante, après être allés dans le répertoire où vous avez sauvé le fichier précédent :
node test.js
Vous venez juste de lancer le serveur NodeJS qui répond à cet URL : http://localhost:8124
Ouvrez cet URL dans votre navigateur !
Vous devriez avoir :
Vous venez d'exécuter votre première application NodeJS ;-)
Le fichier test.js contient le code pour un serveur web archi minimal qui n'est même pas capable de lire un fichier html. Il a juste renvoyé des Strings en parlant de protocole HTTP.
- 4.1.1. Installation de modules complémentaires
- 5.
NodeJS est extensible grace à un système de "modules", sortes de "packages" à la linux. Lors de l'installation, si vous avez mis le répertoire d'installation dans le PATH, vous devriez avoir accès à la commande "npm" ou Node Package Manager, qui permet d'installer (il faut avoir une connexion qui fonctionne, évidemment...) des modules supplémentaires. Il en existe des centaines aujourd'hui, mais nous allons utiliser deux des plus populaires:
express donne la possibilité de servir des pages HTML avec CSS, JavaScript etc. Par défaut NodeJS ne sait pas faire cela. Permet aussi de faire des web services REST mais cela n'est pas étudié pendant ce cours. socket.io : librairie pour les WebSockets. Donne aussi des solutions de fallbacks si le navigateur utilisé ne supporte pas les WebSockets (comet, ajax, flash sockets), permet de gérer des groupes (sortes de "salles" de chat/ sessions de jeux entre amis, etc.)
Travail à faire :
Créer un répertoire "chatWithSocketIO" quelque part sur votre disque dur, Aller dans ce répertoire (cd...) Exécuter la commande "npm install express", attendre qu'elle se termine, Exécuter la commande "npm install socket.io", attendre qu'elle se termine.
Dans votre répertoire vous devriez avoir un répertoire supplémentaires intitulé "node_modules" qui contient les modules que vous venez d'ajouter à votre projet. En général on ajoute les modules par projet, non pas globalement.
Si vous développez une autre application, vous pourrez simplement copier ce répertoire si vous ne voulez pas re-télécharger les modules express et socket.io, vous pourrez éventuellement en ajouter.
- 5.1. Test d'une application de chat "simple", utilisant express et socket.io
- 6.
Nous avons repris ici le code d'un tutorial bien connu du web, qui explique comment réaliser un petit chat avec nodeJS, express et socket.io. Le code a été mis à jour pour fonctionner avec les dernières versions de nodeJS et de express (v3), et socket.io.
Récupérez donc cette archive : ChatSocketIOFixed.zip, le lien est en bas de cette page.
Désarchivez là et faites en sorte pour qu'on ait dans ce répertoire le répertoire node_modules qui contient socket.io et express. Vous devez avoir ceci dans le répertoire à la fin:
Bon, maintenant, executez la commande "node simpleChatServer.js" et ouvrez dans votre navigateur "http://localhost:8080", si votre port 8080 est occupé, changez le port dans le fichier simpleChatServer.js.
Ouvrez cet URL dans deux tabs, et regardez le résultat. Le chat devrait fonctionner. Ouvrez la console de debug de Chrome (y'a que Chrome qui permet de debugger efficacement les WS aujourd'hui, tab "network" et tout en bas "websocket", puis tab "frames"), chattez et regardez les trames échangées....
Maintenant, étudiez le code de simpleChatServer.js, de simpleChat.html, et lisez le tutorial (en anglais) qui correspond : http://psitsmike.com/2011/09/node-js...chat-tutorial/, la partie installation est inutile, nous avons fait autrement pour prendre des versions plus à jour, et les premières lignes du serveur sont aussi différentes, pour les mêmes raisons.
Principes de base de socket.io (on trouve de nombreux tutoriaux en français et en anglais sur le web, nous résumons ici l'essentiel):
socket.on(type d'un événement, callback) : exécute la fonction de callback quand un événement arrive (le type = une chaine quelconque). Quelques événements comme 'connection' ou 'disconnect' sont prédéfinis. Tout le traitement des messages se fait dans io.sockets.on('connection', function (socket) {...}, sans connexion, on ne peut rien faire! socket.emit(type de l'évenement, data1, data2) : envoie juste au client connecté un événement (une chaine) et deux paramètres pouvant contenir des données io.sockets.emit(type de l'évenement, data1, data2) : envoie à tous les clients connectés un événement (une chaine) et deux paramètres pouvant contenir des données socket.broadcast.emit((type de l'évenement, data1, data2) : envoie à tous les clients connectés sauf le client courant un événement (une chaine) et deux paramètres pouvant contenir des données
simpleChatServer.js:
// We need to use the express framework: have a real web server that knows how to send mime types etc.
var express=require('express');
// Init globals variables for each module required
var app = express()
, http = require('http')
, server = http.createServer(app)
, io = require('socket.io').listen(server);
// Indicate where static files are located
app.configure(function () {
app.use(express.static(__dirname + '/'));
});
// launch the http server on given port
server.listen(8080);
// routing
app.get('/', function (req, res) {
res.sendfile(__dirname + '/simpleChat.html');
});
// usernames which are currently connected to the chat
var usernames = {};
io.sockets.on('connection', function (socket) {
// when the client emits 'sendchat', this listens and executes
socket.on('sendchat', function (data) {
// we tell the client to execute 'updatechat' with 2 parameters
io.sockets.emit('updatechat', socket.username, data);
});
// when the client emits 'adduser', this listens and executes
socket.on('adduser', function(username){
// we store the username in the socket session for this client
socket.username = username;
// add the client's username to the global list
usernames[username] = username;
// echo to client they've connected
socket.emit('updatechat', 'SERVER', 'you have connected');
// echo globally (all clients) that a person has connected
socket.broadcast.emit('updatechat', 'SERVER', username + ' has connected');
// update the list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
});
// when the user disconnects.. perform this
socket.on('disconnect', function(){
// remove the username from global usernames list
delete usernames[socket.username];
// update list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
// echo globally that this client has left
socket.broadcast.emit('updatechat', 'SERVER', socket.username + ' has disconnected');
});
});
Et la page simpleChat.html
<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
var socket = io.connect(); // we might pass the URL of the WS server as parameter here
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});
// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updateusers', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('<div>' + key + '</div>');
});
});
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
</script>
<div style="float:left;width:100px;border-right:1px solid black;height:300px;padding:10px;overflow:scroll-y;">
<b>USERS</b>
<div id="users"></div>
</div>
<div style="float:left;width:300px;height:250px;overflow:scroll-y;padding:10px;">
<div id="conversation"></div>
<input id="data" style="width:200px;" />
<input type="button" id="datasend" value="send" />
</div>
- 6.1. Test d'une version "multi-rooms" permettant de gérer des "salles de chat"
- 7.
Dans cette version, on commence à se rapprocher d'une véritable plate-forme permettant de gérer des "sessions" permettant à plusieurs groupes d'utilisateur de participer en temps réel à un chat privé, de rejoindre d'autres salles de chat etc. Cet exemple porte en lui les éléments fondamentaux pour réaliser un serveur de jeux en réseau, remplacez "salle de chat" par "session de jeu" et vous avez tout compris. Imaginez qu'au lieu de taper du texte dans un champ de saisie, le code client qui tourne dans le navigateur envoie des objets JavaScript sur le même canal, par exemple {type:'position', player:'1', x:'120', y:'130'} ou {type:'event', value':endOfGame'}, et que le serveur les fasse passer aux autres clients connectés.
Principes de gestion de "salles":
socket.join(nom de la salle) et socket.leave(nom de la salle) pour indiquer que le client actuellement connecté entre dans une salle ou la quitte. Si on entre dans une salle qui n'existe pas on la créée. socket.broadcast.to(nom de la salle).emit(...) idem à socket.broadcast.emit mais que pour les gens connectés à la salle (le client courant est exclu du broadcast comme pour socket.broadcast.emit) io.sockets.in(nom de la salle).emit(...) : envoie à tout les gens de la salle, y compris le client courant.
Il existe de nombreuses autres fonctions propres à la gestion de "salles" comme "connaitre la liste des salles, et qui est dedans, savoir dans quelle salle est un client donné, etc", voir ce tutorial en anglais très complet: https://github.com/LearnBoost/socket.io/wiki/Rooms
multiRoomChatServer.js
// We need to use the express framework: have a real web server that knows how to send mime types etc.
var express=require('express');
// Init globals variables for each module required
var app = express()
, http = require('http')
, server = http.createServer(app)
, io = require('socket.io').listen(server);
// Indicate where static files are located. Without this, no external js file, no css...
app.configure(function () {
app.use(express.static(__dirname + '/'));
});
// launch the http server on given port
server.listen(8080);
// routing with express, mapping for default page
app.get('/', function (req, res) {
res.sendfile(__dirname + '/multiRoomChat.html');
});
// usernames which are currently connected to the chat
var usernames = {};
// rooms which are currently available in chat
var rooms = ['room1','room2','room3'];
io.sockets.on('connection', function (socket) {
// when the client emits 'adduser', this listens and executes
socket.on('adduser', function(username){
// store the username in the socket session for this client
socket.username = username;
// store the room name in the socket session for this client
socket.room = 'room1';
// add the client's username to the global list
usernames[username] = username;
// send client to room 1
socket.join('room1');
// echo to client they've connected
socket.emit('updatechat', 'SERVER', 'you have connected to room1');
// echo to room 1 that a person has connected to their room
socket.broadcast.to('room1').emit('updatechat', 'SERVER', username + ' has connected to this room');
socket.emit('updaterooms', rooms, 'room1');
});
// when the client emits 'sendchat', this listens and executes
socket.on('sendchat', function (data) {
// we tell the client to execute 'updatechat' with 2 parameters
io.sockets.in(socket.room).emit('updatechat', socket.username, data);
});
socket.on('switchRoom', function(newroom){
socket.leave(socket.room);
socket.join(newroom);
socket.emit('updatechat', 'SERVER', 'you have connected to '+ newroom);
// sent message to OLD room
socket.broadcast.to(socket.room).emit('updatechat', 'SERVER', socket.username+' has left this room');
// update socket session room title
socket.room = newroom;
socket.broadcast.to(newroom).emit('updatechat', 'SERVER', socket.username+' has joined this room');
socket.emit('updaterooms', rooms, newroom);
});
// when the user disconnects.. perform this
socket.on('disconnect', function(){
// remove the username from global usernames list
delete usernames[socket.username];
// update list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
// echo globally that this client has left
socket.broadcast.emit('updatechat', 'SERVER', socket.username + ' has disconnected');
socket.leave(socket.room);
});
});
multiRoomChat.html
<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
//var socket = io.connect('http://localhost:8080');
var socket = io.connect('http://localhost:8080');
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});
// listener, whenever the server emits 'updaterooms', this updates the room the client is in
socket.on('updaterooms', function(rooms, current_room) {
$('#rooms').empty();
$.each(rooms, function(key, value) {
if(value == current_room){
$('#rooms').append('<div>' + value + '</div>');
}
else {
$('#rooms').append('<div><a href="#" onclick="switchRoom(\''+value+'\')">' + value + '</a></div>');
}
});
});
function switchRoom(room){
socket.emit('switchRoom', room);
}
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
</script>
<div style="float:left;width:100px;border-right:1px solid black;height:300px;padding:10px;overflow:scroll-y;">
<b>ROOMS</b>
<div id="rooms"></div>
</div>
<div style="float:left;width:300px;height:250px;overflow:scroll-y;padding:10px;">
<div id="conversation"></div>
<input id="data" style="width:200px;" />
<input type="button" id="datasend" value="send" />
</div>
Prenez la peine d'étudier ce code. Les explications détaillées de l'exemple dont nous sommes servi sont ici : http://psitsmike.com/2011/10/node-js-and-socket-io-multiroom-chat-tutorial/ (mais le code a été mis à jour afin de coller aux nouvelles versions de express et socket.io)
- 7.1. Integration du chat de base (pour commencer) au TP3 (le mini jeu)
- 8.
Reprenez le fichier HTML du TP3 (le mini jeu, qui était à rendre, au passage) et intégrez le code du chat. Si vous faites un peu attention, il est possible de faire ça proprement en mettant le code JavaScript dans un fichier chat.js par exemple, ajoutez juste à la page les parties HTML.
Modifiez simplement le code de simpleChatServer.js pour qu'il serve la page HTML avec le bon nom (celle de votre jeu).
Testez en lancant "node simpleChatServer.js", normalement si tout va bien votre jeu marche et le chat aussi.
Maintenant essayez d'envoyer, lorsqu'un joueur se déplace, sa position par le canal du chat, sous la forme d'une chaine de caractères : "le joueur 1 s'est déplacé vers la position x,y"... Normalement cela doit générer pas mal de trafic dans le chat mais dans les tabs/fenêtres de tous les joueurs on doit voir défiler les déplacements des différents joueurs connectés.
Essayez maintenant d'envoyer non pas ces messages sous forme de chaines de caractères, mais directement des objets JavaScript.
Vous pourrez vous inspirez de cet exemple complet et très court qui montre comment partager une position avec NodeJS et socket.io (exemple en bas de la page) : http://stackoverflow.com/questions/9865167/socket-io-and-complex-json-with-node-js-jquery
Testez donc que vous avez bien reçu une position en tant qu'objet.
- 8.1.1. Synchroniser les positions des joueurs quand ils bougent
- 9.
Maintenant que vous avez compris le principe, et bien il ne reste plus qu'à dessiner les joueurs dont on vient de recevoir la position là où il faut afin que les "vues" des différents joueurs soient syncrhonisées.
Bon, normalement, pour faire un vrai jeu il y a encore pas mal de travail. La bonne manière de gérer un jeu en réseau est d'avoir un état complet sur le serveur afin de gérer les joueurs qui arrivent en cours de partie, les deconnexions, le lag, etc. Chaque code client tournant dans le navigateur a une copie synchronisée de cet état. Lire cet article très intéressant sur la manière dont on doit gérer un jeu en réseau: http://gafferongames.com/networking-...me-networking/
Pour l'écriture d'un jeu HTML5, beaucoup d'information dans http://io-2011-html5-games.appspot.com, attention cependant ils utilisent du JavaScript objet assez avancé. Il existe aussi des livres sur la programmation de jeux multi joueurs en HTML5/NodeJS comme par exemple http://books.google.fr/books?id=Lq5h9QrCzUUC&printsec=frontcover&hl=fr#v=onepage&q&f=false
- 9.1.1. Trucs et astuces pour améliorer votre jeu
- 10.
Le site http://www.mapeditor.org/ propose un outil pour générer des "maps" basées sur des "tiles", des images répétitives, mais sans aller aussi loin, il permet de générer au format JSON des formes polygonales dans le menu "layers/masks", pour mettre sur les cartes des masques de collision. Il est très facile de lire ce format côté serveur:
var fs = require('fs');var file = __dirname + '/test.json'; fs.readFile(file, 'utf8', function (err, data) {if (err) {console.log('Error: ' + err);return;} var dataObject = JSON.parse(data); console.dir(data);});
Il est possible de détecter des collisions avec un polygone fermé à l'aide de la fonction context.isPointInPath() de l'API HTML5 (il faudra évidemment tester pour chaque côté du polygone). Voir : http://digitalarts.bgsu.edu/faculty/bonniem/Spring11/artc4330_1/notes/notes26.html
Introduction
Dans ce TP nous allons étudier les WebSockets faisant partie de ce qu'on appelle WebRTC (Real Time Communication), c'est "à côté de HTML5" et c'est un nouveau protocole (et une API JavaScript que doivent implémenter les navigateurs) synchrone, à faible latence, bi-directionnel.
Pour faire des WebSockets il faut un serveur supportant ce protocole. Aujourd'hui presque tous les serveurs (apache, glassfish, tomcat, jetty, etc.) supportent ce protocole nativement ou à l'aide d'extensions. Comme l'application que nous développons est un jeu multi-participants, nous allons utiliser un micro serveur très à la mode en ce moment: NodeJS. Ce serveur intègre l'interpréteur JavaScript V8 de Google et permet de coder la couche applicative côté serveur en JavaScript. On pourra ainsi échanger entre le serveur et les clients des objets JavaScript, sans devoir systématiquement les transformer dans un autre langage.
Nous commencerons par étudier deux applications de chat synchrones basés sur la librairie socket.io très populaire dans le monde des WebSockets (il existe des clients pour Java, C#, etc. il est par exemple possible d'écrire une application Android qui parle avec un serveur nodeJS via des WebSockets).
Livre gratuit en français, sur NodeJS, écrit par d'anciens élèves de la Miage de Nice : http://nodejs.xomis.com/fr/
Installation de NodeJS et des modules express et socket.io
Quel que soit votre système d'exploitation, allez sur le site http://www.nodejs.org et suivez la procédure d'installation (click sur le bouton "install"). Installez la dernière version.
Une fois nodeJS installé, vérifiez qu'il fonctionne en tapant dans une fenêtre dos/shell la commande "node --version", cela devrait afficher quelque chose comme: "node 0.10.x". Ok, tout va bien.
Test d'une première application NodeJS
- Créez un fichier test.js file avec ce contenu :
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
Sauvez le et exécutez la commande suivante, après être allés dans le répertoire où vous avez sauvé le fichier précédent :
node test.js
Vous venez juste de lancer le serveur NodeJS qui répond à cet URL : http://localhost:8124
Ouvrez cet URL dans votre navigateur !
Vous devriez avoir :
Vous venez d'exécuter votre première application NodeJS ;-)
Le fichier test.js contient le code pour un serveur web archi minimal qui n'est même pas capable de lire un fichier html. Il a juste renvoyé des Strings en parlant de protocole HTTP.
Installation de modules complémentaires
NodeJS est extensible grace à un système de "modules", sortes de "packages" à la linux. Lors de l'installation, si vous avez mis le répertoire d'installation dans le PATH, vous devriez avoir accès à la commande "npm" ou Node Package Manager, qui permet d'installer (il faut avoir une connexion qui fonctionne, évidemment...) des modules supplémentaires. Il en existe des centaines aujourd'hui, mais nous allons utiliser deux des plus populaires:
- express donne la possibilité de servir des pages HTML avec CSS, JavaScript etc. Par défaut NodeJS ne sait pas faire cela. Permet aussi de faire des web services REST mais cela n'est pas étudié pendant ce cours.
- socket.io : librairie pour les WebSockets. Donne aussi des solutions de fallbacks si le navigateur utilisé ne supporte pas les WebSockets (comet, ajax, flash sockets), permet de gérer des groupes (sortes de "salles" de chat/ sessions de jeux entre amis, etc.)
Travail à faire :
- Créer un répertoire "chatWithSocketIO" quelque part sur votre disque dur,
- Aller dans ce répertoire (cd...)
- Exécuter la commande "npm install express", attendre qu'elle se termine,
- Exécuter la commande "npm install socket.io", attendre qu'elle se termine.
Dans votre répertoire vous devriez avoir un répertoire supplémentaires intitulé "node_modules" qui contient les modules que vous venez d'ajouter à votre projet. En général on ajoute les modules par projet, non pas globalement.
Si vous développez une autre application, vous pourrez simplement copier ce répertoire si vous ne voulez pas re-télécharger les modules express et socket.io, vous pourrez éventuellement en ajouter.
Test d'une application de chat "simple", utilisant express et socket.io
Nous avons repris ici le code d'un tutorial bien connu du web, qui explique comment réaliser un petit chat avec nodeJS, express et socket.io. Le code a été mis à jour pour fonctionner avec les dernières versions de nodeJS et de express (v3), et socket.io.
Récupérez donc cette archive : ChatSocketIOFixed.zip, le lien est en bas de cette page.
Désarchivez là et faites en sorte pour qu'on ait dans ce répertoire le répertoire node_modules qui contient socket.io et express. Vous devez avoir ceci dans le répertoire à la fin:
Bon, maintenant, executez la commande "node simpleChatServer.js" et ouvrez dans votre navigateur "http://localhost:8080", si votre port 8080 est occupé, changez le port dans le fichier simpleChatServer.js.
Ouvrez cet URL dans deux tabs, et regardez le résultat. Le chat devrait fonctionner. Ouvrez la console de debug de Chrome (y'a que Chrome qui permet de debugger efficacement les WS aujourd'hui, tab "network" et tout en bas "websocket", puis tab "frames"), chattez et regardez les trames échangées....
Maintenant, étudiez le code de simpleChatServer.js, de simpleChat.html, et lisez le tutorial (en anglais) qui correspond : http://psitsmike.com/2011/09/node-js...chat-tutorial/, la partie installation est inutile, nous avons fait autrement pour prendre des versions plus à jour, et les premières lignes du serveur sont aussi différentes, pour les mêmes raisons.
Principes de base de socket.io (on trouve de nombreux tutoriaux en français et en anglais sur le web, nous résumons ici l'essentiel):
- socket.on(type d'un événement, callback) : exécute la fonction de callback quand un événement arrive (le type = une chaine quelconque). Quelques événements comme 'connection' ou 'disconnect' sont prédéfinis.
- Tout le traitement des messages se fait dans io.sockets.on('connection', function (socket) {...}, sans connexion, on ne peut rien faire!
- socket.emit(type de l'évenement, data1, data2) : envoie juste au client connecté un événement (une chaine) et deux paramètres pouvant contenir des données
- io.sockets.emit(type de l'évenement, data1, data2) : envoie à tous les clients connectés un événement (une chaine) et deux paramètres pouvant contenir des données
- socket.broadcast.emit((type de l'évenement, data1, data2) : envoie à tous les clients connectés sauf le client courant un événement (une chaine) et deux paramètres pouvant contenir des données
simpleChatServer.js:
// We need to use the express framework: have a real web server that knows how to send mime types etc.
var express=require('express');
// Init globals variables for each module required
var app = express()
, http = require('http')
, server = http.createServer(app)
, io = require('socket.io').listen(server);
// Indicate where static files are located
app.configure(function () {
app.use(express.static(__dirname + '/'));
});
// launch the http server on given port
server.listen(8080);
// routing
app.get('/', function (req, res) {
res.sendfile(__dirname + '/simpleChat.html');
});
// usernames which are currently connected to the chat
var usernames = {};
io.sockets.on('connection', function (socket) {
// when the client emits 'sendchat', this listens and executes
socket.on('sendchat', function (data) {
// we tell the client to execute 'updatechat' with 2 parameters
io.sockets.emit('updatechat', socket.username, data);
});
// when the client emits 'adduser', this listens and executes
socket.on('adduser', function(username){
// we store the username in the socket session for this client
socket.username = username;
// add the client's username to the global list
usernames[username] = username;
// echo to client they've connected
socket.emit('updatechat', 'SERVER', 'you have connected');
// echo globally (all clients) that a person has connected
socket.broadcast.emit('updatechat', 'SERVER', username + ' has connected');
// update the list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
});
// when the user disconnects.. perform this
socket.on('disconnect', function(){
// remove the username from global usernames list
delete usernames[socket.username];
// update list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
// echo globally that this client has left
socket.broadcast.emit('updatechat', 'SERVER', socket.username + ' has disconnected');
});
});
Et la page simpleChat.html
<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
var socket = io.connect(); // we might pass the URL of the WS server as parameter here
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});
// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updateusers', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('<div>' + key + '</div>');
});
});
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
</script>
<div style="float:left;width:100px;border-right:1px solid black;height:300px;padding:10px;overflow:scroll-y;">
<b>USERS</b>
<div id="users"></div>
</div>
<div style="float:left;width:300px;height:250px;overflow:scroll-y;padding:10px;">
<div id="conversation"></div>
<input id="data" style="width:200px;" />
<input type="button" id="datasend" value="send" />
</div>
Test d'une version "multi-rooms" permettant de gérer des "salles de chat"
Dans cette version, on commence à se rapprocher d'une véritable plate-forme permettant de gérer des "sessions" permettant à plusieurs groupes d'utilisateur de participer en temps réel à un chat privé, de rejoindre d'autres salles de chat etc. Cet exemple porte en lui les éléments fondamentaux pour réaliser un serveur de jeux en réseau, remplacez "salle de chat" par "session de jeu" et vous avez tout compris. Imaginez qu'au lieu de taper du texte dans un champ de saisie, le code client qui tourne dans le navigateur envoie des objets JavaScript sur le même canal, par exemple {type:'position', player:'1', x:'120', y:'130'} ou {type:'event', value':endOfGame'}, et que le serveur les fasse passer aux autres clients connectés.
Principes de gestion de "salles":
- socket.join(nom de la salle) et socket.leave(nom de la salle) pour indiquer que le client actuellement connecté entre dans une salle ou la quitte. Si on entre dans une salle qui n'existe pas on la créée.
- socket.broadcast.to(nom de la salle).emit(...) idem à socket.broadcast.emit mais que pour les gens connectés à la salle (le client courant est exclu du broadcast comme pour socket.broadcast.emit)
- io.sockets.in(nom de la salle).emit(...) : envoie à tout les gens de la salle, y compris le client courant.
Il existe de nombreuses autres fonctions propres à la gestion de "salles" comme "connaitre la liste des salles, et qui est dedans, savoir dans quelle salle est un client donné, etc", voir ce tutorial en anglais très complet: https://github.com/LearnBoost/socket.io/wiki/Rooms
multiRoomChatServer.js
// We need to use the express framework: have a real web server that knows how to send mime types etc.
var express=require('express');
// Init globals variables for each module required
var app = express()
, http = require('http')
, server = http.createServer(app)
, io = require('socket.io').listen(server);
// Indicate where static files are located. Without this, no external js file, no css...
app.configure(function () {
app.use(express.static(__dirname + '/'));
});
// launch the http server on given port
server.listen(8080);
// routing with express, mapping for default page
app.get('/', function (req, res) {
res.sendfile(__dirname + '/multiRoomChat.html');
});
// usernames which are currently connected to the chat
var usernames = {};
// rooms which are currently available in chat
var rooms = ['room1','room2','room3'];
io.sockets.on('connection', function (socket) {
// when the client emits 'adduser', this listens and executes
socket.on('adduser', function(username){
// store the username in the socket session for this client
socket.username = username;
// store the room name in the socket session for this client
socket.room = 'room1';
// add the client's username to the global list
usernames[username] = username;
// send client to room 1
socket.join('room1');
// echo to client they've connected
socket.emit('updatechat', 'SERVER', 'you have connected to room1');
// echo to room 1 that a person has connected to their room
socket.broadcast.to('room1').emit('updatechat', 'SERVER', username + ' has connected to this room');
socket.emit('updaterooms', rooms, 'room1');
});
// when the client emits 'sendchat', this listens and executes
socket.on('sendchat', function (data) {
// we tell the client to execute 'updatechat' with 2 parameters
io.sockets.in(socket.room).emit('updatechat', socket.username, data);
});
socket.on('switchRoom', function(newroom){
socket.leave(socket.room);
socket.join(newroom);
socket.emit('updatechat', 'SERVER', 'you have connected to '+ newroom);
// sent message to OLD room
socket.broadcast.to(socket.room).emit('updatechat', 'SERVER', socket.username+' has left this room');
// update socket session room title
socket.room = newroom;
socket.broadcast.to(newroom).emit('updatechat', 'SERVER', socket.username+' has joined this room');
socket.emit('updaterooms', rooms, newroom);
});
// when the user disconnects.. perform this
socket.on('disconnect', function(){
// remove the username from global usernames list
delete usernames[socket.username];
// update list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
// echo globally that this client has left
socket.broadcast.emit('updatechat', 'SERVER', socket.username + ' has disconnected');
socket.leave(socket.room);
});
});
multiRoomChat.html
<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
//var socket = io.connect('http://localhost:8080');
var socket = io.connect('http://localhost:8080');
// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});
// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});
// listener, whenever the server emits 'updaterooms', this updates the room the client is in
socket.on('updaterooms', function(rooms, current_room) {
$('#rooms').empty();
$.each(rooms, function(key, value) {
if(value == current_room){
$('#rooms').append('<div>' + value + '</div>');
}
else {
$('#rooms').append('<div><a href="#" onclick="switchRoom(\''+value+'\')">' + value + '</a></div>');
}
});
});
function switchRoom(room){
socket.emit('switchRoom', room);
}
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});
// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});
});
</script>
<div style="float:left;width:100px;border-right:1px solid black;height:300px;padding:10px;overflow:scroll-y;">
<b>ROOMS</b>
<div id="rooms"></div>
</div>
<div style="float:left;width:300px;height:250px;overflow:scroll-y;padding:10px;">
<div id="conversation"></div>
<input id="data" style="width:200px;" />
<input type="button" id="datasend" value="send" />
</div>
Prenez la peine d'étudier ce code. Les explications détaillées de l'exemple dont nous sommes servi sont ici : http://psitsmike.com/2011/10/node-js-and-socket-io-multiroom-chat-tutorial/ (mais le code a été mis à jour afin de coller aux nouvelles versions de express et socket.io)
Integration du chat de base (pour commencer) au TP3 (le mini jeu)
Reprenez le fichier HTML du TP3 (le mini jeu, qui était à rendre, au passage) et intégrez le code du chat. Si vous faites un peu attention, il est possible de faire ça proprement en mettant le code JavaScript dans un fichier chat.js par exemple, ajoutez juste à la page les parties HTML.
Modifiez simplement le code de simpleChatServer.js pour qu'il serve la page HTML avec le bon nom (celle de votre jeu).
Testez en lancant "node simpleChatServer.js", normalement si tout va bien votre jeu marche et le chat aussi.
Maintenant essayez d'envoyer, lorsqu'un joueur se déplace, sa position par le canal du chat, sous la forme d'une chaine de caractères : "le joueur 1 s'est déplacé vers la position x,y"... Normalement cela doit générer pas mal de trafic dans le chat mais dans les tabs/fenêtres de tous les joueurs on doit voir défiler les déplacements des différents joueurs connectés.
Essayez maintenant d'envoyer non pas ces messages sous forme de chaines de caractères, mais directement des objets JavaScript.
Vous pourrez vous inspirez de cet exemple complet et très court qui montre comment partager une position avec NodeJS et socket.io (exemple en bas de la page) : http://stackoverflow.com/questions/9865167/socket-io-and-complex-json-with-node-js-jquery
Testez donc que vous avez bien reçu une position en tant qu'objet.
Synchroniser les positions des joueurs quand ils bougent
Maintenant que vous avez compris le principe, et bien il ne reste plus qu'à dessiner les joueurs dont on vient de recevoir la position là où il faut afin que les "vues" des différents joueurs soient syncrhonisées.
Bon, normalement, pour faire un vrai jeu il y a encore pas mal de travail. La bonne manière de gérer un jeu en réseau est d'avoir un état complet sur le serveur afin de gérer les joueurs qui arrivent en cours de partie, les deconnexions, le lag, etc. Chaque code client tournant dans le navigateur a une copie synchronisée de cet état. Lire cet article très intéressant sur la manière dont on doit gérer un jeu en réseau: http://gafferongames.com/networking-...me-networking/
Pour l'écriture d'un jeu HTML5, beaucoup d'information dans http://io-2011-html5-games.appspot.com, attention cependant ils utilisent du JavaScript objet assez avancé. Il existe aussi des livres sur la programmation de jeux multi joueurs en HTML5/NodeJS comme par exemple http://books.google.fr/books?id=Lq5h9QrCzUUC&printsec=frontcover&hl=fr#v=onepage&q&f=false
Trucs et astuces pour améliorer votre jeu
Le site http://www.mapeditor.org/ propose un outil pour générer des "maps" basées sur des "tiles", des images répétitives, mais sans aller aussi loin, il permet de générer au format JSON des formes polygonales dans le menu "layers/masks", pour mettre sur les cartes des masques de collision. Il est très facile de lire ce format côté serveur:
var fs = require('fs'); var file = __dirname + '/test.json'; fs.readFile(file, 'utf8', function (err, data) { if (err) { console.log('Error: ' + err); return; } var dataObject = JSON.parse(data); console.dir(data); });
Il est possible de détecter des collisions avec un polygone fermé à l'aide de la fonction context.isPointInPath() de l'API HTML5 (il faudra évidemment tester pour chaque côté du polygone). Voir : http://digitalarts.bgsu.edu/faculty/bonniem/Spring11/artc4330_1/notes/notes26.html
|