Développer un chat temps-réel avec Socket.io (partie 1 / 3)

Après avoir introduit les WebSocket et Socket.io dans un précédent billet, nous allons cette fois voir comment développer une application basique mais fonctionnelle avec cet outil. Comme expliqué dans la première partie, Socket.io permet de développer des applications temps-réel. L'application temps-réel la plus simple à développer pour se faire la main semble évidente : un chat.

Trève de bavardages, au boulot.

Sources

Vous pouvez retrouver les sources de ce tutoriel sur GitHub. Il est possible de les télécharger directement depuis cette page ou de cloner la branche Git correspondant à cette partie du tutoriel.

git clone --branch part-1 https://github.com/BenjaminBini/socket.io-chat.git

Prérequis

Je supposerai dans ce tutoriel que vous avez Node.js et npm installés sur votre machine et que vous savez un minimum les utiliser.
Ce billet est une adaptation de la documentation officiel de Socket.io.

Initialisation du projet

Comme tout bon projet basé sur Node.js, nous allons créer un fichier package.json avec les informations de base sur notre programme.

{
	"name": "my-first-chat",
    "version": "0.0.1",
    "description": "my first socket.io app",
    "dependencies": {}
}

Afin de nous faciliter la tâche nous allons utiliser le framework Express. Si vous ne le connaissez pas, sachez qu'Express est un mini-framework web permettant de développer beaucoup plus rapidement certaines tâches fastidieuses avec Node.js nu (la gestion des routes, entre autres).

On va donc l'installer et l'ajouter aux dépendances définies dans notre package.json.

npm install --save express@4.10.2

Passons à l'initialisation du front-end !

Initialisation du client

La partie cliente va être placée dans un dossier /public, où l'on va mettre l'ensemble des fichiers accessibles à l'utilisateur (en gros l'html, le css et le js).

Créez donc un répertoire /public à la racine de votre projet et mettez-y trois fichiers :

  • index.html
  • client.js
  • style.css

L'HTML va contenir simplement la structure de notre application avec une liste prête à accueillir nos messages ainsi qu'un champ de saisie. On en profite également pour importer notre fichier de style (style.css), JQuery et notre fichier js (client.js).

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
    <title>Socket.IO chat</title>
    <style>
    </style>
  </head>
  <body>
    <section id="chat">
      <ul id="messages"></ul>
      <form action="">
        <input id="m" autocomplete="off" /><button>Send</button>
      </form>
    </section>
    <script src="http://code.jquery.com/jquery-1.11.1.js"></script>
    <script src="client.js"></script>
  </body>
</html>

Un peu de CSS pour rendre le tout moins moche.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font: 13px Helvetica, Arial;
    transition: all 0.5s;
}

section#chat form {
    background: #000;
    padding: 3px;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 50px;
}
section#chat form input {
    border: 0;
    padding: 5px 10px;
    width: 90%;
    height: 100%;
    margin-right: .5%;
	font-size: 20px;
}
section#chat form button {
    width: 9%;
    height: 100%;
    background: #e67e22;
    float: right;
    border: none;
    margin-right: 0.5%;
    font-size: 17px;
    color: white;
}
section#chat #messages {
    list-style-type: none;
    margin: 0;
    padding: 0;
    font-size: 15px;
}
section#chat #messages li {
    padding: 5px 10px;
}
section#chat #messages li:nth-child(odd) {
    background: #eee;
}

Pour l'instant on laisse client.js vide, ce sera pour la suite.

Initialisation du serveur

On doit créer le fichier principal de notre application, server.js.
Ca va rester très basique, on va écouter sur un port (3000) et renvoyer le contenu du dossier /public aux utilisateurs.

// Tout d'abbord on initialise notre application avec le framework Express 
// et la bibliothèque http integrée à node.
var express = require('express');
var app = express();
var http = require('http').Server(app);

// On gère les requêtes HTTP des utilisateurs en leur renvoyant les fichiers du dossier 'public'
app.use("/", express.static(__dirname + "/public"));

// On lance le serveur en écoutant les connexions arrivant sur le port 3000
http.listen(3000, function(){
  console.log('Server is listening on *:3000');
});

Exécutez node server depuis le dossier où se trouve votre application pour la démarrer.
Si tout s'est bien passé, vous devriez voir le message Server is listening on *:3000 s'afficher. Par ailleurs si vous vous rendez à l'adresse http://localhost:3000, vous devriez voir votre fenêtre de chat avec une zone de saisie en bas.

Buenissimo, mais jusqu'à maintenant on n'a pas encore vu de Socket.io là-dedans ! Alors allons-y.

Intégration de Socket.io

Comme notre application a deux parties (client et serveur), de même, Socket.io se présente en deux parties :

  • La partie serveur, qui vient s'intégrer comme module à l'application Node.js
  • La partie client, qui est une bibliothèque Javascript à intégrer à notre client web

Ajoutons donc Socket.io à notre projet.

npm install --save socket.io

Plus qu'à l'initialiser dans server.js. Modifiez les premières lignes de votre fichier pour qu'elles ressemblent à cela :

var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);

On a ici simplement créé la variable io, qui va nous permettre de travailler avec Socket.io !

Côté client rien de plus simple : la bibliothèque est automatiquement mise à disposition à l'URL /socket.io/socket.io.js. Ajoutez donc la ligne suivante juste avant <script src="client.js"></script> dans votre fichier index.html.

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

Et initialisions le tout dans notre fichier client.js :

var socket = io();

Pas besoin de spécifier l'URL à laquelle le client Socket.io doit se connecter, par défaut il va tenter de se connecter sur le serveur qui héberge la page cliente.

L'événement 'connection'

Pour vérifier que tout fonctionne, nous allons utiliser un premier événement spécifique à Socket.io, l'événement connection. A chaque fois qu'un utilisateur se connecte sur la page, l'événement est déclenché. Nous allons donc l'écouter et afficher un message dans la console à chaque déclenchement. Ajoutez ce code à server.js.

io.on('connection', function(socket){
  console.log('a user connected');
});

Redémarrez l'application (node server) et rendez-vous sur votre page (http://localhost:3000/).
Regardez la console :

Server is listening on *:3000
a user connected

Ouvrez d'autres onglets (ou actualisez la page), le message réapparaît à chaque fois. Parfait !

L'événement 'disconnect'

On peut également écouter l'événement disconnect qui est déclenché à chaque fois qu'un utilisateur se déconnecte de la socket sur laquelle il était connecté. Logiquement, cet événement est rattaché à la socket d'un utilisateur en particulier et non au module Socket.io en général.
C'est donc sur l'instance de l'objet socket (passé en argument à la fonction de callback de l'événement connection) qu'il faut écouter.

io.on('connection', function(socket){
  console.log('a user connected');
  socket.on('disconnect', function(){
    console.log('user disconnected');
  });
});

Relançez l'application, ouvrez un onglet, refermez-le, la console affiche bien user disconnected à chaque déconnexion (fermeture du navigateur, de l'onglet, actualisation de la page).

Emettre des événements

Bien, on voit donc que le serveur reçoit des événements du client en temps réel, par exemple la connexion et la déconnexion d'un utilisateur. Maintenant, le principe de Socket.io c'est que le client et le serveur peuvent émettre et recevoir des événements en temps réel et ainsi réagir en conséquence. De plus, il est possible de transmettre des données avec ces événements ! La plupart du temps il s'agira d'objets encodés en JSON mais des données binaires sont aussi une option.

Quel est l'événement le plus important à émettre dans une application de chat ? L'envoi d'un message évidemment.
Il nous faut donc émettre un événement quand l'utilisateur va valider le formulaire (c'est à dire cliquer sur "Send"). Il faut également envoyer le texte du message. Cela se fait très simplement ! Modifions client.js.

var socket = io();

$('form').submit(function(e) {
	e.preventDefault(); // On évite le recharchement de la page lors de la validation du formulaire
    // On crée notre objet JSON correspondant à notre message
	var message = {
		text : $('#m').val()
	}
	socket.emit('chat-message', message); // On émet l'événement avec le message associé
    $('#m').val(''); // On vide le champ texte
    if (message.text.trim().length !== 0) { // Gestion message vide
      socket.emit('chat-message', message);
    }
    $('#chat input').focus(); // Focus sur le champ du message
});

Maintenant que l'événement est émis, il faut le réceptionner côté serveur. Pour cela nous allons modifier server.js.

[...]
io.on('connection', function (socket) {

  /**
   * Log de connexion et de déconnexion des utilisateurs
   */
  console.log('a user connected');
  socket.on('disconnect', function () {
    console.log('user disconected');
  });

  /**
   * Réception de l'événement 'chat-message' et réémission vers tous les utilisateurs
   */
  socket.on('chat-message', function (message) {
    console.log('message : ' + message.text);
  });
});
[...]

Relancez votre application, rendez-vous sur votre page et envoyez un message. Regardez la console, les messages devraient s'afficher.

node server
Server is listening on *:3000
a user connected
message : Hello !
message : How are you ?
message : Socket.io is great !

Voilà un bon début. Ce n'est bien sûr pas terminé, il faut maintenant gérer l'affichage des messages chez les utilisateurs.

Les événements envoyés par le serveur

Pour l'instant, nos événements ont été envoyés d'un client vers le serveur.
L'inverse est bien sûr tout aussi important. Il existe trois types d'événements envoyés par le serveur :

  • l'événement simple, envoyé au travers d'une seule socket (vers un seul utilisateur)
    • Exemple :
socket.emit('random-event', randomContent);
  • le broadcast, envoyé à tout le monde sauf à la socket courante (c'est à dire sauf à l'utilisateur courant)
    • Exemple :
socket.broadcast.emit('random-event', randomContent);
  • la combinaison des deux : l'émission d'un événement à tous les clients connectés au serveur
    • Exemple :
    io.emit('random-event', randomContent);

Ici, nous devons transmettre les messages à tous les utilisateurs. On va donc modifier server.js comme suit.

[...]
  /**
   * Réception de l'événement 'chat-message' et réémission vers tous les utilisateurs
   */
  socket.on('chat-message', function (message) {
    io.emit('chat-message', message);
  });
[...]

Comprenez bien ce qu'il se passe :

  • un utilisateur accède à la page et se connecte au server via socket.io
  • l'utilisateur envoie un message et émet donc un événement chat-message
  • le serveur reçoit l'événement, ce qui déclenche l'émission d'un autre événement (aussi appelé chat-message, mais cela aurait pu être ce que l'on veut) qui lui, sera envoyé à tous les utilisateurs connectés

Il faut donc réceptionner l'événement côté client et afficher le message. Très simple, ajoutez ces quelques lignes à client.js.

/**
 * Réception d'un message
 */
socket.on('chat-message', function (message) {
  $('#messages').append($('<li>').text(message.text));
});

Ouvrez deux fenêtres côtes à côtes, tapez un message dans l'une, validez... il s'affiche dans l'autre. Et vice-versa ! Le chat est fonctionnel.


Source de l'animation : SOCKET.IO (MIT)

Sources

Vous pouvez retrouver les sources de ce tutoriel sur GitHub. Il est possible de les télécharger directement depuis cette page ou de cloner la branche Git correspondant à cette partie du tutoriel.

git clone --branch part-1 https://github.com/BenjaminBini/socket.io-chat.git

Conclusion

Je ne sais pas ce que vous en pensez, mais la première fois que j'ai découvert ces WebSocket et socket.io, je ne m'attendais pas à faire quelque chose comme cela en si peu de lignes de codes et si facilement ! Là est toute la puissance de Socket.io, une programmation événementielle basée sur la transmission de données en temps réel entre un serveur et des clients.

La suite

C'est un bon début, mais on a de nombreuses limitations avec cette version ! Pas de pseudos, pas d'identification, pas de liste des utilisateurs connectés... Il faut désormais aller plus loin en ajoutant des fonctionnalités. Celles proposées par le tutoriel officiel sont :

  • Afficher un message lors de la connexion et la déconnexion des utilisateurs
  • Ajouter le support des noms d'utilisateurs
  • Affichage de la liste des utilisateurs connectés
  • Affichage d'un message "xxx is typing" quand un utilisateur est en train d'écrire
  • Gestion d'un historique de messages (affichage des derniers messages envoyés avant la connexion de l'utilisateur)
  • ...

Dans les deux prochains billets, nous nous occuperons d'ajouter plusieurs de ces fonctionnalités !

Cliquez ici pour accéder à la partie 2


javascript | nodejs | websocket | socket.io | chat | tutoriel


Strasbourg, France

Ingénieur en informatique chez Sully Group.