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

Empêchez votre application de se noyer dans des messages WebSocket ou d'inonder un serveur WebSocket de messages en appliquant une contre-pression.

Contexte

API WebSocket

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. Avec cette API, vous pouvez envoyer des messages à un serveur et recevoir des réponses basées sur les é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 fragments de données reçus sur le réseau et de les traiter selon vos besoins. Dans le contexte des flux, la contre-pression est un concept important. Il s'agit du processus par lequel un seul flux ou une chaîne de tuyaux régule la vitesse de lecture ou d'écriture. Lorsque le flux lui-même ou un flux situé plus tard dans la chaîne de pipelines est toujours occupé et n'est pas encore prêt à accepter plus de fragments, il envoie un signal en arrière dans la chaîne pour ralentir la diffusion, le cas échéant.

Problème lié à 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 a lieu dans WebSocket.onmessage, un EventHandler appelé lorsqu'un message est reçu du serveur.

Supposons que vous disposiez d'une application qui doit effectuer de lourdes opérations de traitement de données à chaque réception d'un nouveau message. Vous allez probablement configurer le flux de la même manière que le code ci-dessous. Comme vous utilisez await pour le résultat de l'appel process(), vous devriez donc être correct, 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 la contre-pression. Lorsque les messages arrivent plus rapidement que la méthode process() ne peut les gérer, le processus de rendu remplit la mémoire en mettant ces messages en mémoire tampon ou ne répond plus en raison d'une utilisation de 100% du processeur, ou les deux.

L'application de la 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, qui est inefficace et non ergonomique. Cette propriété en lecture seule renvoie le nombre d'octets de données mis en file d'attente à l'aide d'appels à WebSocket.send(), mais qui n'ont pas encore été transmises au réseau. Cette valeur est réinitialisée une fois que toutes les données en file d'attente ont été envoyées, mais si vous continuez à appeler WebSocket.send(), l'augmentation continue.

Qu'est-ce que l'API WebSocketStream ?

L'API WebSocketStream résout le problème des contre-pressions inexistantes ou non ergonomiques en intégrant des flux à l'API WebSocket. Cela signifie que la contre-pression peut être appliquée "sans frais", sans frais supplémentaires.

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

Voici quelques exemples de sites qui peuvent utiliser cette API:

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

État actuel

| Étape | État | | ------------------------------------------ | ---------------------------- | | 1. Créer une vidéo explicative | [Complète][explication] | | 2. Créer une ébauche initiale de spécifications | [En cours][spécification] | | 3. Recueillir les commentaires et itérer la conception | [En cours](#feedback) | | 4. Phase d'évaluation | [Terminé][o] | | 5. Lancement | Pas commencé |

Utiliser l'API WebSocketStream

Exemple d'introduction

L'API WebSocketStream est basée sur des promesses, ce qui la rend naturelle dans un monde JavaScript moderne. Commencez par construire un nouveau WebSocketStream et lui transmettez l'URL du serveur WebSocket. Attendez ensuite que la connexion soit opened, ce qui génère un ReadableStream et/ou un WritableStream.

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

Par conséquent, en appelant la méthode WritableStream.getWriter(), vous obtenez enfin un WritableStreamDefaultWriter, dans lequel vous pouvez ensuite write() des données.

  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 annoncée ? Comme je l'ai écrit ci-dessus, cette fonctionnalité est "sans frais", et aucune étape supplémentaire n'est nécessaire. Si process() prend plus de temps, le message suivant ne sera utilisé qu'une fois le pipeline prêt. De même, l'étape WritableStreamDefaultWriter.write() ne se déroule que si vous pouvez le faire sans risque.

Exemples avancés

Le deuxième argument de WebSocketStream est un sac d'options permettant une extension future. Actuellement, 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;

Le protocol sélectionné ainsi que le extensions potentiel 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 en cas d'échec de la connexion.

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

Informations sur la connexion WebSocketStream fermée

Les informations qui étaient 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 incorrecte, sinon elle est résolue en fonction du code et du 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.

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 vous permettre de spécifier le code et le motif envoyés au serveur.

wss.close({code: 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, l'application de contre-pression aux messages reçus n'est pas possible. Il est possible d'appliquer une contre-pression aux messages envoyés, mais cela implique d'interroger la propriété WebSocket.bufferedAmount, 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émonstration

Sur 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.

Décrivez-nous la conception de l'API

Y a-t-il un problème avec l'API qui ne fonctionne pas comme prévu ? Ou manque-t-il des méthodes ou des propriétés dont vous avez besoin pour mettre en œuvre votre idée ? Vous avez une question ou un commentaire sur le modèle de sécurité ? Signalez un problème spécifique dans le dépôt GitHub correspondant ou ajoutez vos réflexions à un problème existant.

Signaler un problème d'implémentation

Avez-vous rencontré un bug dans l'implémentation de Chrome ? Ou la mise en œuvre est-elle différente des spécifications ? Signalez un bug sur new.crbug.com. Veillez à inclure autant de détails que possible, ainsi que des instructions simples pour le reproduire, puis saisissez Blink>Network>WebSockets dans la zone Composants. Glitch est idéal pour partager des cas de reproduction rapides et faciles.

Montrer votre compatibilité avec l'API

Prévoyez-vous d'utiliser l'API WebSocketStream ? Votre assistance publique permet à l'équipe Chrome de 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 en utilisant le hashtag #WebSocketStream, et indiquez-nous où et comment vous l'utilisez.

Liens utiles

Remerciements

L'API WebSocketStream a été implémentée par Adam Rice et Yutaka Hirano. Image principale de Daan Mooij sur Unsplash.