首頁 > 軟體

Server-sent events實時獲取伺服器端資料技術詳解

2023-02-08 22:01:31

正文

實時獲取伺服器端的資料,大家第一時間想到的是輪詢和 WebSocket 兩種方案,其實還有一種新方案 Server-sent events 下文簡稱(SSE)。SSE 中的資料只能由伺服器端推向使用者端

SSE 是基於 http 協定的伺服器推播技術,資料只能從伺服器端到使用者端。伺服器端把序列化後的資料傳送給使用者端, 整個過程持續不斷直至連線關閉

WebSocket vs 輪詢 vs SSE

下面是 WebSocket、輪詢和 SSE 的功能對比

  • SSE 和輪詢使用 HTTP 協定,現有的伺服器軟體都支援。WebSocket 是一個獨立協定
  • SSE 屬於輕量級的 WebSocket,使用簡單;WebSocket 使用相對複雜,輪詢使用簡單
  • SSE 預設支援斷線重連,WebSocket 需要自己實現斷線重連
  • SSE 一般只用來傳送文字,二進位制資料需要編碼後傳送,WebSocket 預設支援傳送二進位制資料
  • SSE 支援自定義傳送的訊息型別
  • WebSocket 支援雙向推播訊息,SSE 是單向的
  • 輪詢效能開銷大、輪詢時間久導致使用者端及時更新資料

使用場景

基於伺服器端單向的向用戶端推播資訊的特性,SSE 使用場景主要有

  • Sass 平臺的訊息通知
  • 資訊流網站實時更新資料

使用方式

下面講解如何在使用者端使用 SSE

  • 建立一個 EventSource 範例,向伺服器發起連線
const evtSource = new EventSource();
  • 自定義事件

對於自定義事件,伺服器端和使用者端一定要保持事件名一致。伺服器端通過自定義事件傳送資料, 就會觸發自定義事件。SSE 預設支援 message 事件,下面以 message 事件為例

evtSource.addEventListener("message", (event) => {
  let payload;
  try {
    payload = JSON.parse(event.data); // <--- event.data 需要反序列化
    console.log("receiving data...", payload);
  } catch (error) {
    console.error("failed to parse payload from server", error);
  }
});

自定義事件的回撥函數接收 event 物件,event.data 存著伺服器端發給使用者端的資料但是需要反序列化

可以通過 Chrome Devtool 工具檢視 eventsource 通訊情況,如圖所示

  • 1 - 自定義事件名,伺服器端和使用者端需要保持一致
  • 2 - EventStream Tab,資料都在這裡
  • 3 - 伺服器端推播給使用者端的資料
  • 錯誤處理

如果連線發生錯誤,就會觸發 error 事件

evtSource.addEventListener("error", (err) => {
  console.error("EventSource failed:", err);
});
  • 關閉連線

SSE 提供 close 方法,用來關閉 SSE 連線

evtSource.close();

瀏覽器相容性

通過 caniuse 檢視 SSE 瀏覽器相容性,如圖所示

除了 IE 瀏覽器不支援,其它現代瀏覽器都支援,所以放心大膽在專案中使用 SSE

簡單封裝

在平常的工作中,每次寫 SSE 的事件監聽和錯誤處理會很麻煩。多個業務場景需要使用 SSE 時,就需要對 SSE 進行封裝。接下來我們嘗試封裝一個簡單的 SSE SDK,方便在專案中使用

當我們決定寫 SSE 的 SDK 時,首先想到使用物件導向(OOP)進行封裝。根據 SSE 的特性,那麼庫需要實現 subscribeunsubscribe 兩個方法。通過確定 SSE 庫使用方式,根據使用方式確定 SDK 的實現。我們可以在程式碼中這樣使用,如下所示

// SSESdk 範例化
const SSE = new SSESdk(url, options);
// 訂閱來自伺服器端的訊息
SSE.subscribe("message", (data) => {
  console.log("receive message from server", data);
});
// 取消訂閱
SSE.unsuscribe();

我們要封裝的庫對外僅僅提供 subscribeunsubscribe 兩個 Api,非常方便開發人員使用。 subscribe 用來訂閱來自伺服器端的訊息, unsubscribe 用來取消訂閱,關閉 SSE 連線,通過使用形式可以看出,使用 ES6 中的類語法。接下來我們先確定 SSE SDK 的大體結構

class SSEClient {
  constructor() {}
  subscribe(type, handler) {}
  unsunscribe() {}
}

SSEClient 類中有三個方法需要實現,通過 constructor 接受可設定的引數,比如 SSE 建立連線失敗後的重試次數和重試時間。 subscribe 接收一個與後端保持一致的事件名和一個回撥函數。unsunscribe 不需要傳遞任何引數,呼叫 unsunscribe 方法關閉 SSE 連線

// SSE-client.js
class SSEClient {
  constructor(url) {
    this.url = url;
    this.es = null;
  }
  subscribe(type, handler) {
    this.es = new EventSource(url);
    this.es.addEventListener("open", () => {
      console.log("server sent event connect created");
    });
    this.es.addEventListener(type, (event) => {
      let payload;
      try {
        payload = JSON.parse(event.data);
        console.log("receiving data...", payload);
      } catch (error) {
        console.error("failed to parse payload from server", error);
      }
      if (typeof handler === "function") {
        handler(payload);
      }
    });
    this.es.addEventListener("error", () => {
      console.error("EventSource connection failed for subscribe.Retry");
    });
  }
  unsunscribe() {
    if (this.es) {
      this.es.close();
    }
  }
}

就這樣實現了一個簡單的 SSE SDK。首先根據 url 引數建立一個 SSEClient 範例,當呼叫 subscribe 方法時,才會根據傳入的 url 建立 SSE 連線,然後監聽對應的事件,一旦 連線建立成功,後端向用戶端傳送資料,就可以從 handler 方法中拿到資料

這個庫僅僅實現了非常基本的功能,程式碼封裝上存在很多問題。比如 es 的事件全部雜糅在 subscribe 方法中、缺少 SSE 連線建立失敗的重試等等功能。接下來我們對剛剛實現的 SSEClient SDK 進行優化

const defaultOptions = {
  retry: 5,
  interval: 3 * 1000,
};
class SSEClient {
  constructor(url, options = defaultOptions) {
    this.url = url;
    this.es = null;
    this.options = options;
    this.retry = options.retry;
    this.timer = null;
  }
  _onOpen() {
    console.log("server sent event connect created");
  }
  _onMessage(handler) {
    return (event) => {
      this.retry = options.retry;
      let payload;
      try {
        payload = JSON.parse(event.data);
        console.log("receiving data...", payload);
      } catch (error) {
        console.error("failed to parse payload from server", error);
      }
      if (typeof handler === "function") {
        handler(payload);
      }
    };
  }
  _onError(type, handler) {
    return () => {
      console.error("EventSource connection failed for subscribe.Retry");
      if (this.es) {
        this._removeAllEvent(type, handler);
        this.unsunscribe();
      }
      if (this.retry > 0) {
        this.timer = setTimeout(() => {
          this.subscribe(type, handler);
        }, this.options.interval);
      } else {
        this.retry--;
      }
    };
  }
  _removeAllEvent(type, handler) {
    this.es.removeEventListener("open", this._onOpen);
    this.es.removeEventListener(type, this._onMessage(handler));
    this.es.removeEventListener("error", this._onError(type, handler));
  }
  subscribe(type, handler) {
    this.es = new EventSource(url);
    this.es.addEventListener("open", this._onOpen);
    this.es.addEventListener(type, this._onMessage(handler));
    this.es.addEventListener("error", this._onError(type, handler));
  }
  unsunscribe() {
    if (this.es) {
      this.es.close();
      this.es = null;
    }
    if (this.timer) {
      clearTimeout(this.timer);
    }
  }
}

我們將 SSEClient 中的三個事件方法分別提取為三個私有方法,_onOpen 方法在 event 觸發 open 時呼叫,向控制檯輸出連結已經建立。 _onMessage 方法在後端向前端傳送資料時觸發,負責解析資料,並呼叫 handler 方法。_onError 方法在 SSE 發生錯誤時觸發, 會在控制檯輸出錯誤的提示,根據開發者傳入的重試次數,先關閉上一次的 SSE 連結,取消所有的事件監聽,關閉定時器, 再開啟遞迴呼叫 subscribe 方法進行重連, 一旦重連成功,重試次數恢復為設定的重試次數,如果超過重試次數依舊沒有連線成功,那麼 SSE 會徹底終止。需要開發人員排查具體原因

一個可以用在專案上的簡單 SSE SDK 封裝完

第三方庫

SSE 雖然很好,但是也有它先天不足,主要問題是不能通過 headers 傳遞 Authorization token。雖然可以把 token 放在 url 上 解決不能傳 token 的問題,但是又會引發 token 安全隱患。所以社群裡有使用 xhrfetch 模擬原生 Server-sent events 的功能,解決不能 通過 headers 傳遞 Authorization token 的問題。主要有兩個第三方庫,分別是 eventsourceevent-source-polyfill, 下面筆者詳細講述這兩個庫的使用

eventsource

此庫是 EventSource 使用者端的純 JavaScript 實現。使用方式很簡單。在專案中安裝依賴

yarn add eventsource
# Or npm install eventsource

然後從 eventsource 中匯出 EventSource 類,然後範例化得到 es 範例

import EventSource from "eventsource";
const eventSourceInitDict = { headers: { authorization: "Bearer token" } };
const es = new EventSource(url, eventSourceInitDict);
es.addEventListener("message", (event) =&gt; {
  console.log("receiving data from server:", JSON.parse(event.data));
});

eventsource 的實現用到了一些 node 標準庫。分別是 httpshttp。 筆者將 eventsource 的部分原始碼列在下面。

// eventsource.js 原始碼如下
const https = require("https");
const http = require("http");

然而,瀏覽器環境並不支援 httpshttp 標準庫。所以當我們在瀏覽器環境中使用 eventsource 時,需要做一些額外的工作。下面以 webpack5 為例子講解解決辦法

  • 需要在 webpack 組態檔中新增 node-polyfill-webpack-plugin 外掛
yarn add node-polyfill-webpack-plugin -D

然後在 webpack 組態檔使用該外掛

// 專案中的 webpack 組態檔,比如 webpack.config.js
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
module.exports = {
  // Other rules...
  plugins: [new NodePolyfillPlugin()],
};
  • 或者在 webpackcallback 中對使用的庫進行單獨的設定
module.exports = {
  // other configuration ...
  resolve: {
    fallback: {
      https: false,
      http: false,
    },
  },
};

做完上面的步驟後,eventsource 可以在瀏覽器中正常執行

如果不想改動 webpack 的設定,那麼可以試試 event-source-polyfill 這個庫

event-source-polyfill

event-source-polyfill 的使用非常簡單,使用 EventSourcePolyfill 替換原生的 EventSource

import { EventSourcePolyfill } from "event-source-polyfill";
var es = new EventSourcePolyfill(url, {
  headers: {
    authorization: "Bearer token",
  },
});
es.addEventListener("message", (event) => {
  console.log("receiving data from server:", JSON.parse(event.data));
});

不足之處

eventsourceevent-source-polyfill 只是在一定的程度上解決了 Authorization token 的問題,但它們也存在問題。 這兩個庫提供的 close 方法只能關閉處於 pending 狀態的 SSE 連線,因為 fetch 一旦從 pending 變為 resolvedreject, 其結果無法改變。當頻繁的斷開 SSE 連線和建立新 SSE 連線時,舊的 SSE 連線實際上並沒有關閉,系統裡會存在多個 SSE 連線,這樣會帶來很大的效能開銷

FAQ

  • SSE 不能向伺服器端傳送資料?

可以將資料放入 url 中,斷開當前的 SSE 連線,根據新 url 重新建立 SSE 連線

總結

本篇文章講述一種伺服器端向用戶端推播資訊的技術、它比 WebSocket 更簡單更輕量化,比輪詢效能好。 簡單介紹 Server-sent events 的技術原理和使用場景,並進行簡單的封裝,方便日常在專案中使用。推薦使用 eventsourceevent-source-polyfill 第三方庫解決不能通過 headers 傳遞 Authorization token 的問題。

參考連結 Server-sent events

以上就是Server-sent events實時獲取伺服器端資料技術詳解的詳細內容,更多關於Server-sent events的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com