Précédent Server-Sent Events (SSE)
Suivant Index B-tree et performances SQL

WebSockets

Ce billet fait partie d'une série en 10 volets : #1, #2, #3, #4, #5, #6, #7, #8, #9 et #10.

Notes de lecture de WebSocket.

Les WebSockets permettent une communication bidirectionnelle (full-duplex) de données textuelles UTF-8 ou binaires entre un client et un serveur.

Le concept englobe deux choses : l'API WebSocket et le protocole WebSocket.

Les WebSockets sont ce qu'il y a de plus proche d'une socket avec des services supplémentaires :

  • application de la politique de même origine
  • interopérabilité avec l'infrastructure HTTP existante
  • communication orientée messages
  • segmentation (ou tramage) des messages
  • extensibilité du protocole
  • négociation de sous-protocoles

Les WebSockets permettent de créer des canaux de communication via des protocoles arbitraires et d'échanger n'importe quel type de données (JSON, messages binaires personnalisés etc.) en mode streaming.

De l'avis de nombreuses personnes, les WebSockets sont complexes :

What started out as a really small simple thing ended up as an abomination of (what feels like) needles complexity.

Cette citation aussi est marrante :

WebSocket est un protocole totalement alambiqué pour contourner la stupidité du monde

Quoi qu'il en soit, cet outil existe et il est prêt pour HTTP/2 (RFC 8441).

Je conseille la lecture de WebSockets - A Conceptual Deep-Dive avant de poursuivre.

API WebSocket

L'API WebSocket est relativement simple.

Parfois il faut envisager un framework real-time (type Socket.IO) car une connexion WebSocket n'est pas rétablie automatiquement par exemple. Dans ce cas, il faut faire attention à l'implémentation et aux méthodes de transport utilisées.

Les URIs WebSockets ont leur propre schéma en ws:// ou wss:// (TCP+TLS).

Recevoir des données textuelles ou binaires

Une communication WebSocket est composée de messages.

Il n'y a pas besoin de se soucier de la mise en mémoire tampon, de l'analyse et de la reconstitution des données reçues. Si le serveur envoie un payload de 1 Mo, onmessage ne sera appelé côté client que quand le message entier sera disponible.

Le protocole n'a que deux informations sur les messages : la longueur du payload et son type pour distinguer les transferts UTF-8 des transferts binaires. Un nouveau message est automatiquement converti par le navigateur en un objet DOMString ou en un objet Blob pour les données binaires.

Il est possible d'utiliser un ArrayBuffer à la place d'un Blob s'il s'avère plus efficace de conserver les données binaires en mémoire pour effectuer un traitement supplémentaire, ce qui permet par exemple de streamer du binaire dans le navigateur.

Envoyer des données textuelles ou binaires

L'échange de données dans les deux sens se fait au dessus d'une seule et même connexion TCP.

Pour envoyer, on utilise la méthode send() qui est asynchrone. Elle se termine immédiatement mais les données sont mises en file d'attente côté client. La taille de la file d'attente peut être inspectée via l'attribut bufferedAmount.

Tous les messages WebSocket sont envoyés dans l'ordre exact dans lequel ils sont mis en file d'attente.

Du coup, beaucoup de messages en attente (ou un seul gros message) retarderont la livraison des messages en attente : le protocole est soumis au head-of-line blocking. Stratégies possibles de contournement :

  • segmenter les messages en morceaux plus petits et surveiller le bufferedAmount
  • mettre en place son propre système de priorité des messages

Négociation de sous-protocoles

Un seul bit permet de savoir si un message contient du texte ou des données binaires, mais le contenu du message est opaque.

Il n'y a pas de mécanisme permettant d'ajouter des métadonnées (type en-têtes HTTP) à un message WebSocket.

Si des métadonnées sont nécessaires à l'échange, le client et le serveur doivent mettre en œuvre leur propre sous-protocole.

La négociation de sous-protocoles permet au client d'annoncer au serveur les protocoles qu'il prend en charge pendant le handshake de connexion.

Protocole WebSocket

Le protocole WebSocket se compose d'un handshake HTTP et d'un système de segmentation binaire des messages.

En théorie, le design du protocole WebSocket ne le limite pas à HTTP, des implémentations futures pourraient utiliser un handshake plus simple. En pratique ça paraît compliqué à cause de l'infrastructure réseau actuelle.

Segmentation binaire des messages

Un format de trame binaire spécial est utilisé qui segmente chaque message en une ou plusieurs trames (frames), les transporte, les reconstitue, puis notifie le destinataire une fois le message entier reçu.

Un message est une séquence complète de trames qui sont la plus petite unité de communication.

Handshake HTTP

Le protocole a choisi HTTP pour le handshake de façon pragmatique afin de tirer partie de l'infrastructure HTTP existante : les serveurs WebSocket peuvent fonctionner sur les ports 80 et 443.

Le principe est d'utiliser un en-tête Upgrade.

Les requêtes initiées par le navigateur pendant le handshake sont soumise à la politique de même origine : l'en-tête Origin est ajoutée automatiquement et le serveur peut utiliser CORS pour accepter ou refuser la communication.

Après un handshake réussi, il n'y a plus aucune autre communication HTTP explicite entre le client et le serveur, le protocole WebSocket prend le relais au dessus de TCP.

WebSocket vs XHR vs SSE

WebSocket permet une communication bidirectionnelle sur la même connexion TCP : client et serveur peuvent échanger des messages à volonté. La latence est faible dans les deux directions mais en cas de perte de la connexion elle n'est pas rétablie automatiquement.

XHR permet une communication "transactionnelle" : le client envoie une requête HTTP complète et le serveur retourne une réponse complète.

SSE permet une communication unidirectionnelle en continu efficace et à faible latence de données uniquement textuelles du serveur vers le client. Le client ne peut pas envoyer de données au serveur après le handshake.

Déployer les WebSockets

De nombreux serveurs et intermédiaires sont configurés de façon agressive pour faire expirer les connexions HTTP inactives.

Ça pose problème pour les sessions WebSocket qui doivent être de longue durée.

Une solution (pas infaillible) est de passer par une connexion sécurisée de bout en bout pour contourner les proxies.

Mais même un routeur BGP reste soumis à l'électricité, et s'il redémarre, la connexion TCP est perdue :

Websockets suck here. BGP routers here flap very often, nuking TCP connections because hey, power cuts. Guess what happens?