WebSocketStream: integrazione dei flussi con l'API WebSocket

Impedisci che la tua app venga annegata nei messaggi WebSocket o inonda di messaggi un server WebSocket applicando la contropressione.

Contesto

L'API WebSocket

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 chiedere al server una risposta.

API Streams

L'API Streams consente a JavaScript di accedere in modo programmatico ai flussi di blocchi di dati ricevuti sulla rete ed elaborarli come desiderato. Un concetto importante nel contesto dei flussi di dati è la contropressione. Si tratta del processo mediante il quale un singolo flusso o una pipeline regola la velocità di lettura o scrittura. Quando il flusso stesso o uno stream successivo nella catena di pipeline è ancora occupato e non è ancora pronto ad accettare altri blocchi, invia un segnale a ritroso attraverso la catena per rallentare la distribuzione, come opportuno.

Il problema con l'API WebSocket attuale

Non è possibile applicare la contropressione ai messaggi ricevuti

Con l'API WebSocket attuale, la reazione a un messaggio avviene in WebSocket.onmessage, un EventHandler richiamato alla ricezione di un messaggio dal server.

Supponiamo che tu abbia un'applicazione che deve eseguire pesanti operazioni di analisi dei dati ogni volta che viene ricevuto un nuovo messaggio. Probabilmente configureresti il flusso in modo simile al codice seguente. Dato che 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'API WebSocket attuale è che non esiste un modo per applicare la contropressione. Quando i messaggi arrivano più velocemente di quanto il metodo process() sia in grado di gestirli, il processo di rendering riempirà la memoria eseguendo il buffering di questi messaggi, non risponderà più a causa dell'utilizzo del 100% della CPU o entrambi.

La contropressione ai messaggi inviati non è ergonomica

È possibile applicare la contropressione ai messaggi inviati, 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 messi in coda utilizzando le chiamate a WebSocket.send(), ma non ancora trasmessi alla rete. Questo valore viene reimpostato su zero una volta che tutti i dati in coda sono stati inviati, ma se continui a chiamare WebSocket.send(), il numero continuerà a salire.

Che cos'è l'API WebSocketStream?

L'API WebSocketStream affronta il problema di contropressione inesistente o non ergonomica integrando i flussi con l'API WebSocket. Ciò significa che la contropressione può essere applicata "senza costi", senza alcun costo aggiuntivo.

Casi d'uso suggeriti per l'API WebSocketStream

Esempi di siti che possono utilizzare questa API includono:

  • Applicazioni WebSocket a larghezza di banda elevata che devono mantenere l'interattività, in particolare la condivisione di video e schermo.
  • Analogamente, all'acquisizione di video e ad altre applicazioni che generano nel browser molti dati da caricare sul server. Con la contropressione, il client può smettere di produrre dati anziché accumulare dati in memoria.

Stato attuale

| Passaggio | Stato | | ------------------------------------------ | ---------------------------- | | 1. Crea un messaggio esplicativo | [Completo][spiegazione] | | 2. Crea la bozza iniziale della specifica | [In corso][spec] | | 3. Raccogli feedback e ottimizza il design | [In corso](#feedback) | | 4. Prova dell'origine | [Completa][ot] | | 5. Lancio | Non iniziato |

Come utilizzare l'API WebSocketStream

Esempio introduttivo

L'API WebSocketStream si basa sulle promesse, il che fa sembrare naturale in un mondo JavaScript moderno. Per iniziare, crea un nuovo WebSocketStream e trasmettilo l'URL del server WebSocket. Successivamente, attendi che la connessione sia opened, che genererà ReadableStream e/o WritableStream.

Chiamando il metodo ReadableStream.getReader(), ottieni finalmente un ReadableStreamDefaultReader, da cui puoi read() usare i dati fino al completamento del flusso, ovvero fino a quando non restituisce un oggetto nel formato {value: undefined, done: true}.

Di conseguenza, chiamando il metodo WritableStream.getWriter(), si ottiene infine un WritableStreamDefaultWriter, su cui è possibile write() utilizzare 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? Come ho scritto sopra, l'offerta è "senza costi" e non sono necessari passaggi aggiuntivi. Se process() richiede più tempo, il messaggio successivo verrà utilizzato solo quando la pipeline è pronta. Analogamente, il passaggio WritableStreamDefaultWriter.write() verrà eseguito solo se è sicuro farlo.

Esempi avanzati

Il secondo argomento per WebSocketStream è un riquadro di opzioni per consentire l'estensione futura. Attualmente l'unica opzione è protocols, che si comporta come il secondo argomento del costruttore WebSocket:

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

Il protocol selezionato e il potenziale extensions fanno parte del dizionario disponibile tramite la promessa WebSocketStream.opened. Tutte le informazioni sulla connessione in tempo reale sono fornite da questa promessa, poiché non sono pertinenti in caso di interruzione della connessione.

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

Informazioni sulla connessione WebSocketStream chiusa

Le informazioni che erano disponibili negli 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 imprevista, altrimenti viene risolta nel codice e nel 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. Di conseguenza, passa un AbortSignal al costruttore WebSocketStream.

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

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

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

Miglioramento progressivo e interoperabilità

Attualmente 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. È possibile applicare la contropressione ai messaggi inviati, ma comporta il polling della proprietà WebSocket.bufferedAmount, che è inefficiente e non ergonomica.

Rilevamento funzionalità

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

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

Demo

Sui browser che supportano i browser, puoi vedere l'API WebSocketStream in azione nell'iframe incorporato o direttamente su Glitch.

Feedback

Il team di Chrome vuole conoscere le tue esperienze con l'API WebSocketStream.

Parlaci del design dell'API

C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà per realizzare la tua idea? Hai domande o commenti sul modello di sicurezza? Segnala un problema di specifiche nel repository GitHub corrispondente oppure aggiungi le tue riflessioni su un problema esistente.

Segnalare un problema di implementazione

Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione è diversa dalle specifiche? Segnala un bug all'indirizzo new.crbug.com. Assicurati di includere il maggior numero possibile di dettagli, semplici istruzioni per la riproduzione e inserisci Blink>Network>WebSockets nella casella Componenti. Glitch è perfetto per condividere custodie di riproduzione facile e veloce.

Mostra il supporto per l'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 è importante supportarle.

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

Link utili

Ringraziamenti

L'API WebSocketStream è stata implementata da Adam Rice e Yutaka Hirano. Immagine hero di Daan Mooij su Unsplash.