NodeJs ~ 04 : Construction d’un mini projet (partie2)

Ce tutoriel fait partie d’une série, cliquez ici pour accéder au sommaire.

Après avoir créé notre serveur http de fortune NodeJs, je vous ai vilement laissé sur votre faim,
je profite donc de ces quelques minutes à moi pour avancer un peu cette partie avec vous.

Aujourd’hui nous allons nous intéresser à une librairie particulièrement utile permettant d’exploiter
toute la puissance de NodeJs : j’ai nommé socket.io, une librairie de gestion des websocket.

Les websocket, quoi qu’est-ce ?

Pour faire court, c’est un protocole qui vas nous permettre une communication instantanée entre le serveur et le client, aussi notre chat n’utiliseras pas une fumeuse technique de rafraîchissement ajax tous les x secondes pour vérifier les nouveau messages, tout se fera de manière instantanée.
Ainsi pas d’échanges inutiles client/serveur, pas de latences,pas d’attentes synchrones, pas de surcharge useless du navigateur, pas d’ajax et une structure plus “propre”

Pas mal ton truc ! C’est une techno nouvelle que propose nodejs ?

Non, les websockets c’est assez vieux, ils peuvent être utilisés dans pas mal de langages et sous pas mal de serveurs ^^, c’est juste un peu obscure à utiliser la plupart du temps.

Mais alors on vas se jouer la vie ?

Pas cette fois mon tit pote (parce que par défaut, tu es mon tit pote :p) !! En nodeJs comme en php,python…[“fill here with your favorite langage”] il existe une librairie pour tout.!!

Cette fois c’est socket.io qui vas nous sauver la mise, en l’installant nous feront d’une pierre deux coup : apprendre à utiliser une librairie, et simplifier les échanges de notre chat grâce a celle ci.

Prêt ? On se lance !!

Installation de socket.io

Comment installer une librairie sous nodejs ? Avec le gestionnaire de packet NPM que nous avons déjà évoqué ! (vous suivez rien bande de gaufres).

On ouvre une console, on se place dans notre dossier hi!

cd /mon/chemin/vers/hi!

 

On installe la librairie à l’aide de la commande “npm install” + nom de la librairie à installer (hardcoooore!!)

npm install socket.io

Après tout un tas de baragouin coloré et quelques messages de warning flippants plus tard (oui le gros message “error” en rouge sous Windows n’est pas aussi méchant qu’il le parait), on se retrouve avec un dossier node_modules dans notre dossier hi, c’est lui qui contiendra toutes les librairies installées avec npm sur ce projet.

On peut maintenant utiliser socket.io dans notre projet.

Notez qu’un nombre effarant de librairies sont proposées sur le site de npm, (et son donc installable de la même manière) je vous invite à y faire un tour
ça risque de vite devenir votre “cave à vin” du nodejs.

De plus tous le monde est autorisé à transformer sont projet nodejs en librairie et à la proposer à npm n’hésitez donc pas à contribuer si vous sortez une bafouille réutilisable dans d’autres projets.

Mais je digresse (“gresseuhh!!“), focalisons nous sur socket.io

Fonctionnement de socket.io

Tout vas s’articuler autour d’un système d’événements, le serveur et le client pouvant chacun “écouter” ou “émettre un événement”, par exemple :

1.Client : émet l’événement : “création message” avec pour données “contenu du message”
2.Serveur : écoute l’événement “création message” avec pour conséquence l’émission d’un autre événement “envoyer message a tous” avec pour données “contenu du message”
3.Client : écoute l’événement  “envoyer message a tous” avec pour conséquence l’affichage de ce message.

Donc en gros on vas “créer” un événement avec la fonction “emit” et on vas “écouter” les événements avec la fonction “on“.

Installation coté serveur

On reprends donc le code du précédent tuto,commençons par le code coté serveur :

En début de fichier, on récupère la librairie socket.io qui simplifie les échanges websocket

io = require('socket.io');

Puis on greffe socket.io au serveur http qu’on à déjà créé (après …httpServer.listen(69);)

io.listen(httpServer);

Voila, coté serveur on peux déjà commencer à utiliser les événements.

Installation coté client

C’est encore plus simple, il suffit d’appeler une page js de manière traditionnelle

<script src="/socket.io/socket.io.js"></script>

Puis quelque part dans notre javascript (disons dans main.js) on initialise la variable

socket qui vas nous permettre d’utiliser socket.io par la suite

<script>
var socket = io.connect('http://localhost:69');
</script>

Utilisation

On a plus qu’a placer nos événements comme on le souhaite 🙂

Pour commencer, créons une écoute d’événement qui vas se déclencher lorsqu’un visiteur se connecte au serveur
Pour le moment on met juste un message de log qui devrais s’afficher dans la console serveur quand quelqu’un se connecte.

Coté serveur (server.js)

io.sockets.on('connection',function(socket){
console.log("Quelqu'un est connecté");
});

On relance le serveur pour tester

node server.js

On rafraîchis la page 127.0.0.1:69 et tataaaaa ! On se retrouve avec une connexion dans la console.

Appliquons cette techno à notre chat

Vous l’aurez compris il nous faudra créer tout un tas d’événements chez le client qui puissent être écouté et traité par le serveur, pour le moment on a un seul événement : “écoute d’une connexion anonyme à la page” mais nous allons devoir en créer d’autres pour les actions suivantes :

– Connexion (Identification d’un utilisateur par login + mot de passe)
– Déconnexion
– Envois d’un message

Mais il faudra également que le serveur créé lui aussi des événements pour informer les autres clients des choses importantes, prenons pour exemple une nouvelle connexion au chat :

1.Client : l’utilisateur se connecte avec son login + mot de passe –> Événement connexion

2.Serveur : reçoit le login + mdp, teste si il est bon, créé un événement pour
informer les autres clients –Événement nouveau connecté

3.Clients : notifie le nouvel arrivé avec un avatar + un message “xxx s’est connecté”.

Bref l’info n’est pas a sens unique, le serveur comme les clients ont besoin de communiquer.

Faisons tout de suite un “annuaire” des événements reçu et émis de chaque cotés histoire de ne pas nous perdre:

Client
======

> Envoyé
– Connexion anonyme à la page
– Identification (login + mot de passe)
– Nouveau message (contenu du message)
– Déconnexion
> Reçu
– Résultat de l’identification (succès ou échec)
– Nouvel utilisateur connecté (infos sur l’utilisateur)
– Nouveau message reçu (infos sur le message, texte, auteur, date…)
– Déconnexion d’un utilisateur (info sur l’utilisateur)

Serveur
=======

> Reçu
– Connexion anonyme à la page
– Identification (login + mot de passe)
– Nouveau message (contenu du message)
– Déconnexion

> Envoyé
– Résultat de l’identification (succès ou échec)
– Nouvel utilisateur connecté (infos sur l’utilisateur)
– Nouveau message reçu (infos sur le message, texte, auteur, date…)
– Déconnexion d’un utilisateur (info sur l’utilisateur)

Vous l’aurez constatés les reçu du client sont les envoyés du serveur et réciproquement, ce qui se tient, pas de réception sans émission :), évidemment vous pouvez créer un événement avec personne pour l’écouter mais c’est un peu useless :p.

Une dernière petite précision :

La librairie socket.io permet l’envois d’un événement a des “cibles” particulières, c’est a dire que vous n’êtes pas obligés d’envoyer un événement a tous le monde à chaque fois.

On envoie l’événement à tous le monde sans exception
io.sockets.emit(‘nom_evenement’,data);

On envoie l’événement uniquement à l’utilisateur courant
socket.emit(‘nom_evenement’,data);

On envoie l’événement à tous le monde sauf à l’utilisateur courant
socket.broadcast.emit(‘nom_evenement’,data);

Pratique dans certains cas, par exemple pour n’avertir que l’utilisateur courant qu’il à bien été connecté
et pour n’avertir que les autres qu’un nouvel utilisateur est connecté.

Assez palabré bande de moules, au turbin !
On vas repartir de nos sources du tuto précédent histoire de ne pas se retaper
toute la partie ‘gestion du serveur http’.

Partie client

Dans main.js

var socket;
$(document).ready(function(){
socket = io.connect('http://localhost:69');
//Au chargement du site, on charge la page d'accueil : home.html
page('home');
});
//Fonction permettant de charger un contenu de page dans la div centrale en ajax
function page(page){
$('.window-page').load(page+'.html',function(){
switch(page){
case 'chat':
//ECOUTE D'EVENEMENTS
socket.on('connected', function (data) {
$('#login-container').fadeOut(200);
info('Je suis connecté !');

$('#chat-avatar').attr('src',data.user.avatar);
for(var key in data.connected){
console.log(data.connected);
var usr =  data.connected[key];
$('#chat-users').prepend('<li id="user-'+usr.id+'"><img src="'+usr.avatar+'"><span>'+usr.login+'</span></li>');
}
});
socket.on('new_user', function (user) {
info(user.login+' est arrivé!!');
$('#chat-users').prepend('<li id="user-'+user.id+'"><img src="'+user.avatar+'"><span>'+user.login+'</span></li>');
});
socket.on('new_message', function (data) {
message(data);
});
socket.on('left_user', function (user) {
$('#user-'+user.id).remove();
info(user.login+' est partis!!');
});
$('#chat-message').keyup(function(e){
if(e.keyCode==13) send_message();

});
$('#login-container input').keyup(function(e){
if(e.keyCode==13) send_login();
});
break;
}
});
}

//Affichage d'un message dans le chat : on remplis un "template" de message avec les parametres donnés en arguments et on affiche.
function message(message){
var template = $('#chat-message-template').clone();
$('.chat-message-avatar',template).prepend('<img src="'+message.avatar+'"/>');
$('login',template).replaceWith(message.login);
$('message',template).replaceWith(message.message);
$('date',template).replaceWith(message.date);
$(template).removeAttr('id');
$('#chat-messages').prepend(template);
$(template).fadeIn(300);
}
//Affichage d'une infos serveur dans le chat
function info(message){
$('#chat-messages').prepend('<li><i></i> '+message+'</li>');
}
//Fonction executée lors de l'appuis sur "connexion"
function send_login(){
socket.emit('login',{login:$('#login').val(),password:$('#password').val()});
}
//Fonction executée lors de l'appuis sur entrée dans le champs message de chat
function send_message(){
socket.emit('message',{message:$('#chat-message').val()});
$('#chat-message').val('').focus();
}

//Fonctions permettant d'afficher et de faire disparaitre le préloader à chaques action ajax
$(document).ajaxStop(function() {
$('.preloader').fadeOut(200);
});
$(document).ajaxStart(function() {
$('.preloader').show();
});

 

Dans index.html

<!DOCTYPE html>
<html>
<head>
<title>Hi!</title>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/main.js"></script>
<link type="text/css" href="css/gui.css" rel="stylesheet" />

</head>
<body>
<div>
<ul>
<li onclick="page('home');"><i></i></li>
<li onclick="page('chat');"><i></i></li>
<li onclick="page('about');"><i></i></li>
</ul>
<div>
<section></section>
<div></div>
</div>
</div>
</body>
</html>

 

dans chat.html

<h1><i></i> Chat</h1>
<hr/>
<js>console.log('test');</js>
<div id="login-container">
<input type="text" placeholder="Identifiant" id="login"> <input type="text" placeholder="Mot de passe" id="password"> <button onclick="send_login();">Connexion</button>
</div>

<div >
<div>
<img src="avatars/default.jpg" id="chat-avatar">
<textarea type="text" placeHolder="Bafouillez Ici..." id="chat-message"></textarea>
</div>

<ul id="chat-messages"></ul>
<ul id="chat-users"></ul>

<li style="display:none;" id="chat-message-template">
<div>
</div>
<div>
<span><login/> <span>- <date/></span></span>
<p><message/></p>
</div>
</li>

</div>

 

Partie serveur

server.js

//Récuperation de nos configuration perssonalisées
conf = require('./conf.json');
//Inclusion de la librairie http permettant de créer un serveur web
http = require('http');
//Récuperation de la librairie url qui facilite l'analyse de l'url des requetes reçues
url = require('url');
//Récuperation de la librairie fs qui permet la gestion de fichiers (ecriture, lecture...)
fs = require('fs');

//Création d'un serveur http sur le port spécifié dans conf.json (section http)
//Dès qu'une requetes est reçu sur 127.0.0.1:port, la fonction onRequest est executée
//et prends pour parametre un objet requete (ce qui est recu) et un objet réponse (ce que le serveur vas retourner)
var httpServer = http.createServer(onRequest).listen(conf.http.port);

//Fonction onRequest est executée à chaques requete sur le serveur
function onRequest(request, response) {
//On récupere l'url requise et on la parse pour n'avoir que la partie qui nous interesse
var pathname = url.parse(request.url).pathname;
//On récupère l'extension du fichier requis (html,css,js,jpg...)
var extension = pathname.split('.').pop();
//Si l'utilisateur ne spécifie pas de page, on prend l'index par défaut 'index.html' (spécifié dans conf.json)
if(pathname=='/') pathname = conf.http.index;
//On définis l'entete de réponse au code 200 signifiant : "succès de la requete" et
//on définit le type mime du fichier de retour (image/jpg pour un jpg, text/css pour un css etc..)
//Les types mimes sont définit dans le fichier json conf.
response.writeHead(200, {'Content-Type': conf.http.mime[extension]});
try {
//On lit le contenu du fichier demandé (en ajoutant la racine www spécifiée dans conf.json)
//et on le retour a l'utilisateur
response.end(preprocessor(fs.readFileSync(conf.http.www + pathname)));
}catch(e){
//Si la lecture du fichier à échouée on renvoie un code d'erreur 404
response.writeHead(404, {'Content-Type': 'text/html'});
//on affiche le contenu de la page 404.html (chemin spécifiée dans conf.json)
response.end(e+fs.readFileSync(conf.http.error['404']));
}
}

function preprocessor(src){

test = src.toString().match(/<js>(.*?)<\/js>/);
if(test!=null) eval(test[1]);

return src;
}

//Utilisation de la librairie socket.io sur le port 69
var io = require('socket.io').listen(httpServer,{ log: false });
//Tableau contenant les utilisateurs connectés
var connected = new Array();
//Tableau des comptes utilisateurs (base de donnée utilisateurs)
var users = [
{id:1,login:'valentin',password:'valentin',mail:'valentin@valentin.fr',avatar:'avatars/valentin@valentin.fr.png'},
{id:2,login:'idleman',password:'idleman',mail:'monmail@monndd.fr',avatar:'avatars/monmail@monndd.fr.jpg'},
];

//Tout commence lorsqu'un visiteur se connecte au serveur, un socket personnel lui est alloué
io.sockets.on('connection', function (socket) {
console.log('Nouvelle connexion anonyme');
socket.set('user', {login:'anonymous',password:'',mail:'ano@nymous.com',avatar:'avatars/default.jpg'});
//Si le visiteur anonyme fait une demande d'authentification
socket.on('login', function (data) {
//Si le visiteur possede bien un compte avec ce login et ce password
var user = exist(data.login,data.password);
if(user!=false){
//utilisateur authentifié
console.log("Connexion authentifiée : "+user.login);

//On met a jour la liste des connectés
connected.push({id:user.id,login:user.login,avatar:user.avatar});
//On lie l'utilisateur connecté au socket de manière a pouvoir le récuperer partout
socket.set('user', user, function () {
//On avrtis tous le monde (sauf l'utilisateur) que l'utilisateur est connecté et on leur retournes quelques infos sur lui
socket.broadcast.emit('new_user', {id:user.id,login:user.login,avatar:user.avatar});
//On avertis l'utilisateur qu'il est bien connecté et on lui retourne toutes ses infos de compte
socket.emit('connected', {connected:connected,user:user});
});
}
});

//Lorsqu'un message est envoyé
socket.on('message', function(data) {
//On recupere l'utilisateur lié au socket
socket.get('user', function (err, user) {
console.log("Message envoyé par : "+user.login);
var currentdate = new Date();
var date = currentdate.getDate() + "/"
+ (currentdate.getMonth()+1)  + "/"
+ currentdate.getFullYear() + " @ "
+ currentdate.getHours() + ":"
+ currentdate.getMinutes() + ":"
+ currentdate.getSeconds();
//On renvois son message a tous le monde (lui compris)
io.sockets.emit('new_message', {id:user.id,login:user.login,avatar:user.avatar,message:data.message,date:date});
});
});

//Lorsqu'un socket se déconnecte
socket.on('disconnect', function () {
//On recupere l'utilisateur lié au socket
socket.get('user', function (err, user) {
console.log("Déconnexion : "+user.login);
//On informe ceux qui restent que l'utilisateur est partis
removeConnected(user.id);
socket.broadcast.emit('left_user', {id:user.id,login:user.login});
});
});

});

//Fonction permettant la recherche d'un utilisateur à partir d'un login + mdp, retourne false si rien n'est trouvé
//ou l'objet utilisateur si les login + mdp sont bons.
function exist(login,password){
var response = false;
for(var key in users){
if(users[key].login == login && users[key].password == password)response = users[key];
}
return response;
}

function removeConnected(id){
for(var key in connected){
if(connected[key].id == id) connected[key] = null;
}
}

 

Utilisation

On peux maintenant tester en lançant le programme:

node server.js

On rafraîchis la page 127.0.0.1:69 sur le navigateur et on laisse la magie du socket opérer

Le compte par défaut est login:idleman password:idleman, vous pouvez en ajouter ou modifier le compte existant dans la page server.js

Notez bien qu’on utilise ici les sockets pour un simple chat de manière à illustrer le côté “instantané” de cette techno,
mais les sockets peuvent servir à bien des choses et vous donner un sacré coup de pouce sur toutes les applications
nécessitant une haute réactivité (statistiques en temps réel, surveillance/monitoring, domotique *sifflotte* et autres…)

Pour les mauvais élèves, l’archive du tuto est disponible ICI : nodejs-partie-2.