6

前言

最近有一个新需求,就是使用chatGpt的推流技术实现微信扫码交互的功能,查看了资料,发现chatgpt的推流效果是采用SSE(Server-Sent Events)的通信机制

什么是 Server-Sent Events (SSE)?

我们知道HTTP协议本质是一次请求一次响应的通信模式,也就是说,客户端发送一次请求,服务区端只会处理一次响应,服务器返回响应结果后,通常会连接就会关闭连接。而SSE是一种服务区单向推送数据到客户端(的通信机制,SSE支持从服务器到客户端的单向数据流。

HTTP
image.png

SSE和WebSocket作用很相似,都是建立浏览器与服务器之间的通信,之后服务器向浏览器发送。

我们知道WebSocket是全双工通道,可以进行双向通信,而SSE时单向通道,只能服务器向浏览器发送,我们知道流信息的本质就是下载,如果浏览器向服务器发送请求,那么就变成了一次HTTP请求。

WebSocket

image.png

HTTP with service sent envent(服务发送事件)

image.png

从上面2张图可以看出,使用SSE不是一次性的数据包,而是一个数据流,会连续不断的发送过来,客户端不会上得到响应就进行关闭连接,会一直等待服务区发过来的数据流,这种通信就是以流信息的方式,完成一次用时很长的下载

当我们使用ChatGPT进行问问题的时候,我们可以发上数据流是不断进行发送,而不是一次性全部响应

image.png

SSE的工作原理

SSE通过HTTP协议的特点实现:服务器可以通过保持HTTP连接打开。
整个过程如下:

  1. 客户端向服务器发送请求:客户端发出一个普通的 HTTP 请求,服务器处理请求并保持连接不断开。
  2. 服务器推送数据:服务器可以在一段时间内不关闭连接,而是通过该连接持续推送数据给客户端。
  3. 客户端接收数据:客户端使用 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

image.png

基于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 个元素的流

代码整体讲解

  1. Flux.interval(Duration.ofSeconds(1)) 每隔 1 秒发出一个递增的数字,从 0 开始。
  2. map 操作符将每个数字转换成字符串 "消息发送 X\n\n",其中 X 是计数器值加 1。
  3. take(10) 操作符限制消息流只发送前 10 条消息。
  4. 最终,方法返回一个 Flux<String>,该 Flux 每秒发送一条格式化的消息,持续 10 秒后自动结束。

开始测试

当前服务端运行在8080端口,客户端运行在4200端口,中间使用nginx进行解决跨域问题,启动在8018。

进行访问,发现当前数据是一起过来的,而不是一条一条过来跟推流效果一样

image.png

查看网络,发现一直在peng状态中

image.png

到最后请求成功后,发现数据是一起过来的,而不是一条推送

image.png

使用curl进行测试,这个时候发现是流的模式推送过来的,那么问题出现在别的地方了

image.png

查看了前台的代码没有问题,最后发现在nginx缓存上

image.png

之后关闭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;   # 禁用长连接超时
    }

理想效果

image.png

在进行测试,发现已经实现了效果

image.png

总结

SSE使用适合那种一次请求数据量大的请求,他是以数据流的方式过来的,会一点点响应,而不会说出现说请求过大数据而出现的长时间未响应效果,而使用sse就能很好解决这个问题


kexb
519 声望16 粉丝

引用和评论

0 条评论