当前位置: 首页 > news >正文

SpringBoot-模拟SSE对话交互

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-工作原理
  1. 建立连接:客户端通过创建一个 EventSource 对象来向服务器发起 HTTP 请求,请求的响应头中包含 Content-Type: text/event-stream,表明这是一个 SSE 连接。
  2. 服务器推送数据:服务器保持连接打开,并在有新数据时,以特定的格式将数据发送给客户端。数据以文本流的形式传输,每条消息以两个换行符 \n\n 分隔。
  3. 客户端接收数据:客户端的 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 = '';

            // 创建一个新的EventSource
            eventSource = 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>

相关文章:

  • 全局异常处理器为什么不能处理过滤器异常,有那些解决方案
  • vxe-table开启表尾和el-collapse-transition不兼容,动画卡顿
  • 【Qt QML】Loader动态加载组件(续)
  • 《安富莱嵌入式周报》第351期:DIY半导体制造,工业设备抗干扰提升方法,NASA软件开发规范,小型LCD在线UI编辑器,开源USB PD电源,开源锂电池管理
  • Web后端开发-总结
  • 多线程-JUC源码
  • 利用opencv_python(pdf2image、poppler)将pdf每页转为图片
  • 2025年夸克网盘自动签到程序,验证通过!
  • android App主题颜色动态更换
  • IO进程线程3
  • 【AD】5-3 PCB板框的内缩与外扩
  • OpenBMC:BmcWeb app获取socket
  • 嵌入式 ARM Linux 系统构成(1):Bootloader层
  • oracle通过dmp导入数据
  • PHP之运算符
  • python-串口助手(OV7670图传)
  • 文献分享: ConstBERT固定数目向量编码文档
  • java 查找连个 集合的交集部分数据
  • 生命周期总结(uni-app、vue2、vue3生命周期讲解)
  • Linux总结
  • 陕西省市监局通报5批次不合格食品,涉添加剂超标、微生物污染等问题
  • 国寿资产获批参与第三批保险资金长期投资改革试点
  • 媒体评欧阳娜娜遭民进党当局威胁:艺人表达国家认同是民族大义
  • “AD365特应性皮炎疾病教育项目”启动,助力提升认知与规范诊疗
  • 博柏利上财年营收下降17%,计划裁员1700人助推股价涨超18%
  • 微软将在全球裁员6000人,目标之一为减少管理层