Dans la vague de développement du HTML5, la spécification permet de s’attaquer à la problématique du Web temps réel sans contournement et d’offrir un appareillage technologique universel dans tous les navigateurs modernes. Entrez avec moi dans ce monde fascinant qui ouvre les portes à de nouveaux types d’applications...
Le temps réel a longtemps été simulé avec HTTP, le protocole du Web. L’objectif des WebSockets est de fournir un mécanisme aux applications Web qui ont besoin d’une communication bidirectionnelle avec des serveurs qui ne requiert pas d’ouvrir plusieurs connexions HTTP et d’éviter ainsi la surcharge inhérente au protocole HTTP.
Le protocole HTTP est un protocole transactionnel : le navigateur se connecte au serveur, obtient le contenu et se déconnecte. Les besoins du Web en temps réel pour faire des applications de clavardage ou des jeux réseaux, pour ne mentionner que ceux-là, utilisaient soit des astuces (scrutation AJAX, iframe infini, COMET) qui étaient plutôt lentes et complexes à mettre en place, ou sinon des modules externes (applets Java, Flash ou Silverlight), mais qui avaient le défaut de ne pas fonctionner partout.
Au début des années 2000, notre équipe a eu à développer une application d’encans en temps réel qui fonctionnait sous le principe d’un clavardage spécialisé avec des messages contenant les enchères. Après avoir évalué le volume de données qui transiterait sur le réseau en fonction de nos prédictions d’achalandage pour l’application, nous avons vite pris la décision de se rouler les manches et de développer notre propre protocole maison avec des sockets TCP et des clients sous forme d’applets Java. C’était dans l’ère du temps. Maintenant les WebSockets nous offrent une solution toute prête et qui à terme sera universelle.
WebSocket c’est un protocole et une interface de programmation (API) qui sont sous la gouverne de deux organismes différents : le protocole est sous la responsabilité du IETF avec la RFC 6455 finalisée en décembre 2011 et l’API WebSocket, sous la gouverne du W3C, est candidate à la recommandation depuis septembre 2012 et est considérée comme faisant partie de la famille des technologies HTML5. Le support de l’API WebSocket a commencé à être généralisé sur les navigateurs à partir de 2012.
Les WebSockets ont les avantages suivants :
En fait, un des gros problèmes résolu avec les WebSockets est que le serveur puisse initier des envois. De plus, l’idée de la spécification est qu’à terme elle soit implantée de façon native dans tous les navigateurs modernes, rendant ainsi la technologie du temps réel accessible technologiquement et efficace.
L’usage des WebSockets est pertinent pour toute application qui a besoin de fonctionnalités en temps réel. Mentionnons par exemple :
Un bel exemple d’une application qui utilise les WebSockets est l’aquarium WebGL disponible sur Google Code : huit machines qui ont chacune un navigateur Chrome en action se servent des WebSockets pour synchroniser l’affichage d’un immense aquarium avec poissons en mouvement à travers les écrans associés à chaque machine.
WebSocket est un protocole conversationnel : un hôte échange des
messages avec des terminaux. C’est un mécanisme bidirectionnel
(full duplex), à état
(stateful), maître-esclave. Il nécessite un nouveau
type de serveur et par le fait même un nouvel identifiant de protocole
pour les URL a été créé : ws://…
et wss://…
en mode
sécurisé.
La mécanique de communication va comme suit. Il y a d’abord établissement de la liaison HTTP ou HTTPS et mise à niveau au protocole WebSocket sur la même connexion TCP. Après la mise à niveau faite, les données peuvent être échangées sous forme de « trames » texte (UTF-8) ou binaire et sont bidirectionnelles, pouvant être envoyées dans les deux sens simultanément. Chaque trame de texte a seulement deux octets pour encadrer le message :
Ceci résulte en une grande réduction au niveau du trafic puisque les métadonnées sont minimales (en comparaison avec les en-têtes HTTP). De plus, il n’y a pas de latence pour établir de nouvelles connexions pour chaque message.
Un exemple de communication initiée par le client pourrait être comme suit :
GET /mychat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat Sec-WebSocket-Version: 13 Origin: http://example.com
et un serveur qui comprendrait le protocole pourrait répondre :
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
Dans la requête, le client fait d’abord une connexion standard HTTP
GET qui permet de passer à travers les barrières pare-feu, les serveurs
proxy et autres intermédiaires. La requête demande un changement de
protocole de HTTP à WebSocket et envoie une clé qui sera transformée par
le serveur dans sa réponse pour démontrer qu’il comprend bien le
protocole. La version du protocole désirée est spécifiée : ici, il s’agit
de la version 13 qui correspond à la version finale du standard IETF.
Finalement, la requête spécifie de limiter à l’origine
example.com
pour appliquer la politique de
sécurité de la même origine.
Si le serveur accepte la requête de changer la couche applicative du
protocole, il retourne un code 101. Pour montrer qu’il comprend le
protocole WebSocket, une transformation standardisée de la
Sec-WebSocket-Key
reçue dans la requête du client est faite
et le serveur retourne le résultat dans l’item
Sec-WebSocket-Accept
de l’en-tête. Cette information sera
validée par le client. Si tout est conforme, la couche d’application
changera à WebSocket à travers la même connexion TCP et le HTTP sortira du
décor.
La spécification WebSocket propose aussi une API pour les
navigateurs HTML5. Celle-ci expose deux méthodes, send()
et
close()
et permet d’enregistrer les gestionnaires
d’événements open
, message
, error
et close
.
Examinons et analysons le code suivant :
var ws = new WebSocket('ws://echo.websocket.org'); ws.onopen = function() { console.log('Connexion établie'); ws.send("un message"); } ws.onclose = function() { console.log('Connexion fermée'); } ws.onmessage = function(evt) { if (typeof(event.data) === "string") { console.log("Message reçu: " + evt.data); } };
Dans ce petit bout de code, une connexion est faite avec le serveur. Des gestionnaires d’événement ont été enregistrés pour traiter l’ouverture de la connexion, la fermeture de la connexion et la réception de messages. Ici, lorsque la connexion est établie, notre petit programme envoie un message texte au serveur WebSocket. Lorsqu’un message texte est reçu du serveur WebSocket, le programme client l’affiche sur la console.
La fermeture d’une conversation peut être initiée par un des deux côtés qui envoie une trame de fermeture. Le deuxième côté a la possibilité de retourner des données en attente de livraison et finalement enverra sa trame de fermeture. À partir de ce moment, la connexion est brisée.
Une approche commune est d’utiliser le format JSON sur WebSocket pour la charge utile des messages, mais il pourrait être judicieux d’utiliser des protocoles existants. Nous avons par exemple :
Tout ceci pourrait nous demander de faire transiter du binaire entre le client et le serveur. Même avec un protocole maison, nous pouvons vouloir optimiser le trafic en utilisant du binaire. Un exemple de code simple pour envoyer et récupérer du binaire serait comme suit1 :
var ws = new WebSocket('ws://echo.websocket.org'); ws.binaryType = "arraybuffer"; ws.onopen = function() { var bytearray = new Uint8Array(4); var i; for (i=0;i<bytearray.length;++i) bytearray[i] = 2*i; ws.send(bytearray.buffer); } ws.onmessage = function (event) { if (event.data instanceof ArrayBuffer) { console.log('Réception de données binaires ArrayBuffer'); var bytearray = new Uint8Array(event.data); var i; for (i=0;i<bytearray.length;i++) console.log(bytearray[i]); console.log('Fin.'); } };
Avec ws sur Node.js, le noyau du code serveur pour faire l’écho des messages binaires reçus, serait comme suit :
wss.on('connection', function(ws) { console.log('Partir le client.'); ws.on('message', function(data, flags) { var msg = 'Réception de données ' if (flags.binary) { console.log(msg+'binaires!'); ws.send(data, {binary: true, mask: false}); } }); ws.on('close', function() { console.log('Arrêter le client.'); }); });
Le fichier index.html
sur gitHub met le tout ensemble
avec un client pour l’écho texte et l’écho binaire comme présenté à la
Figure 2.
Maintenant que les bases ont été expliquées, il peut être tentant de passer à l’action. Il faut pour cela avoir accès à un serveur qui supporte le protocole WebSocket, écrire l’application avec ses deux morceaux : code serveur et code client.
Il y a différents serveurs qui implantent le protocole. En janvier 2014, nous avions entre autres2 :
Plusieurs exemples et turoriels disponibles en ligne vont utiliser
Socket.IO avec Node.js. Avec cet appareillage, nous travaillons avec du
JavaScript côté client et côté serveur. Socket.IO est une couche
d’abstraction qui encapsule divers protocoles dont WebSockets. Socket.IO
fonctionne selon le principes d’un bus de messages : du côté serveur ou
client, nous émettons des messages avec la méthode emit()
et
nous réagissons aux messages avec la méthode on()
, dont ceux
correspondant à une connexion ou une déconnexion. Les émissions de
messages peuvent être faites à un socket spécifique ou à tous les
sockets.
Un autre avantage de la librairie Socket.IO est qu’elle offre des reprises avec d’autres types de clients si l’API WebSocket du HTML5 n’est pas supportée (ce qui peut être possible avec des navigateurs plus anciens). Un désavantage cependant est qu’au moment de la rédaction de cet article, Socket.IO ne supportait pas les communications binaires à cause des reprises qui n’implantent pas toutes ce type de transmission. Dans ce cas, si nous voulons travailler avec Node.js, nous pouvons utiliser un serveur pur WebSocket comme ws.
Nous allons mettre les bases pour un système qui va synchroniser l’affichage d’un diaporama d’images contrôlé par une application maîtresse sur un ensemble d’applications esclaves. L’application maîtresse dictera quel morceau sera affiché. Les applications esclaves rafraîchiront l’item courant lorsque l’application maîtresse enverra une instruction à cet effet3.
Le système fonctionne comme suit :
var io = require('socket.io'); var connect = require('connect'); var port = 3000; var app = connect().use(connect.static('public')).listen(port); var poll = io.listen(app); var nconnected = 0; var current = ""; poll.sockets.on('connection', function(socket) { nconnected++; poll.sockets.emit('connected-count',nconnected); socket.emit('pull',current); socket.on('disconnect', function() { nconnected--; poll.sockets.emit('connected-count',nconnected); }); socket.on("push", function(data) { current = data.url; poll.sockets.emit("pull", current); }); });
Le serveur compte le nombre de clients connectés. Lorsqu’un
client se connecte ou se déconnecte, un message
connected-count
est envoyé à tous avec le nombre clients
connectés. L’application maîtresse enverra un message
push
au serveur avec le URL courant qui sera propagé aux
applications esclaves pour changer l’image affichée avec l’émission
d’un message pull
.
Le code HTML est semblable pour les deux morceaux, mais des hyperliens dans l’application maîtresse permettront d’envoyer les commandes d’affichage au serveur à propager aux applications esclaves.
HTML pour l’application maîtresse :
<ul id="menu"> <li><a href="#slide1">1</a></li> <li><a href="#slide2">2</a></li> <li><a href="#slide3">3</a></li> </ul> <div id="slides"> <div id="slide1"><img src="images/thumb01.jpg" width="128" height="96"/></div> <div id="slide2"><img src="images/thumb02.jpg" width="128" height="96"/></div> <div id="slide3"><img src="images/thumb03.jpg" width="128" height="96"/></div> </div>
HTML pour les applications esclaves :
<div>Nombre de spectateurs: <span id="connected-count">0</span>.</div> <div id="slides"> <div id="slide1"><img src="images/thumb01.jpg" width="128" height="96"/></div> <div id="slide2"><img src="images/thumb02.jpg" width="128" height="96"/></div> <div id="slide3"><img src="images/thumb03.jpg" width="128" height="96"/></div> </div>
Feuille de style pour tous :
div.on { display: block !important; } #slides div { display: none; }
Par défaut les blocs avec les images sont cachés. Avec les liens du menu de l’application maîtresse, nous changeons le style pour montrer un bloc et propager ce choix aux applications esclaves qui l’appliqueront elles aussi à leur affichage.
Client de l’application maîtresse :
jQuery(function($) { var socket = io.connect('http://localhost:3000/'); $('#menu a').click(function(e) { var url = $(this).attr('href'); $('#slides div').removeClass('on'); $(url).addClass("on"); $('#menu a').removeClass('on'); $('#menu a[href="' + url + '"]').addClass('on'); socket.emit('push', {url: url}); }); });
Lorsqu’on clique sur un item du menu de l’application maîtresse,
nous ajoutons la classe on
au bloc correspondant et nous
propageons le URL du bloc actif aux applications esclaves avec
l’émission d’un message push
.
Client de l’application esclave :
jQuery(function($) { var socket = io.connect('http://localhost:3000'); socket.on('pull', function (data) { if (data) { $('#slides div').removeClass('on'); $(data).addClass('on'); } }); socket.on('connected-count', function (data) { $('#connected-count').text(data); }); });
À la réception d’un message pull
, nous changeons le
bloc actif par celui correspondant dans le URL transmis. Lors de la
réception d’un message connected-count
, le décompte du
nombre d’applications esclaves connectées est mis à jour.
Nous avons vu comment bâtir une petite application qui permet de synchroniser en temps réel des commandes d’affichage. J’espère vous avoir mis l’eau à la bouche. Vous pourriez vouloir installer Node.js et Socket.IO pour bâtir votre propre application maison, ou sinon trouver comment l’appareillage technologique compatible avec votre environnement applicatif vous permettrait de faire du développement avec les WebSockets.
Une autre option serait de voir comment étager des protocoles standards par-dessus les WebSockets. Les WebSockets serviraient alors de couche de transport pour des couches applicatives standard comme le clavardage, la messagerie, l’affichage d’interface et permettraient ainsi de connecter des applications Web à un réseau interactif. Une belle publication qui explique comment ceci peut être fait est le livre The Definitive Guide to HML5 WebSocket par Vanessa Wang, Frank Salim et Peter Moskovits.
Finalement, vous pouvez suivre la formation HTML5 offerte chez Technologia, dans laquelle nous allons couvrir le sujet des WebSockets et construire quelques applications temps réel.
Au plaisir!