Vers un Web en temps réel avec les WebSockets

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…

Motivation

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.

Morceaux de la spécification

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.

Avantages des WebSockets

Les WebSockets ont les avantages suivants :

  • Nécessitent un faible délai de transit (latency) : pas de nouvelle connexion TCP à créer comme c’était le cas par requête HTTP
  • N’ont besoin que d’un petit surdébit (overhead) : seulement 2 octets par message pour un message texte, au lieu des centaines parfois nécessaires dans les en-têtes HTTP
  • Résultent en une diminution de trafic : puisque les clients n’ont plus besoin de faire de scrutation (polling), les messages sont envoyés seulement lorsqu’il y a des données

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.

Exemples d’usages des WebSockets

L’usage des WebSockets est pertinent pour toute application qui a besoin de fonctionnalités en temps réel. Mentionnons par exemple :

  • Clavardage et messagerie instantanée
  • Jeux multi-joueurs en temps réel sur le réseau
  • Tableaux de bord en temps réel
  • Veille sur les systèmes
  • Cotes de la bourse en temps réel
  • Présentation d’animations liées à la couverture en temps réel d’événements sportifs
  • Assistance à distance
  • Travail collaboratif

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.

Figure 1 — Aquarium WebGL: huit machines qui se synchronisent avec les WebSockets pour l’affichage.

Protocole WebSocket

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 :

  • Elles commencent par l’octet 0x00
  • Contiennent des données en UTF-8 à l’intérieur
  • Et se terminent par l’octet 0xFF

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.

API WebSocket pour les navigateurs HTML5

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 :

  • Protocoles de messagerie (ex.: XMPP ou Jabber)
  • Protocoles de queue de messagerie (ex.: AMQP)
  • Protocoles de frame buffer (ex.: RFB ou VNC)
  • Protocoles de flux de texte (ex.: STOMP)

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.

Figure 2 — L’application client écho avec le mode texte et le mode binaire. Le mode binaire prend une image, l’envoie au serveur écho qui la retourne à l’application client, qui elle l’affiche à côté de l’original.

Mettre en place un système

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 :

  • Node.js avec Socket.IO ou ws
  • Gevent socketio en Python
  • System.Web.WebSockets pour .NET
  • Jetty en Java
  • Vert.x, petit nouveau des cadres applicatifs « asynchrones » avec implémentation de SockJS qui ressemble beaucoup à Socket.IO, à la différence qu’il est polyglotte (Node.js, Erlang, Python, Java etc.)

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.

Un petit projet : affichage synchronisé

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.

Figure 3 — Une petite application pour synchroniser les commandes d’affichage pour un diaporama d’images.

Le système fonctionne comme suit :

  1. Par le biais de l’application maîtresse, le pilote de la présentation choisit l’image à afficher
  2. Le serveur reçoit les commandes de l’application maîtresse et redistribue les commandes d’affichage aux applications esclaves
  3. Les applications esclaves reçoivent les instructions d’affichage du serveur dictées par l’application maîtresse

Serveur

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.

Code HTML de l’application maîtresse et des applications esclaves

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.

Code client pour l’application maîtresse : le contrôleur d’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.

Code client pour l’application esclave

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.

Conclusion

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!

2Les URL mis dans la liste ci-dessous ont été consultés en janvier 2014. Pour une liste plus exhaustive de serveurs, consulter l’article HTML5 WebSockets.
3L’application maître-esclave qui fonctionne en mode local peut être téléchargée sur gitHub.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*