长轮询和短轮询
长轮询和短轮询是两种不同的轮询机制,都是用于从服务器定期获取数据。
短轮询是最简单的轮询方式,客户端在固定的时间间隔发送请求到服务器,不管服务器是否有新的数据更新。
javascript
import axios from 'axios';
function shortPolling() {
setInterval(() => {
axios.get('/api/data')
.then(response => {
console.log('短轮询数据:', response.data);
// 处理数据
})
.catch(error => {
console.error('短轮询错误:', error);
});
}, 5000); // 每5秒请求一次
}
shortPolling();
而长轮询是一种更高效的轮询方式,客户端发送请求到服务器后,服务器会保持这个请求开启,直到有新的数据可以发送给客户端,或者达到某个超时时间后,才会响应请求。
javascript
import axios from 'axios';
function longPolling() {
function poll() {
axios.get('/api/data', {
timeout: 60000 // 设置长时间的超时时间
})
.then(response => {
console.log('长轮询数据:', response.data);
// 处理数据
poll(); // 数据处理完毕后,再次发起长轮询请求
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('长轮询被取消:', error.message);
} else {
console.error('长轮询错误:', error);
}
setTimeout(poll, 5000); // 发生错误后,等待5秒再次发起请求
});
}
poll();
}
longPolling();
从上面例子看出:
- 短轮询适合实时性要求不高、服务器负载需要控制在较低水平的场景。
- 长轮询适合需要较高实时性、减少请求次数的场景,
但是这两者都不如 Websocket 实时性和效率好。
WebSocket 和 SSE 通信过程
WebSocket 双向通信,客户端和服务器可以在任何时候互相发送数据。
如果想更轻量级,而且只需要服务端单向往客户端推送消息,我们可以使用 Server Send Event(SSE),类似私信、股票行情,订阅新闻就很适用 SSE。
WebSocket 的通信过程是这样的:
- 客户端发起握手:客户端通过发送一个 HTTP 请求到服务器来初始化一个 WebSocket 连接。这个请求被称为握手请求,它包含了一个特殊的 Upgrade 头,这表明客户端想要切换协议从 HTTP 到 WebSocket。
- 服务器响应握手:如果服务器支持 WebSocket,它会发送一个 101 状态码的 HTTP 响应,其中包含 Upgrade: websocket 和 Connection: Upgrade 头,表示同意协议切换。
双方握手完成后就可以传输文本数据和二进制数据了。
任何一方都可以发起关闭握手(close handshake),这是通过发送一个特殊的控制帧来完成的,该控制帧包含了关闭连接的代码和原因。
而基于 HTTP 协议的 Server Send Event 通信过程:
- 客户端请求:客户端(通常是一个浏览器)发起一个对服务器的普通 HTTP GET 请求。这个请求的头部会包含一个特殊的 Accept 字段,值为 text/event-stream,这表明客户端希望建立一个 SSE 连接。
- 服务器响应:服务器识别这个请求,并保持这个连接打开,而不是像通常的 HTTP 请求那样返回数据后关闭连接。服务器设置响应的 Content-Type 为 text/event-stream,并开始发送数据,可以多次发送数据。
- 发送消息:服务器按照 SSE 的格式发送消息,每条消息通常包含一个事件类型(event)、数据(data)和一个可选的 id(id)。消息以两个换行符 \n\n 结尾。例如
kotlin
data: 第一条消息内容\n\n
或者,如果一条消息包含多行数据,它会这样发送:
arduino
data: first line\n
data: second line\n\n
如果指定了事件类型和 id,它们将作为消息的一部分被发送:
makefile
id: 1
event: myMessage
data: 第二条消息内容\n\n
4.客户端处理:客户端监听这个流,并在收到新消息时触发事件。在 JavaScript 中,这是通过创建一个 EventSource 对象并监听它的 onmessage 事件来实现的。如果服务器指定了事件类型,例如上面指定了 myMessage,客户端需要监听 myMessage 事件类型以收到数据。
5.保持连接:如果连接意外断开,客户端会自动尝试重新连接到服务器。如果服务器提供了消息的 id,客户端会在重新连接时发送一个 Last-Event-ID 头部,包含上次接收到的消息 id,这样服务器可以从断点继续发送。而 WebSocket 如果断开之后是需要手动重连的。
6.关闭连接:客户端可以通过调用 EventSource 对象的 close() 方法来关闭连接。服务器也可以通过发送特定的关闭消息来关闭连接。
CICD 平台的日志是实时打印的,ChatGPT 一段段加载回答,其实都是基于 SSE。
注意:SSE 通常传输的数据是文本格式,具体来说是使用 UTF-8 编码的文本数据。如果使用 SSE 传输大量二进制则需要编解码处理,不推荐。
Nest 实现 SSE 接口
我们实现一下,创建 nest 项目:
bash
npx nest new sse-project
运行:
bash
npm run start:dev
在 AppController 添加一个 stream 接口:
使用 @Sse()
装饰器来标记为 SSE 端点。
返回的是一个 Observable 对象,然后内部用 observer.next 返回消息。
sse1 我们先返回了 'hello',三秒后返回了 'world'。
我们支持下跨域:
React 接收 SSE 接口数据
写一个前端页面,创建 react 项目:
bash
npx create-react-app --template=typescript sse-project-frontend
在 App.tsx 里写如下代码:
通过 new EventSource 这个原生 API,监听上面的 onmessage 回调函数,获取 sse 接口的响应。
将渲染 App 外层的严格模式注释,它会导致多余的渲染。
执行 npm run start。
因为 3000 端口被 nest 应用占用了,react 应用跑在 3001 端口。
点击 event1 按钮:
控制台先打印 'hello',三秒后打印 'world',我们可以取里面的 data 属性拿到最终数据:
点击 even2 按钮:
控制台不断打印:
表明我们不断收到服务端推送的数据。
响应的 Content-Type 是 text/event-stream:
然后在 EventStream 可以看到每次收到的消息:
SSE 日志实时推送
tail -f
命令可以实时看到文件的最新内容:
我们可以通过 child_process 模块的 exec 来执行这个命令,监听 log 文件改动,返回给客户端改动内容:
./log 指的是当前工作目录下名为 log 的文件。在这里,. 表示当前工作目录。
可以输入 node
然后再输入 process.cwd()
来查看当前的工作目录。
前端连接这个新接口:
输入 111 保存,再输入 222 保存:
控制台打印两条信息:
浏览器收到了实时的日志,可以对 data 属性值进行 JSON.parse()
。
很多构建日志都是通过 SSE 的方式实时推送的。