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

Pour éviter que votre application ne soit submergée de messages WebSocket ou n'inonde un serveur WebSocket, appliquez une contre-pression.

Contexte

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 des événements sans avoir à interroger le serveur pour obtenir une réponse.

API Streams

L'API Streams permet à JavaScript d'accéder de manière automatisée aux flux de fragments de données reçus sur le réseau et de les traiter comme vous le souhaitez. Le concept d'contre-pression est important dans le contexte des flux. Il s'agit du processus par lequel un seul flux ou une chaîne de canaux régule la vitesse de lecture ou d'écriture. Lorsque le flux lui-même ou un flux plus tard dans la chaîne de canalisation est toujours occupé et n'est pas encore prêt à accepter d'autres segments, il envoie un signal à rebours dans la chaîne pour ralentir la diffusion, le cas échéant.

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

Supposons qu'une application doit effectuer des opérations de traitement de données lourdes chaque fois qu'un nouveau message est reçu. Vous allez probablement configurer le flux de la même manière que le code ci-dessous et, comme vous utilisez await pour obtenir le résultat de l'appel process(), tout devrait fonctionner correctement, 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 pression en arrière. 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 en mémoire tampon ces messages, devient inactif 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, 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 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 la contre-pression inexistante ou non ergonomique en intégrant les flux à l'aide de l'API WebSocket. Cela signifie que la contre-pression peut être appliquée "gratuitement", 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 pour la vidéo 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 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 un message d'explication Fin
2. Créer l'ébauche initiale de la spécification En cours
3. Recueillir les commentaires et itérer la conception En cours
4. Phase d'évaluation Fin
5. Lancer Non démarrée

Utiliser l'API WebSocketStream

L'API WebSocketStream est basée sur des promesses, ce qui rend son traitement naturel dans un 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 génère un ReadableStream et/ou un WritableStream.

En appelant la méthode ReadableStream.getReader(), vous obtenez finalement un ReadableStreamDefaultReader, à partir duquel vous pouvez read() des données jusqu'à ce que le flux soit terminé, c'est-à-dire jusqu'à ce qu'il renvoie un objet de type {value: undefined, done: true}.

Par conséquent, en appelant la méthode WritableStream.getWriter(), vous obtenez finalement un WritableStreamDefaultWriter, auquel 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 ? Vous l'obtenez "gratuitement", 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 elle peut être effectuée en toute sécurité.

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;

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 il n'est pas pertinent si la connexion échoue.

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

Informations sur la connexion WebSocketStream fermée

Les informations disponibles dans les événements WebSocket.onclose et WebSocket.onerror de l'API WebSocket sont désormais disponibles via la promesse WebSocketStream.closed. La promesse est refusée en cas de fermeture incorrecte. Dans le cas contraire, elle est liée au code et au 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 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 assurer 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, qui est inefficace et non ergonomique.

Détection de fonctionnalités

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ée ou directement sur Glitch.

Commentaires

L'équipe Chrome souhaite connaître votre expérience avec l'API WebSocketStream.

Présentez-nous la conception de l'API

Y a-t-il un aspect de l'API qui ne fonctionne pas comme prévu ? Ou s'il manque 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 de 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 détecté 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. Veillez à inclure autant de détails que possible, des instructions simples pour reproduire le problème et saisissez Blink>Network>WebSockets dans le champ Composants. Glitch est idéal pour partager rapidement et facilement des cas de reproduction.

Afficher la compatibilité avec l'API

Prévoyez-vous d'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 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.