服务器推送数据你还在用 WebSocket么?
当涉及到推送数据时,人们首先会想到 WebSocket。
的确,WebSocket 允许双向通信,可以自然地用于服务器到浏览器的消息推送。
然而,如果只需要单向的消息推送,HTTP 通过服务器发送的事件也有这种功能。
WebSocket 的通信过程如下:
首先,通过 HTTP 切换协议。服务器返回 101 状态码后,协议切换成功。
然后,开始以 WebSocket 格式的数据通信,任意一方都可以随时向另一方推送消息。
至于 HTTP 中的服务器发送的事件:
服务器返回的 Content-Type 是 text/event-stream,这是一种可以多次返回内容的流。
服务器发送的事件通过这种类型的消息随时推送数据。
你可能是第一次听说 SSE,但你已经使用过基于它的应用程序。
例如,你使用的 CI/CD 平台会实时打印日志。
那么它如何实时传输构建日志呢?
它需要分次传输,SSE 通常用于以这种方式推送数据。
另一个例子是 ChatGPT。它在回答问题时不会一次给你所有答案,而是逐步分块加载。
这也是基于 SSE 的。
现在我们已经知道 SSE 是什么以及它的应用,让我们自己实现它。
创建一个 Nest 项目。
npx nest new sse-test
运行它:
访问 http://localhost:3000 会显示“Hello World”,表示服务器运行成功。
然后在 AppController 中添加一个流接口。
这里没有用 @Get、@Post 等装饰器进行标识,而是 @Sse 装饰器表示这是一个事件流类型的接口。
@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,2 秒后返回 bbb,5 秒后返回 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 (
hello
);
}
export default App;
这个 EventSource 是浏览器的原生 API,用于获取 SSE 接口的响应。它会将每个消息传入回调函数 onmessage 中。
我们在 Nest 服务中启用跨域支持。
然后删除 react 项目中的 index.tsx 文件中的这几行代码,因为它们会导致额外的渲染:
执行 npm run start
因为 3000 端口被占用,它将在 3001 上运行:
访问浏览器:
看到响应了吗?
这就是服务器发送的事件。
在 devtools 中,你可以看到响应的 Content-Type 是 text/event-stream。
然后在 EventStream 中,你可以看到接收到的每条消息。
通过这种方式,服务器可以随时向网页推送消息。
它的兼容性如何?
你可以在 MDN 上看到。
除了 IE 和 Edge 外,与其他浏览器没有兼容问题。
一般来说,安全使用。
它可以在哪里使用?
服务器发送的事件 特别适合只需要服务器端推送的场景。
例如日志的实时推送。
让我们测试一下:
“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.js 中,二进制数据存储在 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 的服务器发送事件(SSE)从服务器推送实时数据。
通过在 HTTP 响应中返回一个 Content-Type 为 text/event-stream 的头,可以通过流多次发送消息。
传输的内容是 JSON 格式,可以用来传输文本或二进制内容。
我们使用 Nest 实现了 SSE 接口。方法使用 @Sse 装饰器进行注释,它返回一个 Observable 对象。可以使用 observer.next 随时返回数据。
在前端,使用 EventSource 的 onmessage 来接收消息。
这个 API 在除 IE 和 Edge 外的其他浏览器有很好的兼容性,可以安全使用。
它有各种应用,如内部消息传递、构建日志的实时显示和 chatgpt 的消息响应。
当遇到需要消息推送的场景时,考虑使用服务器发送的事件而不是 WebSocket。