服务器发送事件Server-sent events(SSE)和流式网络请求EventStream
Server-sent events(SSE)简介以及接口
服务器发送事件。通常来说,一个网页获取新的数据通常需要发送一个请求到服务器,也就是向服务器请求的页面。使用服务器发送事件,服务器可以随时向我们的
Web页面推送数据和信息。这些被推送进来的信息可以在这个页面上以 事件 + 数据 的形式来处理。
使用服务器发送事件我们需要通过一个接口:EventSource,一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件。
EventStream
EventStream是什么?
EventStream(事件流)是指在网络传输中持续发送事件的一种技术。
EventStream和Server-sent events的关联?
EventStream 可以被视作是 SSE 的一种具体实现方式。SSE 是基于 HTTP 的服务器向客户端推送数据的机制,它使用了简单的文本格式(通常是使用 text/event-stream ),允许服务器单向持续地向客户端发送事件。
EventStream的使用场景
概念性的内容往往很难被理解,下面给出一个例子来体现。
当下,越来越多的用户开始在工作生活中使用ChatGPT,以ChatGPT中的一个功能举例。在我们使用的过程中,存在这个一种场景:我们提出问题后,GPT会将回答的内容一个个拼凑成答案,逐步展示在界面上。那么它是怎么去实现这么一个功能的呢?为什么需要这种形式展示内容呢?
如何实现的?
我们f12切到Network,我们向GPT发送提问后,发送出一个请求,服务端在持续给予回复,并不是一直pending到数据完全获取到再给予响应,可以看到该请求服务端设置 response headers Content-Type 为 "text/event-stream",表示数据将以事件流的方式,通过 HTTP 响应给客户端。可以看到并且没有response,取而代之的是EventStream。
为什么需要使用事件流的形式?
根据GPT的回答,在生成长篇文本或复杂内容时可能包括对语法、逻辑和上下文的审查和调整,以确保输出的内容通顺、合乎逻辑并且相关联。在这庞大的计算过程中,为了避免用户等待回答时界面没有交互,以逐步展示回答内容是很好的选择。
简而言之,就是存在某个场景需要展示很大的数据或者接口需要请求很长时间,使用常规的请求,可能会pending很长的时间,还可能导致超时的问题,如果可以通过服务端以这种流的形式持续分批次输出给客户端,那么会很大程度提升用户体验。
简单实现EventStream
服务器端的简单实现
先使用
http.createServer创建一个服务,并且在接受SSE请求的时候设置其HTTP头的参数,大致如下1
2
3
4
5
6
7const server = createServer((request, response) => {
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
});
}).listen('8086', () => {});使用流的方式从
test.txt文件中读取期望展示的内容,每次读取一个字,将其作为一个事件发送到客户端1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const server = createServer((request, response) => {
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
});
const readStream = createReadStream('test.txt', {
encoding: 'utf-8',
highWaterMark: 1
});
readStream.on('data', (content) => {
response.write(`event:SSE\ndata: ${content}\n\n`); // 每读取一个内容都会将数据以名为“SSE”的事件名发送给客户端
if (!content) {
response.end();
}
});
}).listen('8086', () => {});启动服务
前端的简单实现
新建一个
EventSource实例,这个EventSource实例会对HTTP服务器开启一个持久化的连接,以text/event-stream格式发送事件1
const eventSource = new EventSource('/sse')
监听服务端发送的具名事件,并将内容输出在界面上,由于我们上方服务端定义的事件名叫“
SSE”1
2
3
4
5const content = document.querySelector('#content');
eventSource.addEventListener('SSE', (event) => {
content.innerHTML += event.data;
});模拟
GPT的中断请求使用实例方法
close()1
eventSource.close()
实现效果

可以看到每次接收到的都是传递过来一个字的SSE事件。
如果感觉数据流读取展示太快了,可以在服务端发送事件后将文件读取流暂停(.pause()),设置定时器短暂时间间隔后再恢复(.resume())。
1 | readStream.on('data', (content) => { |
效果如下:

更多
使用下来,可以感觉SSE的实现和WebSocket有些类似。其实两者虽然存在相似的地方,但是很多地方还是不一样的。网上有很多关于两者的对比和介绍,这里就不再过多赘述了,简单来说首先不同于WebSocket的全双工通信可以客户端服务端双向发送信息,而SSE是单向通信,只能服务端向客户端发送信息;其次SSE 是基于标准的 HTTP/HTTPS 协议,使用常规的 HTTP 请求。
观察较仔细的会发现ChatGPT使用的是POST请求,通过查询资料,找到一个可以和ChatGPT一样使用POST请求的方法:可以使用一个库:fetch-event-source,改写发起的方法。
为了性能考虑,在实际应用场景如果只需要单向的服务端输出到客户端,我们可以考虑使用此种方案。