0


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

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方法。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.mybatis.spring.boot</groupId>
  7. <artifactId>mybatis-spring-boot-starter</artifactId>
  8. <version>3.0.3</version>
  9. </dependency>
  10. <dependency>
  11. <groupId>com.mysql</groupId>
  12. <artifactId>mysql-connector-j</artifactId>
  13. <scope>runtime</scope>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.projectlombok</groupId>
  17. <artifactId>lombok</artifactId>
  18. <optional>true</optional>
  19. </dependency>
  20. <dependency>
  21. <groupId>com.baomidou</groupId>
  22. <artifactId>mybatis-plus-boot-starter</artifactId>
  23. <version>${mybatis-plus.version}</version>
  24. </dependency>

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

  1. @Data
  2. public abstract class BaseEntity<T> implements Serializable {
  3. private static final long serialVersionUID = 1L;
  4. /**
  5. * 主键ID
  6. */
  7. @TableId(type = IdType.ASSIGN_ID)
  8. @JsonSerialize(using = ToStringSerializer.class)
  9. private Long id;
  10. /**
  11. * 创建时间,使用MyBatis-Plus的自动填充功能
  12. */
  13. @TableField(value = "create_time", fill = FieldFill.INSERT)
  14. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
  15. private Date createTime;
  16. /**
  17. * 更新时间,使用MyBatis-Plus的自动填充功能
  18. */
  19. @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
  20. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
  21. private Date updateTime;
  22. @TableField(value = "create_by", fill = FieldFill.INSERT)
  23. private Long createBy;
  24. @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
  25. private Long updateBy;
  26. }

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

  1. /**
  2. * mybatis-plus拦截器,自动填充相关字段
  3. **/
  4. @Component
  5. public class MyMetaObjectHandler implements MetaObjectHandler {
  6. @Override
  7. public void insertFill(MetaObject metaObject) {
  8. this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
  9. this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
  10. this.strictInsertFill(metaObject, "createBy", Long.class, UserContext.getCurrentUser().getId());
  11. this.strictInsertFill(metaObject, "updateBy", Long.class, UserContext.getCurrentUser().getId());
  12. }
  13. @Override
  14. public void updateFill(MetaObject metaObject) {
  15. this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
  16. this.strictInsertFill(metaObject, "updateBy", Long.class, UserContext.getCurrentUser().getId());
  17. }
  18. }

3.2、SSE相关代码

创建SseController

  1. @RestController
  2. public class SseController {
  3. @Autowired
  4. private SseService sseService;
  5. //客户端用户连接服务器方法
  6. @GetMapping("/sse/{userId}")
  7. public SseEmitter streamSseMvc(@PathVariable Long userId) {
  8. return sseService.streamSseMvc(userId);
  9. }
  10. }

创建service接口

  1. public interface SseService {
  2. /**
  3. * 连接方法
  4. * @param userId
  5. * @return
  6. */
  7. SseEmitter streamSseMvc(Long userId);
  8. /**
  9. * 制定userId发送消息
  10. * @param userId
  11. * @param message
  12. */
  13. void sendMessage(Long userId, String message);
  14. }

创建service实现类

  1. @Service
  2. public class SseServiceImpl implements SseService {
  3. //创建线程安全的map,维护每个客户端的sseEmitter对象
  4. private ConcurrentHashMap<Long, SseEmitter> userEmitters = new ConcurrentHashMap<>();
  5. @Override
  6. public SseEmitter streamSseMvc(Long userId) {
  7. //设置监听器永不过期,一直监听消息
  8. SseEmitter emitter = new SseEmitter(0L);
  9. userEmitters.put(userId, emitter);
  10. emitter.onCompletion(() -> userEmitters.remove(userId, emitter));
  11. emitter.onTimeout(() -> userEmitters.remove(userId, emitter));
  12. emitter.onError((e) -> userEmitters.remove(userId, emitter));
  13. return emitter;
  14. }
  15. @Override
  16. public void sendMessage(Long userId, String message) {
  17. SseEmitter emitter = userEmitters.get(userId);
  18. if (emitter != null) {
  19. try {
  20. emitter.send(SseEmitter.event().name("message").data(message));
  21. } catch (IOException e) {
  22. userEmitters.remove(userId, emitter);
  23. }
  24. }
  25. }
  26. }

3.3、消息类相关代码

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

创建表结构

  1. CREATE TABLE `system_notify` (
  2. `id` bigint NOT NULL COMMENT '主键',
  3. `title` varchar(255) DEFAULT NULL COMMENT '消息标题',
  4. `level` varchar(2) DEFAULT NULL COMMENT '消息级别',
  5. `content` varchar(500) DEFAULT NULL COMMENT '消息内容',
  6. `to_user` bigint DEFAULT NULL COMMENT '接收人',
  7. `to_role` bigint DEFAULT NULL COMMENT '接收角色',
  8. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  9. `state` varchar(255) CHARACTER DEFAULT NULL COMMENT '消息状态 01 未读 02 已确认 03 已忽略',
  10. `create_by` bigint DEFAULT NULL COMMENT '创建者',
  11. `update_by` bigint DEFAULT NULL COMMENT '更新者',
  12. `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  13. PRIMARY KEY (`id`)
  14. ) ENGINE=InnoDB;
  1. /**
  2. * 消息类
  3. */
  4. @Data
  5. @TableName("system_notify")
  6. public class Notify extends BaseEntity<Notify> {
  7. /**
  8. * 消息标题
  9. */
  10. @TableField("title")
  11. private String title;
  12. /**
  13. * 消息内容
  14. */
  15. @TableField("content")
  16. private String content;
  17. /**
  18. * 消息级别
  19. */
  20. @TableField("level")
  21. private String level;
  22. /**
  23. * 发送至用户id
  24. */
  25. @TableField("to_user")
  26. private Long toUser;
  27. /**
  28. * 发送至用户角色id
  29. */
  30. @TableField("to_role")
  31. private Long toRole;
  32. /**
  33. * 消息状态
  34. */
  35. @TableField("state")
  36. private String state;
  37. }

Notify的controller控制层

  1. @RestController
  2. @RequestMapping("/notify")
  3. public class NotifyController {
  4. @Autowired
  5. private NotifyService notifyService;
  6. @RequestMapping("findAllNotifyByUser/{userId}")
  7. public Result<List<Notify>> findAllNotifyByUser(@PathVariable Long userId) {
  8. List<Notify> notifyList = notifyService.findAllNotifyByUser(userId);
  9. return Result.success(notifyList);
  10. }
  11. @RequestMapping("addNotify")
  12. public Result<String> addNotify(@RequestBody Notify notify) {
  13. notifyService.addNotify(notify);
  14. return Result.success();
  15. }
  16. }

Notify的service接口

  1. public interface NotifyService {
  2. void addNotify(Notify notify);
  3. List<Notify> findAllNotifyByUser(Long userId);
  4. }

Mapper接口

  1. public interface NotifyMapper extends BaseMapper<Notify> {
  2. }

service实现类

  1. @Service
  2. public class NotifyServiceImpl implements NotifyService {
  3. @Autowired
  4. private NotifyMapper notifyMapper;
  5. @Autowired
  6. private SseService sseService;
  7. /**
  8. * 添加消息
  9. * @param notify
  10. */
  11. @Override
  12. public void addNotify(Notify notify) {
  13. //添加消息
  14. notifyMapper.insert(notify);
  15. //发送sse
  16. sseService.sendMessage(notify.getToUser(), notify.getContent());
  17. }
  18. /**
  19. * 查询用户相关消息
  20. * @param userId
  21. * @return
  22. */
  23. @Override
  24. public List<Notify> findAllNotifyByUser(Long userId) {
  25. return notifyMapper.selectList(new LambdaQueryWrapper<Notify>().eq(Notify::getToUser, userId).eq(Notify::getState, NotifyState.n1.getCode()));
  26. }
  27. }

3.4 、前端代码

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

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

4、实机演示

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

2024-08-11 15-41-07

标签: spring boot 后端 java

本文转载自: https://blog.csdn.net/select_myname/article/details/141105206
版权归原作者 热爱编程的申同学 所有, 如有侵权,请联系我们删除。

“SpringBoot+Vue3+SSE实现实时消息语音播报”的评论:

还没有评论