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>