Tutoriel WebSocket

Basé sur la spécification W3C, comment échanger des données avec un serveur à l'initiative du client ou du serveur.

WebSocket est une alternative à Ajax plus simple à mettre en oeuvre coté client, mais avec une compatibilité limitée aux navigateurs récents. Le protocole complet est supporté par Internet Explorer 10, Chrome depuis la version 16, Firefox depuis la version 11, Safari depuis la version 6.0.

Mais pourquoi utiliser WebSocket? Contrairement à l'objet XMLHttpRequest d'Ajax, qui envoie des requêtes au serveur et met à jour la page web de façon asynchrone lorsque un script sur le serveur renvoie les résultats, WebSocket permet d'envoyer des données à la page à l'initiative du serveur. On peut donc lancer un traitement sur le serveur - ou plusieurs traitements simultanés - qui enverront des données à un navigateur fonctionnant comme un tableau de bord avec différents widgets qui présenteront les informations reçues.
Il y a bien sûr d'autres moyens d'utiliser une page web comme interface d'application, par exemple installer un framework supportant le data binding (React, Angular, Polymer, etc.), mais WebSocket est plus simple et fonctionne sans le besoin d'inclure une bibliothèque de taille imposante.

L'API standard coté client

Le W3C a décrit l'objet WebSocket et ses méthodes qui sont maintenant standards sur tous les navigateurs.

On déclare une instance avec en paramètre l'URL contenant le protocole ws: pour une connexion simple ou wss: pour une connexion sécurisée.

var ws = new WebSocket("ws://www.example.com");

Ce pourrait aussi être une connexion locale, dans ce cas on précisera aussi le port, par exemple 8100.

var ws = new WebSocket("ws://localhost:8100");

L'objet envoie des données au serveur sous forme de chaîne avec la méthode send.

ws.send("Hello serveur...");

Mais cela permet aussi d'envoyer des données structurées, il suffit de les convertir en chaîne de caractères pour la transmission.

var data = { "type" : "texte", "message" : "Hello" };
var message = JSON.stringify(data);
ws.send(message);

L'objet réagit aux messages du serveur avec trois gestionnaires d'évènement.

  1. onopen détecte l'ouverture d'une connexion avec le serveur.
  2. onmessage reçoit les données.
  3. onclose détecte la fermeture de la connexion à l'initiative du serveur.

Onopen

La connexion est établie.

socket.onopen = function (event) {      
  socket.send('{ "type": "texte", "message": "Prêt" }' );     
};

On fait quelque chose quand le serveur est connecté, comme par exemple envoyer un message pour signaler que le navigateur est prêt aussi à recevoir des données.
Noter qu'au lieu d'utiliser stringify, on peut plus simplement mettre l'objet entre guillemets!

Onclose

Le serveur a coupé la connexion.

socket.onclose = function (event) {      
  alert("Fin de communication");
};

Là aussi on fait quelque chose en réaction à l'évènement, comme par exemple afficher une alerte.

Onmessage

Le serveur à envoyé des données.

socket.onmessage=function(event) { 
var data = JSON.parse(event.data); switch(data.type) { case "text": document.getElementById("message").innerHTML = data.message; break; }
};

On suppose que le serveur envoie aussi des données structurées, donc on convertit la chaîne reçue, event.data, en objet JavaScript avec la méthode parse de JSON.

On dispose alors d'un objet avec la propriété type comme celui que l'on a envoyé au serveur et la propriété message ayant pour valeur un texte, que l'on, dans l'exemple, insère dans la page dans un calque ayant l'ID "message".

<div id="message"></div>

L'objet peut proposer plusieurs types accompagnés de données de types différents, ce que l'on verra plus loin.

Méthode close

Le navigateur peut aussi lui-même fermer la communication. Il utilisera la méthode close, cela sera détecté par le serveur.

ws.close();

Il n'y a pas de méthode open, la création d'une instance de WebSocket suffit à ouvrir une connexion.

Coté serveur en JavaScript

Coté serveur, on a le choix du langage pourvu qu'il supporte le protocole WebSocket. L'avantage de JavaScript est qu'on peut l'installer facilement localement sur son ordinateur pour le développement. Plus facilement qu'avec PHP ou autre langage, même si ceux-ci offrent aussi leur solution.
On peut écrire soi-même un serveur en JavaScript fonctionnant sur Node.js, mais on dispose en fait de nombreuses bibliothèques prêtes à l'emploi, simples ou complexe.

Le code coté serveur dépend du framework que l'on aura choisi. Voici un example réalisé avec ws.

  1. Installer Node.js.
  2. Installer le framework:
    npm install --save ws
  3. Coté serveur, on lance le fichier wsserver.js:
    node wsserver.js
  4. Coté client, on charge la page wsclient.html dans un navigateur. C'est tout!

La connexion s'établit automatiquement.

On échangera des messages entre le client et le serveur par objets, transmise sous forme de chaîne de caractère. Il suffit pour cela de placer l'objet entre guillemets.

{ 
  "type":"text", 
  "content":"Un message..."
}

La méthode JSON.parse permet convertir la chaîne en objet et d'accéder à ses propriété: "type", le type de données transmises, "content", le contenu.

Dans notre exemple, le client envoie soit un texte (type text), soit demande une image (type image). De même le serveur envoie un texte ou le chemin d'une image, que le client pourra alors afficher.

Code coté serveur

var WebSocketServer = require("ws").Server;
var ws = new WebSocketServer( { port: 8100 } );

console.log("Server started...");

ws.on('connection', function (ws) {
  console.log("Browser connected online...")
   
  ws.on("message", function (str) {
     var ob = JSON.parse(str);
     switch(ob.type) {
     case 'text':
         console.log("Received: " + ob.content)
         ws.send('{ "type":"text", "content":"Server ready."}')
         break;
     case 'image':
         console.log("Received: " + ob.content)         
         console.log("Here is an apricot...")
         var path ="apricot.jpg";   
         var data = '{ "type":"image", "path":"' + path + '"}';
         ws.send(data); 
         break;
      }   
    })

    ws.on("close", function() {
        console.log("Browser gone.")
    })
});

Le chemin de l'image doit être relatif à la racine de l'application, sous la forme "/sousrepertoire/image.png". Le navigateur Chrome refusera d'assigner à l'attribut src de img un chemin sur le système local de fichier, de la forme "c:/app/sousrepertoire/image.png".

Code coté client

La page en s'affichant envoie un message au serveur pour confirmer que la connexion est établie. Le serveur répond par le message "Server ready.".

Quand l'utilisateur clique sur le bouton "Send me a picture" (envoie moi une image), le serveur reçoit un message du type "image". Il envoie à son tour un message de type "image", accompagné de l'URL de l'image.

Le client affiche cette image dans une balise canvas. Les messages textuels sont affiché dans un calque dont l'ID est "message".

  var ws = new WebSocket("ws://localhost:8100");
  var canvas;
  var context;
  var image = new Image();

  window.onload=function() {
    canvas = document.getElementById("picture");
    context = canvas.getContext("2d");
  }

  function dispMessage(str) {
    document.getElementById("message").innerHTML = str;
  }

  ws.onopen = function (event) {
    ws.send('{ "type":"text", "content":"Browser ready."}' ); 
  };

  ws.onmessage=function(event) { 
   var message = JSON.parse(event.data);
   switch(message.type) {
     case "text":
        dispMessage(message.content);
        break;
     case "image":
        var iname = message.path;
        dispMessage("Received " + iname); 
        image.src= iname
        image.onload = function () { context.drawImage(image, 0, 0); }
        break;	
   }
 };

 function helloServer()
 {
    ws.send('{ "type": "image", "content":"Send me a picture"}'); 
 }
<form>
<input type="button" onclick="helloServer()" value="Send me a picture">
</form>
<fieldset><legend>Message</legend>
<div id="message"></div>
</fieldset>
<fieldset><legend>Picture</legend>
<canvas id="picture"></canvas>
</fieldset>

Problème de latence

Quelquefois, le navigateur n'est pas encore prêt à enclencher une communication avec le serveur, vous recevez alors le message d'erreur suivant:

Uncaught InvalidStateError: 
Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

Pour éviter ce problème, remplacez la fonction helloServer par ce code:

function helloServer()
 {
    setTimeout(function() {
        ws.send('{ "type": "image", "content":"Send me a picture"}'); 
    }, 50);
 }

Ces cinquante millisecondes de délai suffiront au navigateur passer à l'état "connecté".

Vous pouvez aussi attendre que la connection soit établie avant de lancer le programme, avec la méthode onopen:

socket.onopen = function (event) {      
  start();    
};

Coté serveur en PHP

La bibliothèque standard PHP ne supporte pas WebSocket (même s'il utilise la communication locale par socket), et il faut encore utiliser un framework. Quelques exemples...

Quelque soit le framework utilisé, le code coté client est le même que dans l'exemple JavaScript, mais coté PHP, il faut pouvoir convertir une chaîne JSON en objet pour accéder au contenu sous forme clé-valeur.

Cependant on à la chance cette fois de disposer des fonctions nécessaires dans la bibliothèque standard PHP...

json_decode convertit une chaîne contenant un objet JSON en objet PHP.

Exemple:

$json = '{ "type":"text", "content":"un message..." }';
$phpobj = json_decode($json);

On peut alors accéder à la propriété "type" ou "content":

$type = $phpobj->{"type"};
$content = $phpobj->{"content"};

json_encode convertit un tableau associatif (équivalent à un objet JSON JavaScript) en chaîne que l'on peut envoyer au client.

Exemple:

$path="apricot.jpg";
$phparr = array("type"=>"image", "path"=>$path);
$data = json_encode($phparr);

Notre programme ressemblera alors à ceci:

$json = $event->getMessage();
$phpobj = json_decode($json);
$type = $phpobj->{"type"};
switch($type) {
case 'text':
$content = $phpobj->{"content"};
echo "Received: $content\n";
$phparr = array("type"=>"text", "path"=>"Server ready.");
$data = json_encode($phparr);
$server.sendMessage($data);
break;
case 'image':
$content = $phpobj->{"content"};
echo "Received: $content\n";
$path="apricot.jpg";
$phparr = array("type"=>"image", "path"=>$path);
$data = json_encode($phparr);
$server.sendMessage($data);
break;
}

Tout le reste dépend du framework PHP que vous allez choisir, reportez vous donc au manuel sur le site de ce framework.

Téléchargement

Vous pouvez télécharger le code source complet d'une démonstration fonctionnelle avec JavaScript coté serveur.

L'archive contient les fichiers suivants:

© 28 novembre 2014 - Mis à jour le 13 avril 2016 - Par Denis Sureau / Xul.fr