WebSocketStream:將串流與 WebSocket API 整合

運用背壓,防止應用程式因 WebSocket 訊息而變得不堪負荷,或運用背壓來使 WebSocket 伺服器充斥訊息。

背景

WebSocket API 提供 WebSocket 通訊協定的 JavaScript 介面,方便您在使用者瀏覽器和伺服器之間開啟雙向互動式通訊工作階段。 透過這個 API,您可以傳送訊息至伺服器及接收事件導向的回應,而不用輪詢伺服器來回覆。

Streams API

Streams API 可讓 JavaScript 以程式輔助方式存取透過網路接收的資料區塊串流,並視需要加以處理。串流的一個重要概念就是「背壓」。這是單一串流或管道鏈條控管讀取或寫入速度的程序。如果串流本身或管道鏈結中的串流仍處於忙碌狀態,且尚未準備好接受更多區塊,便會透過鏈結反向傳送信號,並視情況減慢傳遞速度。

目前使用的 WebSocket API 問題

無法對收到的訊息套用背壓

使用目前的 WebSocket API 回應訊息時,系統會在 WebSocket.onmessage 中回應訊息;收到伺服器傳來的訊息時,就會呼叫 EventHandler

假設您有一個應用程式,需要在收到新訊息時執行大量資料處理作業。您可能會設定類似下方程式的流程,而且由於您 awaitprocess() 呼叫的結果,因此應該沒問題,對吧?

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

答錯了!目前的 WebSocket API 問題在於無法套用回壓。當訊息送達的速度比 process() 方法可處理訊息的速度還快時,轉譯程序會透過緩衝處理這些訊息來填滿記憶體,在 CPU 使用率 100% 或兩者皆有時無回應。

為已傳送的郵件套用背壓並非符合人體工學

您可以將回壓套用至已傳送的訊息,但這會涉及輪詢 WebSocket.bufferedAmount 屬性,效率不彰且不符合人體工學。這個唯讀屬性會傳回使用 WebSocket.send() 呼叫排入佇列,但尚未傳送至網路的資料位元組數。所有排隊的資料都已傳送後,這個值會重設為零,但如果您持續呼叫 WebSocket.send(),這個值會持續增加。

什麼是 WebSocketStream API?

WebSocketStream API 會將串流與 WebSocket API 整合,處理不存在或不符合人體工學的回壓問題。也就是說,您可以「免費」套用回壓,不必額外付費。

WebSocketStream API 的建議用途

以下列舉可使用這個 API 的網站:

  • 需要保留互動功能 (尤其是影片和螢幕分享功能) 的高頻寬 WebSocket 應用程式。
  • 同樣地,錄影和其他應用程式會在瀏覽器中產生大量資料,而這些資料需要上傳至伺服器。有了回壓機制,用戶端就能停止產生資料,而不會在記憶體中累積資料。

目前狀態

步驟 狀態
1. 建立說明 完成
2. 建立規格初稿 進行中
3. 收集意見回饋並重複設計 進行中
4. 來源試用 完成
5. 啟動 尚未開始

如何使用 WebSocketStream API

WebSocketStream API 是以 Promise 為基礎,因此在現代 JavaScript 世界中,處理這項 API 會顯得相當自然。首先請建構新的 WebSocketStream,並向其傳送 WebSocket 伺服器的網址。接下來,您等待連線變成 opened,這會產生 ReadableStream 和/或 WritableStream

呼叫 ReadableStream.getReader() 方法後,您最終會取得 ReadableStreamDefaultReader,您可以從中read() 資料,直到串流完成,也就是傳回 {value: undefined, done: true} 形式的物件為止。

因此,呼叫 WritableStream.getWriter() 方法後,您最終會取得 WritableStreamDefaultWriter,然後可用於 write() 資料。

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

背壓

我們承諾會提供背壓功能嗎? 您可以「免費」取得,不需要採取額外步驟。如果 process() 需要額外時間,則系統會在管道就緒後才使用下一個訊息。同樣地,只有在安全無虞的情況下,WritableStreamDefaultWriter.write() 步驟才會繼續執行。

進階範例

WebSocketStream 的第二個引數是選項包,允許日後擴充。唯一選項是 protocols,運作方式與 WebSocket 建構函式的第二個引數相同:

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

所選的 protocol 和潛在 extensions 都屬於透過 WebSocketStream.opened 承諾提供的字典。與即時連線有關的所有資訊均由這個承諾提供,因為如果連線失敗,這並不相關。

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

關閉的 WebSocketStream 連線相關資訊

從 WebSocket API 中的 WebSocket.oncloseWebSocket.onerror 事件取得資訊,現在可透過 WebSocketStream.closed 承諾取得。在發生不潔關閉事件時,承諾會遭到拒絕,否則會解析為伺服器傳送的程式碼和原因。

CloseEvent 狀態碼清單會說明所有可能的狀態碼及其含義。

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

關閉 WebSocketStream 連線

您可以使用 AbortController 關閉 WebSocketStream。因此,請將 AbortSignal 傳遞至 WebSocketStream 建構函式。

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

您也可以使用 WebSocketStream.close() 方法,但這項方法的主要用途是允許您指定傳送至伺服器的代碼和原因。

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

漸進增強與互通性

Chrome 目前是唯一實作 WebSocketStream API 的瀏覽器。為了與傳統版 WebSocket API 互通,我們不允許對收到的訊息套用背壓。您可以對已傳送的訊息套用背壓,但這涉及輪詢 WebSocket.bufferedAmount 屬性,這效率低落且沒有人因工程。

特徵偵測

如要檢查是否支援 WebSocketStream API,請使用:

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

示範

在支援的瀏覽器上,您可以在嵌入式 iframe 中查看 WebSocketStream API 的實際運作情形,也可以直接在 Glitch 上查看。

意見回饋

Chrome 團隊希望瞭解你使用 WebSocketStream API 的體驗。

請說明 API 設計

API 是否有任何部分無法正常運作?或者,您是否缺少實作想法所需的方法或屬性?您對安全性模型有疑問或意見嗎?在對應的 GitHub 存放區上提出規格問題,或將您的想法新增至現有問題。

回報導入問題

你是否發現 Chrome 實作項目有錯誤?或者實作方式與規格不同?前往 new.crbug.com 回報錯誤。請盡可能提供最多細節、重製操作說明,並在「Components」(元件) 方塊中輸入 Blink>Network>WebSocketsGlitch 則是分享可輕鬆快速重現保護殼的工具。

顯示對 API 的支援

您是否打算使用 WebSocketStream API?你的公開支持有助於 Chrome 團隊決定功能的優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。

使用主題標記 #WebSocketStream 發送推文給 @ChromiumDev,告訴我們你在何處使用這項功能,以及使用方式。

實用連結

特別銘謝

WebSocketStream API 由 Adam RiceYutaka Hirano 實作。