SpringBoot-模拟SSE对话交互
后端使用SSE进行会话,前端使用Html模拟大模型的问答交互->【前端】+【后端】
1-学习目的
本项目代码仓库:https://gitee.com/enzoism/springboot_sse
1-核心知识点
- 1)什么是SSE协议->客户端发起一次请求,服务器保持该连接打开,并在有新数据时主动向客户端推送
- 2)后端-开发SSE控制器
- 3)前端-如何与SSE协议交互
2-附带知识点
- 1)什么是请求跨域-后端如何解决跨域问题
2-动手实践
1-什么是SSE协议
SSE(Server-Sent Events)协议是一种用于在 Web 浏览器和服务器之间实现服务器向客户端单向实时通信的技术。下面将从多个方面详细介绍 SSE 协议:
1-基本概念
SSE 允许服务器在建立连接后,持续向客户端发送更新信息。与传统的请求 - 响应模式不同,在 SSE 中,客户端发起一次请求,服务器保持该连接打开,并在有新数据时主动向客户端推送。
2-工作原理
- 建立连接:客户端通过创建一个
EventSource
对象来向服务器发起 HTTP 请求,请求的响应头中包含Content-Type: text/event-stream
,表明这是一个 SSE 连接。 - 服务器推送数据:服务器保持连接打开,并在有新数据时,以特定的格式将数据发送给客户端。数据以文本流的形式传输,每条消息以两个换行符
\n\n
分隔。 - 客户端接收数据:客户端的
EventSource
对象会监听服务器发送的消息,并在接收到消息时触发相应的事件。
3-消息格式
SSE 消息由多个字段组成,每个字段以键值对的形式表示,字段之间用换行符分隔。常见的字段有:
data
:表示消息的实际内容。如果消息内容较长,可以分成多行,每行以data:
开头。event
:可选字段,用于指定事件类型。如果指定了事件类型,客户端可以针对不同的事件类型进行不同的处理。id
:可选字段,用于为消息指定一个唯一的标识符。客户端可以使用这个标识符来实现消息的断点续传。retry
:可选字段,用于指定客户端在连接中断后重试连接的时间间隔(以毫秒为单位)。
4-优点和缺点
- 优点
- 简单易用:客户端只需要创建一个
EventSource
对象,服务器端只需要按照特定的格式发送数据即可。 - 自动重连:当连接中断时,客户端会自动尝试重新连接,并且可以使用
id
字段实现消息的断点续传。 - 浏览器原生支持:现代浏览器都原生支持
EventSource
对象,无需额外的库或插件。
- 简单易用:客户端只需要创建一个
- 缺点
- 单向通信:SSE 只支持服务器向客户端单向通信,客户端不能向服务器发送消息。
- 兼容性问题:虽然现代浏览器都支持 SSE,但在一些旧版本的浏览器中可能不支持。
- 连接数量限制:浏览器对每个域名的 SSE 连接数量有一定的限制,通常为 6 个。
5-应用场景
- 实时新闻推送:服务器可以实时向客户端推送最新的新闻消息。
- 股票行情更新:服务器可以实时向客户端推送股票价格的变化。
- 在线聊天系统:服务器可以实时向客户端推送新的聊天消息。
2-后端-开发SSE控制器-Java版本
1. 编写SSE控制器
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@RestController
public class ChatController {private final DeepSeekService deepSeekService;public ChatController(DeepSeekService deepSeekService) {this.deepSeekService = deepSeekService;}@GetMapping("/chat")public SseEmitter chat(@RequestParam String message) {SseEmitter emitter = new SseEmitter();ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.submit(() -> {try {// 调用DeepSeek的chat模型deepSeekService.streamChatResponse(message, emitter);} catch (Exception e) {emitter.completeWithError(e);} finally {executorService.shutdown();}});return emitter;}
}
2. 模拟DeepSeek服务
import org.springframework.stereotype.Service;@Service
public class DeepSeekService {public void streamChatResponse(String message, SseEmitter emitter) {// 调用DeepSeek的API并流式输出try {// 模拟流式输出String[] responses = {"Hello! ", "How can I help you? ", "I'm an AI assistant."};for (String response : responses) {emitter.send(SseEmitter.event().data(response));Thread.sleep(1000); // 模拟延迟}} catch (Exception e) {emitter.completeWithError(e);}}
}
3-前端:简单的HTML/JavaScript界面
1. 创建一个前端页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DeepSeek Chat</title><style>body { font-family: Arial, sans-serif; margin: 20px; }#messageInput { width: 300px; padding: 5px; }#response { width: 400px; height: 200px; border: 1px solid #ccc; padding: 10px; }</style>
</head>
<body><h1>DeepSeek Chat</h1><input type="text" id="messageInput" placeholder="Enter your message"><button onclick="sendMessage()">Send</button><div id="response"></div><script>let eventSource;function sendMessage() {const messageInput = document.getElementById('messageInput');const responseDiv = document.getElementById('response');const message = messageInput.value;if (message.trim() === '') return;responseDiv.innerHTML = '';// 创建一个新的EventSourceeventSource = new EventSource(`/chat?message=${encodeURIComponent(message)}`);eventSource.onmessage = function (event) {responseDiv.innerHTML += event.data;};eventSource.onerror = function (error) {console.error('EventSource error:', error);eventSource.close();};}</script>
</body>
</html>
3-其他版本SSE-NodeJs
1-服务器端代码示例(Node.js + Express)
const express = require('express');
const app = express();app.get('/sse', (req, res) => {res.setHeader('Content-Type', 'text/event-stream');res.setHeader('Cache-Control', 'no-cache');res.setHeader('Connection', 'keep-alive');res.flushHeaders();let counter = 0;const intervalId = setInterval(() => {res.write(`data: Message ${counter}\n\n`);counter++;}, 1000);req.on('close', () => {clearInterval(intervalId);});
});const port = 3000;
app.listen(port, () => {console.log(`Server running on port ${port}`);
});
2-客户端代码示例(JavaScript)
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
</head><body><div id="messages"></div><script>const eventSource = new EventSource('/sse');eventSource.onmessage = function (event) {const messagesDiv = document.getElementById('messages');const newMessage = document.createElement('p');newMessage.textContent = event.data;messagesDiv.appendChild(newMessage);};eventSource.onerror = function (error) {console.error('EventSource failed:', error);};</script>
</body></html>