WebSocketStream: intégrer des flux à l'API WebSocket

Évitez que votre application ne soit submergée par des messages WebSocket ou qu'elle n'inonde un serveur WebSocket de messages en appliquant une contre-pression.

Arrière-plan

L'API WebSocket fournit une interface JavaScript au protocole WebSocket, ce qui permet d'ouvrir une session de communication interactive bidirectionnelle entre le navigateur de l'utilisateur et un serveur. Cette API vous permet d'envoyer des messages à un serveur et de recevoir des réponses basées sur des événements sans interroger le serveur pour obtenir une réponse.

API Streams

L'API Streams permet à JavaScript d'accéder de manière programmatique aux flux de blocs de données reçus sur le réseau et de les traiter comme souhaité. Un concept important dans le contexte des flux est la rétropression. Il s'agit du processus par lequel un flux unique ou une chaîne de pipes régule la vitesse de lecture ou d'écriture. Lorsque le flux lui-même ou un flux ultérieur de la chaîne de pipeline est toujours occupé et n'est pas encore prêt à accepter d'autres blocs, il envoie un signal en arrière dans la chaîne pour ralentir la diffusion, le cas échéant.

Problème avec l'API WebSocket actuelle

Il est impossible d'appliquer une contre-pression aux messages reçus.

Avec l'API WebSocket actuelle, la réaction à un message se produit dans WebSocket.onmessage, un EventHandler appelé lorsqu'un message est reçu du serveur.

Imaginons que vous disposiez d'une application qui doit effectuer des opérations de traitement de données intensives chaque fois qu'un nouveau message est reçu. Vous configurerez probablement le flux de manière semblable au code ci-dessous. Comme vous await le résultat de l'appel process(), tout devrait bien se passer, n'est-ce pas ?

// A heavy data crunching operation.
const process = async (data) => {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      console.log('WebSocket message processed:', data);
      return resolve('done');
    }, 1000);
  });
};

webSocket.onmessage = async (event) => {
  const data = event.data;
  // Await the result of the processing step in the message handler.
  await process(data);
};

Faux ! Le problème avec l'API WebSocket actuelle est qu'il n'existe aucun moyen d'appliquer une contre-pression. Lorsque les messages arrivent plus vite que la méthode process() ne peut les traiter, le processus de rendu remplit la mémoire en mettant ces messages en mémoire tampon, ne répond plus en raison d'une utilisation du processeur à 100 %, ou les deux.

L'application d'une contre-pression aux messages envoyés n'est pas ergonomique

Il est possible d'appliquer une contre-pression aux messages envoyés, mais cela implique d'interroger la propriété WebSocket.bufferedAmount, ce qui est inefficace et non ergonomique. Cette propriété en lecture seule renvoie le nombre d'octets de données qui ont été mis en file d'attente à l'aide d'appels à WebSocket.send(), mais qui n'ont pas encore été transmis au réseau. Cette valeur est réinitialisée à zéro une fois que toutes les données en file d'attente ont été envoyées, mais si vous continuez à appeler WebSocket.send(), elle continuera d'augmenter.

Qu'est-ce que l'API WebSocketStream ?

L'API WebSocketStream résout le problème de contre-pression inexistante ou non ergonomique en intégrant des flux à l'API WebSocket. Cela signifie que la contre-pression peut être appliquée "sans frais", sans aucun coût supplémentaire.

Cas d'utilisation suggérés pour l'API WebSocketStream

Voici quelques exemples de sites pouvant utiliser cette API :

  • Applications WebSocket à bande passante élevée qui doivent conserver l'interactivité, en particulier pour la vidéo et le partage d'écran.
  • De même, la capture vidéo et d'autres applications génèrent beaucoup de données dans le navigateur, qui doivent être importées sur le serveur. Avec la contre-pression, le client peut arrêter de produire des données au lieu de les accumuler en mémoire.

État actuel

Étape État
1. Créer une explication Fin
2. Créer une première ébauche de spécification En cours
3. Recueillir des commentaires et améliorer la conception En cours
4. Essai Origin Trial Fin
5. Lancer Non démarré

Utiliser l'API WebSocketStream

L'API WebSocketStream est basée sur des promesses, ce qui la rend naturelle à utiliser dans le monde JavaScript moderne. Commencez par créer un WebSocketStream et transmettez-lui l'URL du serveur WebSocket. Ensuite, vous attendez que la connexion soit opened, ce qui entraîne un ReadableStream et/ou un WritableStream.

En appelant la méthode ReadableStream.getReader(), vous obtenez enfin un ReadableStreamDefaultReader, que vous pouvez ensuite read() les données jusqu'à la fin du flux, c'est-à-dire jusqu'à ce qu'il renvoie un objet de la forme {value: undefined, done: true}.

Par conséquent, en appelant la méthode WritableStream.getWriter(), vous obtenez enfin un WritableStreamDefaultWriter, que vous pouvez ensuite write().

  const wss = new WebSocketStream(WSS_URL);
  const {readable, writable} = await wss.opened;
  const reader = readable.getReader();
  const writer = writable.getWriter();

  while (true) {
    const {value, done} = await reader.read();
    if (done) {
      break;
    }
    const result = await process(value);
    await writer.write(result);
  }

Contre-pression

Qu'en est-il de la fonctionnalité de contre-pression promise ? Vous l'obtenez "sans frais", sans aucune étape supplémentaire. Si process() prend plus de temps, le message suivant n'est consommé qu'une fois le pipeline prêt. De même, l'étape WritableStreamDefaultWriter.write() ne se poursuit que si cela ne présente aucun risque.

Exemples avancés

Le deuxième argument de WebSocketStream est un sac d'options permettant une extension future. La seule option est protocols, qui se comporte de la même manière que le deuxième argument du constructeur WebSocket :

const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.opened;

Les protocol sélectionnés ainsi que les extensions potentiels font partie du dictionnaire disponible via la promesse WebSocketStream.opened. Toutes les informations sur la connexion en direct sont fournies par cette promesse, car elles ne sont pas pertinentes si la connexion échoue.

const {readable, writable, protocol, extensions} = await chatWSS.opened;

Informations sur la connexion WebSocketStream fermée

Les informations disponibles à partir des événements WebSocket.onclose et WebSocket.onerror dans l'API WebSocket sont désormais disponibles via la promesse WebSocketStream.closed. La promesse est rejetée en cas de fermeture non propre. Sinon, elle est résolue avec le code et le motif envoyés par le serveur.

Tous les codes d'état possibles et leur signification sont expliqués dans la liste des codes d'état CloseEvent.

const {code, reason} = await chatWSS.closed;

Fermer une connexion WebSocketStream

Un WebSocketStream peut être fermé avec un AbortController. Par conséquent, transmettez un AbortSignal au constructeur WebSocketStream. AbortController.abort() ne fonctionne qu'avant l'établissement de la connexion, et non après.

const controller = new AbortController();
const wss = new WebSocketStream(URL, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);

Vous pouvez également utiliser la méthode WebSocketStream.close(), mais son objectif principal est de permettre de spécifier le code et le motif qui sont envoyés au serveur.

wss.close({closeCode: 4000, reason: 'Game over'});

Amélioration progressive et interopérabilité

Chrome est actuellement le seul navigateur à implémenter l'API WebSocketStream. Pour l'interopérabilité avec l'API WebSocket classique, il n'est pas possible d'appliquer une contre-pression aux messages reçus. Il est possible d'appliquer une contre-pression aux messages envoyés, mais cela implique d'interroger la propriété WebSocket.bufferedAmount, ce qui est inefficace et non ergonomique.

Détection de caractéristiques

Pour vérifier si l'API WebSocketStream est compatible, utilisez :

if ('WebSocketStream' in window) {
  // `WebSocketStream` is supported!
}

Démo

Dans les navigateurs compatibles, vous pouvez voir l'API WebSocketStream en action dans l'iframe intégré ou directement sur Glitch.

Commentaires

L'équipe Chrome souhaite connaître votre avis sur l'API WebSocketStream.

Parlez-nous de la conception de l'API

Y a-t-il quelque chose dans l'API qui ne fonctionne pas comme prévu ? Manque-t-il des méthodes ou des propriétés dont vous avez besoin pour implémenter votre idée ? Vous avez une question ou un commentaire sur le modèle de sécurité ? Signalez un problème lié à la spécification dans le dépôt GitHub correspondant ou ajoutez vos commentaires à un problème existant.

Signaler un problème d'implémentation

Avez-vous trouvé un bug dans l'implémentation de Chrome ? Ou l'implémentation est-elle différente de la spécification ? Signalez un bug sur new.crbug.com. Assurez-vous d'inclure autant de détails que possible, des instructions simples pour reproduire le problème et saisissez Blink>Network>WebSockets dans la zone Composants.

Soutenir l'API

Comptez-vous utiliser l'API WebSocketStream ? Votre soutien public aide l'équipe Chrome à hiérarchiser les fonctionnalités et montre aux autres fournisseurs de navigateurs à quel point il est essentiel de les prendre en charge.

Envoyez un tweet à @ChromiumDev avec le hashtag #WebSocketStream pour nous indiquer où et comment vous l'utilisez.

Liens utiles

Remerciements

L'API WebSocketStream a été implémentée par Adam Rice et Yutaka Hirano.