揭秘ChatGPT官网、相对于websocket 更为轻量的服务端推送 SSE

328 阅读3分钟

SSE(Server-Sent Events)

SSE(Server-Sent Events,服务器发送事件)是一种用于实现服务器向客户端单向推送数据的Web技术。它允许服务器将实时数据发送到客户端,使客户端能够实时接收服务器上的更新。SSE是基于HTTP协议的一种轻量级通信协议。

工作原理

SSE通过建立一个持久的HTTP连接,服务器通过这个连接发送数据到客户端。与传统的Ajax轮询或长轮询相比,SSE具有更低的延迟和更高的效率,因为它不需要在每次请求数据时都建立新的HTTP连接。

数据传输格式

SSE使用简单的文本格式传输数据,数据以事件流(Event Stream)的形式发送。事件流由多个事件组成,每个事件由一个事件标识符、事件类型和事件数据组成。客户端通过监听事件流来接收服务器发送的事件。

优点

  • 实时性:服务器可以主动推送数据到客户端,实现实时更新。
  • 简单易用:SSE使用简单的文本格式传输数据,易于实现和理解。
  • 轻量级:相比其他实时通信技术,SSE的开销较小,不需要额外的握手和协议。

应用场景

SSE适用于需要实时数据更新的应用场景,如股票行情、即时消息、实时交通信息等。它可以与其他Web技术(如HTML5、JavaScript)结合使用,为用户提供更好的交互体验。

样例1:基于webflux下的SSE 实现ChatGPT消息推送


@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> gptFlux(){
    WebClient.Builder builder = WebClient.builder();
    return builder.baseUrl("https://chatgpt1.nextweb.fun/api/proxy/v1")
            .defaultHeader("Authorization", "Bearer " + openKey)
            .defaultHeader("Content-Type", "application/json")
            .build()
            .post()
            .uri("/chat/completions")
            .bodyValue("""
                            {
                                 "model": "gpt-3.5-turbo",
                                 "messages": [
                                    {"role": "user", "content": "在干嘛!"},
                                    {"role": "assistant","content": "作为一名AI语言模型,我在这里等待人类用户的提问和交流,帮助他们解决问题、获取信息或提供娱乐。请问有什么可以帮到您的吗?"},
                                    {"role":"user", "content":"我刚刚说了什么?"}],
                                 "temperature": 0.7
                            }
                    """)
            .retrieve()
            .bodyToFlux(String.class)
            .timeout(Duration.ofSeconds(60L));
}

样例二:基于webMVC下的SSE 实现ChatGPT消息推送


@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter gptFlux(){
    SseEmitter emitter = new SseEmitter(0L);
    WebClient.Builder builder = WebClient.builder();
    builder.baseUrl("https://chatgpt1.nextweb.fun/api/proxy/v1")
            .defaultHeader("Authorization", "Bearer " + openKey)
            .defaultHeader("Content-Type", "application/json")
            .build()
            .post()
            .uri("/chat/completions")
            .bodyValue("""
                            {
                                 "model": "gpt-3.5-turbo",
                                 "messages": [
                                    {"role": "user", "content": "在干嘛!"},
                                    {"role": "assistant","content": "作为一名AI语言模型,我在这里等待人类用户的提问和交流,帮助他们解决问题、获取信息或提供娱乐。请问有什么可以帮到您的吗?"},
                                    {"role":"user", "content":"我刚刚说了什么?"}],
                                 "temperature": 0.7
                            }
                    """)
            .retrieve()
            .bodyToFlux(String.class)
            .map(content -> {
                if (content.equals("[DONE]")) {
                    return SseEmitter.event().name("DONE").data(-1);
                }
                GptResponseDto.Choices choices = JsonUtils.readFromJsonString(content, GptResponseDto.class).getChoices().get(0);
                //由于单独传输容易丢失空格
                return SseEmitter.event().name("chat")
                    .data(Objects.requireNonNull(JsonUtils.writeToJson(choices)), MediaType.TEXT_PLAIN);
            })
            .subscribe(objectServerSentEvent -> {
                try {
                    emitter.send(objectServerSentEvent);
                } catch (IOException e) {
                    log.error("错误数据:{}", objectServerSentEvent);
                    throw ChatExceptionEnum.SSE_SEND_ERROR.getChatException();
                }
            }, error -> {
                if(!(error instanceof ChatExceptionEnum.ChatException)){
                    userProvider.addFrequency(uuid.split(":")[1], 1);
                }
                log.error("获取消息异常:{}", error.getMessage());
            try {
                emitter.send(SseEmitter.event().name("DONE").data("服务器连接异常"));
            } catch (IOException e) {
                throw ChatExceptionEnum.SSE_SEND_ERROR.getChatException();
            }
}, emitter::complete);
return emitter;
}

提示

  1. webflux 和 webmvc 两种web框架 并不能共存 如果使用了webmvc 那么基于flux的webflux接口就会失效
  2. SseEmitter这个对象 在创建是需要设置超时时间应该设置为0L 不然 如果gpt消息过长,那么浏览器会在 30秒之内自动断开sse
  3. 一定要设置sse的出口 浏览器会一直访问 sse 的接口导致消息重复

个人项目成果

  1. 在线网站:Vite App (error.cool)
  2. 开源项目:chatgpt: 搭建属于自己的gpt站点 (gitee.com)