SpringBoot / Vue 对SSE的基本使用

news2025/1/15 23:42:59

一、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。

三、前端示例代码

// 建立连接
 createSseConnect(clientId){
    if(window.EventSource){
        const eventSource = new EventSource('http://127.0.0.1:8083/sse/createSseConnect?clientId='+clientId);
        console.log(eventSource)
        
        eventSource.onmessage = (event) =>{
            console.log("onmessage:"+clientId+": "+event.data)
        };
        
        eventSource.onopen = (event) =>{
            console.log("onopen:"+clientId+": "+event)
        };
        
        eventSource.onerror = (event) =>{
            console.log("onerror :"+clientId+": "+event)
        };
        
        eventSource.close = (event) =>{
            console.log("close :"+clientId+": "+event)
        };
 
    }else{
        console.log("你的浏览器不支持SSE~")
    }
    console.log(" 测试 打印")
},

四、后端示例代码

SseController

package com.joker.cloud.linserver.controller;
 
import com.joker.cloud.linserver.conf.sse.sseUtils;
import com.joker.common.message.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
import java.util.Map;
 
/**
 * SseController
 *
 **/
@RestController
@Slf4j
@CrossOrigin
@RequestMapping("/sse")
public class SseController {
 
    @Autowired
    private sseUtils sseUtils;
 
 
    @GetMapping(value = "/createSseConnect", produces="text/event-stream;charset=UTF-8")
    public SseEmitter createSseConnect(@RequestParam(name = "clientId", required = false) Long clientId) {
        return sseUtils.connect(clientId);
    }
 
 
    @PostMapping("/sendMessage")
    public void sendMessage(@RequestParam("clientId") Long clientId, @RequestParam("message")  String message){
        sseUtils.sendMessage(clientId, "123456789", message);
    }
 
    @GetMapping(value = "/listSseConnect")
    public Result<Map<Long, SseEmitter>> listSseConnect(){
        Map<Long, SseEmitter> sseEmitterMap = sseUtils.listSseConnect();
        return Result.success(sseEmitterMap);
    }
 
 
    /**
     * 关闭SSE连接
     *
     * @param clientId 客户端ID
     **/
    @GetMapping("/closeSseConnect")
    public Result closeSseConnect(Long clientId) {
        sseUtils.deleteUser(clientId);
        return Result.success();
    }
 
}

sseUtils工具类

package com.joker.cloud.linserver.conf.sse;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
 
 
/**
 * sseUtils
 **/
@Slf4j
@Component
public class sseUtils {
 
    private static final Map<Long, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
 
    /**
     * 创建连接
     */
    public SseEmitter connect(Long userId) {
        if (sseEmitterMap.containsKey(userId)) {
            SseEmitter sseEmitter =sseEmitterMap.get(userId);
            sseEmitterMap.remove(userId);
            sseEmitter.complete();
        }
        try {
            UUID uuid = UUID.randomUUID();
            String str = uuid.toString();
            String temp = str.substring(0, 8) + str.substring(9, 13) + str.substring(14, 18) + str.substring(19, 23) + str.substring(24);
 
            // 设置超时时间,0表示不过期。默认30秒
            SseEmitter sseEmitter = new SseEmitter(30*1000L);
            sseEmitter.send(SseEmitter.event().id(temp).data(""));
//            reconnectTime(10*1000L)
            // 注册回调
            sseEmitter.onCompletion(completionCallBack(userId));
//            sseEmitter.completeWithError(errorCallBack(userId));
            sseEmitter.onTimeout(timeoutCallBack(userId));
            sseEmitterMap.put(userId, sseEmitter);
            log.info("创建sse连接完成,当前用户:{}", userId);
            return sseEmitter;
        } catch (Exception e) {
            log.info("创建sse连接异常,当前用户:{}", userId);
        }
        return null;
    }
 
    /**
     * 给指定用户发送消息
     *
     */
    public boolean sendMessage(Long userId,String messageId, String message) {
        if (sseEmitterMap.containsKey(userId)) {
            SseEmitter sseEmitter = sseEmitterMap.get(userId);
            try {
                sseEmitter.send(SseEmitter.event().id(messageId).data(message));
//                reconnectTime(10*1000L)
                log.info("用户{},消息id:{},推送成功:{}", userId,messageId, message);
                return true;
            }catch (Exception e) {
                sseEmitterMap.remove(userId);
                log.info("用户{},消息id:{},推送异常:{}", userId,messageId, e.getMessage());
                sseEmitter.complete();
                return false;
            }
        }else {
            log.info("用户{}未上线", userId);
        }
        return false;
    }
 
    /**
     * 删除连接
     * @param userId
     */
    public void deleteUser(Long userId){
        removeUser(userId);
    }
 
    private static Runnable completionCallBack(Long userId) {
        return () -> {
            log.info("结束sse用户连接:{}", userId);
            removeUser(userId);
        };
    }
 
    private static Throwable errorCallBack(Long userId) {
        log.info("sse用户连接异常:{}", userId);
        removeUser(userId);
        return new Throwable();
    }
 
    private static Runnable timeoutCallBack(Long userId) {
        return () -> {
            log.info("连接sse用户超时:{}", userId);
            removeUser(userId);
        };
    }
 
    /**
     * 断开
     * @param userId
     */
    public static void removeUser(Long userId){
        if (sseEmitterMap.containsKey(userId)) {
            SseEmitter sseEmitter = sseEmitterMap.get(userId);
            sseEmitterMap.remove(userId);
            sseEmitter.complete();
        }else {
            log.info("用户{} 连接已关闭",userId);
        }
    }
 
    public Map<Long, SseEmitter> listSseConnect(){
         return sseEmitterMap;
    }
}

五、模拟测试

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

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

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

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

相关文章

深入内核buddy分配器(芯驰X9/杰发8015 buddy系统明明还有几十M到100多M内存,却分配4k内存失败)

如上图内核打印分配4K内存失败&#xff0c;但是normal 类型的buddy系统还有大量内存。居然分配失败。源码分析&#xff1a; 根据logfaddr2line定位到&#xff0c;调用栈为__alloc_pages_slowpath——》get_page_from_freelist——》zone_watermark_fast 可以看到buddy内存低于…

node使用fs模块(一)—— 写入文件的基本使用

文章目录 前言一、写入文件的使用&#xff08;fs.writeFile&#xff09;1.参数说明2.基本使用(1)新建app.js 文件(2)代码如下(3)执行命令(4&#xff09;效果 3.写入文件的同步和异步&#xff08;1&#xff09;默认异步&#xff08;2&#xff09; 同步方法&#xff08;writeFile…

【HeidiSql_01】python在heidisql当中创建新表的注意事项

python在heidisql当中创建新表的注意事项 假设你已经在python当中弄好了所有的结果&#xff0c;并且保存在df_all这个dataframe当中&#xff0c;然后要将其导入数据库当中并创建一张新的表进行保存。 # 构建数据库连接,将merged_df写回数据库 from sqlalchemy import create_e…

5000张照片怎么快速发给别人?分享三个简单的方法!

有的时候我们不得不一次性发送很多图片&#xff0c;一张一张发实在让人头疼&#xff0c;这个时候就需要借助一些图片压缩工具打包成文件压缩包发送。下面介绍了三种好用的方法&#xff0c;一起来看看吧&#xff5e; 方法一&#xff1a;使用微信助手 可以使用微信助手&#xff…

设计思想培养:装饰者模式下的RecyclerView添加头、尾

用一个设计模式培养高复用、低耦合思想 前言Android中的装饰者代码实现第一步&#xff1a;创建装饰器DecorateAdapter第二步&#xff1a;处理头部、中间内容、尾部的绑定关系第三步&#xff1a;装饰器的使用第四步&#xff1a;改进、直接封装一个View出来 总结 前言 一个高复用…

操作系统备考学习 day11 (4.1.1~4.1.9)

操作系统备考学习 day11 第四章 文件管理4.1文件系统基础4.1.1 文件的基本概念文件的属性文件的逻辑结构操作系统向上提供的功能文件如何存放在外存 4.1.2 文件的逻辑结构顺序文件索引文件索引顺序文件 4.1.3 文件目录文件控制块单级目录结构两级目录结构多级目录结构 又称树形…

2023年四川省网络与信息安全技能大赛 决赛个人赛Writeup

文章目录 Web前端验证PHP_Try MiscHelloWorld密码在这easy_log Cryptobaser 线下“断网”CTF个人赛&#xff0c;题都很简单(新手级难度)&#xff0c;总共10道题目&#xff0c;解了6题。 赛题附件请自取&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1lgNEBO7a1L4KLE2t…

Chrome如何解决http自动转为https问题

开发中总遇到http被浏览器转为https导致无法访问404 具体配置如下&#xff1a; 就能正常访问你的http不安全地址

逻辑(css3)_强制不换行

需求 如上图做一个跑马灯数据&#xff0c;时间、地点、姓名、提示文本字数都不是固定的。 逻辑思想 个人想法是给四个文本均设置宽度&#xff0c;不然会出现不能左对齐的现象。 此时四个文本均左对齐&#xff0c; 垂直排列样式也比较好看&#xff0c;但是出现一个缺点&#…

eDNA放大招:看完这篇文献,你的茶包还香吗?

eDNA在过去几年彻底改变了生物监测领域&#xff0c;一起来看它在生活中的应用吧。节肢动物&#xff08;无脊椎动物&#xff0c;如昆虫、甲壳类等&#xff09;在全球生态系统平衡维护中发挥重要作用。eDNA作为传统节肢动物监测的替代方案发挥出巨大的潜力。 最近一项发表于《Bi…

设备树(以STM32MP1为例)

1.设备树&#xff08;Device Tree&#xff09; 是一种用于描述硬件信息和配置的数据结构&#xff0c;以提供一个统一的方式来描述各种硬件设备的特性和连接方式。 设备树并不是从开始就存在&#xff0c;而是后来加入到Linux中&#xff0c;设备树主要用来描述系统的硬件结构 它是…

Hikari源码分析

总结 连接池关系 1、HikariDataSource构建函数->生成HikariPool对象->调用HikariPool的getConection得到连接 2、HikariPool包含ConcurrentBag 3、ConcurrentBag保存连接&#xff1a;三个集合threadList、sharedList、handoffQueue 4、ConcurrentBag管理连接&#xff1…

maven:编译出现Process terminated解决方法(超全)

maven:编译出现Process terminated解决方法&#xff08;超全&#xff09; 一. 情况一&#xff1a;配置文件 settings. xml 出错&#xff08;解决方法1&#xff09;1.1 项目编译报错如下&#xff1a;1.2 点击【项目名】提示找到出错文件1.3 点击查看出错文件1.4 原因及解决办法 …

LeetCode 2401.最长优雅子数组 ----双指针+位运算

数据范围1e5 考虑nlog 或者n的解法&#xff0c;考虑双指针 因为这里要求的是一段连续的数组 想起我们的最长不重复连续子序列 然后结合一下位运算就好了 是一道双指针不错的题目 class Solution { public:int longestNiceSubarray(vector<int>& nums) {int n nums…

微信朋友圈如何关闭?

怎样关闭微信朋友圈&#xff1f;由于一些比较特殊的原因&#xff0c;有些人不想再发朋友圈了&#xff0c;或者想跟过去的自己说“拜拜”&#xff0c;所以就想把微信朋友圈给关掉。虽然这种需求的人并不多&#xff0c;但却真实存在着。 微信早期版本是有关闭朋友圈开关的&#x…

【C# Programming】委托和lambda表达式、事件

目录 一、委托和lambda表达式 1.1 委托概述 1.2 委托类型的声明 1.3 委托的实例化 1.4 委托的内部机制 1.5 Lambda 表达式 1.6 语句lambda 1.7 表达式lambda 1.8 Lambda表达式 1.9 通用的委托 1.10 委托没有结构相等性 1.11 Lambda表达式和匿名方法的内部机制 1.1…

vite+vue3路由切换滚动条位置重置el-scrollbar

vitevue3路由切换滚动条位置重置 本文目录 vitevue3路由切换滚动条位置重置使用原生滚动条使用el-scrollbaruseRoute和useRouter 当切换到新路由时&#xff0c;想要页面滚到顶部&#xff0c;或者是保持原先的滚动位置&#xff0c;就像重新加载页面那样&#xff0c;vue-router 可…

升级版运算放大器应用电路(二)

网友&#xff1a;你上次分享的经典运算放大电路太棒了&#xff0c;再分享几个吧&#xff01; 工程师&#xff1a;好吧&#xff0c;那你瞪大耳朵&#xff0c;看好了~ 1、仪器放大电路&#xff0c;此电路使用于小信号的放大&#xff0c;一般用于传感器信号的放大。传感器的输出信…

紫光同创PGL50H图像Sobel边缘检测

本原创文章由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处 适用于板卡型号&#xff1a; 紫光同创PGL50H开发平台&#xff08;盘古50K开发板&#xff09; 一&#xff1a;软硬件平台 软件平台&#xff1a;PDS_…

ROS中如何实现将一个基于A坐标系下的三维向量变换到基于B坐标系下?

摘要 ROS中通过tf.TransformListener.lookupTransform方法获取从A坐标系到B坐标系的旋转四元数rot&#xff0c;通过quaternion_multiply函数实现 p ′ q p q − 1 p qpq^{-1} p′qpq−1中的矩阵乘法&#xff0c;从而可以方便获取在新坐标系下的四元数坐标表示 p ′ p p′. 基…