扒一扒 Chatgpt 背后的 web 开发技术(一)
ChatGPT 我就不多说了,最近一直都很火。在 ChatGPT 的页面上我们输入一个问题后,答案是以渐进式的输出展示出来的,为了实现实时通信和高效的数据传输,ChatGPT 选择了 SSE(Server-Sent Events) 技术。在本篇博客中,我们将详细介绍这一项重要技术。
SSE 技术概述
SSE 是一种基于 HTTP 的实时通信技术,允许服务器向客户端(如 Web 浏览器)实时推送消息。SSE 的基本原理是通过建立一个持久的 HTTP 连接,服务器可以主动将事件发送到客户端,而无需客户端重复发送请求。
以下是 SSE 技术的一些主要特点:
- 基于文本:SSE 使用纯文本格式传输数据,这使得数据的解析和处理变得相对简单。
- 单向通信:与 Websockets 等双向通信技术相比,SSE 主要用于服务器向客户端的单向通信,减少了通信的复杂性。
- 重连机制:在连接中断时,SSE 客户端会自动尝试重新连接服务器,从而确保实时数据的传输不会中断。
- 事件标识:SSE 支持为不同类型的事件设置标识,使客户端能够根据事件类型进行相应的处理。
与 Websockets 的比较
尽管 SSE 和 Websockets 都是实时通信技术,但它们在某些方面有所不同:
- 通信方向:Websockets 支持双向实时通信,而 SSE 主要用于服务器向客户端的单向通信。
- 协议:Websockets 使用独立的协议进行通信,而 SSE 则基于 HTTP 协议。
- 数据格式:Websockets 支持传输二进制和文本数据,而 SSE 仅支持文本数据。
- 根据项目需求和场景,开发者可以选择适合的实时通信技术。在 ChatGPT 中,我们主要关注服务器向客户端推送消息,因此选择了 SSE 作为实现实时通信的技术。
SSE 的代码示例
在 Nestjs 里面使用 SSE 作为后端接口的代码示例如下:
1 | import { MessageEvent, Sse } from '@nestjs/common'; |
Nestjs 中已经内置了 SSE 的实现,只需要使用装饰符Sse
即可,再通过rxjs
的 Observable 对象返回流式数据。
然后再看看客户端的代码示例:
1 | const eventSource = new EventSource('/sse'); |
通过创建一个EventSource
对象来建立后端的 SSE 接口连接,并通过监听 message 事件来获取后端数据。
更多详细代码请参考 这里。
SSE 的局限性
只能使用 GET 请求
SSE 是基于 HTTP GET 请求的,因此无法直接使用 POST、PUT 等其他请求,也就是说无法在请求中传递 body 类的参数。这是因为 SSE 的设计初衷是用于服务器向客户端发送实时事件和数据,而不是用于客户端向服务器发送数据。
无法在请求中传递 header
由于浏览器的安全策略,SSE 请求无法直接携带自定义 HTTP 头部。这可能在某些情况下带来限制,例如在需要身份验证时传递令牌。
虽然有以上局限性,但我们还是可以通过一些方法来解决这些问题。
- 使用 URL 参数来传递数据,将所需的数据作为 URL 参数附加到 SSE 请求,适合传递一些简单的参数
1 | // 客户端 |
- npm 上有个
EventSource
的 polyfill, 使用它替换掉EventSource
后就可以在请求中携带 header 了,像身份令牌这种重要信息,还是建议放到 header 中。
1 | const es = new EventSourcePolyfill('/sse', { |
用 SSE 实现 ChatGPT
既然 SSE 有这些限制,那么 ChatGPT 是怎么做到将prompt
传到后端,然后后端再通过 SSE 返回答案的呢?
可能有人会说通过 GET 请求的 query
参数来传递prompt
参数,比如sse?question=xxx
,但是这样的话,如果prompt
很大,比如一个几百上千字的prompt
就会有问题了,一个是不安全,另一个是可能超过 URL 的长度限制。同样地,将prompt
放到 header
里也不太合适,毕竟是业务字段,不应该放到 header 里。
如果是局限在一个请求里面是去思考的话,可能确实无法做到,但如果是多个请求呢?我们可以通过多个请求来实现,比如先通过一个请求将prompt
传到后端,后端返回一些参数给前端后,前端再通过 SSE 去后端请求答案,这样就可以避免上面的问题了。
见证奇迹的时刻
我们可以先发送一个 POST 请求,将prompt
放到请求的body
里,后端接收到 POST 请求后,将prompt
存到数据库里,并返回一个id
给前端。
1 | @Post('new') |
前端接收到这个 id 参数后,将 id 放到 SSE 的请求里,再向后端发送请求。
1 | const es = new EventSourcePolyfill(`/sse/${id}`, { |
可以看到这里将 id 参数作为 url 的param
,这样后端就可以通过 sse 请求获取到这个 id 了,然后再通过id
去数据库里取出prompt
,然后再去调用 ChatGPT 的 API,最后将答案返回给前端。
1 | @Sse('sse/:id') |
总结
SSE 作为一种基于 HTTP 的实时通信技术,使得服务器能够主动将事件发送到客户端,而无需客户端重复发送请求。尽管 SSE 存在一定的局限性,在特定场景下,其简单、易用的特点使其成为实现单向实时数据传输的理想选择。