SpringBoot+vue+SSE+Nginx实现消息实时推送

news2025/6/1 17:43:21

一、背景

        项目中消息推送,简单的有短轮询、长轮询,还有SSE(Server-Sent Events)、以及最强大复杂的WebSocket。

        至于技术选型,SSE和WebSocket区别,网上有很多,我也不整理了,大佬的链接

《网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket》。

        其实实现很简单,写这篇文章的目的,主要是将处理过程中的一些问题,记录解决方案。

二、后端实现

        其实这里网上也是很多demo,我简写一点demo

1、引入依赖

这里需要用的下面依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、sse工具类

package com.asiainfo.common.utils.sse;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

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

/**
 * Des: sse工具类
 * Author: SiQiangMing 2025/5/28 15:50
 */
@Slf4j
public class SseEmitterUtil {
    /**
     * 使用map对象,便于根据userId来获取对应的SseEmitter,或者放redis里面
     */
    private final static Map<Long, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    /**
     * 用户创建sse链接
     * Author: SiQiangMing 2025/5/28 17:21
     * @param userId: 用户id
     * @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter
     */
    public static SseEmitter connect(Long userId) {
        // 设置超时时间,0表示不过期。默认30S,超时时间未完成会抛出异常:AsyncRequestTimeoutException
        SseEmitter sseEmitter = new SseEmitter(0L);

        // 注册回调
        sseEmitter.onCompletion(completionCallBack(userId));
        sseEmitter.onError(errorCallBack(userId));
        sseEmitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId, sseEmitter);

        log.info("----------------------------创建新的 SSE 连接,当前用户 {}, 连接总数 {}", userId
                , sseEmitterMap.size());
        return sseEmitter;
    }

    /**
     * 给制定用户发送消息
     * Author: SiQiangMing 2025/5/28 17:21
     * @param userId: 用户id
     * @param sseMessage: 消息
     * @return void
     */
    public static void sendMessage(Long userId, String sseMessage) {
        if (sseEmitterMap.containsKey(userId)) {
            try {
                sseEmitterMap.get(userId).send(sseMessage);
                log.info("----------------------------用户 {} 推送消息 {}", userId, sseMessage);
            } catch (IOException e) {
                log.error("----------------------------用户 {} 推送消息异常", userId);
                disconnect(userId);
            }
        } else {
            log.error("----------------------------消息推送 用户 {} 不存在,链接总数 {}"
                    , userId, sseEmitterMap.size());
        }
    }

    /**
     * 判断用户是否存在sse链接
     * Author: SiQiangMing 2025/5/29 10:02
     * @param userId:
     * @return boolean
     */
    public static boolean checkSseExist(Long userId) {
        if (userId == null) {
            return false;
        }
        return sseEmitterMap.containsKey(userId);
    }

    /**
     * 群发所有人,这里用来测试异常的排除链接
     * Author: SiQiangMing 2025/5/28 17:20
     * @param message: 消息
     * @return void
     */
    public static void batchSendMessage(String message) {
        sseEmitterMap.forEach((k, v) -> {
            try {
                v.send(message, MediaType.APPLICATION_JSON);
            } catch (IOException e) {
                log.error("----------------------------用户 {} 推送异常", k);
                disconnect(k);
            }
        });
    }

    /**
     * 移除用户连接
     * Author: SiQiangMing 2025/5/28 17:20
     * @param userId: 用户id
     * @return void
     */
    public static void disconnect(Long userId) {
        if (sseEmitterMap.containsKey(userId)) {
            sseEmitterMap.get(userId).complete();
            sseEmitterMap.remove(userId);
            log.info("----------------------------移除用户 {}, 剩余连接 {}", userId, sseEmitterMap.size());
        } else {
            log.error("-----------------------------移除用户 {} 已被移除,剩余连接 {}", userId, sseEmitterMap.size());
        }
    }

    /**
     * 结束回调
     * Author: SiQiangMing 2025/5/28 17:19
     * @param userId: 用户id
     * @return java.lang.Runnable
     */
    private static Runnable completionCallBack(Long userId) {
        return () -> {
            log.info("----------------------------用户 {} 结束连接", userId);
        };
    }

    /**
     * 超时回调
     * Author: SiQiangMing 2025/5/28 17:20
     * @param userId: 用户id
     * @return java.lang.Runnable
     */
    private static Runnable timeoutCallBack(Long userId) {
        return () -> {
            log.error("----------------------------用户 {} 连接超时", userId);
            disconnect(userId);
        };
    }

    /**
     * 异常回调
     * Author: SiQiangMing 2025/5/28 17:20
     * @param userId: 用户id
     * @return java.util.function.Consumer<java.lang.Throwable>
     */
    private static Consumer<Throwable> errorCallBack(Long userId) {
        return throwable -> {
            log.error("----------------------------用户 {} 连接异常", userId);
            disconnect(userId);
        };
    }
}

三、前端

前端创建链接,请求后端的创建接口,注意页面销毁的时候,关闭sse链接

mounted() {
    // sse链接
    createEventSource() {
      // new
      this.eventSource = new EventSource("后端的创建sse路径");
      // 收到消息
      this.eventSource.onmessage = (e) => {
        // 消息处理 e.data
      };
      // 异常处理
      this.eventSource.onerror = (error) => {
        console.error(error);
      };
    },
},
unmounted() {
    // 组件销毁时关闭 SSE 连接
    if (this.eventSource) {
      this.eventSource.close();
    }
},

3、创建sse

    /**
     * 处理客户端的连接请求
     * Author: SiQiangMing 2025/5/26 16:06
     * @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter
     */
    @GetMapping(value = "/xxx", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter xxx() {
        // 返回 SseEmitter 给客户端
        Long userId = SecurityUtils.getUserId();
        SseEmitter sseEmitter = SseEmitterUtil.connect(userId);
        // 可以直接带初始化信息返回
        SseEmitterUtil.sendMessage(userId, "消息");
        return sseEmitter;
    }

 四、nginx配置

        在这里遇到了一些问题,记录下解决方案。

1、在idea开发工具都正常,部署到生产环境后,sse后端能推送,前端没有收到消息。排查浏览器网络请求,nginx日志发现,客户端主动关闭了链接。

        在nginx.conf中增加配置

location /精准匹配sse创建路径 {
    add_header 'Cache-Control' 'no-cache'; //不缓存数据,每次请求时都会从服务器获取最新的内容
    proxy_set_header Connection ''; // 的作用是清除或覆盖 Connection头
    proxy_http_version 1.1;
    proxy_set_header Host $host; //确保后端服务器接收到的 Host 值与客户端原始请求的 Host 一致,或符合后端服务器的预期。
    proxy_pass http://xxx:port/sse创建路径;
}

2、SSE链接一分钟请求一次,频繁创建。

        在之前的配置中追加配置

location /精准匹配sse创建路径 {
    proxy_connect_timeout 3600s; // 解决1分钟重连,
    proxy_send_timeout 3600s; // 解决1分钟重连,
    proxy_read_timeout 3600s; // 解决1分钟重连,
    add_header 'Cache-Control' 'no-cache'; //不缓存数据,每次请求时都会从服务器获取最新的内容
    proxy_set_header Connection ''; // 的作用是清除或覆盖 Connection头
    proxy_http_version 1.1;
    proxy_set_header Host $host; //确保后端服务器接收到的 Host 值与客户端原始请求的 Host 一致,或符合后端服务器的预期。
    proxy_pass http://xxx:port/sse创建路径;
}

3、正常情况,链接保持了40分钟,还正常

4、并发数问题

        因为这里使用的http,所以版本是HTTP/1.1,同一个端口并发sse,只有6个,有两种解决方案,后期使用HTTP/2.0,默认100并发,满足要求。

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

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

相关文章

哈工大计算机系统大作业 程序人生-Hello’s P2P

计算机系统 大作业 题 目 程序人生-Hello’s P2P 专 业 计算机与电子通信 学   号 2023111772 班   级 23L0503 学 生 张哲瑞     指 导 教 师 …

计算机一次取数过程分析

计算机一次取数过程分析 1 取址过程 CPU由运算器和控制器组成&#xff0c;其中控制器中的程序计数器(PC)保存的是下一条指令的虚拟地址&#xff0c;经过内存管理单元(MMU)&#xff0c;将虚拟地址转换为物理地址&#xff0c;之后交给主存地址寄存器(MAR)&#xff0c;从主存中取…

Halcon联合QT ROI绘制

文章目录 Halcon 操纵界面代码窗口代码 Halcon 操纵界面代码 #pragma once#include <QLabel>#include <halconcpp/HalconCpp.h> #include <qtimer.h> #include <qevent.h> using namespace HalconCpp;#pragma execution_character_set("utf-8&qu…

力扣面试150题--二叉树的右视图

Day 53 题目描述 思路 采取层序遍历&#xff0c;利用一个high的队列来保存每个节点的高度&#xff0c;highb和y记录上一个节点的高度和节点&#xff0c;在队列中&#xff0c;如果队列中顶部元素的高度大于上一个节点的高度&#xff0c;说明上一个节点就是上一层中最右边的元素…

江西某石灰石矿边坡自动化监测

1. 项目简介 该矿为露天矿山&#xff0c;开采矿种为水泥用石灰岩&#xff0c;许可生产规模200万t/a&#xff0c;矿区面积为1.2264km2&#xff0c;许可开采深度为422m&#xff5e;250m。矿区地形为东西一北东东向带状分布&#xff0c;北高南低&#xff0c;北部为由浅变质岩系组…

C# 类和继承(所有类都派生自object类)

所有类都派生自object类 除了特殊的类object&#xff0c;所有的类都是派生类&#xff0c;即使它们没有基类规格说明。类object是唯 一的非派生类&#xff0c;因为它是继承层次结构的基础。 没有基类规格说明的类隐式地直接派生自类object。不加基类规格说明只是指定object为 基…

02业务流程的定义

1.要想用好业务流程&#xff0c;首先必须得了解流程与认识流程&#xff0c;什么是业务流程。在认识流程之前&#xff0c;首先要理清两个基本概念&#xff0c;业务和流程。 业务指的是&#xff1a;个人的或者摸个机构的专业工作。流程&#xff0c;原本指的是水的路程&#xff0…

cursor rules设置:让cursor按执行步骤处理(分析需求和上下文、方案对比、确定方案、执行、总结)

写在前面的话&#xff1a; 直接在cursor rules中设置一下内容&#xff1a; RIPER-5 MULTIDIMENSIONAL THINKING AGENT EXECUTION PROTOCOL 目录 RIPER-5 MULTIDIMENSIONAL THINKING AGENT EXECUTION PROTOCOL 目录 上下文与设置 核心思维原则 模式详解 模式1: RESEARCH…

Linux操作系统之进程(四):命令行参数与环境变量

目录 前言&#xff1a; 什么是命令行参数 什么是环境变量 认识环境变量 PATH环境变量 HOME USER OLDPWD 本地变量 本地变量与环境变量的差异 核心要点回顾 结语&#xff1a; 前言&#xff1a; 大家好&#xff0c;今天给大家带来的是一个非常简单&#xff0c;但也十…

Typora-macOS 风格代码块

效果&#xff1a; 替换 Typora安装目录中 themes 文件夹下的 base.user.css 文件&#xff0c;直接替换即可&#xff0c;建议先备份。 css&#xff1a; /* 语法高亮配色 */ .CodeMirror-line .cm-number { color: #b5cea8; } /* 数字 - 浅绿色 */ .CodeMirror-line .…

ansible自动化playbook简单实践

方法一&#xff1a;部分使用ansible 基于现有的nginx配置文件&#xff0c;定制部署nginx软件&#xff0c;将我们的知识进行整合 定制要求&#xff1a; 启动用户&#xff1a;nginx-test&#xff0c;uid是82&#xff0c;系统用户&#xff0c;不能登录 启动端口82 web项目根目录/…

20250526惠普HP锐14 AMD锐龙 14英寸轻薄笔记本电脑(八核R7-7730U)的显卡驱动下载

20250526惠普HP锐14 AMD锐龙 14英寸轻薄笔记本电脑(八核R7-7730U)的显卡驱动下载 2025/5/26 14:44 百度&#xff1a;AMD 7700 显卡驱动 amd APU 显卡驱动 https://item.jd.com/100054819707.html 惠普HP【国家补贴20%】锐14 AMD锐龙 14英寸轻薄笔记本电脑(八核R7-7730U 16G 1T…

2025年5月蓝桥杯stema省赛真题——象棋移动

上方题目可点下方去处&#xff0c;支持在线编程&#xff5e; 象棋移动_scratch_少儿编程题库学习中心-嗨信奥 程序演示可点下方&#xff0c;支持源码和素材获取&#xff5e; 象棋移动-scratch作品-少儿编程题库学习中心-嗨信奥 题库收集了历届各白名单赛事真题和权威机构考级…

AI重构SEO关键词精准定位

内容概要 随着AI技术深度渗透数字营销领域&#xff0c;传统SEO关键词定位模式正经历系统性重构。基于自然语言处理&#xff08;NLP&#xff09;的智能语义分析引擎&#xff0c;可突破传统关键词工具的局限性&#xff0c;通过解析长尾搜索词中的隐含意图与语境关联&#xff0c;…

SPSS跨域分类:自监督知识+软模板优化

1. 图1:SPSS方法流程图 作用:展示了SPSS方法的整体流程,从数据预处理到模型预测的关键步骤。核心内容: 领域知识提取:使用三种词性标注工具(NLTK、spaCy、TextBlob)从源域和目标域提取名词或形容词(如例句中提取“excellent”“good”等形容词)。词汇交集与聚类:对提…

vscode的Embedded IDE创建keil项目找不到源函数或者无法跳转

创建完Embedded IDE项目后跳转索引很容易找不到源函数或者无法跳转&#xff0c;原因是vscode工作区被eide覆盖了&#xff0c;需要手动往当前目录下的.vscode/c_cpp_properties.json里添加路径 打开eide.json &#xff0c;找到folders&#xff0c; 里面的name是keil里工程的虚拟…

构建高效智能客服系统的8大体验设计要点

构建一流的客户服务中心体验&#xff0c;企业需要以用户需求为核心&#xff0c;将智能化流程、前沿科技与人文关怀有机结合&#xff0c;打造流畅、高效且富有温度的服务生态。在客户需求日益多元化的今天&#xff0c;单纯的问题解决能力已无法满足期待&#xff0c;关键在于通过…

CppCon 2014 学习:Making C++ Code Beautiful

你说的完全正确&#xff0c;也很好地总结了 C 这门语言在社区中的两种典型看法&#xff1a; C 的优点&#xff08;Praise&#xff09; 优点含义Powerful允许底层控制、系统编程、高性能计算、模板元编程、并发等多种用途Fast无运行时开销&#xff0c;接近汇编级别性能&#x…

据传苹果将在WWDC上发布iOS 26 而不是iOS 19

苹果可能会对其操作系统的编号方式做出重大改变&#xff0c;基于年份的新版系统会将iOS 19重新命名为 iOS 26&#xff0c;同时 macOS 也会以同样的方式命名。 苹果的编号系统相当简单&#xff0c;版本号每年都会像钟表一样定期更新。然而&#xff0c;今年秋天情况可能有所不同&…

嵌入式开发STM32 -- 江协科技笔记

1.背景介绍及基础认知 8大输入输出 斯密特触发器&#xff1a;高于设定阈值输出高电平&#xff0c;低于设定阈值输出低电平 有关上拉输入、下拉输入、推挽输出、开漏输出、复用开漏输出、复用推挽输出以及浮空输入、模拟输入的区别 1、上拉输入&#xff1a;上拉就是把电位拉高…