SSE (server-sent events) 是什么
MDN 上有关于 SSE 的介绍,可以把 SSE 理解为单向的 WebSocket。普通请求 + WebSocket + SSE 联手一起完善了浏览器网络请求体系。它们的区别如下:
- 普通请求 (GET,POST,PUT 等等):浏览器 ---> 服务器,单向非持续数据流。
- WebSocket:浏览器 <---> 服务器,双向持续数据流。
- SSE:浏览器 <--- 服务器,单向持续数据流。
SSE 的表现形式上和 WebSocket 差不多,依旧由浏览器主动发起请求,但是服务端不会立即返回,而是保持长连接,伺机返回数据,相应的,浏览器端则需要监听事件。
场景解析
知道了 SSE 能实现什么功能,你可能已经跃跃欲试了,但是 SSE 的使用场景是什么呢?为什么不直接使用 WebSocket 呢? 一个经典的使用场景就是 AI 聊天。在用户输入提问后,AI 会流式地返回消息,所以浏览器发送提问后,就只需要持续接受消息,不需要再发送消息了。 以下内容以 AI 聊天为切入点,详解 Nuxt3 中如何实现这类需求。
需求
假设我们有一个 api_key,用这个 key 可以和 ChatGPT 聊天,需求是:开发一个 web 应用,让所有访问者都可以和 ChatGPT 聊天,而用户不需要知道 key 是什么。简单来说,就是实现一个 ChatGPT 套壳。(假设 ChatGPT 的接口地址为 https://chatgpt.com/chat
是基于 SSE 的)
相关工具
在网上搜索后,我找到了以下信息 / 文档:
- Nuxt3 目前已经原生支持 SSE,提供
createEventStream()
函数,详见官方样例 - 一个很有价值的 gist 参考,用 hookable 实现 NuxtServer 的 api 间通信:gist
- 一个 npm 包,可以在 nodejs 里方便地创建 SSE 客户端:@fortaine/fetch-event-source
流程解析
流程图如下 (第一次用 obsidian 的 excalidraw 做这么复杂的图,做得有点差,见谅):
流程详解:
- 浏览器生成一个随机 uid,用来区分其他聊天。uid 是必须的,否则无法区分聊天。
- 浏览器请求
/api/start-sse
接口,携带 uid 和 prompt。 - NuxtServer 收到请求,开启一个
fetch()
请求 ChatGPT 的服务器,携带 api_key 和 prompt (Nuxt 作为 sse 的 client)。随即返回{ success: true }
。 - 浏览器开启一个
EventSource()
,接口地址是/api/sse
,同时也携带 uid。 - NuxtServer 收到请求,创建一个 eventStream 并返回 (Nuxt 作为 sse 的 server),浏览器收到响应,触发
onopen
事件。 - 在第 3 步中,NuxtServer 开启了一个
fetch()
,其响应是流式的,每次收到响应都会调用callHook()
,触发/api/sse
中的hook()
,最终push()
推送消息到了浏览器。
样例代码
我写了一个简单的示例项目,开源在 Github,项目里不会真的调用 ChatGPT,而是用 Nuxt 另外起了一个 SSE 接口,每隔一秒返回当前时间: