SSE(Server Sent Event)实战(2)- Spring MVC 实现

news2025/2/22 5:55:55

一、服务端实现

  1. 使用 @RestController 注解创建一个控制器类(Controller)

  2. 创建一个方法来创建一个客户端连接,它返回一个 SseEmitter,处理 GET 请求并产生(produces)文本/事件流 (text/event-stream)

  3. 创建一个新的 SseEmitter, 保存它并从方法中返回

  4. 在另一个线程中异步发送事件, 先拿到保存的 SseEmitter 并根据需要多次调用调用SseEmitter.send()方法

  5. 完成事件发送, 调用 SseEmitter.complete() 方法

  6. 要异常完成发送事件,请调用 SseEmitter.completeWithError() 方法

/*
 * xxx.com
 * Copyright (C) 2021-2024 All Rights Reserved.
 */
package com.sse.demo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author xxx
 * @version SseController.java, v 0.1 2024-07-11 10:11
 */
@Slf4j
@RestController
@RequestMapping("/sse")
public class SseController {

    private static final Map<String, SseEmitter> SSE_EMITTER_MAP = new ConcurrentHashMap<>();

    /**
     * 创建连接
     */
    @GetMapping("/create-connect")
    public SseEmitter createConnect(@RequestParam("userId") String userId) {

        try {
            // 设置超时时间,0表示不过期。默认30秒
            SseEmitter sseEmitter = new SseEmitter(0L);

            // 注册回调
            sseEmitter.onCompletion(() -> removeSseConnection(userId, "SSE连接已关闭"));
            sseEmitter.onError(throwable -> removeSseConnection(userId, "SSE连接出现错误"));
            sseEmitter.onTimeout(() -> removeSseConnection(userId, "SSE连接超时"));

            SSE_EMITTER_MAP.put(userId, sseEmitter);

            log.info("创建了用户[{}]的SSE连接", userId);
            return sseEmitter;
        } catch (Exception e) {
            log.error("创建新的SSE连接异常,当前用户:" + userId, e);
            return null;
        }
    }

    /**
     * 发送消息
     */
    @GetMapping("/send-message")
    public void sendMessage(@RequestParam("userId") String userId, @RequestParam("message") String message) {

        SseEmitter sseEmitter = SSE_EMITTER_MAP.get(userId);
        if (sseEmitter != null) {
            try {
                sseEmitter.send(SseEmitter.event()
                        .name("message")
                        .data(message)
                        .reconnectTime(5000));
                log.info("给用户[{}]发送消息成功: {}", userId, message);
            } catch (Exception e) {
                log.error("给用户[{}]发送消息失败: {}", userId, e.getMessage(), e);
                // 如果发送失败,尝试从map中移除失效的SseEmitter
                removeSseConnection(userId, "发送消息失败");
            }
        } else {
            log.info("用户[{}]的SSE连接不存在或已关闭,无法发送消息", userId);
        }
    }

    private void removeSseConnection(String userId, String reason) {
        SSE_EMITTER_MAP.computeIfPresent(userId, (key, sseEmitter) -> {
            sseEmitter.complete();
            log.info("用户[{}]的SSE连接已移除,原因:{}", userId, reason);
            return null;
        });
    }
} 

二、客户端实现

创建多个 index.html文件,放在 static 目录下,用不同的浏览器打开,实现向多个用户推送的场景。
在这里插入图片描述


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SSE Demo</title>
    <script>        document.addEventListener('DOMContentLoaded', function () {
        var userId = "1";

        // 创建一个新的EventSource对象
        var source = new EventSource('http://localhost:8080/sse/create-connect?userId=' + userId);

        // 当连接打开时触发
        source.onopen = function (event) {
            console.log('SSE连接已打开');
        };

        // 当从服务器接收到消息时触发
        source.onmessage = function (event) {
            // event.data 包含服务器发送的文本数据
            console.log('接收到消息:', event.data);
            // 在页面上显示消息
            var messagesDiv = document.getElementById('messages');
            if (messagesDiv) {
                messagesDiv.innerHTML += '<p>' + event.data + '</p>'; // 直接使用event.data
            } else {
                console.error('未找到消息容器元素');
            }
        };

        // 当发生错误时触发
        source.onerror = function (event) {
            console.error('SSE连接错误:', event);
        };
    });
    </script>
</head>
<body>
<div id="messages">
    <!-- 这里将显示接收到的消息 -->
</div>
</body>
</html>

三、启动项目

  1. 运行 Spring 项目
    在这里插入图片描述
  2. 浏览器打开 index.html文件
    在这里插入图片描述
  3. 调用发送消息接口
    curl http://localhost:8080/sse/send-message\?userId\=1\&message\=test0001
    在这里插入图片描述

打开多个连接,用 userId 就可以实现向不同的用户推送的逻辑了。

四、总结

上面已经实现了最基本的消息推送需求,但是我们还可以思考一下实际生产中,我们还需要做哪些优化?

  1. 如果我们服务设置了最大连接时间,比如 3 分钟,而服务端又长时间没有消息推送给客户端,导致长连接被关闭该怎么办?
  2. 实际生产环境,我们肯定是多个实例部署,那么怎么保证创建连接和发送消息是在同一个实例完成?如果不是一个实例,就意味着用户没有建立连接,消息肯定发送失败。

下一篇博客,再做具体优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1931849.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT小细节

QT小细节 1 QTextToSpeech1.1 cmake1.2 qmake QT6 6.7.2 1 QTextToSpeech 从下图可以看到&#xff0c;分别使用qmake或者cmake编译情况下的&#xff0c;QTextToSpeech的使用方法 QTextToSpeech官方链接&#xff0c;也可以直接在QT Creator的帮助中搜索 1.1 cmake 将上图中的…

无人机之机型区别与应用领域

一、多旋翼无人机 特点&#xff1a;多旋翼无人机依靠产生升力以平衡飞行器的重力&#xff0c;通过改变每个旋翼的转速来控制飞行姿态&#xff0c;能够悬停和垂直起降。他们具备体积小、重量轻、噪音小、隐蔽性好的特点&#xff0c;操作灵活且易于维护。 应用&#xff1a;多旋…

django踩坑(四):终端输入脚本可正常执行,而加入crontab中无任何输出

使用crontab执行python脚本时&#xff0c;有时会遇到脚本无法执行的问题。这是因为crontab在执行任务时使用的环境变量与我们在终端中使用的环境变量不同。具体来说&#xff0c;crontab使用的环境变量是非交互式(non-interactive)环境变量&#xff0c;而终端则使用交互式(inter…

补充.IDEA的使用

首先我们要了解在idea中Java工程由项目&#xff08;project&#xff09;、模块&#xff08;module&#xff09;包&#xff08;package&#xff09;、类&#xff08;class&#xff09;组成。 他们之间的关系是project包含module包含package包含class。 所以我们要按照先建一个pr…

启智畅想火车类集装箱号码识别技术,软硬件解决方案

集装箱号码识别需求&#xff1a; 实时检测车皮号、火车底盘号码、集装箱号码&#xff0c;根据火车类型分为以下三种情况&#xff1a; 1、纯车皮&#xff0c;只检测车皮号&#xff1b; 2、火车拉货箱&#xff08;半车皮&#xff09;&#xff0c;检测车皮号集装箱号码&#xff1b…

巧用通义灵码助力护网面试

前言 前几年护网还算是一个比较敏感的话题&#xff0c;但是随着近段时间的常态化开始&#xff0c;护网行动也是逐渐走进了大众的视野&#xff0c;成为了社会各界共同关注的安全盛事。本篇也是受通义灵码备战求职季活动的启发&#xff0c;结合近期要开始的护网行动&#xff0c…

监控系统怎样做?

监控类型自底向上分为资源监控、服务监控和业务监控。希望打造公司级的监控系统最好的时机是系统规划时&#xff0c;如果把监控设计往后放&#xff0c;将会面临一个巨大的难题&#xff1a;推行和现有不兼容的规范。 三种监控类型 资源监控 这个相对简单&#xff0c;随着k8s的兴…

Python 如何使用列表推导式(list comprehensions)?

列表推导式&#xff08;List Comprehensions&#xff09;是 Python 中一种简洁且强大的创建列表的方式。通过使用列表推导式&#xff0c;可以用一行代码来生成列表&#xff0c;而不是通过多行代码的循环或其他方法。 一、列表推导式的基本语法 列表推导式的基本语法如下&…

QT开发笔记:信号和槽

乱码问题&#xff1a; 出现乱码问题原因只有一个&#xff1a;就是编码方式不匹配&#xff01;&#xff01;&#xff01; 中文常见汉字4K,算上各种生僻字差不多六万字 仍然使用一个大表格&#xff0c;给每个汉字&#xff0c;分配一个整数即可。 字符集~~表示汉字的字符集&#…

基于若依的ruoyi-nbcio流程管理系统修正自定义业务表单的回写bug

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…

差异分析的结果各种热图,火山图,箱式图可视化作图教程

1. 基因表达的热图绘制 1.1 根据所有差异基因绘制基因表达的聚类热图 视频教程: https://www.bilibili.com/video/BV13m421g7wv/ 1.2 绘制top差异基因表达的聚类热图 视频教程: https://www.bilibili.com/video/BV1jZ42147KP/ 1.3 绘制感兴趣基因的聚类热图 视频教程: http…

一招教你选出独立站爆品

独立站选品其实是让很多卖家感到头大的问题&#xff0c;明明选了一个在其他平台是爆款的品&#xff0c;放到独立站上就是卖不起量&#xff0c;有自己想卖的产品却找不到好的供应商。对于这些问题&#xff0c;主要还是因为在选品时照搬电商平台的选品思路&#xff0c;没有认清自…

2024年中级消防设施操作员(考前冲刺)证考试题库及中级消防设施操作员(考前冲刺)试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年中级消防设施操作员&#xff08;考前冲刺&#xff09;证考试题库及中级消防设施操作员&#xff08;考前冲刺&#xff09;试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作…

5个超牛的Java开源OA项目(强烈推荐)

1. O2OA ——开源地址&#xff1a;https://gitee.com/o2oa/O2OA 概述&#xff1a; O2OA 是一款真正全代码&#xff08;包含服务器、安卓以及IOS客户端&#xff09;开源的企业应用定制化开发平台&#xff0c;适用于企业OA、协同办公类信息化系统的建设和开发。技术&#xff1a;…

HarmonyOS 开发者联盟高级认证最新题库

本篇文章包含 Next 版本更新后高级认证题库中95%的题目。 答案正确率 50-60%&#xff0c;答案仅做参考。 请在考试前重点看一遍题目&#xff0c;勿要盲目抄答案。 欢迎在评论留言正确答案和未整理的题目。 1、下面关于方舟字节码格式PREF_IMM16_v8_v8描述正确的是 16位前缀操作…

【wyTest自动化测试】快照校验模式的创新实践

本文将以遥遥领先的【华为商城】web端自动化为例&#xff0c;进行测试脚本开发的讲解。仅用于教学指导使用&#xff0c;如有侵权&#xff0c;请联系我删除。 一、测试脚本结构 按照教程惯例&#xff0c;开篇先展示一下测试脚本的标准结构模版: case_xx特性_xx001.py class Cas…

挖矿宝藏之硬盘分区

目录 一、硬盘分区的相关知识 二、主分区、活动分区、扩展分区、逻辑盘和盘符 三、硬盘分区原因 1.减少硬盘空间的浪费 2.便于文件的分类管理 3.有利于病毒的防治 四、硬盘分区的原则 1.方便性 2.实用性 3.安全性 五、利用Diskpart进行分区 1.命令行工具Diskpart …

信创学习笔记(四),信创之数据库DB思维导图

创作不易 只因热爱!! 热衷分享&#xff0c;一起成长! “你的鼓励就是我努力付出的动力” 一. 信创学习回顾 1.信创内容 信创内容思维导图 2.信创之CPU芯片架构 信创之CPU芯片架构思维导图 3.信创之操作系统OS 信创之操作系统OS思维导图 二. 信创之国产数据库DB思维导图 …

# Redis 入门到精通(六)-- redis 事务

Redis 入门到精通&#xff08;六&#xff09;-- redis 事务 一、redis 事务–redis事务简介 1、Redis 执行指令过程中&#xff0c;多条连续执行的指令被干扰&#xff0c;打断&#xff0c;插队&#xff0c;就会造成结果偏差。 2、什么是 redis 事务&#xff1f; redis 事务&…

关于Ubuntu22.04中的Command ‘vim‘ not found, but can be installed with:

前言 在Ubuntu终端编辑文本内容时需要利用vim&#xff0c;但新安装的虚拟机中并未配置vim&#xff0c;本文记录了vim的安装过程。 打开终端后&#xff0c;在home目录中输入 vim test.txt但提示报错&#xff0c;提示我们没有找到vim&#xff0c;需要通过以下命令进行安装&…