WebSocketStream: integrazione degli stream con l'API WebSocket

Evita che la tua app venga sommersa da messaggi WebSocket o che un server WebSocket venga inondato di messaggi applicando la contropressione.

Sfondo

L'API WebSocket fornisce un'interfaccia JavaScript al protocollo WebSocket, che consente di aprire una sessione di comunicazione interattiva bidirezionale tra il browser dell'utente e un server. Con questa API, puoi inviare messaggi a un server e ricevere risposte basate su eventi senza eseguire il polling del server per una risposta.

API Streams

L'API Streams consente a JavaScript di accedere in modo programmatico ai flussi di blocchi di dati ricevuti tramite la rete ed elaborarli come preferisci. Un concetto importante nel contesto degli stream è backpressure. Questo è il processo mediante il quale un singolo stream o una catena di pipe regola la velocità di lettura o scrittura. Quando lo stream stesso o uno stream successivo nella catena di pipeline è ancora occupato e non è ancora pronto ad accettare altri chunk, invia un segnale all'indietro attraverso la catena per rallentare la distribuzione in modo appropriato.

Il problema con l'attuale API WebSocket

L'applicazione del backpressure ai messaggi ricevuti è impossibile

Con l'attuale API WebSocket, la reazione a un messaggio avviene in WebSocket.onmessage, un EventHandler chiamato quando viene ricevuto un messaggio dal server.

Supponiamo di avere un'applicazione che deve eseguire operazioni di elaborazione di grandi quantità di dati ogni volta che viene ricevuto un nuovo messaggio. Probabilmente configurerai il flusso in modo simile al codice riportato di seguito. Poiché await il risultato della chiamata process(), dovresti essere a posto, giusto?

// 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);
};

Sbagliato. Il problema con l'attuale API WebSocket è che non è possibile applicare la contropressione. Quando i messaggi arrivano più velocemente di quanto il metodo process() possa gestirli, il processo di rendering riempirà la memoria memorizzando nel buffer questi messaggi, diventerà non reattivo a causa dell'utilizzo della CPU al 100% o entrambe le cose.

L'applicazione della contropressione ai messaggi inviati non è ergonomica

L'applicazione della contropressione ai messaggi inviati è possibile, ma comporta il polling della proprietà WebSocket.bufferedAmount, che è inefficiente e non ergonomica. Questa proprietà di sola lettura restituisce il numero di byte di dati che sono stati accodati utilizzando le chiamate a WebSocket.send(), ma non ancora trasmessi alla rete. Questo valore viene reimpostato su zero una volta inviati tutti i dati in coda, ma se continui a chiamare WebSocket.send(), continuerà ad aumentare.

Che cos'è l'API WebSocketStream?

L'API WebSocketStream risolve il problema della contropressione inesistente o non ergonomica integrando i flussi con l'API WebSocket. Ciò significa che la contropressione può essere applicata "senza costi", senza costi aggiuntivi.

Casi d'uso suggeriti per l'API WebSocketStream

Ecco alcuni esempi di siti che possono utilizzare questa API:

  • Applicazioni WebSocket a larghezza di banda elevata che devono mantenere l'interattività, in particolare video e condivisione dello schermo.
  • Allo stesso modo, la registrazione video e altre applicazioni che generano molti dati nel browser che devono essere caricati sul server. Con la contropressione, il client può interrompere la produzione di dati anziché accumularli in memoria.

Stato attuale

Passaggio Stato
1. Creare una spiegazione Completato
2. Crea la bozza iniziale delle specifiche In corso
3. Raccogli feedback e itera il design In corso
4. Prova dell'origine Completato
5. Lancio Non avviato

Come utilizzare l'API WebSocketStream

L'API WebSocketStream è basata su promesse, il che la rende naturale in un mondo JavaScript moderno. Inizia creando un nuovo WebSocketStream e passando l'URL del server WebSocket. A questo punto, attendi che la connessione sia opened, il che comporta un ReadableStream e/o un WritableStream.

Chiamando il metodo ReadableStream.getReader(), ottieni infine un ReadableStreamDefaultReader, che puoi quindi read() fino al termine dello stream, ovvero fino a quando non restituisce un oggetto del modulo {value: undefined, done: true}.

Di conseguenza, chiamando il metodo WritableStream.getWriter(), ottieni infine un WritableStreamDefaultWriter, che puoi quindi write() i dati.

  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);
  }

Contropressione

E la funzionalità di contropressione promessa? Lo ricevi "senza costi", senza passaggi aggiuntivi. Se process() richiede più tempo, il messaggio successivo viene utilizzato solo quando la pipeline è pronta. Allo stesso modo, il passaggio WritableStreamDefaultWriter.write() procede solo se è sicuro farlo.

Esempi avanzati

Il secondo argomento di WebSocketStream è un insieme di opzioni per consentire l'estensione futura. L'unica opzione è protocols, che si comporta allo stesso modo del secondo argomento del costruttore WebSocket:

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

Il protocol selezionato, nonché i potenziali extensions, fanno parte del dizionario disponibile tramite la promessa WebSocketStream.opened. Tutte le informazioni sulla connessione live vengono fornite da questa promessa, poiché non è importante se la connessione non va a buon fine.

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

Informazioni sulla connessione WebSocketStream chiusa

Le informazioni disponibili dagli eventi WebSocket.onclose e WebSocket.onerror nell'API WebSocket sono ora disponibili tramite la promessa WebSocketStream.closed. La promessa viene rifiutata in caso di chiusura non pulita, altrimenti viene risolta con il codice e il motivo inviati dal server.

Tutti i possibili codici di stato e il loro significato sono spiegati nell'elenco dei codici di stato CloseEvent.

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

Chiusura di una connessione WebSocketStream

Un WebSocketStream può essere chiuso con un AbortController. Pertanto, passa un AbortSignal al costruttore WebSocketStream. AbortController.abort() funziona solo prima dell'handshake, non dopo.

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

In alternativa, puoi anche utilizzare il metodo WebSocketStream.close(), ma il suo scopo principale è consentire di specificare il codice e il motivo che viene inviato al server.

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

Miglioramento progressivo e interoperabilità

Al momento, Chrome è l'unico browser a implementare l'API WebSocketStream. Per l'interoperabilità con l'API WebSocket classica, non è possibile applicare la contropressione ai messaggi ricevuti. L'applicazione della contropressione ai messaggi inviati è possibile, ma comporta il polling della proprietà WebSocket.bufferedAmount, che è inefficiente e non ergonomica.

Rilevamento delle funzionalità

Per verificare se l'API WebSocketStream è supportata, utilizza:

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

Demo

Nei browser supportati, puoi vedere l'API WebSocketStream in azione nell'iframe incorporato o direttamente su Glitch.

Feedback

Il team di Chrome vuole conoscere la tua esperienza con l'API WebSocketStream.

Descrivi la progettazione dell'API

C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà che ti servono per implementare la tua idea? Hai una domanda o un commento sul modello di sicurezza? Invia una segnalazione relativa alle specifiche nel repository GitHub corrispondente o aggiungi i tuoi commenti a una segnalazione esistente.

Segnalare un problema relativo all'implementazione

Hai trovato un bug nell'implementazione di Chrome? L'implementazione è diversa dalla specifica? Segnala un bug all'indirizzo new.crbug.com. Assicurati di includere il maggior numero di dettagli possibile, istruzioni semplici per la riproduzione e inserisci Blink>Network>WebSockets nella casella Componenti.

Mostra il tuo sostegno all'API

Intendi utilizzare l'API WebSocketStream? Il tuo supporto pubblico aiuta il team di Chrome a dare la priorità alle funzionalità e mostra ad altri fornitori di browser quanto sia fondamentale supportarle.

Invia un tweet a @ChromiumDev utilizzando l'hashtag #WebSocketStream e facci sapere dove e come lo utilizzi.

Link utili

Ringraziamenti

L'API WebSocketStream è stata implementata da Adam Rice e Yutaka Hirano.