背景
我们在使用chartGPT的时候,在输入框输入文字发送时,页面是逐步回复的,打开控制台,发现请求接口返回数据与平时HTTP请求返回数据是不同的,请求接口是一段一段返回的,与平时调用http接口不一样,倒类似于WebSockets技术。
于是在网上调查了该技术,才知道这个技术是Server Send Event(SSE)。
概述
Server Send Event 服务器推送事件,简称SSE,浏览器发一个接口请求到服务器,服务器则是通过事件来推送数据。
基于SSE与WebSockets,从服务器接收到数据有相似之处,我就做如下比较
Server Send Event | WebSockets |
---|---|
基于传统的 HTTP 协议 | 基于独立的 TCP 连接,使用自定义协议 |
服务器向客户端单向通信 | 客户端和服务器之间的双向实时通信 |
简单易用 | 相对复杂 |
某些旧版本浏览器中可能存在兼容性问题 | 广泛支持 |
使用场景: 实时新闻,股票行情 | 使用场景:聊天室、实时协作应用 |
为什么选择SSE
- SSE 更加轻量化
- 不需要双向通信,占用服务器的资源
- 聊天发送消息,只是短暂触发,不需要实时监控后端发出的数据
服务端实现
协议
对于SSE请求时,需要在相应头设置以下参数
javascript
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 指定内容类型为 text/event-stream
'Cache-Control': 'no-cache', // 禁用缓存
'Connection': 'keep-alive' // 保持长连接
});
- SSE 规定的消息体的MIME 类型 规定为 'text/event-stream'
- 设置 'no-cache' 或者 'no-store' 用以确保服务端无法保存 客户端的数据
- SSE 是要保持持久链接的,不是链接后关闭,需要设置成 keep-alive
示例
javascript
// 导入 http 模块
const http = require('http');
// 定义主机和端口号
const hostname = '127.0.0.1';
const port = 3000;
// 存储客户端连接信息
const clients = new Set();
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
// 设置 HTTP 响应头
res.statusCode = 200;
if (req.url === '/sse') {
// 设置响应头,指明使用 SSE
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 指定内容类型为 text/event-stream
'Cache-Control': 'no-cache', // 禁用缓存
'Connection': 'keep-alive' // 保持长连接
});
// 每5秒发送一条消息
let intervalId = setInterval(() => {
// 发送数据,消息类型为"message"
const data = `data: ${JSON.stringify({ message: 'Hello, client!' })}\n\n`;
res.write(data);
res.write(data);
res.write(data);
setTimeout(() => res.flushHeaders(), 0)
// res.socket.write();
}, 1000);
res.on('message', (event) => {
console.log(event)
})
setTimeout(() => {
res.end()
}, 6000)
// 当客户端关闭连接时,停止发送数据
req.on('close', () => {
clearInterval(intervalId);
});
console.log(req.on)
}
});
// 监听指定的主机和端口号
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
客户端
EventSource
介绍
浏览器提供的HTTP协议的持久化连接的接口,以 text/event-stream 格式发送事件,链接会一直保持开启,断线会自动重连,直到调用EventSource.close()关闭。浏览器则是通过EventSource对象的onmessage、onopen和onerror事件来处理这些消息
浏览器兼容
建立连接
EventSource()构造函数只接受两个参数 url 和 configuration
参数
url 远程连接的路由,
configuration ,是一个可选的对象,里面只有一个参数withCredentials的,如果为true,表示客户端向服务端传送凭据(Cookie,会话标识等)
连接状态
EventSource() 会返回一个对象,里面有readyState 只读属性,这个则表示连接状态
连接状态 | 含义 |
---|---|
0 | 未建立连接 |
1 | 连接中 |
2 | 连接关闭 |
连接关闭
利用EventSource.close()关闭连接,如果调用该方法,则将readyState改变为2,如果不调用的话,则会与服务端一直连接
监听事件
EventSource继承于EventTarget,可以接收事件,并且可以创建侦听器。主要有三个事件error、message和open
- open 事件:当连接服务端后触发
- message 事件: 当客户端接收到服务端数后触发,该事件的data包括服务端的数据
- error 事件: 当发生错误后触发或者关闭服务,该事件对象event包含了错误信息
示例
javascript
const eventSource = new EventSource('/api/sse');
eventSource.onopen = (event) => {
console.log("Connection to server opened.", event);
}
pc.addEventListener('message', (event) => {
console.log('Received message:' + event.data)
})
eventSource.addEventListener('error', event => {
if (event.readyState === EventSource.CLOSED) {
console.log('Connection with the server was closed.');
} else {
console.error('EventSource failed:', event);
}
})
setTimeout(() => {
pc.close()
}, 8000)
Axios
刚开始时,第一个想到的就是axios,相信大家也是一样的。然后在网上找到了调SSE的代码,如下
ini
axios({
method: 'get',
url: '/event-stream',
responseType: 'stream'
}).then(response => {
const eventSource = new EventSource('/event-stream');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
// 处理数据
};
eventSource.onerror = function(event) {
// 处理错误
};
});
相信大家已经看出问题来了,在axios调取后,又用了EventSource又调取一次接口。
后来尝试将EventSource去掉,返回返回的接口不像EventSource接口,是一段一段返回的,而是将接口一次性返回,跟需求上的要求完全不一致
根据上面两个问题,就放弃了axios
Fetch
为什么使用Fetch
EventSource 只能发送GET请求,无法设置请求头,无法向服务端发送DATA参数,无法发POST请求,导致了EventSource无法适配大多数情况,例如URL只能限制2000个字符,像GhatGPT 肯定是无法满足其请求
所以可以利用Fetch模拟SSE实现。
浏览器兼容
示例
javascript
fetch('/api/post/sse', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
// body: JSON.stringify({ })
})
.then(async response => {
const reader = response.body.getReader();
let buffer = '';
reader.read().then(function process({ done, value }) {
if (done) {
console.log('Stream closed');
return;
}
buffer += new TextDecoder('utf-8').decode(value);
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(line => {
console.log(line);
});
// Continue reading
return reader.read().then(process);
});
})
.catch(error => {
console.error('Error:', error);
});
效果
总结
- SSE 相比于 WebSockets 更加轻量化
- SSE是单向通信,服务端向客户端发送流的信息
- 如果对于客户端与服务端双方通信要求比较高,还是建议使用WebSockets
- EventSource虽然浏览器API,使用起来比较方便,但其的通用性不是很强
- SSE前端调用,建议使用Fetch浏览器提供的API