为什么ChatGPT采用SSE协议而不是WebSocket?
为什么ChatGPT采用SSE协议而不是WebSocket?
在ChatGPT官网我们可以看到,对话的方式仅仅只有一个post
请求,而没有使用IM
中使用的websocket
链接。同时我们可以看到与普通的post
请求不一样的是,返回信息Response
没有了,取而代之的是EventStream
。
若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。
那么这个EventStream
是什么东西?一通查证后,发现这个是Web API中的通过SSE
协议返回的数据。在现代Web应用开发中,实时通信技术尤为重要。Server-Sent Events(SSE)和WebSocket是两种常见的实现方式。虽然WebSocket能够提供双向通信,但在特定的场景下,SSE协议更适合一些实际需求。本文将结合代码示例深入探讨为什么 ChatGPT 采用SSE协议而不是 WebSocket。下面 SSE
和 Websocket
协议的介绍和优缺点:
Server-Sent Events(SSE)协议
1. SSE协议简介
Server-Sent Events (SSE) 是基于HTTP协议的一种单向通信协议,允许服务器通过单个HTTP连接持续向客户端发送更新。SSE最常用于向浏览器推送实时数据,例如实时通知、消息更新、股票行情等。
与WebSocket不同,SSE连接是单向的,客户端无法向服务器发送消息。服务器推送的数据通常以纯文本格式发送,并使用标准的HTTP协议传输。
2. SSE的工作原理
连接建立: 客户端通过HTTP GET请求向服务器请求数据流。服务器响应并保持连接打开,不断发送数据。服务器发送的数据使用特定的格式,包括事件名(event)、数据(data)和事件标识符(id)等字段。
数据推送: 服务器向客户端推送数据时,按照文本流的形式发送,浏览器端的JavaScript通过
EventSource
对象接收和处理这些数据。连接管理: SSE具有自动重连机制,客户端在连接断开时会自动尝试重新连接,并且可以从上次中断的地方继续接收数据。
3. SSE的优点
简单性: SSE基于标准的HTTP协议,易于实现和管理,不需要特别的握手过程,也不需要客户端和服务器之间的复杂协议切换。
自动重连和断点续传: SSE内置了自动重连机制,并支持断点续传(利用事件标识符),提高了连接的可靠性,尤其在网络不稳定的环境中表现良好。
广泛的HTTP支持: SSE能够更好地与现有的HTTP基础设施兼容,如负载均衡器、代理服务器和防火墙。
4. SSE的缺点
单向通信: SSE仅支持服务器向客户端发送数据,客户端无法主动向服务器发送消息,因此不适合需要双向通信的场景。
消息格式限制: SSE仅支持文本格式的数据推送,无法直接发送二进制数据,虽然可以通过Base64编码实现,但增加了复杂性。
WebSocket协议
1. WebSocket协议简介
WebSocket 是一种全双工通信协议,允许客户端和服务器之间建立持久的双向连接。WebSocket协议在传统的HTTP协议基础上扩展,通过升级请求将连接从HTTP切换为WebSocket,从而实现更高效的实时数据传输。
在WebSocket连接建立后,客户端和服务器可以相互发送消息,而无需再次发起HTTP请求。这种持久连接避免了传统HTTP请求中的“请求-响应”延迟,适用于需要频繁双向通信的场景。
2. WebSocket的工作原理
连接建立: WebSocket连接始于客户端发送的HTTP升级请求(Upgrade: websocket),服务器响应101状态码表示切换协议成功,随后连接切换为WebSocket协议。
消息交换: 连接建立后,客户端和服务器可以通过这个单一的连接自由地发送和接收消息。消息可以是文本或二进制数据。
连接关闭: 任一方可以随时关闭连接,连接关闭后需要重新建立才能恢复通信。
3. WebSocket的优点
低延迟: WebSocket的全双工通信能够显著降低通信延迟,适合对实时性要求较高的应用,如实时聊天、在线游戏、股票交易等。
高效的数据传输: 由于WebSocket省略了HTTP请求头等冗余数据,相比HTTP请求,WebSocket的数据传输更加高效。
4. WebSocket的缺点
复杂性: WebSocket协议的实现和管理相对复杂,特别是在需要维护大量连接的场景中,管理连接状态和处理错误需要额外的逻辑。
防火墙和代理问题: WebSocket在穿越防火墙和代理服务器时可能遇到问题,因为它不是标准的HTTP流量,需要额外的配置。
SSE与WebSocket的适用场景
在实际应用中,SSE和WebSocket有各自的适用场景:
WebSocket适用场景: WebSocket适合需要频繁的双向通信、低延迟和高吞吐量的应用场景,例如在线游戏、股票交易、协同编辑等。这些场景需要服务器和客户端之间保持持续的双向通信。
SSE适用场景: SSE则适合需要服务器单向推送数据的应用,例如实时通知、新闻更新、社交媒体信息流等。这些场景通常只需要服务器向客户端发送数据,并且对低延迟的要求没有WebSocket那么高。
为什么选择SSE协议?
简化服务器设计
SSE只允许服务器向客户端发送数据,这使得服务器的设计更加简化。在很多情况下,如聊天应用或实时通知系统,客户端主要需要接收服务器推送的信息,使用SSE可以避免不必要的双向通信带来的复杂性。
对于ChatGPT这种以文本流为主的应用,服务器主要向客户端推送生成的内容,使用SSE即可满足需求。
更好的HTTP集成
SSE基于HTTP协议之上,使用标准的HTTP协议进行传输,能够更好地集成现有的HTTP基础设施,比如代理服务器和负载均衡器。
WebSocket虽然也基于HTTP建立连接,但它并不是HTTP协议的扩展,因此在某些场景下需要额外的配置来支持WebSocket的通信。
自动重连
SSE内置了断线重连机制,能够在连接断开后自动重连,并且还可以支持重连后的断点续传功能。对于网络环境不稳定的场景,SSE提供了更好的用户体验。
低开销
对于大量客户端连接的场景,SSE的服务器开销较小。WebSocket虽然提供了更强大的功能,但其双向通信和保持连接的特性会带来额外的资源消耗。SSE的单向通信和简单的消息格式更适合大规模的客户端连接场景。
项目配置和代码示例
下面我们结合一个简单的 Spring Boot3.3 项目,展示如何使用 SSE 协议推送数据,并通过 Thymeleaf 模板引擎和JavaScript在前端实时显示数据。
运行效果:
Maven项目文件(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>chatgptsse</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chatgptsse</name>
<description>Demo chatgptsse project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Spring Boot配置(application.yml)
server:
port: 8080
spring:
thymeleaf:
cache: false
后端代码示例
应用启动类:
package com.icoderoad.chatgptsse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ChatgptsseApplication {
public static void main(String[] args) {
SpringApplication.run(ChatgptsseApplication.class, args);
}
}
ChatGptRestController 类
package com.icoderoad.chatgptsse.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class ChatGptRestController {
@GetMapping("/stream")
public SseEmitter stream() {
// 创建一个SseEmitter对象,超时时间为30秒
SseEmitter emitter = new SseEmitter(30000L);
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 10; i++) {
// 模拟服务器端的事件推送
emitter.send("消息编号: " + i, MediaType.TEXT_PLAIN);
Thread.sleep(1000); // 模拟延时
}
// 结束推送
emitter.complete();
} catch (IOException | InterruptedException e) {
// 出现异常时,发送错误信息并完成推送
emitter.completeWithError(e);
}
});
executor.shutdown();
return emitter;
}
}
视图显示类
package com.icoderoad.chatgptsse.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String index(Model model) {
return "index";
}
}
前端代码示例
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>SSE示例</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* 使用nth-child选择器为每一行设置不同的背景颜色 */
.list-group-item:nth-child(odd) {
background-color: #f8f9fa; /* 浅灰色 */
}
.list-group-item:nth-child(even) {
background-color: #e9ecef; /* 更浅的灰色 */
}
.list-group-item:nth-child(3n) {
background-color: #dee2e6; /* 另一种灰色 */
}
</style>
</head>
<body>
<div class="container">
<h1 class="mt-5">实时消息</h1>
<ul id="messages" class="list-group mt-3"></ul>
</div>
<script>
// 创建一个新的EventSource实例,指向后端的SSE流
const eventSource = new EventSource('/stream');
// 监听消息事件,当服务器推送消息时触发
eventSource.onmessage = function(event) {
const message = event.data;
const messageList = document.getElementById('messages');
// 创建一个新的li元素并添加到消息列表中
const newMessage = document.createElement('li');
newMessage.textContent = message;
newMessage.classList.add('list-group-item');
messageList.appendChild(newMessage);
};
// 处理连接关闭的情况
eventSource.onopen = function() {
console.log("连接已建立");
};
eventSource.onerror = function() {
console.log("连接出现错误或已关闭");
eventSource.close();
};
</script>
</body>
</html>
结论
通过以上的代码示例,我们可以看出SSE协议在实现实时数据推送时具有其独特的优势,特别是在服务器需要向大量客户端推送数据的场景中。与WebSocket相比,SSE的实现更加简单,开销更低,并且更好地与HTTP生态系统集成。因此,对于ChatGPT这种主要需要服务器向客户端推送数据的场景,SSE协议无疑是一个更为合适的选择。
在未来的Web应用开发中,开发者应根据实际需求选择合适的通信协议,以充分发挥协议的优势,实现高效的实时通信。