Socket.io
Cet article est en construction: un auteur est en train de le modifier.
En principe, le ou les auteurs en question devraient bientôt présenter une meilleure version.
Introduction
Socket.io est un module de Node.js qui permet de créer des Web Sockets, c'est-à-dire des connections bi-directionnelles entre clients et serveur qui permettent une communication en temps réel sur un autre protocole que le protocole http normalement utilisé dans les pages web. Ce type de technique est utilisée pour créer des applications telles que des système de communication en temps réel (i.e. des Chats), des jeux multi-utilisateur, des applications de collaboration, etc.
Bien que Socket.io soit un module qui peut être utilisé individuellement, la plupart des exemples contenus dans la page utilise également le module Express.js.
Objectifs de cette page
Cette page fait principalement référence au cours STIC I du Master MALTT et s'inscrit dans la perspective d'aborder les fondamentaux de la programmation à travers JavaScript. Plus en détail, Socket.io permet de montrer le fonctionnement d'un protocole plus complexe par rapport au protocole http. Grâce à ce type de communication, il est possibile de créer des applications pédagogiques, notamment en relation avec l'Apprentissage collaboratif.
De plus, la communication WebSocket renforce ultérieurement les concepts de programmation événementielle et d'interactivité, car pour mettre en place des applications qui se mettent à jour en temps réel il faut entremêler des événements côté-client et côté-serveur.
Pour comprendre le contenu de cette page la lecture des pages suivantes est conseillée :
Avertissement de sécurité
Le même avertissement de sécurité de Node.js s'applique dans le cas de Socket.io, car les technologies utilisées sont très similaires.
Veuillez également être attentif au fait que les exemples illustrés dans cette page ont exclusivement une finalité propédeutique à l’apprentissage des concepts fondamentaux du développement web et ne sont pas conçus pour une utilisation en dehors d’un environnement contrôlé par le développeur (e.g. localhost).
Example conceptuel: messagerie instantanée
Un exemple conceptuel peut contribuer à mettre les informations théoriques et pratiques de cette page dans une perspective plus concrète: un application de messagerie instantanée. Le principe est assez simple : un client envoie un message au serveur, le serveur reçoit ce message et le transmets aux autres clients connectés. Voici un enregistrement vidéo d'une simple application dans deux fenêtres du navigateur (représentant deux utilisateurs différents) :
Dans ce simple mécanisme il y a tout de même plusieurs événements qui se passent :
- Côté client : l’utilisateur écrit du texte et clique sur un bouton pour envoyer le message. On a donc un événement qu’on peut identifier avec le nom « messageFromClient » associé à des données de type textuel ;
- Côté serveur : le serveur doit « écouter » pour des événements de type « messageFromClient » pour être prêt à gérer l’information envoyé par le client ;
- Côté serveur : lorsque le serveur reçoit un événement « messageFromClient », il émet à son tour un événement qui transmet le message aux clients connectés. On peut identifier cet événement avec le nom « messageFromServer », associé encore une fois à des données de type textuel ;
- Côté client : le client doit « écouter » pour des événements de type « messageFromServer » pour être prêt à gérer l’information retransmise par le serveur ;
- Côté client : lorsque le client reçoit un événement « messageFromServer », il utilise les données textuelles reçues pour mettre à jour le DOM de la page HTML et afficher le nouveau message ;
Comprendre les WebSockets
Différence par rapport au protocole http
Le protocole http est basée sur l’architecture requête/réponse qui peut être considéré comme la somme de deux mouvements unidirectionnels : la requête du client dans un sens et la réponse du serveur dans l’autre sens. Une fois que cet échange a eu lieu, il n’y a plus de contact entre le client et le serveur, si ce n’est à travers l’établissement d’un nouvel échange requête/réponse (Figure 1). Le protocole WebSocket, au contraire, crée une véritable connexion entre le client et le serveur, ce qui permet une communication bidirectionnelle en temps réel : le client peut envoyer un message au serveur, et le serveur peut envoyer un message à un client, sans la nécessité d’une requête/réponse http. À travers ce nouveau type de connexion, il est également possible de créer un « groupe » de communication entre tous les clients qui ont établi une connexion WebSocket avec le même serveur (Figure 2). Le principe est très simple :
- Un client envoie un message au serveur
- Le serveur reçoit le message
- Le serveur envoie ce même message (ou un message modifié) à tous les clients connectés
Un upgrade du protocole http
Il faut cependant considérer que le protocole des WebSockets nécessite quand même du protocole http pour établir la première connexion. En effet, le protocole des WebSockets est une sorte de « upgrade » du protocole http. Pour que cet upgrade ait lieu, il y a deux prérequis nécessaires :
- Le serveur est capable de gérer le protocole WebSockets : cette capacité est disponible justement grâce à la combinaison Express.js/Socket.io
- Le client est capable de gérer le protocole WebSockets : cette capacité est disponible à travers l’API HTML5 des WebSockets qui est compatible avec pratiquement tous les browsers récents (en 2016)
Le « handshake »
Lorsque ces prérequis sont atteints, une connexion WebSockets s’établie de la manière suivante :
- Un client établie une simple connexion http avec un serveur, ce qui se fait par exemple à travers la requête d’une simple page HTML avec un URL
- Sur cette page HTML est présent un script JavaScript qui s’occupe de démarrer une connexion WebSocket. Cette connexion s’établi seulement si le navigateur supporte l’API HTML5 des WebSockets
- La demande de connexion WebSockets est donc envoyé au serveur. Si le serveur supporte ce type de connexion, alors la connexion bidirectionnelle est formée : le client et le serveur seront en contact tout au long que le client garde la page avec le script JavaScript ouverte dans son navigateur
Ce type de mécanisme s’appelle « handshake », car c’est comme si les deux entités (le client et le serveur) s’accordent sur la possibilité d’établir un nouveau « pacte » (la connexion WebSockets) sur la base d’une prémisse existante (une connexion http établie et la capacité des deux entités de gérer le nouveau protocole).
L’échange de messages
Une fois le « handshake » achevé, l’échange de messages entre client et serveur peut commencer. L’échange se fait à tout moment dans lequel un événement côté client ou côté serveur déclenche l’envoie d’un message. L’échange entre les deux entités se compose de deux éléments :
- Le type d’événement (obligatoire)
- Des données associées à cet événement : les données peuvent être de différent type, du simple texte dans le cas d’un système de messagerie instantanée à des streams audio/vidéo pour des applications plus complexes
Pour que l’échange se passe correctement il faut donc que le client et le serveur, dans la logique de leurs applications, disposent d’un gestionnaire d’événement qui :
- Sache quoi faire pour émettre un événement (avec ou sans données)
- Sache quoi faire pour recevoir (et éventuellement réagir) à un événement
Avantages des WebSockets
Le protocole WebSockets présente plusieurs avantages par rapport au protocole http :
- Le temps de latence pour l’envoie et la réception d’un message et d’environ 50-100 ms. En fonction du temps de réaction des êtres humains (perception, traitement, etc.), ce délai peut être considéré de facto du « real time ».
- L’échange entre client et serveur (ou entre serveur et client) se fait seulement s’il y a un événement qui propose un changement. Ceci réduit le trafique des requêtes/réponses à l’essentiel, tandis que dans le cas des applications qui utilisent le protocole http, souvent la plupart des requêtes asynchrones est faite juste pour contrôler s’il y a quelque chose de nouveau. Toutes se requêtes ne sont pas nécessaire avec les WebSockets.
- La communication en temps réel permet une vraie synchronisation entre les clients, ce qui permet de créer des applications collaboratives où les apprenants partagent à tout moment le même écran et peuvent donc discuter/contribuer/etc. plus facilement.
- L’échange de messages au niveau client se fait à travers JavaScript, ceci signifie que les informations obtenues dans les échanges peuvent être utilisées par la logique de l’application front-end.
Utilisation de Socket.io
Installation des modules
Comme tout module Node.js, Socket.io s'installe avec npm :
npm install socket.io
Bien que Socket.io puisse être utilisé individuellement, nous allons le combiner avec Express.js pour fournir également le protocole http nécessaire pour le "handshake" (voir plus haut dans la section théorique).
npm install express
Mettre en place un serveur HTTP+WebSocket
Le fait qu'on nécessite de deux protocoles différents implique conceptuellement deux serveurs différents: un serveur qui gère le protocole http et un serveur qui gère le protocole WebSocket. En effet, il est tout à fait possible que la connexion WebSocket se fasse sur un autre serveur par rapport au serveur qui met à disposition la page HTML dans lequel se trouve le script qui crée la connexion WebSocket. En utilisant l'environnement de Node.js, il est possibile de combiner les deux serveurs dans une seule application:
- Express.js "écoute" la requête http et se charge d'envoyer en réponse une page qui contient le JavaScript pour établir une connexion WebSocket et gérer par la suite l'échange des messages ;
- Socket.io "écoute" et retransmet les messages entre les clients connectés et le serveur
La possibilité de combiner les deux serveurs dans la même application permet notamment de partager des informations (e.g. bases des données, etc.).
Côté serveur
D'abord, il faut mettre en place un fichier JavaScript qui contient l'application pour créer le serveur. Pour comprendre le code suivant il faut se rappeler que Express.js n'est qu'une extension du module native http de Node.js. Le code sera ensuite expliqué en détail :
//Code du script Node app.js
//1. Http
var http = require('http');
//2. Express.js
var express = require('express');
var app = express();
//Create a route for the first http connection
app.get('/', function (request, response) {
//The file will include the socket.io.js file to establish the socket connection
response.sendFile(__dirname + '/index.html');
});
//3. Create a server that will serve both http and socket connection using the app function of Express.js
var server = http.createServer(app);
//4. Socket.io
//Pass the server to the socket.io to handle socket connection
var io = require('socket.io')(server);
//5. This function will be executed every time a user connect to the socket through the "/" express route
io.on('connection', function (socket) {
console.log("A new client connected!");
});
//6. Listen to the "shared" server (not the Express.js app)
server.listen(3000, console.log("Listening to port 3000"));
Voici les passages plus importants de ce code :
- D'abord il faut inclure le module natif http qui nous permet par la suite de "relier" les serveurs Express et Socket.io dans une seule application
- Ensuite nous créons une "simple" application Express avec une seule route principale. Cette route envoie simplement en réponse le contenu d'un fichier index.html dont le code est illustré dans la partie côté-client.
- Nous utilisons la méthode createServer() du module natif http pour créer un serveur http et nous utilisons l'application Express.js pour gérer le mécanisme requête/réponse
- Par la suite, nous passons au serveur Socket.io qui est associé au même serveur http à travers la variable (server)
- Le socket "écoute" pour les connexions qui seront établies à travers le fichier index.html servi par Express
- On lance les "deux serveurs" sur la porte 3000 pour que notre application combinée HTTP + WebSocket soit disponible à l'adresse http://localhost:3000
Côté client
Il nous faut à ce point le fichier HTML qui contient le script JavaScript qui démarre la connexion WebSocket. Il faut savoir à ce propos que le module Socket.io met à disposition un fichier JavaScript avec tout le code nécessaire pour gérer la connexion et l'échange des messages côté-client. Ce fichier est mis à disposition à travers une route /socket.io/socket.io.js
. Cela signifie que ce fichier n'existe pas physiquement dans l'arborescence de votre application qui se compose (dans ce cas) de la manière suivante :
|- myapp |- node_modules --> contient express et socket.io (qui nécessite de beaucoup d'autre modules) |- ... - app.js --> votre script côté-serveur - index.html --> votre page
Voici le code du fichier HTML :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Socket.io basic example</title>
</head>
<body>
<h1>This is a page connected with a web socket</h1>
<p><a href="/" target="_blank">Open the page in a new tab</a> to add a new connection.</p>
<p>Look at the console log of your command line to see the messages.</p>
<!-- include the socket.io.js file, which does not "physically" exists, but is provided directly by the server -->
<script src="/socket.io/socket.io.js"></script>
<script>
//Start the socket connection
var socket = io();
</script>
</body>
</html>
Le script côté-client est également disponible en forme de CDN, voir le site officiel pour obtenir l'URL absolu.
Résultat attendu
La vidéo montre le résultat attendu de cette mise en place: chaque fois qu'une nouvelle fenêtre du navigateur (représentant un utilisateur différent) accède à la page http://localhost:3000, la console du script app.js affiche un message qui confirme la connexion.
L'échange de messages: .on() et .emit()
La communication entre client et serveur, et entre serveur et client se fait à travers deux méthodes :
.emit('événement', [données])
pour l'émission.on('événement', callback([données]))
pour la réception
.emit()
La méthode .emit()
détermine quel type d'événement produire. Le nom de l'événement peut être arbitraire, il suffit de choisir un nom qui peut bien décrire se qui se passe, par exemple :
//Un utilisateur se connecte .emit('user added', 'User145') //Un message de bienvenu est envoyé .emit('greeting', { from: 'User145', message: 'Hello!' })
.on()
La méthode .on()
est un type d'eventListener qui déclanche une fonction de callback lorsqu'un événement est émis. En rélation avec les exemples de .emit() :
//Réagir à la connexion d'un utilisateur .on('user added', function(user) { console.log(user + " just entered the room!"); //--> User145 just entered the room }); //Réagir à l'envoie d'un message de bienvenu .on('greeting', function (data) { console.log(data.from + " says: + data.message); //--> User145 says: Hello! });
Les deux méthodes sont appliquées de manière spéculaire selon la direction du message. Pour convention, nous allons garder les noms de variables de la mise en place (voir plus haut) :
- io pour le serveur
- socket pour le client
Message du serveur au client
Dans le cas d'un message du serveur au client, nous avons la combinaison suivante. Au niveau serveur :
//In the app.js file
//... Same code as above
//5. This function will be executed every time a user connect to the socket through the "/" express route
io.on('connection', function (socket) {
//1. This function will emit a message from the server to all clients connected, saying that a new user has connected
io.emit('for everyone', 'A new client has joined us!');
//2. This function will emit a message just to the client itself
socket.emit('just for you my socket', 'Welcome to this socket, my Client!');
//3. This function will emit a message to every client connected EXCEPT the client itself
socket.broadcast.emit('for everyone else', 'Psst, the new client cannot read this...');
});
Le serveur peut envoyer trois types de messages:
- Un message déstiné à tous les clients connectés (inclut le client lui-même)
- Un message déstiné seulement au client lui-même, identifié grâce à l'argument socket dans la fonction de callback io.on('connection', function (socket) {});
- Un message déstiné à tous les clients connectés, sauf le client lui-même
Au niveau client, dans le fichier index.html à l'intérieur de la balise script
:
//Inside the script in index.html
//Start the socket connection
var socket = io();
//Listen for the "for everyone" event from the server
socket.on('for everyone', function (msg) {
console.log(msg)
});
//Listen for the "just for you my socket" event from the server
socket.on('just for you my socket', function (msg) {
console.log(msg)
});
//Listen for the "for everyone else" event from the server
socket.on('for everyone else', function (msg) {
console.log(msg);
});
</script>
Message du client au serveur
Dans le cas d'un message du client au serveur, par contre, la combinaison serait la suivante. Au niveau client, dans le fichier index.html à l'intérieur du tag script
:
//In the script tag of index.html
//Start the socket connection
var socket = io();
//Send a message to the server
socket.emit('greeting', 'Hello!');
Au niveau serveur:
//In the app.js file
//... Same code as above
//5. This function will be executed every time a user connect to the socket through the "/" express route
io.on('connection', function (socket) {
console.log("A new client connected!");
//Receives greetings from the socket using the socket reference in callback function of io.on('connection', function (socket) ...
socket.on('greeting', function (msg) {
console.log("The client " + socket.id + " says " + msg + " to the server");
});
});
Veuillez noter que l'événement .on('greeting') au niveau serveur est associé à la référence à l'argument (socket) dans la fonction de callback qui démarre la connexion et non pas au serveur lui-même. En effet, lorsqu'on déclare le listener
io.on('connection', function (socket) { //Tous les échanges vont ici });
nous établissons une connexion bi-directionnelle avec un client spécifique (le socket de l'argument). Donc il faut qu'on puisse déterminer quel client particulier à envoyé le 'greeting'. On peut s'apercevoir qu'il s'agit d'un client particulier grâce à la propriété socket.id
qui génère automatiquement une suite de caractères pour créer un identifiant unique pour chaque client connecté au serveur.
L'événement io.on() est donc pratiquement limité à l'établissement d'une nouvelle connexion entre chaque client et le serveur, mais à l'intérieur de cette connexion, les événements en réception concerne le socket particulier au quel la connexion se réfère.
Code de l'exemple conceptuel
Enfin, nous pouvons combiner les messages du client au serveur et du serveur au client pour créer une application qui partage les informations selon le mécanisme suivant :
- Le client envoie un message au serveur
- Le serveur écoute ce message et retrasmet le message aux clients connectés
- Les clients connectés reçoivent le message et mettent à jour leur interface avec la nouvelle information
C'est le principe à la base de l'exemple conceptuel en introduction de cette page, dont le code complet est le suivant.
Côté serveur
Le code du fichier app.js est le suivant:
//Http
var http = require('http');
//Express.js
var express = require('express');
var app = express();
//Create a route for the first http connection
app.get('/', function (request, response) {
//The file will include the socket.io.js file to establish the socket connection
response.sendFile(__dirname + '/index.html');
});
//Create a server that will serve both http and socket connection using the app function of Express.js
var server = http.createServer(app);
//Socket.io
//Pass the server to the socket.io to handle socket connection
var io = require('socket.io')(server);
//This function will be executed every time a user connect to the socket through the "/" express route
io.on('connection', function (socket) {
console.log("A new client connected!");
//Give the client a random username
socket.username = "User" + Math.ceil(Math.random() * 100);
//This function will be executed when the user disconnect (i.e. leaves the "/" express route)
socket.on('disconnect', function (socket) {
console.log("A client has disconnected!");
});
//This function will emit a message from the server to all clients connected, saying that a new user has connected
//See the index.html to see how this communication is handled on the client
io.emit('user connected');
//This function will be executed when the client send a 'message' event
socket.on('messageFromClient', function (data) {
//Add the socket.username to identify the client (should be replaced by a chosen username in a real application)
var text = socket.username + " says: " + data;
//Emit the message to all clients, included the client itself that has sent the message
io.emit('messageFromServer', text);
});
});
//Listen to the "shared" server (not the Express.js app)
server.listen(3000, console.log("Listening to port 3000"));
Côté-client
Le code du fichier index.html est le suivant:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Socket.io simple chat example</title>
</head>
<body>
<h1>Socket.io simple chat example</h1>
<p>Use the text input to send a message to the server:</p>
<input id="messageTxt" type="text">
<button id="sendBtn">Send</button>
<ul id="messagesList">
<!-- Dynamically generated list of messages sent from the different clients connected to the server -->
</ul>
<script src="/socket.io/socket.io.js"></script>
<script>
//Start the socket connection
var socket = io();
//Listen for the "user connected" event
socket.on('user connected', function () {
console.log("A new user has connected!")
});
//Identify the interactive elements in the DOM
var msg = document.getElementById("messageTxt");
var btn = document.getElementById("sendBtn");
var list = document.getElementById("messagesList");
//Send a message when the user clicks on the button
btn.addEventListener('click', function () {
//Emit a message event with the content of the text input as value
socket.emit('messageFromClient', msg.value);
msg.value = '';
});
//Add the content of each message that the clients receive from the server
socket.on('messageFromServer', function (data) {
var item = document.createElement('li');
item.innerHTML = data;
list.appendChild(item);
});
</script>
</body>
</html>
Techniques avancées
Socket.io propose des techniques plus complexes par rapport à l'exemple présenté dans cette page, mais le principe de base reste le même : l'échange .emit() vs. .on() entre client et serveur. Ces techniques peuvent ensuite être combinée avec les API de JavaScript pour créer des applications complexes. Voici un aperçu de quelques possibilités.
"Rooms" et "namespaces"
Une technique qui pourrait être utile dans le développement d'application multi-utilisateur concerne la possibilité d'utiliser des "rooms" et des "namespaces" pour créer des cannaux de communication restreints à certains utilisateurs ou contextes.
La technique des rooms, par exemple, permet de créer des "groupes" d'utilisateurs. Pour créer un "groupe" on utilise la méthode .join(groupe);
io.on('connection', function(socket){
socket.join('stic-1');
});
À ce moment, l'émission des messages peut être limité aux sockets qui font partie du "groupe/room" stic-1 avec la notation suivante :
io.to('stic-1').emit('welcome', 'Welcome to the STIC I room!"):
- Voir la documentation officielle pour plus de détails.
Échange non limité à du texte
Les échanges à travers un WebSocket ne se limitent pas à du contenu textuel comme dans l'exemple de la messagerie instantanée. On peut en effet envoyé tout type de données, et par conséquent développer des applications qui favorisent la collaboration à distance avec différents types de médias. La vidéo suivante, par exemple, montre une simple application qui utilise l'API HTML5 audio et video pour snychroniser la lecture d'une vidéo, ce qui pourrait être accompagné d'autre système de communication pour commenter, critiquer, etc. le contenu de la vidéo:
Le même principe peut s'appliquer notamment à l'élément Canvas pour créer des dessins, ou encore à la technique Drag&Drop disponible gâce à jQuery UI.
Master/Client
Le fait de disposer de "deux serveurs" (http + socket) permet d'avoir des clients connectés au même socket à travers deux pages http différentes (i.e. avec URL différents). Cette option peut être exploitée par exemple pour créer des associatins Master/Client, dans lesquelles le "Master" peut faire des opérations que le "Client" ne peut pas faire, mais peut quand même béneficier de la communication en temps réel. Voici l'enregistrement d'une application qui prévoit:
- une page "master" (avec route "/master") dans laquelle l'utilisateur peut écrire du code HTML
- une page "client" (avec route "/") dans laquelle les utilisateurs peuvent voir le code et le résultat que ça donne sur une page
Liens
- Ressources
- Tutoriels