Le WebSocket protocole, décrit dans la spécification RFC 6455 fournit un moyen d’échanger des données entre le navigateur et le serveur via une connexion persistante. Les données peuvent être transmises dans les deux sens sous forme de « paquets », sans rompre la connexion et sans requêtes HTTP supplémentaires.

WebSocket est particulièrement idéal pour les services qui nécessitent un échange de données continu, par exemple les jeux en ligne, les systèmes de trading en temps réel, etc.

Un exemple simple

Pour ouvrir une connexion websocket, nous devons créer new WebSocket en utilisant le protocole spécial ws dans l’url :

let socket = new WebSocket("ws://javascript.info");

Il existe également un protocole crypté wss://. C’est comme HTTPS pour les websockets.

Toujours préférer wss://

Le protocole wss:// est non seulement crypté, mais aussi plus fiable.

C’est parce que ws:// les données ne sont pas cryptées, visibles pour tout intermédiaire. Les anciens serveurs proxy ne connaissent pas WebSocket, ils peuvent voir des en-têtes « étranges » et interrompre la connexion.

D’autre part, wss:// est WebSocket sur TLS, (de même que HTTPS est HTTP sur TLS), la couche de sécurité du transport chiffre les données à l’expéditeur et les déchiffre au récepteur. Ainsi, les paquets de données sont transmis cryptés à travers les proxies. Ils ne peuvent pas voir ce qu’il y a à l’intérieur et les laisser passer.

Une fois que le socket est créé, nous devons écouter les événements sur celui-ci. Il y a totalement 4 événements :

  • open – connexion établie,
  • message – données reçues,
  • error – erreur de websocket,
  • close – connexion fermée.

…Et si nous souhaitons envoyer quelque chose, alors socket.send(data) le fera.

Voici un exemple :

let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};

Pour la démonstration, il y a un petit serveur server.js écrit en Node.js, pour l’exemple ci-dessus, en cours d’exécution. Il répond par « Bonjour du serveur, John », puis attend 5 secondes et ferme la connexion.

Vous verrez donc les événements openmessageclose.

C’est en fait ça, nous pouvons déjà parler WebSocket. Assez simple, n’est-ce pas ?

Maintenant, parlons plus en profondeur.

Ouvrir un websocket

Lorsque new WebSocket(url) est créé, il commence à se connecter immédiatement.

Pendant la connexion, le navigateur (à l’aide d’en-têtes) demande au serveur : « Est-ce que vous supportez Websocket ? » Et si le serveur répond « oui », alors la conversation se poursuit en protocole WebSocket, qui n’est pas du tout HTTP.

Voici un exemple d’en-têtes de navigateur pour la requête faite par new WebSocket("wss://javascript.info/chat").

GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13

  • Origin – l’origine de la page du client, par exemple https://javascript.info. Les objets WebSocket sont inter-origine par nature. Il n’y a pas d’en-têtes spéciaux ou d’autres limitations. Les anciens serveurs sont de toute façon incapables de gérer WebSocket, il n’y a donc aucun problème de compatibilité. Mais l’en-tête Origin est important, car il permet au serveur de décider de parler ou non de WebSocket avec ce site web.
  • Connection: Upgrade – signale que le client souhaite changer de protocole.
  • Upgrade: websocket – le protocole demandé est « websocket ».
  • Sec-WebSocket-Key – une clé aléatoire générée par le navigateur pour la sécurité.
  • Sec-WebSocket-Version – version du protocole WebSocket, 13 est la version actuelle.
La poignée de main WebSocket ne peut pas être émulée

Nous ne pouvons pas utiliser XMLHttpRequest ou fetch pour effectuer ce type de requête HTTP, car JavaScript n’est pas autorisé à définir ces en-têtes.

Si le serveur accepte de passer en WebSocket, il doit envoyer une réponse de code 101 :

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

Ici Sec-WebSocket-Accept se trouve Sec-WebSocket-Key, recodée à l’aide d’un algorithme spécial. Le navigateur l’utilise pour s’assurer que la réponse correspond à la requête.

Après, les données sont transférées à l’aide du protocole WebSocket, nous verrons bientôt sa structure (« frames »). Et ce n’est pas du tout du HTTP.

Extensions et sous-protocoles

Il peut y avoir des en-têtes supplémentaires Sec-WebSocket-Extensions et Sec-WebSocket-Protocol qui décrivent les extensions et les sous-protocoles.

Par exemple :

  • Sec-WebSocket-Extensions: deflate-frame signifie que le navigateur prend en charge la compression des données. Une extension est quelque chose liée au transfert des données, une fonctionnalité qui étend le protocole WebSocket. L’en-tête Sec-WebSocket-Extensions est envoyé automatiquement par le navigateur, avec la liste de toutes les extensions qu’il prend en charge.

  • Sec-WebSocket-Protocol: soap, wamp signifie que nous aimerions transférer non pas n’importe quelles données, mais les données des protocoles SOAP ou WAMP (« The WebSocket Application Messaging Protocol »). Les sous-protocoles WebSocket sont enregistrés dans le catalogue IANA. Ainsi, cet en-tête décrit les formats de données que nous allons utiliser.

    Cet en-tête optionnel est défini à l’aide du deuxième paramètre de new WebSocket. C’est le tableau des sous-protocoles, par exemple si nous souhaitons utiliser SOAP ou WAMP :

    let socket = new WebSocket("wss://javascript.info/chat", );

    Le serveur doit répondre avec une liste de protocoles et d’extensions qu’il accepte d’utiliser.

    Par exemple, la requête:

    GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp

Réponse :

101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap

Ici le serveur répond qu’il supporte l’extension « deflate-frame », et uniquement SOAP des sous-protocoles demandés.

Transfert de données

La communication WebSocket est constituée de « trames » – fragments de données, qui peuvent être envoyés de part et d’autre, et peuvent être de plusieurs types :

  • « trames de texte » – contiennent des données textuelles que les parties s’envoient entre elles.
  • « cadres de données binaires » – contiennent des données binaires que les parties s’envoient.
  • « cadres ping/pong » sont utilisés pour vérifier la connexion, envoyés par le serveur, le navigateur y répond automatiquement.
  • Il y a aussi « cadre de fermeture de connexion » et quelques autres cadres de service.

Dans le navigateur, on ne travaille directement qu’avec des trames textuelles ou binaires.

La méthode WebSocket .send() peut envoyer des données textuelles ou binaires.

Un appel socket.send(body) permet d’envoyer body en chaîne de caractères ou dans un format binaire, notamment BlobArrayBuffer, etc. Aucun paramétrage nécessaire : il suffit de l’envoyer dans n’importe quel format.

Lorsque nous recevons les données, le texte arrive toujours sous forme de chaîne. Et pour les données binaires, nous pouvons choisir entre les formats Blob et ArrayBuffer.

C’est défini par la propriété socket.binaryType, c’est "blob" par défaut, donc les données binaires arrivent sous forme d’objets Blob.

Blob est un objet binaire de haut niveau, il s’intègre directement avec <a><img> et d’autres balises, c’est donc un défaut sain. Mais pour le traitement binaire, pour accéder à des octets de données individuels, nous pouvons le changer en "arraybuffer":

socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};

Limitation du débit

Imaginez, notre application génère beaucoup de données à envoyer. Mais l’utilisateur a une connexion réseau lente, peut-être sur un internet mobile, en dehors d’une ville.

Nous pouvons appeler socket.send(data) encore et encore. Mais les données seront mises en mémoire tampon (stockées) et envoyées seulement aussi vite que la vitesse du réseau le permet.

La propriété socket.bufferedAmount stocke le nombre d’octets restant en mémoire tampon à cet instant, en attente d’être envoyés sur le réseau.

Nous pouvons l’examiner pour voir si le socket est effectivement disponible pour la transmission.

// every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);

Fermeture de la connexion

Normalement, lorsqu’une partie veut fermer la connexion (le navigateur et le serveur ont les mêmes droits), elle envoie un « cadre de fermeture de la connexion » avec un code numérique et une raison textuelle.

La méthode pour cela est la suivante :

socket.close(, );

  • code est un code spécial de fermeture de WebSocket (facultatif)
  • .reason

    est une chaîne de caractères qui décrit la raison de la fermeture (facultatif)

Alors l’autre partie dans close gestionnaire d’événement obtient le code et la raison, e.g.:

// closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};

Les valeurs de code les plus courantes :

  • 1000 – la fermeture par défaut, normale (utilisée en l’absence de code fournie),
  • 1006 – aucun moyen de définir ce code manuellement, indique que la connexion a été perdue (pas de cadre de fermeture).

Il existe d’autres codes comme:

  • 1001 – la partie disparaît, par ex. le serveur s’arrête, ou un navigateur quitte la page,
  • 1009 – le message est trop gros pour être traité,
  • 1011 – erreur inattendue sur le serveur,
  • …et ainsi de suite.

La liste complète se trouve dans le RFC6455, §7.4.1.

Les codes WebSocket sont un peu comme les codes HTTP, mais différents. En particulier, tous les codes inférieurs à 1000 sont réservés, il y aura une erreur si on essaie de définir un tel code.

// in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};

Etat de la connexion

Pour obtenir l’état de la connexion, en plus il y a socket.readyState propriété avec des valeurs :

  • 0 – « CONNECTING » : la connexion n’a pas encore été établie,
  • 1 – « OPEN » : en communication,
  • 2 – « CLOSING » : la connexion est en cours de fermeture,
  • 3 – « CLOSED » : la connexion est fermée.

Exemple de chat

Examinons un exemple de chat utilisant l’API WebSocket du navigateur et le module WebSocket de Node.js https://github.com/websockets/ws. Nous porterons l’attention principale sur le côté client, mais le serveur est également simple.

HTML : nous avons besoin d’une <form> pour envoyer des messages et d’une <div> pour les messages entrants :

<!-- message form --><form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"></form><!-- div with messages --><div></div>

Depuis JavaScript, nous voulons trois choses :

  1. Ouvrir la connexion.
  2. Sur soumission du formulaire – socket.send(message) pour le message.
  3. Sur message entrant – l’ajouter à div#messages.

Voici le code :

let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}

Le code côté serveur est un peu au-delà de notre portée. Ici, nous utiliserons Node.js, mais vous n’êtes pas obligé de le faire. D’autres plateformes ont également leurs moyens de travailler avec WebSocket.

L’algorithme côté serveur sera :

  1. Créer clients = new Set() – un ensemble de sockets.
  2. Pour chaque websocket acceptée, l’ajouter à l’ensemble clients.add(socket) et configurer message l’écouteur d’événements pour obtenir ses messages.
  3. Lorsqu’un message est reçu : itérer sur les clients et l’envoyer à tous.
  4. Lorsqu’une connexion est fermée : clients.delete(socket).
const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);});function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}

Voici l’exemple fonctionnel:

Vous pouvez également le télécharger (bouton en haut à droite dans l’iframe) et l’exécuter localement. N’oubliez simplement pas d’installer Node.js et npm install ws avant de l’exécuter.

Sommaire

WebSocket est un moyen moderne d’avoir des connexions persistantes navigateur-serveur.

  • LesWebSockets n’ont pas de limitations inter-origine.
  • Elles sont bien supportées dans les navigateurs.
  • Elles peuvent envoyer/recevoir des chaînes de caractères et des données binaires.

L’API est simple.

Méthodes:

  • socket.send(data),
  • socket.close(, ).

Evènements:

  • open,
  • message,
  • error,

  • close.

WebSocket par lui-même n’inclut pas la reconnexion, l’authentification et de nombreux autres mécanismes de haut niveau. Il existe donc des bibliothèques client/serveur pour cela, et il est également possible d’implémenter ces capacités manuellement.

Parfois, pour intégrer WebSocket dans un projet existant, les gens exécutent le serveur WebSocket en parallèle avec le serveur HTTP principal, et ils partagent une seule base de données. Les requêtes vers WebSocket utilisent wss://ws.site.com, un sous-domaine qui mène au serveur WebSocket, tandis que https://site.com va vers le serveur HTTP principal.

Sûrement, d’autres façons d’intégration sont également possibles.

Laisser un commentaire

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