说明:
- 本文图片/链接较多, 由于微信公众号文章, 过滤掉了超链, 如果跳转不方便, 可以查看末尾的”阅读原文”.
- 文章封面图, 来自阮一峰的 Server-Sent Events 教程

SSE 技术调研
体验过 ChatGPT 官方 Web 工具的同学, 应该了解, AI 对话返回的结果, 不是一次性返回的, 而是会在一段时间内, 持续输出, 就像是人在说话时, 有序地说出每一个字一样.
它是如何实现的呢, 通过 Chrome 检查工具, 可以看到, 它返回的内容类型 (Content-Type) 是 text/event-stream, 而不是我们常用的 application/json.

那么 text/event-stream 又是什么意思呢, 先问问 AI 吧~

原来, 它是一种实现服务器向客户端推送数据的技术, 常用于在线聊天场景^^
我以前听说过 WebSocket, 它跟 Server-Sent Events (SSE), 有什么区别和联系呢?

原来如此, 这好像一道面试题和解答呀, 哈哈, 不知道, 解答是否有问题, 是否不全面, 欢迎评论区讨论~
学习一项技术, 肯定不能只听 AI 一家之言, 还得多查点资料看看, 觉得 实时通讯技术Ajax,WebSocket,SSE, 这篇内容写得比较全面系统, 提到 “SSE 是 WebSocket 的一种轻量代替方案“.
SSE 在 Spring Boot + Vue 前后端分离项目中落地
我的 AI 对话演示项目 (希羽, http://xiyu.zhiyuanbiji.cn/#/chat), 后端采用 Spring Boot, 前端采用 Vue, 那么如何应用 SSE 技术, 优化对话时的交互体验呢?

回答内容比较长, 截图查看不方便, 直接复制 Markdown 内容.
🔽🔽🔽以下是 AI 自动生成的🔽🔽🔽
Spring Boot 可以通过使用 Spring WebFlux 模块来实现 Server-Sent Events。下面是一个简单的示例代码:
首先,需要在 pom.xml 文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
然后,创建一个控制器类,该类使用 SseEmitter 类来发送 Server-Sent Events:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.time.Duration;
@RestController
public class SseController {
@GetMapping("/sse")
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
Flux.interval(Duration.ofSeconds(1))
.map(i -> "Server-Sent Event #" + i)
.doOnCancel(() -> emitter.complete())
.subscribe(
data -> {
try {
emitter.send(data);
} catch (IOException e) {
emitter.completeWithError(e);
}
},
error -> emitter.completeWithError(error),
() -> emitter.complete()
);
return emitter;
}
}
在上面的示例代码中,handleSse() 方法返回一个 SseEmitter 对象,该对象可以用来发送 Server-Sent Events。在这个方法中,我们使用 Flux.interval() 方法来生成一个每秒钟发送一次的数据流,并将其转换为一个字符串流。然后,我们使用 subscribe() 方法来订阅这个数据流,并在每次接收到数据时使用 emitter.send() 方法将数据发送给客户端。如果发生错误或订阅被取消,我们使用 emitter.completeWithError() 或 emitter.complete() 方法来关闭 SseEmitter 对象。
最后,我们可以通过访问 /sse 路径来获取 Server-Sent Events。例如,可以使用 JavaScript 来订阅这个事件流:
const eventSource = new EventSource('/sse');
eventSource.onmessage = function(event) {
console.log(event.data);
};
🔼🔼🔼以上是 AI 自动生成的🔼🔼🔼
我复制代码到项目中, 测试了一下, 发现了一个错误, 这里不能使用 @RestController, 要用 @Controller. 我也在网上找到了一个开源项目 Grt1228/chatgpt-java, 这位大佬已经把接口做了很好的封装, 实现了很多功能.

也给出了在 SpringBoot 项目中, 实现流式输出的 API 接口演示项目 Grt1228/chatgpt-steam-output.

演示项目包含了前后端的实现, 那就站在巨人的肩上, 迅速借鉴实现一下. 调试过程中, 还是有很多细节处理, 有一个困扰我好久的问题, 在后面会介绍, 先来看看实现后的效果吧^^

还不错吧, GIF 动画没有做任何加速处理哦~大家可以如何体验呢?
- 浏览器打开: http://xiyu.zhiyuanbiji.cn/#/chat, 需要登录
- 关注”知源笔记“公众号, 进入之后, 输入”
666“, 直接登录
- 在”知源笔记“公众号中, 输入”
xiaoxi“, 也可以进入 AI 对话, 但不是流式输出的, 可能因为超时, 需要输入”重试”.
上线时遇到的问题
开发、测试还是挺顺利的, 效果也正常, 但发到线上以后, 一直 loading 转圈, 没有输出, 打开 Chrome 检查工具, 发现请求一直处于 pending 状态…

一直等下去, 会失败, 并报 500 错误.

这时, 我有点蒙圈, 赶紧回滚线上代码, 包括前后端[捂脸]
首先是怀疑自己的代码问题, 毕竟是第一次用 SSE, 反复查资料, 对比别人的实现; 其次怀疑是 CORS 跨域问题; 还有怀疑是 Webpack 打包的问题. 试了很多解决方案, 都没有解决问题, 有点绷不住了.
到了下午, 突然想到是不是 nginx 反向代理配置的问题, 我的域名网址 http://xiyu.zhiyuanbiji.cn, 是通过 nginx 反向代理到 80 端口的. 要验证这个猜想也很简单, 直接用线上 Java 项目的 ip+port 端口访问测试一下, 果然没有问题!!!
总算找到问题点了, 解决方案很快也就有了, 参考 nginx代理webSocket 和eventSource 请求超时连接不通 但是本地可以得问题 中的方案, 为了在 nginx 中支持 EventSource, 需要增加如下配置.
location /es/ {
proxy_pass http://请求地址/;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}
大功告成! 工具的交互体验大幅提升, 借助这个 AI 工具提升工作效率的同时, 使用也变得愉悦了. 最后, 打个小广告, 我最近在关注 AIGC 的技术和应用, 建了一个微信群讨论相关话题, 如果您也感兴趣, 欢迎加入^^
