推送數據?也許你不需要 WebSocket

提到推送數據,大家可能會首先想到 WebSocket。

確實,WebSocket 能雙向通信,自然也能做服務器到瀏覽器的消息推送。

但如果只是單向推送消息的話,HTTP 就有這種功能,它就是 Server Send Event。

WebSocket 的通信過程是這樣的:

首先通過 http 切換協議,服務端返回 101 的狀態碼後,就代表協議切換成功。

之後就是 WebSocket 格式數據的通信了,一方可以隨時向另一方推送消息。

而 HTTP 的 Server Send Event 是這樣的:

服務端返回的 Content-Type 是 text/event-stream,這是一個流,可以多次返回內容。

Sever Send Event 就是通過這種消息來隨時推送數據。

可能你是第一次聽說 SSE,但你肯定用過基於它的應用。

比如你用的 CICD 平臺,它的日誌是實時打印的。

那它是如何實時傳輸構建日誌的呢?

明顯需要一段一段的傳輸,這種一般就是用 SSE 來推送數據。

再比如說 ChatGPT,它回答一個問題不是一次性給你全部的,而是一部分一部分的加載回答。

這也是基於 SSE。

知道了什麼是 SSE 以及它的應用,我們來自己實現一下吧:

創建 nest 項目:

npx nest new sse-test

把它跑起來:

npm run start:dev

訪問 http://localhost:3000 可以看到 hello world,代表服務器跑成功了:

然後在 AppController 添加一個 stream 接口:

這裏不是通過  @Get、@Post 等裝飾器標識,而是通過 @Sse 標識這是一個 event stream 類型的接口。

@Sse('stream')
stream() {
    return new Observable((observer) ={
      observer.next({ data: { msg: 'aaa'} });

      setTimeout(() ={
        observer.next({ data: { msg: 'bbb'} });
      }, 2000);

      setTimeout(() ={
        observer.next({ data: { msg: 'ccc'} });
      }, 5000);
    });
}

返回的是一個 Observable 對象,然後內部用 observer.next 返回消息。

可以返回任意的 json 數據。

我們先返回了一個 aaa、過了 2s 返回了 bbb,過了 5s 返回了 ccc。

然後寫個前端頁面:

創建一個 react 項目:

npx create-react-app --template=typescript sse-test-frontend

在 App.tsx 裏寫如下代碼:

import { useEffect } from 'react';

function App() {

  useEffect(() ={
    const eventSource = new EventSource('http://localhost:3000/stream');
    eventSource.onmessage = ({ data }) ={
      console.log('New message', JSON.parse(data));
    };
  }[]);

  return (
    <div>hello</div>
  );
}

export default App;

這個 EventSource 是瀏覽器原生 api,就是用來獲取 sse 接口的響應的,它會把每次消息傳入 onmessage 的回調函數。

我們在 nest 服務開啓跨域支持:

然後把 react 項目 index.tsx 裏這幾行代碼刪掉,它會導致額外的渲染:

執行 npm run start

因爲 3000 端口被佔用了,它會跑在 3001:

瀏覽器訪問下:

看到一段段的響應了沒?

這就是 Server Send Event。

在 devtools 裏可以看到,響應的 Content-Type 是 text/event-stream:

然後在 EventStream 裏可以看到每一次收到的消息:

這樣,服務端就可以隨時向網頁推送消息了。

那它兼容性怎麼樣呢?

可以在 MDN 看到:

除了 ie、edge 外,其他瀏覽器都沒任何兼容問題。

基本是可以放心用的。

那用在哪呢?

一些只需要服務端推送的場景就特別適合 Server Send Event。

比如這個站內信:

這種推送用 WebSocket 就沒必要了,可以用 SSE 來做。

那連接斷了怎麼辦呢?

不用擔心,瀏覽器會自動重連。

這點和 WebSocket 不同,WebSocket 如果斷開之後是需要手動重連的,而 SSE 不用。

再比如說日誌的實時推送。

我們來測試下:

tail -f 命令可以實時看到文件的最新內容:

我們通過 child_process 模塊的 exec 來執行這個命令,然後監聽它的 stdout 輸出:

const { exec } = require("child_process");

const childProcess = exec('tail -f ./log');

childProcess.stdout.on('data'(msg) ={
    console.log(msg);
});

用 node 執行它:

然後添加一個 sse 的接口:

@Sse('stream2')
stream2() {
const childProcess = exec('tail -f ./log');

return new Observable((observer) ={
  childProcess.stdout.on('data'(msg) ={
    observer.next({ data: { msg: msg.toString() }});
  })
});

監聽到新的數據之後,把它返回給瀏覽器。

瀏覽器連接這個新接口:

測試下:

可以看到,瀏覽器收到了實時的日誌。

很多構建日誌都是通過 SSE 的方式實時推送的。

日誌之類的只是文本,那如果是二進制數據呢?

二進制數據在 node 裏是通過 Buffer 存儲的。

const { readFileSync } = require("fs");

const buffer = readFileSync('./package.json');

console.log(buffer);

而 Buffer 有個 toJSON 方法:

這樣不就可以通過 sse 的接口返回了麼?

試一下:

@Sse('stream3')
stream3() {
    return new Observable((observer) ={
        const json = readFileSync('./package.json').toJSON();
        observer.next({ data: { msg: json }});
    });
}

確實可以。

也就是說,基於 sse,除了可以推送文本外,還可以推送任意二進制數據。

總結

服務端實時推送數據,除了用 WebSocket 外,還可以用 HTTP 的 Server Send Event。

只要 http 返回 Content-Type 爲 text/event-stream 的 header,就可以通過 stream 的方式多次返回消息了。

它傳輸的是 json 格式的內容,可以用來傳輸文本或者二進制內容。

我們通過 Nest 實現了 sse 的接口,用 @Sse 裝飾器標識方法,然後返回 Observe 對象就可以了。內部可以通過 observer.next 隨時返回數據。

前端使用 EventSource 的 onmessage 來接收消息。

這個 api 的兼容性很好,除了 ie 外可以放心的用。

它的應用場景有很多,比如站內信、構建日誌實時展示、chatgpt 的消息返回等。

再遇到需要消息推送的場景,不要直接 WebSocket 了,也許 Server Send Event 更合適呢?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CnQntMeMkIrIpe2Js2QcYg