WebSocketStream: Streams in die WebSocket API einbinden

Durch die Anwendung von Backpressure können Sie verhindern, dass Ihre App in WebSocket-Nachrichten untergeht oder ein WebSocket-Server mit Nachrichten überflutet wird.

Hintergrund

Die WebSocket API bietet eine JavaScript-Schnittstelle für das WebSocket-Protokoll, mit der eine interaktive bidirektionale Kommunikationssitzung zwischen dem Browser des Nutzers und einem Server geöffnet werden kann. Mit dieser API können Sie Nachrichten an einen Server senden und ereignisgesteuerte Antworten empfangen, ohne den Server nach einer Antwort zu fragen.

Streams API

Die Streams API ermöglicht es JavaScript, programmatisch auf Streams von Datenblöcken zuzugreifen, die über das Netzwerk empfangen werden, und sie nach Bedarf zu verarbeiten. Ein wichtiges Konzept im Zusammenhang mit Streams ist Gegendruck. So wird die Lese- oder Schreibgeschwindigkeit eines einzelnen Streams oder einer Pipe-Kette reguliert. Wenn der Stream selbst oder ein Stream später in der Pipelinekette noch beschäftigt ist und noch nicht bereit ist, weitere Chunks zu empfangen, wird ein Signal rückwärts durch die Kette gesendet, um die Übertragung entsprechend zu verlangsamen.

Das Problem mit der aktuellen WebSocket API

Es ist nicht möglich, den Rückstau auf empfangene Nachrichten anzuwenden.

Mit der aktuellen WebSocket API erfolgt die Reaktion auf eine Nachricht in WebSocket.onmessage, einer EventHandler, die aufgerufen wird, wenn eine Nachricht vom Server empfangen wird.

Angenommen, Sie haben eine Anwendung, die bei jedem Empfang einer neuen Nachricht umfangreiche Datenverarbeitungen durchführen muss. Sie würden den Ablauf wahrscheinlich ähnlich wie im folgenden Code einrichten. Da Sie das Ergebnis des process()-Aufrufs await, sollte alles in Ordnung sein, oder?

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

Leider falsch! Das Problem mit der aktuellen WebSocket API besteht darin, dass kein Backpressure angewendet werden kann. Wenn Nachrichten schneller eingehen, als die process()-Methode sie verarbeiten kann, wird der Rendering-Prozess entweder den Arbeitsspeicher durch Puffern dieser Nachrichten füllen, aufgrund einer CPU-Auslastung von 100% nicht mehr reagieren oder beides.

Rückstau bei gesendeten Nachrichten ist nicht ergonomisch

Es ist zwar möglich, den Rückstau auf gesendete Nachrichten anzuwenden, dazu muss jedoch die Eigenschaft WebSocket.bufferedAmount abgefragt werden, was ineffizient und nicht ergonomisch ist. Diese schreibgeschützte Eigenschaft gibt die Anzahl der Byte an Daten zurück, die mit Aufrufen von WebSocket.send() in die Warteschlange gestellt, aber noch nicht an das Netzwerk übertragen wurden. Dieser Wert wird auf null zurückgesetzt, sobald alle in die Warteschlange gestellten Daten gesendet wurden. Wenn Sie WebSocket.send() jedoch weiterhin aufrufen, steigt er weiter an.

Was ist die WebSocketStream API?

Die WebSocketStream API löst das Problem des nicht vorhandenen oder nicht ergonomischen Rückstaus, indem sie Streams in die WebSocket API integriert. Das bedeutet, dass Backpressure ohne zusätzliche Kosten angewendet werden kann.

Empfohlene Anwendungsfälle für die WebSocketStream API

Beispiele für Websites, die diese API verwenden können:

  • WebSocket-Anwendungen mit hoher Bandbreite, die Interaktivität erfordern, insbesondere Video- und Bildschirmfreigabe.
  • Ähnlich verhält es sich bei der Videoaufnahme und anderen Anwendungen, die im Browser viele Daten generieren, die auf den Server hochgeladen werden müssen. Durch Backpressure kann der Client die Datenerstellung stoppen, anstatt Daten im Arbeitsspeicher anzusammeln.

Aktueller Status

Schritt Status
1. Erklärung erstellen Abschließen
2. Ersten Entwurf der Spezifikation erstellen In Bearbeitung
3. Feedback einholen und Design iterieren In Bearbeitung
4. Ursprungstest Abschließen
5. Starten Nicht gestartet

WebSocketStream API verwenden

Die WebSocketStream API basiert auf Promise-Objekten, was die Verwendung in einem modernen JavaScript-Umfeld erleichtert. Zuerst erstellen Sie ein neues WebSocketStream und übergeben ihm die URL des WebSocket-Servers. Als Nächstes warten Sie, bis die Verbindung opened ist. Dies führt zu einem ReadableStream und/oder einem WritableStream.

Durch Aufrufen der Methode ReadableStream.getReader() erhalten Sie schließlich ein ReadableStreamDefaultReader, aus dem Sie dann read()-Daten abrufen können, bis der Stream abgeschlossen ist, d. h. bis er ein Objekt der Form {value: undefined, done: true} zurückgibt.

Wenn Sie die Methode WritableStream.getWriter() aufrufen, erhalten Sie schließlich eine WritableStreamDefaultWriter, in die Sie dann write() können.

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

Blockierung

Was ist mit der versprochenen Backpressure-Funktion? Sie erhalten sie „kostenlos“, ohne dass zusätzliche Schritte erforderlich sind. Wenn process() zusätzliche Zeit in Anspruch nimmt, wird die nächste Nachricht erst verarbeitet, wenn die Pipeline bereit ist. Ebenso wird der Schritt WritableStreamDefaultWriter.write() nur ausgeführt, wenn dies sicher ist.

Erweiterte Beispiele

Das zweite Argument für WebSocketStream ist ein Options-Bag, um zukünftige Erweiterungen zu ermöglichen. Die einzige Option ist protocols, die sich genauso verhält wie das zweite Argument für den WebSocket-Konstruktor:

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

Die ausgewählte protocol sowie potenzielle extensions sind Teil des über das WebSocketStream.opened-Promise verfügbaren Dictionarys. Alle Informationen zur Live-Verbindung werden von diesem Promise bereitgestellt, da es nicht relevant ist, ob die Verbindung fehlschlägt.

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

Informationen zur geschlossenen WebSocketStream-Verbindung

Die Informationen, die über die Ereignisse WebSocket.onclose und WebSocket.onerror in der WebSocket API verfügbar waren, sind jetzt über das WebSocketStream.closed-Promise verfügbar. Das Promise wird im Falle eines nicht sauberen Schließens abgelehnt. Andernfalls wird es mit dem vom Server gesendeten Code und Grund aufgelöst.

Alle möglichen Statuscodes und ihre Bedeutung werden in der Liste der CloseEvent-Statuscodes erläutert.

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

WebSocketStream-Verbindung schließen

Ein WebSocketStream kann mit einem AbortController geschlossen werden. Übergeben Sie daher ein AbortSignal an den WebSocketStream-Konstruktor. AbortController.abort() funktioniert nur vor dem Handshake, nicht danach.

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

Alternativ können Sie auch die Methode WebSocketStream.close() verwenden. Ihr Hauptzweck besteht jedoch darin, den Code und den Grund anzugeben, der an den Server gesendet wird.

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

Progressive Enhancement und Interoperabilität

Chrome ist derzeit der einzige Browser, in dem die WebSocketStream API implementiert ist. Aus Gründen der Interoperabilität mit der klassischen WebSocket API ist es nicht möglich, Backpressure auf empfangene Nachrichten anzuwenden. Es ist zwar möglich, den Rückstau auf gesendete Nachrichten anzuwenden, dazu muss jedoch die Eigenschaft WebSocket.bufferedAmount abgefragt werden, was ineffizient und nicht ergonomisch ist.

Funktionserkennung

So prüfen Sie, ob die WebSocketStream API unterstützt wird:

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

Demo

In unterstützten Browsern können Sie die WebSocketStream API im eingebetteten iFrame oder direkt auf Glitch in Aktion sehen.

Feedback

Das Chrome-Team möchte mehr über Ihre Erfahrungen mit der WebSocketStream API erfahren.

Informationen zum API-Design

Funktioniert etwas an der API nicht wie erwartet? Oder fehlen Methoden oder Eigenschaften, die Sie für die Umsetzung Ihrer Idee benötigen? Haben Sie eine Frage oder einen Kommentar zum Sicherheitsmodell? Melden Sie ein Spezifikationsproblem im entsprechenden GitHub-Repository oder fügen Sie einem vorhandenen Problem Ihre Gedanken hinzu.

Problem mit der Implementierung melden

Haben Sie einen Fehler in der Chrome-Implementierung gefunden? Oder weicht die Implementierung von der Spezifikation ab? Melden Sie den Fehler unter new.crbug.com. Geben Sie so viele Details wie möglich an, fügen Sie eine einfache Anleitung zur Reproduktion hinzu und geben Sie Blink>Network>WebSockets in das Feld Components (Komponenten) ein.

API-Support zeigen

Planen Sie, die WebSocketStream API zu verwenden? Ihre öffentliche Unterstützung hilft dem Chrome-Team, Funktionen zu priorisieren, und zeigt anderen Browseranbietern, wie wichtig es ist, sie zu unterstützen.

Senden Sie einen Tweet an @ChromiumDev mit dem Hashtag #WebSocketStream und teilen Sie uns mit, wo und wie Sie die Funktion verwenden.

Nützliche Links

Danksagungen

Die WebSocketStream API wurde von Adam Rice und Yutaka Hirano implementiert.