WebSocketStream:將串流與 WebSocket API 整合

透過套用背壓功能,防止應用程式在 WebSocket 訊息中遭到盜用,或影響 WebSocket 伺服器中的訊息。

背景

WebSocket API

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() 方法能處理的速度更快,轉譯程序就會緩衝處理這些訊息來填滿記憶體、因 100% 的 CPU 使用率而無回應,或同時符合這兩種情況。

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

您可以將背壓套用至已傳送的訊息,但需要輪詢 WebSocket.bufferedAmount 屬性,這樣會效率低落且並非人體工學。這項唯讀屬性會傳回使用 WebSocket.send() 呼叫在佇列中已排入佇列的資料位元組數,但尚未傳送至網路的資料位元組數。所有已排入佇列的資料後,這個值會重設為零,但如果您持續呼叫 WebSocket.send(),該值會繼續攀升。

什麼是 WebSocketStream API?

WebSocketStream API 整合串流與 WebSocket API 以處理不存在或非人體工學背壓的問題。 也就是說,您可以「免費」申請背壓,不需額外付費。

WebSocketStream API 的建議用途

可使用這個 API 的網站包括:

  • 需要維持互動狀態的高頻寬 WebSocket 應用程式,特別是影片和螢幕分享。
  • 同樣地,影片擷取以及其他會在瀏覽器中產生大量資料的應用程式,並將其上傳至伺服器。透過背壓,用戶端可以停止產生資料,而不是累積記憶體中的資料。

目前狀態

| 步驟 | 狀態 | | ------------------------------------------ | ---------------------------- | | 1. 建立說明 | [完整][說明書] | | 2. 建立規格的初始草稿 | [進行中][spec] | | 3. 收集意見回饋並持續改進設計 | [進行中](#feedback) | | 4. 來源試用 | [Complete][ot] | | 5. 啟動 | 尚未開始 |

如何使用 WebSocketStream API

入門範例

WebSocketStream API 以承諾為基礎的,因此在現代 JavaScript 世界中,處理過程十分自然。首先,請建構新的 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>WebSocketsGlitch 非常適合用來分享快速又簡單的重製案例。

顯示對 API 的支援

您打算使用 WebSocketStream API 嗎? 您的公開支援服務有助於 Chrome 團隊優先開發特定功能,並向其他瀏覽器廠商瞭解支援這些功能的重要性。

使用主題標記 #WebSocketStream 將推文傳送至 @ChromiumDev,並告知您的使用位置和方式。

實用連結

特別銘謝

WebSocketStream API 是由 Adam RiceYutaka Hirano 實作。主頁橫幅由 Daan Mooij 提供,位於 Unsplash 上。