之前写过一篇『实现接口流式响应数据』提到了服务端如何分多次将数据传给前端,前端如何接收一点数据处理一点。其应用场景主要是对话式应用,细心的同学可能已经发现它和 ChatGPT 中使用的技术还不太一样,在上一篇文章中响应使用的是application/octet-stream
看看浏览器的网络面板
再看看 ChatGPT 的请求响应信息
在这张图上发现 chatgpt 的请求的响应进类型使用的是text/event-stream
就是这这篇文章的主角
Server-sent events
服务器发送事件,顾名思义,是由服务端发送数据给前端,简称 SSE。在客户端通过特定事件来接受数据。通常情况下,客户端想要从服务端获取数据,需要向服务端发送请求,附加特定参数,在服务处理完成之后,将数据响应给客户端。一次请求只能响应一次数据。在没有请求时,服务端是无法主要向客户端发送数据的。如果需要服务端主要发送数据,则一般会用 websocket, 这是一种双向通信的协议,连接一量建立以后,客户端和服务端均可以主动向对方发送数据。而 SSE 不同,它使用的依然是 http协议,建立连接之后,只能由服务端向客户端发送数据。而要使用 SSE 就需要服务端在响应数据的时候,将类型设置为text/event-stream
并且响应的数据也需要按照指定的格式来发送,客户在收到数据后按规范解析,同时触发相应的事件
EventSource
EventSource是浏览器提供的一个用于和服务器建立连接,接收服务器发送事件的接口。其基本用法如下
1 | const eventSource = new EventSource('/api/ask2'); |
通过 EventSource 构造函数创建一个实例,将会对指定的 url 的 http 服务开启持久连接,服务端以text/event-stream
格式发送事件及数据。此连接将会一直保持开启,直到通过调用 EventSource 的实例的 close 方法关闭
数据格式
服务器除了要设置响应头Content-Type: text/event-stream
之外 ,发送的数据也是有固定格式的
服务器按行发送数据。每行的数据都是以下格式
1 | field: value |
其中 field 可以是event, data, id, retry
不是此格式的数据都会被客户端忽略,通过空行来分割多个 message。每遇到一个空行都会将空行前的数据作为一个 message 触发相应的事件
1 | data: 初始化数据 |
上面的数据将会触发两次事件,第一次事件名为默认的 message, 数据就是 data 部分的内容,第二次触发的事件名为指定的 update, 数据也是data部分的内容,如果一个 message 内, data, id, retry 部分都没有,这个 message 也变得没有意义,会被忽略。
自动重试
当连接建立后,如果因为某些原因,连接和服务端的连接断开了,客户端会自动尝试重新建立连接,同时会携带一个特殊的请求头Last-Event-ID
, 这个值会在以前触发的事件中查找,从最后一次有 id 传回的消息中获取,使用 lastEventId
属性名从事件对象中获取。在上面的示例中,如果第二个消息触发后,连接中断,那么自动重试的时候,请求关中将会增加 Last-Event-ID: 1
当连接中断,客户端并不是立即重连,而是在等待一断时间后再发起重连。而这等待时间正是通过 retry 指定的,单位是 ms, 在服务端发送事件时可以通过 retry: 5000
来告诉客户端如果连接中断,需要等5s 后再发起重连。
请求方式
虽然浏览器给我们提供了 EventSource 接口,但是使用 EventSource 建立的连接使用的是 get 方式,如果仔细观察 ChatGPT 的请求信息,将会发现它使用的 Post 请求
这就说明 ChatGPT 并不是使用的浏览器提供的EventSource 接口,而使用的 Azure/fetch-event-source, 其内部使用 fetch 发送请求,同时在接收到数据后按照规则解析,然后触发相应事件
自己动手
虽然已经有很多封装好的库了,但是为了学习,还是自己动手写一个吧。仓库地址leemotive/fetch-event-source
除了支持 EventSource 和 fetch 的接口配置外, 还支持一些额外的配置
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
json | boolean | false | 如果 data 为 json 是否自动解析返回的 json |
serverEnd | boolean | false | 如果服务端主动关闭连接,客户端不再尝试重连 |
示例
1 | const eventSource = new FetchEventSource.default('/api/ask2', {json: true, serverEnd: true}); |
服务端代码,midway.js作后端示例,只截取了部分代码
1 | @Get('/ask2') |