前言
最近有一个新需求,就是使用chatGpt的推流技术实现微信扫码交互的功能,查看了资料,发现chatgpt的推流效果是采用SSE(Server-Sent Events)的通信机制
什么是 Server-Sent Events (SSE)?
我们知道HTTP协议本质是一次请求一次响应的通信模式,也就是说,客户端发送一次请求,服务区端只会处理一次响应,服务器返回响应结果后,通常会连接就会关闭连接。而SSE是一种服务区单向推送数据到客户端(的通信机制,SSE支持从服务器到客户端的单向数据流。
HTTP
SSE和WebSocket作用很相似,都是建立浏览器与服务器之间的通信,之后服务器向浏览器发送。
我们知道WebSocket是全双工通道,可以进行双向通信,而SSE时单向通道,只能服务器向浏览器发送,我们知道流信息的本质就是下载,如果浏览器向服务器发送请求,那么就变成了一次HTTP请求。
WebSocket
HTTP with service sent envent(服务发送事件)
从上面2张图可以看出,使用SSE不是一次性的数据包,而是一个数据流,会连续不断的发送过来,客户端不会上得到响应就进行关闭连接,会一直等待服务区发过来的数据流,这种通信就是以流信息的方式,完成一次用时很长的下载
当我们使用ChatGPT进行问问题的时候,我们可以发上数据流是不断进行发送,而不是一次性全部响应
SSE的工作原理
SSE通过HTTP协议的特点实现:服务器可以通过保持HTTP连接打开。
整个过程如下:
- 客户端向服务器发送请求:客户端发出一个普通的 HTTP 请求,服务器处理请求并保持连接不断开。
- 服务器推送数据:服务器可以在一段时间内不关闭连接,而是通过该连接持续推送数据给客户端。
- 客户端接收数据:客户端使用 EventSource 对象来接收服务器发送的事件数据。
SSH实现案例
客户端 API
使用SSE的API部署在EventSource对象上,也就是说我们要使用SSE时,需要用到一个EventSource的实例,向服务端发起连接
当前案例使用angular进行实现
定义SseService
@Injectable({
providedIn: 'root'
})
export class SseService {
getServerSentEvent(): Observable<string> {
return new Observable(observer => {
const eventSource = new EventSource('api/sse/stream');
// 当服务器发送消息时,onmessage会被触发
eventSource.onmessage = (event) => {
observer.next(event.data);
};
eventSource.onerror = (error) => {
observer.error(error);
eventSource.close();
};
// close方法用于关闭 SSE 连接。
return () => eventSource.close();
});
}
}
定义AppComponent进行调用
export class AppComponent implements OnInit {
messages: string[] = [];
constructor(private sseService: SseService,
private ngZone: NgZone) {}
ngOnInit() {
this.sseService.getServerSentEvent()
.subscribe({
next: (message) => {
this.ngZone.run(() => {
this.messages.push(message);
})
}, error: (error) => {
console.error('Error:', error);
}
});
}
<div *ngFor="let message of messages">
{{ message }}
</div>
服务器实现
数据格式
服务端向客户端发送SSE数据的请求头格式
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
必须把Content-Type必须指定类似为event-stream
基于Springboot的SSE实例
导入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// 响应式的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
请求实现
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatGPTMessages() {
return Flux.interval(Duration.ofSeconds(1)) // 每秒发送一次消息
.map(counter -> "消息发送 " + (counter + 1) + "\n\n") // 格式化为 SSE 格式
.take(10); // 发送 10 条消息后结束
}
返回类型是 Flux<String>,它是 Reactor 库中的一个反应式类型,表示一个异步数据流,其中包含多个 String 类型的元素。Flux 可以用于表示一个包含 0 到 N 个元素的流
代码整体讲解
- Flux.interval(Duration.ofSeconds(1)) 每隔 1 秒发出一个递增的数字,从 0 开始。
- map 操作符将每个数字转换成字符串 "消息发送 X\n\n",其中 X 是计数器值加 1。
- take(10) 操作符限制消息流只发送前 10 条消息。
- 最终,方法返回一个 Flux<String>,该 Flux 每秒发送一条格式化的消息,持续 10 秒后自动结束。
开始测试
当前服务端运行在8080端口,客户端运行在4200端口,中间使用nginx进行解决跨域问题,启动在8018。
进行访问,发现当前数据是一起过来的,而不是一条一条过来跟推流效果一样
查看网络,发现一直在peng状态中
到最后请求成功后,发现数据是一起过来的,而不是一条推送
使用curl进行测试,这个时候发现是流的模式推送过来的,那么问题出现在别的地方了
查看了前台的代码没有问题,最后发现在nginx缓存上
之后关闭nginx缓存
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off; # 关闭代理缓冲
proxy_cache off; # 关闭缓存
proxy_set_header Connection ''; # 防止 Nginx 关闭连接
chunked_transfer_encoding off; # 确保进行流式传输
keepalive_timeout 0; # 禁用长连接超时
}
理想效果
在进行测试,发现已经实现了效果
总结
SSE使用适合那种一次请求数据量大的请求,他是以数据流的方式过来的,会一点点响应,而不会说出现说请求过大数据而出现的长时间未响应效果,而使用sse就能很好解决这个问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。