Springboot整合SSE实现实时消息推送
SSE详细介绍传送门:SSE实时消息推送
简单描述一下SSE推送在实际项目中应用的常见场景
1,项目页面中有消息通知板块,当信息有变化时,只有手动刷新页面,才会看到最新的数据,这里可以采用SSE技术
实时推送最新消息
.
2,大屏数据,这种场景是可以用SSE进行推送,但是需要注意的是SSE是单向的服务端向前端推数据,一般要求的是大屏基本没有查询框条件这种,比较合适。
注意点:如果对于实时数据要求很高并且连接要求做到安全稳定,这里推荐用WebSocket
,一般来说对于数据量小,并发连接不是很高要求的情况下,SSE足够
,用而且SSE的配置对于前后端都比较简单,但是WebSocket的配置对于后端来说需要花费比较多的时间去完善,而且WebSocket是比较消耗服务器资源和网络带宽资源的,另外一个,如果项目中运维配置了代理服务器的话,可能代理服务器也要配置一些支持WebSocket的属性,
总体来说WebSocket配置的位置比较多,容易出现各种坑bug,这里注意一下即可。
话不多说,总结一下Springboot整合SSE需要的步骤如下:
1,编写SSE的服务类:主要包括建立连接、关闭连接、异常连接、心跳检测、推送消息等
.
2,controller层写入SSE连接和关闭接口
.
3,在所需要的业务模块中直接调用SSE服务类中推送消息功能即可
SSE步骤简单,无需导入maven依赖,踩坑bug少,主要是SSE内部支持断线重连,爽爽爽
1,SSE服务类
package com.bosera.salesioc.home.sse;
import com.alibaba.fastjson.JSONObject;
import com.bosera.salesioc.domain.home.vo.MessageVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;@Slf4j
@Component
public class SseEmitterServer{private static final ConcurrentHashMap<String, Map<String,SseEmitter>> sseEmitterPool = new ConcurrentHashMap<>();private static final ConcurrentHashMap<String, Timer> headerPool = new ConcurrentHashMap<>();public static ConcurrentHashMap<String, Map<String, SseEmitter>> getSseEmitterPool(){return sseEmitterPool;}/*** 建立连接*/public SseEmitter connect(String userCode, String userId){log.info("******************开始建立连接*****************");//设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常SseEmitter sseemitter = new SseEmitter(0L);//注册回调sseemitter.onCompletion(completionCallBack(userCode,userId));sseemitter.onError(errorCallBack(userCode,userId));sseemitter.onTimeout(timeoutCallBack(userCode,userId));sseEmitterPool.computeIfAbsent(userCode, k -> new ConcurrentHashMap<>()).put(userId, sseemitter);// 开启心跳活跃startHeartbeat(sseemitter,userId);return sseemitter;}/*** 关闭当前连接*/public void complete(String userCode, String userId){Map<String, SseEmitter> map = sseEmitterPool.get(userCode);if (map != null)map.get(userId).complete();}/*** 关闭所有连接*/public void completeAll(){if(!sseEmitterPool.isEmpty()){for (Map.Entry<String, Map<String, SseEmitter>> entry : sseEmitterPool.entrySet()) {Map<String, SseEmitter> userIdMap = entry.getValue();if(!userIdMap.isEmpty()){for (Map.Entry<String, SseEmitter> userIdEntry : userIdMap.entrySet()) {userIdEntry.getValue().complete();}}}sseEmitterPool.clear();}}private Runnable completionCallBack(String userCode, String userId) {return () -> {removeUser(userCode,userId);log.info("{}结束连接:{}",userCode,userId);};}private Runnable timeoutCallBack(String userCode, String userId){return ()->{removeUser(userCode,userId);log.error("{}连接超時:{}",userCode,userId);};}private Consumer<Throwable> errorCallBack(String userCode, String userId){return throwable -> {log.error("{}连接异常:{}",userCode,userId);stopHeartbeat(userId);};}/*** 推送消息*/public void sendMessage(String userCode, MessageVO message){Map<String, SseEmitter> map = sseEmitterPool.get(userCode);if (map != null) {for (Map.Entry<String, SseEmitter> entry : map.entrySet()) {try {// 发送事件entry.getValue().send(JSONObject.toJSONString(message));}catch (Exception e){log.error("{}连接信息:{}, 错误消息:{}",userCode,entry.getKey(),e.getMessage());}}}}private void removeUser(String userCode, String userId){try {Map<String, SseEmitter> map = sseEmitterPool.get(userCode);if (map != null) {map.remove(userId);// 如果该用户的所有会话都已关闭,则移除整个映射if (map.isEmpty())sseEmitterPool.remove(userCode);}// 关闭心跳stopHeartbeat(userId);}catch (Exception e){log.error("关闭连接异常{}",e.getMessage());}}/*** 开启心跳*/public void startHeartbeat(SseEmitter sseemitter, String userId) {Timer heartbeatTimer = new Timer();headerPool.put(userId,heartbeatTimer);heartbeatTimer.schedule(new TimerTask() {@Overridepublic void run() {if (Objects.nonNull(headerPool.get(userId))) {// 发送心跳:保持长连接try {sseemitter.send("connect active");} catch (Exception e) {log.error("connect active error");}}}}, 25000, 25000);}/*** 关闭心跳* @param userId*/public void stopHeartbeat(String userId) {Timer timer = headerPool.get(userId);if (timer!= null)timer.cancel();headerPool.remove(userId);}
}
推送的消息可以统一定义一个类来封装信息
2,消息推送响应体
/*** @Author xiaozq* @Date 2024/2/21* @Description: 消息推送响应体*/
public class MessageVO<T> {// 主题:不同位置推送的内容不同private String topic;// 推送消息private T data;public void setTopic(String topic) {this.topic = topic;}public void setData(T data) {this.data = data;}public String getTopic() {return topic;}public T getData() {return data;}
}
3,controller层编写连接和关闭接口
@RestController
@RequestMapping("/sse")
@Slf4j
public class SSEController{@Autowiredprivate SseEmitterServer sseEmitterServer;/*** 用于创建连接*/@GetMapping(value = "/connect/{userCode}/{userId}",produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter connect(@PathVariable("userCode") String userCode, @PathVariable("userId") String userId){return sseEmitterServer.connect(userCode, userId);}/*** 关闭连接*/@GetMapping(value = "/close/{userCode}/{userId}")public void close(@PathVariable("userCode") String userCode,@PathVariable("userId") String userId ) {sseEmitterServer.complete(userCode, userId);}}
4,业务中实际应用:推送消息
@Autowired
SseInfoService sseInfoService;
private void handlerMessageInform() {ConcurrentHashMap<String, Map<String, SseEmitter>> sessionPool = SseEmitterServer.getSseEmitterPool();for (Map.Entry<String, Map<String, SseEmitter>> entry : sessionPool.entrySet()) {// 封装消息MessageVO<List<MessageNotificationVO>> messageVO = new MessageVO();messageVO.setTopic(TopicTypeEnum.MESSAGE_INFORM.getTopic());messageVO.setData(messageService.getMessageList(request));// 推送消息sseEmitterServer.sendMessage(entry.getKey(), messageVO);}}
在实践过程中存在的问题:
1,报错504 gateway timeout:这里主要是原项目中配置了响应超时时间,不支持长连接,这里的做法是心跳活跃,保证连接不会被掐断,可以写一个定时任务,每天晚上定时去关闭所有连接,第二天用新的连接,这样可以尽量保证内存的连接数不会过多占用内存,因为夜深人静的时候谁还会打开web项目工作啊,哈哈太卷了吧,所以把时间定在晚上最好。
.
如果项目是集群模式的话,上述代码就得改造了,建议是把消息推送这块单独抽出一个微服务模块来,这样子保证所有的连接统一走单独的一个服务,因为SSE不是双向的,既然是单项连接,与后端集群下的其中一个服务建立连接产生的IO流这是只属于当前服务的本地IO,关闭IO只能连接对应的这台服务去关闭,否则关闭失效。总之,考虑的点还有很多,一般情况下,SSE够用啦
总体来说,应用是比较简单的,涉及到消息实时推送相关的业务,可以尝试SSE
相关文章:
Springboot整合SSE实现实时消息推送
SSE详细介绍传送门:SSE实时消息推送 简单描述一下SSE推送在实际项目中应用的常见场景 1,项目页面中有消息通知板块,当信息有变化时,只有手动刷新页面,才会看到最新的数据,这里可以采用SSE技术实时推送最新…...
在pytorch中利用GPU训练神经网络时代码的执行顺序并提高训练效率
在pytorch中利用GPU训练神经网络时代码的执行顺序并提高训练效率 在 PyTorch 中,大多数操作在 GPU 上默认是异步执行的,但这并不意味着它们是并行执行的。要理解代码是同步还是异步执行,以及是串行还是并行执行,我们需要考虑几个…...

vue3学习
距离vue2学习已经一年度了,现在开始vue3的学习。 一、webpack (1)创建列表隔行变色项目及webpack使用: 新建项目空白目录,并运行npm init -y命令,初始化包管理配置文件package.json; 新建sr…...

毫秒生成的时间戳如何转化成东八区具体时间
假设现在有一个时间是1709101071419L 后端代码实现 Java代码(东八区时间) 在Java代码中,我们将时区从UTC调整为东八区(UTC8): import java.time.Instant; import java.time.ZoneId; import java.time.Z…...

02. Nginx入门-Nginx安装
Nginx安装 yum安装 编辑yum环境 cat > /etc/yum.repos.d/nginx.repo << EOF [nginx-stable] namenginx stable repo baseurlhttp://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck1 enabled1 gpgkeyhttps://nginx.org/keys/nginx_signing.key module_…...
leetcode73. 矩阵置零
链接见:https://leetcode.cn/problems/set-matrix-zeroes/description/ 题目描述 给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 AC代码 class Solution { public:void setZeroes(vec…...

【中间件】RabbitMQ入门
📝个人主页:五敷有你 🔥系列专栏:中间件 ⛺️稳中求进,晒太阳 MQ的优劣: 优势 应用解耦:提升了系统容错性和可维护性异步提速:提升用户体验和系统吞吐量消峰填谷࿱…...

rtt的io设备框架面向对象学习-电阻屏LCD设备
目录 1.8080通信的电阻屏LCD设备1.1 构造流程1.2 使用2.i2c和spi通信的电阻屏LCD 电阻屏LCD通信接口有支持I2c、SPI和8080通信接口的。 1.8080通信的电阻屏LCD设备 lcd这块不像其他设备类,rtt没有实现的设备驱动框架层,那么是在驱动层直接实现的。 以…...

商城免费搭建之java商城 java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景
1. 涉及平台 平台管理、商家端(PC端、手机端)、买家平台(H5/公众号、小程序、APP端(IOS/Android)、微服务平台(业务服务) 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…...
蓝桥杯刷题--python-16
562. 壁画 - AcWing题库 Tint(input()) j1 while(j<T): N int(input()) ainput() s [0]*(N1) # 求前戳和 for i in range(1, N 1): s[i] int(a[i-1]) s[i - 1] # 枚举 # 区间 max_ float(-inf) k (N 2 - 1) // 2 for i in …...
闰年计算中的计算机Bug
不知道你有没有看过凯瑟琳泽塔琼斯主演的《偷天陷阱》,里面主题思想是用银行结算系统的千年虫bug,精心设计,盗取银行几十亿的精彩动作片。所谓2000 年千禧年的千年虫,其实就是计算机计算闰年的bug。 这个闰年计算的历史源远流长&…...

python水表识别图像识别深度学习 CNN
python水表识别,图像识别深度学习 CNN,Opencv,Keras 重点:项目和文档是本人近期原创所作!程序可以将水表图片里面的数据进行深度学习,提取相关信息训练,lw1.3万字重复15%,可以直接上交那种&…...

Java对接快递100实时快递单号查询API接口
目录 1.引入依赖 2.定义配置信息 3.模块结构 4.Controller 5.Service实现类 6.返回数据dto以及dto中的数据dto 7.测试运行 今天也是接到了这个任务,官网有小demo,可以下载下来参考test中代码 官方文档地址: 实时快递查询接口技术文档…...

Redis常见的15个【坑】,避坑指南
一、常见命令 1.1 过期时间意外丢失 原因: SET命令如果不设置过期时间,那么Redis会自动【擦除】这个key的过期时间 1.2 DEL命令阻塞redis key是String类型时,DEL时间复杂度是O(1)key是List/Hash/Set/ZSet类型,DEL时间复杂度是…...

04. Nginx入门-Nginx WEB模块
测试环境 此处使用的yum安装的Nginx路径。 此处域名均在本地配置hosts。 主配置文件 路径:/etc/nginx/nginx.conf user nginx; worker_processes auto;error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid;events {worker_connection…...
Python在信息安全领域中具有重要的作用
Python在信息安全领域中具有重要的作用。下面是几个方面的说明: 网络安全:Python提供了一系列用于网络安全的库和工具,例如Scapy、Nmap等。这些工具可以应用于漏洞扫描、网络流量分析、数据包嗅探等操作,帮助检测和防御网络攻击。…...
Linux 定时备份文件到另一台服务器
1. 需求 用户要求将 Tomcat 的日志文件定时备份到另一台服务器。同事给我提供了一个写好的 java 框架,但实在不想给用户再维护另一个服务了,所以另寻他法。 2. 问题 使用 scp 等跨服务器传输命令时需要手动输入用户名的密码才可进行文件传输ÿ…...

C++输入输出(I\O)
我们知道C是由C语言发展而来的,几乎完全兼容C语言,换句话说,你可以在C里面编译C语言代码。如下图: C语言是面向过程的语言,C在C语言之上增加了面向对象以及泛型编程机制,因此C更适合中大型程序的开发,然而C…...

基本设计模式
单例模式 ES5 function Duck1(name:string){this.namenamethis.instancenull }Duck1.prototype.getNamefunction(){console.log(this.name) }Duck1.getInstancefunction(name:string){if(!this.instance){this.instance new Duck1(name)} } const aDuck1.getInstance(a) const…...

双通道音频功率放大电路,外接元件少, 通道分离性好,3V 的低压下可正常使用——D2025
D2025 为立体声音频功率放大集成电路,适用于各类袖珍或便携式立体声 收录机中作功率放放大器。 D2025 采用 DIP16 封装形式。 主要特点: 适用于立体声或 BTL 工作模式 外接元件少 通道分离性好 电源电压范围宽(3V~12V ÿ…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...

JVM 内存结构 详解
内存结构 运行时数据区: Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器: 线程私有,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 每个线程都有一个程序计数…...

STM32标准库-ADC数模转换器
文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”:输入模块(GPIO、温度、V_REFINT)1.4.2 信号 “调度站”:多路开关1.4.3 信号 “加工厂”:ADC 转换器(规则组 注入…...

Win系统权限提升篇UAC绕过DLL劫持未引号路径可控服务全检项目
应用场景: 1、常规某个机器被钓鱼后门攻击后,我们需要做更高权限操作或权限维持等。 2、内网域中某个机器被钓鱼后门攻击后,我们需要对后续内网域做安全测试。 #Win10&11-BypassUAC自动提权-MSF&UACME 为了远程执行目标的exe或者b…...