推送數據?也許你不需要 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