WebSocketStream:將串流與 WebSocket API 整合

應用程式可透過施加回壓,避免淹沒在 WebSocket 訊息中,或將訊息淹沒 WebSocket 伺服器。

背景

WebSocket APIWebSocket 通訊協定提供 JavaScript 介面,可在使用者瀏覽器和伺服器之間開啟雙向互動通訊工作階段。有了這個 API,您就能傳送訊息給伺服器,並接收事件驅動回應,而無須輪詢伺服器以取得回覆。

Streams API

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

目前 WebSocket API 的問題

無法將回壓套用至已接收的訊息

在目前的 WebSocket API 中,系統會在 WebSocket.onmessage 中回應訊息,並在從伺服器接收訊息時呼叫 EventHandler

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

// 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 的 API,因此在現代 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 連線

WebSocketStream 可使用 AbortController 關閉。因此,請將 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 回報錯誤。請務必盡可能提供詳細資訊,並在「Component」方塊中輸入 Blink>Network>WebSockets,以便重現問題。Glitch 可讓您輕鬆分享重現問題的簡單方法。

顯示對 API 的支援

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

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

實用連結

特別銘謝

WebSocketStream API 由 Adam RiceYutaka Hirano 實作。