WebSocketStream: Streams mit der WebSocket API integrieren

Verhindere, dass deine App in WebSocket-Nachrichten überflutet wird oder einen WebSocket-Server mit Nachrichten überflutet, indem du Gegendruck anwendest.

Hintergrund

WebSocket-API

Die WebSocket API bietet eine JavaScript-Schnittstelle für das WebSocket-Protokoll. Sie ermöglicht das Öffnen einer wechselseitigen interaktiven Kommunikationssitzung zwischen dem Browser des Nutzers und einem Server. Mit dieser API können Sie Nachrichten an einen Server senden und ereignisgesteuerte Antworten empfangen, ohne den Server nach einer Antwort abzufragen.

Streams API

Mit der Streams API kann JavaScript programmatisch auf Streams von Datenblöcken zugreifen, die über das Netzwerk empfangen wurden, und diese nach Bedarf verarbeiten. Ein wichtiges Konzept im Kontext von Streams ist der Rückdruck. Dies ist der Prozess, bei dem ein einzelner Stream oder eine Pipe-Kette die Lese- oder Schreibgeschwindigkeit steuert. Wenn der Stream selbst oder ein Stream später in der Pipe-Kette noch ausgelastet ist und noch nicht bereit ist, weitere Blöcke aufzunehmen, wird ein Signal durch die Kette rückwärts gesendet, um die Bereitstellung entsprechend zu verlangsamen.

Das Problem mit der aktuellen WebSocket API

Bei empfangenen Nachrichten kann kein Gegendruck angewendet werden

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

Angenommen, Sie haben eine Anwendung, die beim Empfang einer neuen Nachricht umfangreiche Datenverarbeitungsvorgänge ausführen muss. Sie richten den Ablauf wahrscheinlich ähnlich wie im folgenden Code ein. Da Sie das Ergebnis des process()-Aufrufs await ausführen, sollte alles in Ordnung sein.

// 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 es keine Möglichkeit gibt, einen Gegendruck auszuüben. Wenn Nachrichten schneller ankommen, als die Methode process() sie verarbeiten kann, füllt der Renderingprozess entweder den Arbeitsspeicher durch Zwischenspeichern dieser Nachrichten auf, reagiert nicht mehr aufgrund einer CPU-Auslastung von 100% oder beides.

Das Ausüben von Gegendruck auf gesendete Nachrichten ist nicht ergonomisch

Die Anwendung von Rückdruck auf gesendete Nachrichten ist möglich, erfordert jedoch das Abfragen der WebSocket.bufferedAmount-Eigenschaft, die ineffizient und nicht ergonomisch ist. Dieses schreibgeschützte Attribut gibt die Anzahl der Datenbyte zurück, die mithilfe von WebSocket.send()-Aufrufen in die Warteschlange gestellt, aber noch nicht an das Netzwerk übertragen wurden. Dieser Wert wird auf null zurückgesetzt, sobald alle Daten in der Warteschlange gesendet wurden. Wenn Sie jedoch weiterhin WebSocket.send() aufrufen, steigt er weiter.

Was ist die WebSocketStream API?

Die WebSocketStream API befasst sich mit dem Problem eines nicht vorhandenen oder nicht ergonomischen Gegendrucks, indem sie Streams in die WebSocket API integriert. Das bedeutet, dass Gegendruck "kostenlos" und ohne zusätzliche Kosten angewendet werden kann.

Vorgeschlagene 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 erhalten müssen, insbesondere Video- und Bildschirmfreigabe.
  • Ähnlich verhält es sich mit Videoaufnahme- und anderen Anwendungen, die viele Daten im Browser generieren, die auf den Server hochgeladen werden müssen. Bei einem Rückstau kann der Client die Produktion von Daten beenden, anstatt Daten im Speicher anzusammeln.

Aktueller Status

| Schritt | Status | | ------------------------------------------ | ---------------------------- | | 1. Erklärende Erklärung erstellen | [Vervollständigen][Erklärung] | | 2. Ersten Entwurf der Spezifikation erstellen | [In Bearbeitung][spec] | | 3. Feedback einholen und Design iterieren | [In Bearbeitung](#feedback) | | 4. Ursprungstest | [Abgeschlossen][ot] | | 5. Starten | Nicht gestartet |

Verwendung der WebSocketStream API

Einführungsbeispiel

Die WebSocketStream API basiert auf Promise, was den Umgang damit in der modernen JavaScript-Welt ganz natürlich anfühlt. Erstellen Sie zuerst einen neuen WebSocketStream und übergeben Sie 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-Objekt, von dem Sie read()-Daten abrufen können, bis der Stream abgeschlossen ist, also bis ein Objekt der Form {value: undefined, done: true} zurückgegeben wird.

Entsprechend erhalten Sie durch Aufrufen der Methode WritableStream.getWriter() schließlich ein WritableStreamDefaultWriter, an das Sie dann Daten write() übergeben 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);
  }

Gegendruck

Was ist mit der versprochenen Gegendruckfunktion? Wie ich oben beschrieben habe, erhalten Sie es „kostenlos“, es sind keine zusätzlichen Schritte erforderlich. Wenn process() zusätzliche Zeit in Anspruch nimmt, wird die nächste Nachricht erst dann verarbeitet, wenn die Pipeline bereit ist. Ebenso wird der Schritt WritableStreamDefaultWriter.write() nur fortgesetzt, wenn dies sicher ist.

Erweiterte Beispiele

Das zweite Argument für WebSocketStream ist ein Optionspaket, das zukünftige Erweiterungen ermöglicht. Derzeit ist protocols die einzige Option, die sich wie das zweite Argument für den WebSocket-Konstruktor verhält:

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

Die ausgewählte protocol und die potenzielle extensions sind Teil des Wörterbuchs, das über das Promise WebSocketStream.opened verfügbar ist. Alle Informationen über die Live-Verbindung werden von diesem Promise bereitgestellt, da sie nicht relevant sind, wenn 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 Promise WebSocketStream.closed verfügbar. Das Promise lehnt im Falle eines nicht ordnungsgemäßen Abschlusses ab. Andernfalls wird es in den 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. Daher wird ein AbortSignal an den WebSocketStream-Konstruktor übergeben.

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. Hauptzweck ist jedoch die Angabe des Codes und des Grunds, der an den Server gesendet wird.

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

Progressive Enhancement und Interoperabilität

Chrome ist derzeit der einzige Browser, in dem die WebSocketStream API implementiert wird. Für die Interoperabilität mit der klassischen WebSocket API ist die Anwendung eines Rückdrucks auf empfangene Nachrichten nicht möglich. Die Anwendung von Rückdruck auf gesendete Nachrichten ist möglich, erfordert jedoch das Abfragen der WebSocket.bufferedAmount-Eigenschaft, die ineffizient und nicht ergonomisch ist.

Funktionserkennung

Verwenden Sie Folgendes, um zu überprüfen, ob die WebSocketStream API unterstützt wird:

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

Demo

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

Feedback

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

Informationen zum API-Design

Gibt es etwas an der API, das nicht wie erwartet funktioniert? Oder fehlen Methoden oder Eigenschaften, die du zur Umsetzung deiner Idee benötigst? 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 Meinung hinzu.

Problem bei der Implementierung melden

Haben Sie einen Fehler bei der Implementierung von Chrome gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation? Melden Sie einen Fehler unter new.crbug.com. Geben Sie so viele Details wie möglich und eine einfache Anleitung zum Reproduzieren an und geben Sie Blink>Network>WebSockets in das Feld Komponenten ein. Glitch eignet sich hervorragend für die schnelle und einfache Reproduktion von Fällen.

Unterstützung für die API zeigen

Haben Sie vor, 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 sie verwenden.

Hilfreiche Links

Danksagungen

Die WebSocketStream API wurde von Adam Rice und Yutaka Hirano implementiert. Hero-Image von Daan Mooij auf Unsplash