SpringBoot+Vue3+SSE实现实时消息语音播报

目录

1、前言

2、什么是SSE

2.1、与WebSocket有什么异同?

3、代码实现

3.1、前置代码

3.2、SSE相关代码

3.3、消息类相关代码

3.4 、前端代码

4、实机演示


1、前言

有这样一个业务场景,比如有一个后台管理系统,用来监听订单的更新,一有新的订单,就立即发送消息提醒系统用户,进行查看订单,最经典的案例就是美团或饿了么的商家运营后台,网上来新的订单后,立即会进行语音播报:“您有新的外卖订单,请及时查看!”,那么,今天这篇文章来实现一个类似于这样的功能,首先,框架方面选择的是SpringBoot+Vue3进行开发,而消息实时推送选择的是SSE技术(Server-Sent Events),它和WebSocket都是网络通讯技术,但是有一些异同之处。

2、什么是SSE

可能有小伙伴会说SringBoot和vue我听说过,WebSocket也了解过,这个SSE是什么东西?

下面我来解释一下SSE技术是干什么的。

SSE(Server-Sent Events,服务器发送事件)是一种网络通信技术,允许服务器向客户端推送信息,而不需要客户端显式地请求。这项技术通常用于需要实时更新或流式传输数据的场景,例如股票价格更新、社交网络通知、实时消息传递等。

以下是SSE技术的一些特点:

  1. 单向通信:SSE提供的是从服务器到客户端的单向通信。服务器可以不断地将数据推送到客户端,但客户端不能通过同一个连接发送数据到服务器。

  2. 基于HTTP:SSE使用标准的HTTP协议,并通过长连接保持通信。这意味着它不需要任何额外的协议或复杂配置,可以很容易地通过现有的Web基础设施工作。

  3. 事件格式:服务器发送的数据是以事件的形式封装的。每个事件包括类型和数据字段,其中数据字段可以包含任何序列化的数据,通常是文本,也可以是JSON格式的数据。

  4. 自动重连:如果服务器或网络发生故障导致连接断开,SSE规范要求客户端自动尝试重新连接。

  5. 简单易用:客户端通过JavaScript中的EventSource接口可以很容易地使用SSE。创建一个EventSource实例,并指定服务器的URL,就可以开始接收事件。

2.1、与WebSocket有什么异同?

SSE(Server-Sent Events)和WebSocket都是实现服务器与客户端之间实时通信的技术,但它们在通信模式、使用场景和实现细节上存在一些差异:

(1)通信模式方面区别:

  • SSE

    • 单向通信:仅支持从服务器到客户端的数据推送。
    • 基于HTTP:使用HTTP协议,可以穿过大多数防火墙。
    • 保持连接:客户端与服务器之间的连接保持开放,服务器可以不断发送数据。
  • WebSocket

    • 双向通信:支持客户端和服务器之间的全双工通信,即客户端和服务器都可以随时发送消息。
    • 协议升级:最初通过HTTP握手建立连接,然后升级到WebSocket协议,创建持久的TCP连接。
    • 实时性:提供真正的实时通信,延迟更低。

(2)使用场景方面区别:

  • SSE

    • 适用于只需要服务器向客户端推送数据的场景,如新闻推送、实时更新等。
    • 适合处理跨域资源共享(CORS)。
  • WebSocket

    • 适用于需要双向实时通信的应用,如在线聊天室、多人游戏、实时交易系统等。
    • 适合需要低延迟和高频消息交换的场景。

(3)实现细节方面区别:

  • SSE

    • 自动重连:如果连接断开,浏览器会自动尝试重新连接。
    • 简单性:API简单,易于实现。
    • 数据格式:发送的数据通常是文本格式,可以是JSON。
  • WebSocket

    • 自定义协议:WebSocket使用自定义的协议,不是基于HTTP的。
    • 连接维护:需要手动处理连接的维护,如重连逻辑。
    • 数据格式:可以发送文本和二进制数据。

(4)兼容性和复杂性方面区别:

  • SSE

    • 兼容性较好:大多数现代浏览器都支持SSE。
    • 实现简单:服务器端发送事件流,客户端监听事件。
  • WebSocket

    • 兼容性较好:所有现代浏览器都支持WebSocket。
    • 实现复杂:需要服务器和客户端都实现WebSocket协议,可能需要第三方库支持。

总结来说,SSE和WebSocket的主要区别在于通信方向、协议类型、使用场景和实现复杂度。选择哪种技术取决于具体的应用需求。如果只需要单向的数据流,SSE是一个简单有效的选择;如果需要双向实时通信,WebSocket则更为合适。

3、代码实现

3.1、前置代码

SSE技术需要springboot-web的依赖,本文章ORM框架使用了mybatis-plus,数据库用的是mysql5.7,lombok快速生成set/get方法。

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency>

 通用BaseEntitiy类,这个类是存放一些各个表都通用的属性,子类只属于继承即可 

@Data
public abstract class BaseEntity<T> implements Serializable {private static final long serialVersionUID = 1L;/*** 主键ID*/@TableId(type = IdType.ASSIGN_ID)@JsonSerialize(using = ToStringSerializer.class)private Long id;/*** 创建时间,使用MyBatis-Plus的自动填充功能*/@TableField(value = "create_time", fill = FieldFill.INSERT)@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date createTime;/*** 更新时间,使用MyBatis-Plus的自动填充功能*/@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date updateTime;@TableField(value = "create_by", fill = FieldFill.INSERT)private Long createBy;@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)private Long updateBy;
}

添加mybatis-plus自动填充通用属性配置类

/*** mybatis-plus拦截器,自动填充相关字段**/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {this.strictInsertFill(metaObject, "createTime", Date.class, new Date());this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());this.strictInsertFill(metaObject, "createBy", Long.class, UserContext.getCurrentUser().getId());this.strictInsertFill(metaObject, "updateBy", Long.class, UserContext.getCurrentUser().getId());}@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());this.strictInsertFill(metaObject, "updateBy", Long.class, UserContext.getCurrentUser().getId());}
}

3.2、SSE相关代码

创建SseController

@RestController
public class SseController {@Autowiredprivate SseService sseService;//客户端用户连接服务器方法@GetMapping("/sse/{userId}")public SseEmitter streamSseMvc(@PathVariable Long userId) {return  sseService.streamSseMvc(userId);}}

创建service接口

public interface SseService {/*** 连接方法* @param userId* @return*/SseEmitter streamSseMvc(Long userId);/*** 制定userId发送消息* @param userId* @param message*/void sendMessage(Long userId, String message);
}

创建service实现类

@Service
public class SseServiceImpl implements SseService {//创建线程安全的map,维护每个客户端的sseEmitter对象private ConcurrentHashMap<Long, SseEmitter> userEmitters = new ConcurrentHashMap<>();@Overridepublic SseEmitter streamSseMvc(Long userId) {//设置监听器永不过期,一直监听消息SseEmitter emitter = new SseEmitter(0L);userEmitters.put(userId, emitter);emitter.onCompletion(() -> userEmitters.remove(userId, emitter));emitter.onTimeout(() -> userEmitters.remove(userId, emitter));emitter.onError((e) -> userEmitters.remove(userId, emitter));return emitter;}@Overridepublic void sendMessage(Long userId, String message) {SseEmitter emitter = userEmitters.get(userId);if (emitter != null) {try {emitter.send(SseEmitter.event().name("message").data(message));} catch (IOException e) {userEmitters.remove(userId, emitter);}}}
}

3.3、消息类相关代码

有了sse的service还不够,因为我们需要创建一个存储notify的表,也就是消息类

创建表结构

CREATE TABLE `system_notify` (`id` bigint NOT NULL COMMENT '主键',`title` varchar(255) DEFAULT NULL COMMENT '消息标题',`level` varchar(2) DEFAULT NULL COMMENT '消息级别',`content` varchar(500) DEFAULT NULL COMMENT '消息内容',`to_user` bigint DEFAULT NULL COMMENT '接收人',`to_role` bigint DEFAULT NULL COMMENT '接收角色',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`state` varchar(255) CHARACTER DEFAULT NULL COMMENT '消息状态  01   未读   02   已确认  03   已忽略',`create_by` bigint DEFAULT NULL COMMENT '创建者',`update_by` bigint DEFAULT NULL COMMENT '更新者',`update_time` datetime DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB;
/*** 消息类*/
@Data
@TableName("system_notify")
public class Notify extends BaseEntity<Notify> {/*** 消息标题*/@TableField("title")private String title;/*** 消息内容*/@TableField("content")private String content;/*** 消息级别*/@TableField("level")private String level;/*** 发送至用户id*/@TableField("to_user")private Long toUser;/*** 发送至用户角色id*/@TableField("to_role")private Long toRole;/*** 消息状态*/@TableField("state")private String state;
}

Notify的controller控制层

@RestController
@RequestMapping("/notify")
public class NotifyController {@Autowiredprivate NotifyService notifyService;@RequestMapping("findAllNotifyByUser/{userId}")public Result<List<Notify>> findAllNotifyByUser(@PathVariable Long userId) {List<Notify> notifyList = notifyService.findAllNotifyByUser(userId);return Result.success(notifyList);}@RequestMapping("addNotify")public Result<String> addNotify(@RequestBody Notify notify) {notifyService.addNotify(notify);return Result.success();}}

Notify的service接口

public interface NotifyService {void addNotify(Notify notify);List<Notify> findAllNotifyByUser(Long userId);
}

 Mapper接口 

public interface NotifyMapper extends BaseMapper<Notify> {
}

service实现类

@Service
public class NotifyServiceImpl implements NotifyService {@Autowiredprivate NotifyMapper notifyMapper;@Autowiredprivate SseService sseService;/*** 添加消息* @param notify*/@Overridepublic void addNotify(Notify notify) {//添加消息notifyMapper.insert(notify);//发送ssesseService.sendMessage(notify.getToUser(), notify.getContent());}/*** 查询用户相关消息* @param userId* @return*/@Overridepublic List<Notify> findAllNotifyByUser(Long userId) {return notifyMapper.selectList(new LambdaQueryWrapper<Notify>().eq(Notify::getToUser, userId).eq(Notify::getState, NotifyState.n1.getCode()));}
}

3.4 、前端代码

 这是小铃铛和消息列表的前端代码

  <div class="notify-btn" @click="showNotifyBox"><el-badge :value="notifyCount" :max="99" class="item"><i class="iconfont notify">&#xe679;</i></el-badge></div><el-drawer v-model="showNotify" title="消息列表"><div class="notify-drawer"><el-card style="width: 480px" v-for="(item,index) in notifyData" :key="index" class="notify-card"><template #header><div class="card-header"><span class="notify-title">{{ item.title }}</span><el-tag type="primary" v-if="item.level === '01'">普通</el-tag><el-tag type="warning" v-if="item.level === '02'">一般</el-tag><el-tag type="danger" v-if="item.level === '03'">紧急</el-tag></div></template><p class="text item">{{ item.content }}</p><template #footer><el-button color="#626aef">确认</el-button><el-button type="danger">忽略</el-button></template></el-card></div></el-drawer>
const showNotifyBox = () => {showNotify.value = true;
}
//初始化一个ref的消息数组,存放消息
const notifyData = ref([])
const findAllNotifyByUser = (userId: String) => {$http.post('/notify/findAllNotifyByUser/' + userId).then((data) => {notifyData.value = data; //计算消息的个数notifyCount.value = data.length;})
}
let eventSource = null;
const subscribeToSSE = () => {eventSource = new EventSource('http://localhost:8080/sse/' + userInfo.userId);eventSource.onmessage = (event) => {//语音播报speak(event.data);//重新查询消息findAllNotifyByUser(userInfo.userId);};eventSource.onerror = (error) => {console.error('SSE error:', error);};
};
//使用HTML5 Api 进行语音播报服务器推送的消息
const speak = (text) => {if ('speechSynthesis' in window) {const utterance = new SpeechSynthesisUtterance(text);utterance.lang = 'zh-CN';window.speechSynthesis.speak(utterance);} else {alert('您的浏览器不支持语音合成');}
}
onMounted(() => {//页面渲染完后进行连接ssesubscribeToSSE();//根据userId 进行查询相关消息,这里的userInfo我是从pinia中取出的,根据自己业务进行取值findAllNotifyByUser(userInfo.userId);
})
onUnmounted(() => eventSource.close());

4、实机演示

好啦,下面的视频是我进行实机演示的效果,大家可以参考一下。

2024-08-11 15-41-07

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【NUCLEO-G071RB】010——TIM6-基本定时器

NUCLEO-G071RB&#xff1a;010——TIM6-基本定时器 基本定时器设计目标芯片配置程序修改运行测试 基本定时器 基本定时器只能用于计时&#xff0c;可以配置有无上溢出中断&#xff0c;它基本到不支持下溢出中断。它的时钟源&#xff08;应该&#xff09;是TPCLK&#xff0c;内…

ChatGPT首次被植入人类大脑:帮助残障人士开启对话

马斯克在脑机接口中最强大的竞争对手Synchron有了新的技术进展&#xff0c;他们首次将ChatGPT整合到其脑机系统中&#xff0c;以使瘫痪患者更容易控制他们的数字设备。Synchron凭借其独特的脑机接口&#xff08;BCI&#xff09;技术脱颖而出&#xff0c;该技术巧妙地运用了成熟…

【npm】如何将自己的插件发布到npm上

前言 简单说下 npm 是什么&#xff1a; npm 是一个 node 模块管理工具&#xff0c;也是全球最大的共享源。 npm 工具与 nodejs 配套发布&#xff0c;便利开发人员共享代码。npm 主要包括 npm 官方网站、CLI&#xff08;控制台命令行工具&#xff09;、和 registry&#xff08;…

「Pytorch」BF16 Mixed Precision Training

在深度学习领域&#xff0c;神经网络的训练性能瓶颈常常出现在 GPU显存的使用上。主要表现为两方面&#xff1a; 单卡上可容纳的模型和数据量有限&#xff1b;显存与计算单元之间的带宽和延迟限制了运算速度&#xff1b; 为了解决显卡瓶颈的问题&#xff0c;涌现了不同的解决…

Arduino控制带编码器的直流电机速度

Arduino DC Motor Speed Control with Encoder, Arduino DC Motor Encoder 作者 How to control dc motor with encoder:DC Motor with Encoder Arduino, Circuit Diagram:Driving the Motor with Encoder and Arduino:Control DC motor using Encoder feedback loop: How …

深度学习碎碎念——碎片知识1

1、什么叫模型收敛&#xff1f;什么叫模型欠拟合和过拟合&#xff1f; 什么叫模型收敛&#xff1f;——模型收敛是指在训练过程中&#xff0c;模型的损失函数逐渐减小并且趋于稳定的状态。简而言之&#xff0c;当模型的训练过程达到一个稳定的点&#xff0c;使得进一步的训练不…

CV党福音:YOLOv8实现语义分割(一)

前面我们得知YOLOv8不但可以实现目标检测任务&#xff0c;还包揽了分类、分割、姿态估计等计算机视觉任务。在上一篇博文中&#xff0c;博主已经介绍了YOLOv8如何实现分类&#xff0c;在这篇博文里&#xff0c;博主将介绍其如何将语义分割给收入囊中。 YOLOv8语义分割架构图 …

【C++】特殊类的设计与类型转换

文章目录 1. 特殊类的设计1.1 不能被拷贝的类1.2 只能在堆上创建对象的类1.3 只能在栈上创建对象的类1.4 不能被继承的类1.5 只能创建一个对象的类&#xff08;单列模式&#xff09; 2. 类型转换2.1 C/C的类型转换2.2 C规定的四种类型转换2.2.1 static_cast2.2.2 reinterpret_c…

【吊打面试官系列-Elasticsearch面试题】对于 GC 方面,在使用 Elasticsearch 时要注意什么?

大家好&#xff0c;我是锋哥。今天分享关于 【对于 GC 方面&#xff0c;在使用 Elasticsearch 时要注意什么&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 对于 GC 方面&#xff0c;在使用 Elasticsearch 时要注意什么&#xff1f; 1、SEE 2、倒排词典的索引需…

vue3使用pnpm运行项目但是运行不起来

运行项目的时候发现根本运行不起来了 尝试过创建.npmr文件 删除node_modules重新下 但是都出现问题了 创建.npmr&#xff1a;不管用 删除node_modules重新下&#xff1a;文字编译乱码&#xff0c;utf-8可能解析处理问题 最后解决方法&#xff1a; 重新创建项目&#xff0…

网络科技公司官网电商软件开发小程序网站pbootcms模板带手机端

免费授权可商用网站模板 PC端移动端后台测试数据 所有页面均都能完全自定义标题/关键词/描述&#xff0c;PHP程序&#xff0c;安全、稳定、快速&#xff0c;响应式同一个后台&#xff0c;数据即时同步&#xff0c;简单适用&#xff0c;附带测试数据&#xff01;&#xff01;

物流仓库安全视频智能管理方案:构建全方位、高效能的防护体系

一、背景分析 随着物流行业的快速发展和仓储需求的日益增长&#xff0c;仓库安全成为企业运营中不可忽视的重要环节。传统的人工监控方式不仅效率低下&#xff0c;且难以做到全天候、无死角覆盖&#xff0c;给仓库资产和人员安全带来潜在风险。因此&#xff0c;引入仓库安全视…

了解细胞外基质:它是啥?有啥作用?

了解细胞外基质&#xff1a;它是啥&#xff1f;有啥作用&#xff1f; 大家好&#xff0c;今天我们来阅读这篇Biofabrication methods for reconstructing extracellular matrix mimetics发表于《Bioactive Materials》上的文章。细胞外基质在人体中起着至关重要的作用&#xff…

同城门户同城分类信息网站源码discuz插件+pc端+小程序端+49款插件

同城分类信息 同城好店 同城合伙人 同城招聘 同城卡 同城活动 同城优惠抢购 同城商城 同城头条 同城抽奖 同城拼团 同城砍价 同城电话本 同城认证 同城签到 同城拼车 同城红包 同城子站点 同城相亲 同城交友 同城小程序 比较流行的同城信息门户网站源码&#xff0c;基于dz&…

【计算机网络】网络基础概念

目录 计算机网络发展 协议 协议分层 OSI 七层模型 TCP/IP 五层&#xff08;四层&#xff09;模型 究竟什么是协议&#xff1f; 网络与操作系统的关系 网络传输基本流程 局域网网络传输流程 认识 MAC 地址 局域网&#xff08;以太网为例&#xff09;通信原理 数据包…

【前端设计方案】H5 图片懒加载 SDK

实现思路 定义<img srcloading.png data-srcxxx.png/>页面滚动&#xff0c;图片露出时&#xff0c;将 data-src 赋值给 src 注意事项&#xff1a;滚动要节流 技术要点 获取图片的位置 elem.getBoundingClientRect() 图片 top < window.innerHeight 时&#xff0c;图片…

Install pytorch 使用 torch 的例子

如果不知道怎么开始和安装软件 从这里开始 如果需要GPU版本&#xff0c;请选择CUDA&#xff0c;而不是CPU PyTorchhttps://pytorch.org/ Python 3.8.13 | packaged by conda-forge | (default, Mar 25 2022, 06:04:10) [GCC 10.3.0] on linux Type "help", &quo…

opencv 深度图视差图可视化案例

参考:https://www.cnblogs.com/zyly/p/9373991.html(图片这里面下载的) https://blog.csdn.net/He3he3he/article/details/101053457 原理 双目摄像头 视差公式: 三角形对应推算 深度距离转换: 这里d是视差Disparity 代码 下面两种计算视差方法: import os impor…

计算机毕业设计Hadoop+Hive居民用电量分析 居民用电量可视化 电量爬虫 机器学习 深度学习 大数据毕业设计 Spark

《Hadoop居民用电量分析》开题报告 一、研究背景与意义 能源问题在全球范围内一直是热点议题&#xff0c;尤其是随着居民生活水平的提高和城市化进程的加快&#xff0c;居民用电量急剧增长&#xff0c;对电力系统的稳定运行和能源管理提出了更高要求。如何科学合理地管理和分…

T9打卡学习笔记

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 import tensorflow as tfgpus tf.config.list_physical_devices("GPU")if gpus:tf.config.experimental.set_memory_growth(gpus[0], True) #设置…