Spring Boot+Vue使用Server-Sent Events (SSE) 实现实时数据推送

一、SSE是什么?


SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。

注意:因为EventSource对象是SSE的客户端,可能会有浏览器对其不支持

二、sse 与 websoket

1、SSE(Server-Sent Events)


是 HTML5 遵循 W3C 标准提出的客户端和服务端之间进行实时通信的协议。

优点

SSE 客户端可以接收来自服务器的“流”数据,而不需要进行轮询。由于没有浪费的请求,因此 SSE 对于减轻服务器的压力非常有用。
SSE 使用纯 JavaScript 实现简单,不需要额外的插件或库来处理消息。客户端可以使用 EventSource 接口轻松地与 SSE 服务器通信。
SSE 天生具有自适应性,由于 SSE 是基于 HTTP 响应使用 EventStream 传递消息,因此它利用了 HTTP 的开销和互联网上的结构。
SSE 可以与任何服务器语言和平台一起使用,因为 SSE 是一种规定了消息传递方式的技术,不依赖于具体的服务器语言和平台。
缺点

SSE 是单向通信,只能从服务器推送到客户端。如果应用程序需要双向通信,就需要使用 Websocket。
SSE无法发送二进制数据,只能发送 UTF-8 编码的文本。如果应用程序需要发送二进制数据,就需要使用 Websocket。
SSE 不是所有浏览器都支持。虽然 SSE 是 HTML5 的一部分,但具体的浏览器支持性可能会有差异。


2、Websocket


是 HTML5 的一部分,提供了一种双向通信的机制。

优点

Websocket 支持双向通信。使用 Websocket 可以同时向客户端发送和接收数据。
Websocket 协议可以传输二进制数据,这使得 Websocket 更加灵活和强大。
Websocket 连接长期存在,而不需要仅仅为了接收数据而保持 HTTP 连接打开。
Websocket 的实现支持跨域的通信,可以方便地进行跨域通信。
缺点

Websocket 不支持所有浏览器。特别是老浏览器可能不支持 Websocket 协议。
Websocket 是一种全双工的通信方式。由于 Websocket 长期存在,会占用服务器资源。在高并发场景下,应该考虑使用 SSE。

三、依赖添加

  在Spring Boot项目中,无需额外引入特定的依赖,因为Spring Web MVC模块已经内置了对SSE的支持。

辅助Maven

        <!-- 集成beetl -->
        <dependency>
            <groupId>com.ibeetl</groupId>
            <artifactId>beetl-framework-starter</artifactId>
            <version>1.2.30.RELEASE</version>
        </dependency>
 
        <!-- 集成hutool工具类简便操作 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>

四、前端示例代码

      createSseConnect(clientId) {
        if (window.EventSource) {
          //创建sse
          const eventSource = new EventSource(`http://localhost/dev-api/dispatch/sse/createSse?uid=${clientId}`);
          eventSource.onopen = function (event) {
            console.log('SSE链接成功');
          }
          eventSource.onmessage = function (event) {
            console.log('后端返回的数据event:');
            console.log(event);
            console.log('后端返回的数据:' + event.data);
          }
          eventSource.onerror = (error) => {
            console.log('SSE链接失败');
          };
        } else {
          alert("你的浏览器不支持SSE");
        }
      },

注意:如果url是网关地址,要在网关配置文件中添加白名单 

五、后端示例代码

SseController

import cn.hutool.core.util.IdUtil;
import com.mes.dispatch.utils.SseClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

/**
 * @Author: best_liu
 * @Description:
 * @Date Create in 13:20 2024/6/17
 * @Modified By:
 */
@RestController
@RequestMapping("/sse")
public class SseController{
    @Autowired
    private SseClient sseClient;
    @GetMapping("/")
    public String index(ModelMap model) {
        String uid = IdUtil.fastUUID();
        model.put("uid",uid);
        return "index";
    }

    @CrossOrigin
    @GetMapping("/createSse")
    public SseEmitter createConnect(String uid) {
        return sseClient.createSse(uid);
    }
    @CrossOrigin
    @GetMapping("/sendMsg")
    @ResponseBody
    public String sseChat(String uid) {
        for (int i = 0; i < 10; i++) {
            sseClient.sendMessage("910724", "no"+i,IdUtil.fastUUID());
        }
        return "ok";
    }

    /**
     * 关闭连接
     */
    @CrossOrigin
    @GetMapping("/closeSse")
    public void closeConnect(String uid ){

        sseClient.closeSse(uid);
    }
}

1,打开页面默认页面,传递端点标识。

2,连接端点(/createSse),页面需要使用

3,通过ajax(/sendMsg),触发后端业务(循环十条数据发往页面),向页面发送消息。

4,主动关闭连接(/closeSse)


SseClient工具类

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: best_liu
 * @Description: SseClient
 * @Date Create in 13:19 2024/6/17
 * @Modified By:
 */
@Slf4j
@Component
public class SseClient {
    private static final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
    /**
     * 创建连接
     */
    public SseEmitter createSse(String uid) {
        //默认30秒超时,设置为0L则永不超时
        SseEmitter sseEmitter = new SseEmitter(0l);
        //完成后回调
        sseEmitter.onCompletion(() -> {
            log.info("[{}]结束连接...................", uid);
            sseEmitterMap.remove(uid);
        });
        //超时回调
        sseEmitter.onTimeout(() -> {
            log.info("[{}]连接超时...................", uid);
        });
        //异常回调
        sseEmitter.onError(
                throwable -> {
                    try {
                        log.info("[{}]连接异常,{}", uid, throwable.toString());
                        sseEmitter.send(SseEmitter.event()
                                .id(uid)
                                .name("发生异常!")
                                .data("发生异常请重试!")
                                .reconnectTime(3000));
                        sseEmitterMap.put(uid, sseEmitter);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
        );
        try {
            sseEmitter.send(SseEmitter.event().reconnectTime(5000));
        } catch (IOException e) {
            e.printStackTrace();
        }
        sseEmitterMap.put(uid, sseEmitter);
        log.info("[{}]创建sse连接成功!", uid);
        return sseEmitter;
    }

    /**
     * 给指定用户发送消息
     *
     */
    public boolean sendMessage(String uid,String messageId, String message) {
        if (StrUtil.isBlank(message)) {
            log.info("参数异常,msg为null", uid);
            return false;
        }
        SseEmitter sseEmitter = sseEmitterMap.get(uid);
        if (sseEmitter == null) {
            log.info("消息推送失败uid:[{}],没有创建连接,请重试。", uid);
            return false;
        }
        try {
            sseEmitter.send(SseEmitter.event().id(messageId).reconnectTime(1*60*1000L).data(message));
            log.info("用户{},消息id:{},推送成功:{}", uid,messageId, message);
            return true;
        }catch (Exception e) {
            sseEmitterMap.remove(uid);
            log.info("用户{},消息id:{},推送异常:{}", uid,messageId, e.getMessage());
            sseEmitter.complete();
            return false;
        }
    }

    /**
     * 断开
     * @param uid
     */
    public void closeSse(String uid){
        if (sseEmitterMap.containsKey(uid)) {
            SseEmitter sseEmitter = sseEmitterMap.get(uid);
            sseEmitter.complete();
            sseEmitterMap.remove(uid);
        }else {
            log.info("用户{} 连接已关闭",uid);
        }

    }

}
  1. 创建SSE 端点

    创建一个SseEmitter,用uid进行标识,uid可以是用户标识符,也可以是业务标识符。可以理解为通信信道标识。

  2. 通过端点发送事件

    可以定时或在事件发生时调用sseEmitter.send()方法来发送事件。

  3. 关闭端点连接

六、模拟测试

浏览器模拟get请求调用发消息接口 

浏览器建立的连接中会看到服务器推送到客户端的消息内容及ID等基础信息

控制台也可以监听到事件的变化并输出

在这个例子中,前端每接收到一次SSE推送的事件,就会在控制台打印推送信息。 

七、注意事项

1、当客户端断开连接时,SseEmitter会抛出IOException,所以务必捕获并处理这种异常,通常情况下我们会调用emitter.complete()或emitter.completeWithError()来关闭SseEmitter。
2、SSE连接是持久性的,长时间保持连接可能需要处理超时和重连问题。
3、考虑到资源消耗,对于大量的并发客户端,可能需要采用连接池或者其他优化策略。

总结,Spring Boot中利用SSE实现实时数据推送既简单又实用,特别适合实时更新频率不高、实时性要求不严苛的场景。同时,在高并发场景下需要注意资源管理和优化策略的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值