淺談 server-sent events

簡介

Server-sent events(SSE),也即服務器推送事件,是一種基于 HTTP 協議實現的單向實時數據推送技術。它允許服務器在不需要客戶端輪詢的情況下,主動向客戶端發送新的數據。

特性

SSE 主要有以下幾點特性:

  1. 文本格式的數據傳輸:只能發送通過 utf-8 編碼的數據
  2. 自動重連:如果連接斷開,瀏覽器會自動發起重連(默認延遲 3 秒)
  3. 事件 ID 支持:可用于瀏覽器斷線重連后補發數據
  4. 輕量級:相比 WebSocket,SSE 基于 HTTP 協議,且瀏覽器原生支持斷線重連,相對更加簡單易用

到這里,我們很容易就會將 SSE 和 WebSocket 聯系起來,接下來我們就來看看這兩者之間的主要對比。

對比 WebSocket

特性 SSE WebSocket
通信模式 單向(服務端到客戶端) 雙向(服務端和客戶端)
協議 HTTP 協議 WebSocket 協議
數據類型 純文本 文本或二進制數據
連接保持 保持 HTTP 長連接,斷線后自動重連 需要應用層處理斷線重連邏輯
復雜度 簡單易用(類似 HTTP 請求) 較為復雜,需要更多的管理和維護
安全性 支持 HTTPS 支持 WSS
服務器支持 大多數 HTTP 服務器無需配置即可支持 需要服務器支持 WebSocket 協議
瀏覽器支持 除 IE 外,其它瀏覽器普遍支持 現代瀏覽器普遍支持

從以上主要對比來看,在服務端到客戶端單向通信的場景下,SSE 看起來的確更加的簡單易用點。但只要 SSE 能實現的場景,WebSocket 也都能實現。結合這些,我們再來看看 SSE 的一些常見使用場景。

常見使用場景

  1. 服務器日志推送
  2. 服務器狀態監控
  3. 天氣、股票行情等實時更新
  4. 新聞推送或社交媒體動態更新
  5. 大語言模型流式輸出響應內容

當然 SSE 的使用場景絕不僅僅局限于以上幾種,你可以根據業務場景靈活使用。了解完使用場景,下面我們就來看看如何在業務中使用它!

規范及應用

這一節我們將分別從服務端和客戶端來了解 SSE 的一些基本規范及應用。

服務端應用

定義響應頭

要使用 SSE,服務端首先需要將相應接口的響應內容類型設置為 "text/event-stream"。類似這樣:

// 響應內容類型必須是 text/event-stream
res.setHeader('Content-Type', 'text/event-stream')

res.setHeader('Connection', 'keep-alive')

res.setHeader('Cache-Control', 'no-store')

為方便說明,本節所有服務端代碼都將使用 node.js 來示例,其它語言自行調整即可。

定義響應內容

每一次連接的響應內容都由若干條消息組成,消息之間以一對換行符分隔。每條消息又由若干行組成,每一行都是這種格式:[field]: value\n。其中 field 字段的取值只能是 dataeventidretry,其它任何字段都將被客戶端視為無效字段而忽略,以下是具體說明:

data

用于定義每條響應消息的具體內容。內容格式只能是文本格式,類似這樣:

res.write('data: first message.\n\n')
res.write('data: second message.\n\n')

這樣客戶端就會連續收到兩條消息,分別是 first message.second message.。如圖示:

而如果你這樣定義:

res.write('data: first message.\n')
res.write('data: second message.\n\n')

客戶端收到的將是一條消息,如圖示:

可以看到,客戶端會將一對換行符之前的多個 data 字段定義的消息內容合并起來作為一條消息的完整內容,直到遇見上一對換行符。每條消息都必須通過一個或多個 data 字段定義具體的消息內容,否則將被客戶端視為無效消息而忽略

event

用于定義每條消息的事件類型。默認是 message,如上圖示。你可以根據業務需要指定任何其它類型,類似這樣:

res.write('data: running\n')
res.write('event: serverStatus\n\n')

這樣客戶端將會收到一條類型是 serverStatus,內容是 running 的消息,如圖示:

id

用于定義每條消息的事件 id。如下示例,當我們這樣發送消息時:

let number = 0;
const intervalId = setInterval(() => {
  number++;
  res.write(`id: ${number}\n`);
  res.write(`data: ${number}\n\n`);
}, 1000);

客戶端收到的消息將會是這樣的:

可以看到每條消息都有一個自己的 id 值。指定事件 id 后,當客戶端斷線重連時,http 請求頭中的 Last-Event-Id 的值便是上次連接發送的最后一條消息的事件 id,服務端可以此來補發數據等:

retry

定義 SSE 連接斷開后的重連時間。當 SSE 連接斷開后,客戶端默認 3 秒后重新連接。服務端可結合業務邏輯使用 retry 字段自定義重連時間,它的值必須是一個以毫秒為單位的整數值。

res.write('data: the server is still restarting.\n')
res.write('retry: 60000\n\n')
res.end()

一般來講,當服務端明確知道客戶端請求的數據還未準備好,且需等待一定時間時,便可定義好重連時間,并發消息告訴客戶端,之后關閉連接即可??蛻舳嗽谶B接斷掉后,等待時間達到重連時間時,便會再次發起連接。

以上便是如何定義響應內容的主要說明了。當然除此之外,服務端還可以定時發送以冒號開頭的注釋消息,以保持連接不斷:

res.write(': this is a test stream \n\n')

客戶端在收到這種非規范格式的消息后,會選擇忽略它。

到這里 SSE 在服務端的規范及應用基本就介紹完了。需要額外再啰嗦一下的是,以上代碼示例使用的都是 node.js 原生的寫法,實際業務中用起來肯定是沒有像 nestjs 已經集成好的 sse 裝飾器或者是借助于諸如 better-sse 等封裝好的 npm 包那樣簡潔方便。其它語言肯定也都有相應的框架或者工具庫封裝了 SSE 的常用能力,它們會讓你的實現看起來更加優雅!

okkk.. 下一節我們就來看看服務端定義的 SSE 接口如何在客戶端使用。

客戶端應用

實例化 EventSource

在客戶端使用 SSE 的話你需要先實例化 EventSource,實例化時 EventSource 會根據你傳入的 url 發起一個到服務端的持久化連接:

const sse = new EventSource(url);

當這里傳入的 url 是跨域 url 時,你可以通過傳入第二個參數來決定是否帶上當前域的 cookie 來發起請求。默認不帶,需要帶上的話可以這樣傳參:

const sse = new EventSource(url, { withCredentials: true });

默認事件監聽

實例化 EventSource 后,便可以通過 EventSource 實例的 onmessage 方法來監聽默認事件(message)的消息,如下示例:

const sse = new EventSource("/api/v1/sse");
sse.onmessage  = (e) => {
  // 來自服務端的消息
  console.log(e.data)
}

除了可以監聽 onmessage 外,你還可以通過 onopenonerror 這兩個方法來分別監聽連接的建立和異常:

sse.onopen  = () => {
  // sse 連接已建立
}

sse.onerror  = (error) => {
  // sse 連接異常
  console.error(error)
}

自定義事件監聽

除了默認的 message 事件,我們的業務中很可能還會用到其它的自定義事件,比如上面在介紹 event 字段時提到的 serverStatus 事件。通過 EventSource 實例的 addEventListener 方法即可監聽這些自定義事件:

function onServerStatusChange(e) {
  const serverStatus = e.data;
}

sse.addEventListener("serverStatus", onServerStatusChange);

通過 addEventListener 添加的事件監聽當然就可以通過 removeEventListener 移除了:

sse.removeEventListener("serverStatus", onServerStatusChange);

如果你愿意,你當然也可以通過這種方式來監聽前面提到的 messageopenerror 事件了:

// 同 sse.onmessage = onMessage
sse.addEventListener("message", onMessage);

sse.addEventListener("open", onOpen);

sse.addEventListener("error", onError);

查看連接狀態

你可以方便的通過 EventSource 實例的 readyState 屬性來獲取當前連接的狀態。連接的三種狀態的值也可以通過 EventSource 實例的 CONNECTING、OPENCLOSED 這三個屬性分別獲取。如下示例:

const sse = new EventSource("/api/v1/sse")
// 連接中
sse.readyState === sse.CONNECTING
// 連接開啟
sse.readyState === sse.OPEN
// 連接關閉
sse.readyState === sse.CLOSED

中斷連接

實例化后的 EventSource 實例,在 SSE 連接建立成功后,將一直保持連接開啟。無論中途是因為網絡原因還是服務端主動關閉而導致連接臨時中斷,之后 EventSource 實例都會主動發起重連。如果要永久中斷連接,就需要調用 EventSource 實例的 close 方法來關閉連接。

const sse = new EventSource("/api/v1/sse")

// 中斷連接
sse.close()

以上基本就是客戶端建立 SSE 連接并接收服務端推送消息的所有內容了。相比 WebSocket 的話,少了心跳檢測及斷線重連相關的邏輯,使用起來確實簡易一些。了解完基本規范及使用方法,下面我們再來看下使用 SSE 時需要注意的一些點。

注意事項

瀏覽器對于同源請求的并發數限制

如果你們的 Web 服務器配置使用的 http 協議仍是 http/1.1,由于瀏覽器對于同源請求的并發數有限制,大都為 6,而 SSE 的 http 連接又都是長連接,這就導致一旦一個瀏覽器下已經存在了 6 個同源的 SSE 連接,其它的同源請求將不能被正常受理。這是一個非常嚴重的問題,這個問題不解決,我覺得就不要使用 SSE,否則得不償失!常見的解決方案如下:

  1. 將 http 協議從 http/1.1 切換到 http/2。有了多路復用的特性支持,http/2 的同源最大并發請求數將達到 100(默認值)。你可以根據業務需要繼續調整這個值,但基本是不會再有上面的同源請求瓶頸限制了。
  2. 部署一個或多個非同源的 SSE 服務,客戶端直接跨域請求相應服務。
  3. 當頁面不可見時,及時中斷該頁面下的所有 SSE 連接。
  4. 使用其它方案比如 WebSocket 代替。

針對解決方案1,雖然現在主流瀏覽器都已支持 http/2,但仍有一些老版本瀏覽器并不完全支持。你如果要兼容這些老版本瀏覽器,在做好協議回退的同時,還得結合其它解決方案一起使用。

EventSource 僅支持 get 請求

原生的 EventSource 僅支持 get 請求,而實際業務場景中,我們很可能需要發起 post 請求。此時你可以使用已經封裝好的類似 @microsoft/fetch-event-source 這樣的 npm 包,或者你也可以參考這篇文章來原生實現。

Nginx 配置

使用 SSE 時,你可能需要根據情況做以下 nginx 配置:

  1. 禁用代理緩存:proxy_cache off。否則你可能會在后續的連接中拿到之前緩存的數據。
  2. 禁用代理緩沖:proxy_buffering off。這樣 nginx 將不再對 SSE 響應進行緩沖,直接透傳給客戶端。(這里需注意,如果你的請求會經過多個 nginx 網關,那么每一個 nginx 最好都加上這個配置。因為即使服務端可能在接口的響應 header 中設置了 X-Accel-Buffering: no,但響應數據經過的第一個 nginx 便會消耗掉這個 header,后面如果還會經過其它 nginx,那它們還是會默認緩沖數據,此時你同樣需要為這些 nginx 配置 proxy_buffering off 或者 add_header X-Accel-Buffering no
  3. 調整連接超時:proxy_read_timeout 120s。這一步通常也不是必須的,nginx 默認會在兩次讀取超過 60s 時關閉連接,但 SSE 客戶端往往也會在連接斷開后 3s 發起重連, 所以影響倒也不大。何況服務端還可以通過定時發送注釋消息以保持連接不斷,所以這個配置完全可以視業務場景決定是否使用。

總結

拋開使用 http/1.1 時的同源請求并發數限制,SSE 還是挺適合這種從服務端到客戶端的單向文本消息推送場景。尤其像新聞、社交媒體動態的實時更新以及監控系統的狀態更新等。

在這些場景中,SSE 能通過簡單的單向通信和自動重連機制提供穩定可靠的服務!

參考資料

Server-sent events

How to Use Server-sent Events in Node.js

SSE (Server-Sent Events) Using A POST Request Without EventSource

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容