SpringBoot使用SSE进行实时通知前端

news2025/1/12 12:06:37

SpringBoot使用SSE进行实时通知前端

  • 说明
  • maven依赖
    • SSE工具类代码
  • Controller测试代码
  • 测试结果如下:
  • 注意
    • 将超时时间由原来的0改为默认的30秒,会报错。
    • 将springboot降为低版本如1.4.2.RELEASE。

说明

项目有个需求是要实时通知前端,告诉前端这个任务加载好了。然后想了2个方案,一种是用websocket进行长连接,一种是使用SSE(Sever Send Event),是HTTP协议中的一种,Content-Type为text/event-stream,能够保持长连接。
websocket是前端既能向后端发送消息,后端也能向前端发送消息。
SSE是只能后端向前端发送消息。
因为只需要后端通知,所以我这里选择了使用SSE实现。
这里先做个笔记,怕以后忘记怎么使用。

maven依赖

<?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>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.project</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>test</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--web依赖,内嵌入tomcat,SSE依赖于该jar包,只要有该依赖就能使用SSE-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok依赖,用来对象省略写set、get方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

SSE工具类代码

package com.etone.project.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

@Slf4j
public class SseEmitterServer {

    /**
     * 当前连接数
     */
    private static AtomicInteger count = new AtomicInteger(0);

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

    public static SseEmitter connect(String userId){
        //设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
        SseEmitter sseemitter = new SseEmitter(0L);
        //注册回调
        sseemitter.onCompletion(completionCallBack(userId));
        //这个onError在springbooot低版本没有这个方法,公司springboot1.4.2版本,没有这个方法,可以进行注释。
        sseemitter.onError(errorCallBack(userId));
        sseemitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId,sseemitter);
        //数量+1
        count.getAndIncrement();
        log.info("create new sse connect ,current user:{}",userId);
        return sseemitter;
    }

    /**
     * 给指定用户发消息
     */
    public static void sendMessage(String userId, String message){
        if(sseEmitterMap.containsKey(userId)){
            try{
                sseEmitterMap.get(userId).send(message);
            }catch (IOException e){
                log.error("user id:{}, send message error:{}",userId,e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * 想多人发送消息,组播
     */
    public static void groupSendMessage(String groupId, String message){
        if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
            sseEmitterMap.forEach((k,v) -> {
                try{
                    if(k.startsWith(groupId)){
                        v.send(message, MediaType.APPLICATION_JSON);
                    }
                }catch (IOException e){
                    log.error("user id:{}, send message error:{}",groupId,message);
                    removeUser(k);
                }
            });
        }
    }

    public static void batchSendMessage(String message) {
        sseEmitterMap.forEach((k,v)->{
            try{
                v.send(message,MediaType.APPLICATION_JSON);
            }catch (IOException e){
                log.error("user id:{}, send message error:{}",k,e.getMessage());
                removeUser(k);
            }
        });
    }

    /**
     * 群发消息
     */
    public static void batchSendMessage(String message, Set<String> userIds){
        userIds.forEach(userid->sendMessage(userid,message));
    }

    //移除用户
    public static void removeUser(String userid){
        sseEmitterMap.remove(userid);
        //数量-1
        count.getAndDecrement();
        log.info("remove user id:{}",userid);
    }

    public static List<String> getIds(){
        return new ArrayList<>(sseEmitterMap.keySet());
    }

    public static int getUserCount(){
        return count.intValue();
    }

    private static Runnable completionCallBack(String userId) {
        return () -> {
            log.info("结束连接,{}",userId);
            removeUser(userId);
        };
    }
    private static Runnable timeoutCallBack(String userId){
        return ()->{
            log.info("连接超时,{}",userId);
            removeUser(userId);
        };
    }
    private static Consumer<Throwable> errorCallBack(String userId){
        return throwable -> {
            log.error("连接异常,{}",userId);
            removeUser(userId);
        };
    }
}

Controller测试代码

package com.project.test.controller;

import com.hjl.test.util.SseEmitterServer;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping(value = "/test")
public class TestController {

    //sse连接接口
    @GetMapping (value = "/sse/connect/{id}")
    public SseEmitter connect(@PathVariable String id){
        return SseEmitterServer.connect(id);
    }

    //sse向指定用户发送消息接口
    @GetMapping (value = "/sse/send/{id}")
    public Map<String,Object> send(@PathVariable String id,@RequestParam(value = "message", required = false) String message){
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.sendMessage(id,message);
        returnMap.put("message","向id为"+id+"的用户发送:"+message+"成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse向所有已连接用户发送消息接口
    @GetMapping (value = "/sse/batchSend")
    public Map<String,Object> batchSend(@RequestParam(value = "message", required = false) String message){
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.batchSendMessage(message);
        returnMap.put("message",message+"消息发送成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse关闭接口
    @GetMapping (value = "/sse/close/{id}")
    public Map<String,Object> close(@PathVariable String id){
        Map<String,Object> returnMap = new HashMap<>();
        //移除id
        SseEmitterServer.removeUser(id);
        System.out.println("当前连接用户id:"+SseEmitterServer.getIds());
        returnMap.put("message","连接关闭成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

}

测试结果如下:

这里测试SSE连接,就像正常接口那样请求就行。
本地调用接口/sse/connect/1如下:
这里我连接2个用户,用来模拟向指定用户id发送信息和批量向已连接的用户发送消。
在这里插入图片描述
在这里插入图片描述
后端服务打印如下:
在这里插入图片描述

本地调用接口/sse/send/1如下:
在这里插入图片描述
用户1的结果如下,发现它收到了消息:
在这里插入图片描述
用户2没有收到结果,如下:
在这里插入图片描述

本地调用接口/sse/batchSend如下:
批量向所有已经连接的用户发送消息。
在这里插入图片描述
用户1结果如下,发现接收到了消息:
在这里插入图片描述
用户2结果如下,发现也接收到了消息:
在这里插入图片描述
测试结果都符合预期。
点击postman的close按钮,关闭连接:
在这里插入图片描述
在这里插入图片描述
发现前端连接虽然关闭了,但是后端实际还在连接中,根本没有移除用户的提示:
在这里插入图片描述
所以这里还需要自己手动写关闭接口测试。
本地调用接口/sse/close/1如下:
在这里插入图片描述
可以看到把用户id为1的给移除了,只剩用户2还在连接中。
在这里插入图片描述
这里所有测试完成,结果符合预期。

注意

将超时时间由原来的0改为默认的30秒,会报错。

在这里插入图片描述
测试结果如下:
在这里插入图片描述
在这里插入图片描述
这里直接出现了一个异常:org.springframework.web.context.request.async.AsyncRequestTimeoutException
甚至连接都断开了。

将springboot降为低版本如1.4.2.RELEASE。

使用postman进行测试的时候,发现它不是一直在请求中:如下:
将Springboot降为1.4.2.RELEASE
在这里插入图片描述

springboot的1.4.2.RELEASE版本没有onError方法,需要注释掉。在这里插入图片描述
postman测试如下:
低版本测试的时候发现它有一个这个连接可以直接看到,而使用springboot版本2.x版本就发现它一直处于发送请求的状态,什么时候后端向前端发送了消息,它就显示这个。
springboot的1.4.2.RELEASE版本结果:
在这里插入图片描述
springboot的2.7.3版本结果:
在这里插入图片描述

这里先将这种情况先记录下来先,等后面有时间再研究。怎么高版本就不能向低版本那样返回这个连接信息呢?所以SpringBoot高版本使用SSE连接的时候一直处于Sending request这种情况,这种情况是正常的吗?有没有大佬告知下,谢谢。

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

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

相关文章

数字孪生:让ECU在数字环境中“栩栩如生”

很多汽车广告中都会展示这样一个画面&#xff1a;在快要猛烈撞击障碍物之时&#xff0c;汽车所配备的防撞预警功能在最后关头实现停驶&#xff0c;守护车内老少一家人的生命安全。 近年来&#xff0c;汽车安全性能成为众多厂商广告中的主打&#xff0c;侧面印证了安全在当下消…

【传感器】最小二乘法实现磁力计椭球校准

总体思路 磁力计的数据在实际中是椭球的形状&#xff0c;在此之前使用了球体拟合进行校准&#xff0c;也就是简化为正球体的模型&#xff0c;得出的结果比较差&#xff0c;航向计算不准&#xff0c;还是需要用椭球的模型来估计偏移量&#xff0c;先使用标准的椭球方程&#xf…

微信小程序相关问题整理(一)

文章目录 1、引入 vant-weapp UI2、配置小程序 tabBar3、小程序登录授权流程4、小程序支付&#xff08;微信支付&#xff09;5、小程序分享6、小程序打开内置地图&#xff08;腾讯地图&#xff09;7、小程序打开app8、小程序打开其他小程序9、小程序获取当前地理位置额外&#…

项目上线出Bug:我踩过的4个大坑及事后反思

最近参与的拥有7大模块的系统项目&#xff0c;从去年11月开始开发&#xff0c;共5个月左右。 该项目是用JavaNode.js开发&#xff0c;开发人员含外包将近10位&#xff0c;测试人员A从头跟到尾&#xff0c;其他测试人员都是紧急时刻从其他项目临时调来&#xff0c;包含兄弟部门…

【LeetCode】HOT 100(2)

题单介绍&#xff1a; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合初识算法与数据结构的新手和想要在短时间内高效提升的人&#xff0c;熟练掌握这 100 道题&#xff0c;你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

精通编辑和校对:避免常见错误的实用方法

编辑和校对是提高写作质量的关键环节。为了精通编辑和校对&#xff0c;我们需要了解常见的错误并学会避免它们。 以下是一些实用方法&#xff0c;帮助你在编辑和校对过程中避免常见错误。 1.充分理解文本内容和目的 在开始编辑和校对之前&#xff0c;确保充分理解文本的内容和…

今天教会你如何在Chrome中正确使用ChatGPT!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

笔记本电脑无法正常启动怎么办?

随着计算机技术的发展&#xff0c;笔记本电脑在性能上已经不比台式机落后&#xff0c;并且因其便于携带的优势&#xff0c;受到广大用户的喜爱。那么&#xff0c;在笔记本电脑无法正常启动时&#xff0c;我们该怎么办呢&#xff1f; 笔记本电脑无法正常启动的原因 笔记本电脑无…

这就是00后吗,工作没两年,跳槽到我们公司起薪20K都快接近我了

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&…

Vue3-02-响应性 reactive

1. setup setup 是一个组件选项&#xff0c;组合式 API 就定义在 setup 中&#xff0c;包括 data、methods、computed 和 watch 等&#xff0c;都定义在 setup 中。 setup 实际上是一个生命周期钩子函数&#xff0c;执行时间点相当于 Vue2 中 beforeCreate 和 created 的结合…

springcloud-alibaba (03)Sentinel规则-笔记

sentinel Sentinel提供的各种规则的简单说明01&#xff0c;流量控制一&#xff0c;流控规则&#xff08;阈值类型&#xff1a;QPS&#xff09;二&#xff0c;流控规则&#xff08;阈值类型&#xff1a;并发线程数&#xff09;三&#xff0c;高级选项 02&#xff0c;熔断降级一&…

day45_项目

SQL /* 在分页查询中,有一些常见变量名 pageNo 当前页(页码),默认是1 pageSize 每页展示数据的条数,需求给定 pageCount 共多少页pageCounttotal%pageSize0?(total/pageSize):(total/pageSize)1; total 共多少条数据,select count(id)计算出 */ -- 假设,每页展示4条 …

LEAP软件操作基础/安装与注册/基本原理和数据结构

本次内容突出与实例结合&#xff0c;紧密结合国家能源统计制度及《省级温室气体排放编制指南》&#xff0c;深入浅出地介绍针对不同级别研究对象时如何根据数据结构、可获取性、研究目的&#xff0c;构建合适的能源生产、转换、消费、温室气体排放&#xff08;以碳排放为主&…

电力系统直流潮流计算研究【IEEE9节点】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

JavaEE初阶学习:网络原理

1.应用层 应用层和代码直接相关的一层 决定了数据要传输什么,拿到数据之后如何使用 约定应用层数据报,数据格式,就是在自定义协议~~ 如何约定? 1.确定要传输那些信息,(根据需求走的) 外卖程序,有一个核心的功能,加载商家列表 请求 用户ID 用户的位置(经纬度) 响应 …

使用Python脚本和简单的图形用户界面轻松切割MP3文件

应用场景&#xff1a; 需要将大型MP3文件切割成较小的部分以便上传或发送。需要从MP3文件中提取特定的音频片段&#xff0c;以便用于其他目的。需要快速制作铃声或音乐片段&#xff0c;以用于手机等设备。 源代码&#xff1a; import subprocess import wxclass MyFrame(wx.…

常微分方程ODE和Neural Ordinary Differential Equations

微分方程&#xff08;英語&#xff1a;Differential equation&#xff0c;DE&#xff09;是一種數學方程&#xff0c;用來描述某一類函数與其导数之间的关系。微分方程的解是一個符合方程的函數。而在初等数学的代数方程裡&#xff0c;其解是常数值。 常微分方程&#xff08;英…

蓝桥杯青少组python:第十二届国赛

选择题 1、设s"Hi LanQiao"&#xff0c;运行一下哪个选项代码可以输出"LanQiao"子串&#xff08;&#xff09; A、print(S[-7:]) B、print(s[-6:-1]) C、print(s[-7:0]) D、print(s[-7:0]) 2、已知a2021.0529&#xff0c;运行一下代码选项可以输出2021.0…

c# vs2013 制作水晶报表并导出为 pdf

一两年前接触的&#xff0c;当时没有记录&#xff0c;现在把他写下来。 vs2013制作水晶报表&#xff0c;应该要用到插件安装&#xff0c;可以自行搜索安装插件。 在Views/RPT文件夹下创建.rpt后缀的水晶报表文件&#xff08;这里的文件夹位置根据自身随意更改&#xff09;。 在…

JavaScript数组去重的常见方法 Set filter indexOf

JavaScript实现对象深拷贝的方法&#xff08;5种&#xff09; 知识回调&#xff08;不懂就看这儿&#xff01;&#xff09;场景复现实现数组去重的五种方法1.Set()Array.from()2.filter() indexOf()3.for 嵌套 for&#xff0c;splice 去重4.利用Map()5.利用includes 实际开发问…