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 為基礎,因此在現代 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 回報錯誤。請務必盡可能提供詳細資訊,並在「Components」方塊中輸入 Blink>Network>WebSockets,以便重現問題。Glitch 可讓您快速輕鬆地分享重現案例。

顯示 API 支援

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

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

實用連結

特別銘謝

WebSocketStream API 由 Adam RiceYutaka Hirano 實作。