springboot 微信消息推送 springboot sse推送

news2024/9/17 7:50:58

目录

一、springboot 微信消息推送 springboot sse推送

1、在 Spring 框架中实现

2、传统的 Servlet 中实现


一、springboot 微信消息推送 springboot sse推送

关于 SSE
SSE 全程 Server Send Event,是 HTTP 协议中的一种,Content-Type 为 text/event-stream,是服务端主动向前端推送数据。类似于 WebSocket。

SSE 优势我们可以划分为两个:

长链接
服务端能主动向客户端推送数据

这里我们用 sse 与 websocket 对比下:

ssewebsocket
centered 文本居中
right-aligned 文本居右
http 协议独立的websocket 协议轻量,使用简单相对复杂
默认支持断线重连需要本身实现断线重连
文本传输二进制传输
支持自定义发送的消息类型-

另外在 响应式流里面,可以多次返回数据(其实和响应式没有关系),使用的技术就是H5的SSE。

返回的数据有固定的要求格式。

sse 规范

在 html5 的定义中,服务端 sse,通常须要遵循如下要求

请求头

开启长链接 + 流方式传递

Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive

数据格式

服务端发送的消息,由 message 组成,其格式以下:

field:value\n\n

其中 field 有五种可能

  • 空: 即以 : 开头,表示注释,能够理解为服务端向客户端发送的心跳,确保链接不中断
  • data:数据
  • event: 事件,默认值
  • id: 数据标识符用 id 字段表示,至关于每一条数据的编号
  • retry: 重连时间

1、在 Spring 框架中实现

这里需要允许跨域

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

/**
 * @author wuq
 * @Time 2022-12-19 15:02
 * @Description
 */
@CrossOrigin(origins = "*", maxAge = 3600)
@Controller
public class SSEController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Locale locale, Model model) {
        return "sse";
    }

    @RequestMapping(value = "push")
    public void push(HttpServletResponse response) throws IOException {
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");

        for (int i = 0; i < 5; i++) {
            // 指定事件标识  event: 这个为固定格式
            response.getWriter().write("event:me\n");
            // 格式:data: + 数据 + 2个回车
            response.getWriter().write("data:" + i + "\n\n");
            response.getWriter().flush();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

前端测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <div id="message"></div>
</div>
<script type="text/javascript">
    // 初始化,参数为url
    // 依赖H5
    var sse = new EventSource("http://localhost:8080/push")

    // 监听消息并打印
    sse.onmessage = function (evt) {
        console.log("message", evt.data, evt)
    }

    // 如果指定了事件标识需要用这种方式来进行监听事件流
    sse.addEventListener("me", function (evt) {
        console.log("me event", evt.data)
        // 事件流如果不关闭会自动刷新请求,所以我们需要根据条件手动关闭
        if (evt.data == 3) {
            sse.close();    // 注释掉这个可以看到自动重连的效果
        }

        setMessageInnerHTML(evt.data);
    })

    // 将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }
</script>
</body>
</html>

一起来看下效果

2、传统的 Servlet 中实现

至于为什么要关注与 传统的 Servlet 实现,是因为这样子的,之前看过一篇文章,文章上面说离开了 Spring 几乎都不会写代码了,的确是有这样子的感觉,现在离开了 Spring 连传统的 Servlet 都快忘记完了。感觉还是有必要了解下原始的写法,不过下面的工程还是基于 Spring 的工程来的,这样子搭建最简单。

需要在启动类中增加 @ServletComponentScan

@SpringBootApplication
@ServletComponentScan    //  需要增加这个
public class SSEApplication {

    public static void main(String[] args) {
        SpringApplication.run(SSEApplication.class, args);
    }
}

对应的后端代码

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

@WebServlet(name = "SSE", urlPatterns = "/SSE")
public class SSE extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/event-stream");  // 这里可以了解到是使用的 `text/event-stream` 作为响应的 ContentType
        response.setCharacterEncoding("utf-8");

        for (int i = 0; i < 5; i++) {
            // 指定事件标识
            response.getWriter().write("event:me\n");
            // 格式:data: + 数据 + 2个回车
            response.getWriter().write("data:" + i + "\n\n");
            response.getWriter().flush();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用场景:服务器向客户端推送数据,例如聊天室

3、封装工具类实现

SpringMVC封装的SSE实现,Controller 中直接返回 SseEmitter,不调用 complete() 方法,即可保持长链接。

创建一个工具类 SseEmitterUtil

package com.demo.test.sse.controller;

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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

@Slf4j
public class SseEmitterUtil {

    /**
     * 当前连接数
     */
    private static AtomicInteger count = new AtomicInteger(0);

    /**
     * 使用 map 对象,便于根据 userId 来获取对应的 SseEmitter,或者放 redis 里面
     */
    private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    /**
     * 创建用户连接并返回 SseEmitter
     *
     * @param userId 用户ID
     * @return SseEmitter
     */
    public static SseEmitter connect(String userId) {
        // 设置超时时间,0表示不过期。默认30秒,超过时间未完成会抛出异常:AsyncRequestTimeoutException
        SseEmitter sseEmitter = new SseEmitter();
        // 注册回调
        sseEmitter.onCompletion(completionCallBack(userId));
        sseEmitter.onError(errorCallBack(userId));
        sseEmitter.onTimeout(timeoutCallBack(userId));

        // 缓存
        sseEmitterMap.put(userId, sseEmitter);

        // 数量+1
        count.getAndIncrement();
        log.info("创建新的sse连接,当前用户:{}", userId);
        return sseEmitter;
    }

    /**
     * 给指定用户发送信息
     */
    public static void sendMessage(String userId, String message) {
        if (!sseEmitterMap.containsKey(userId)) return;

        try {
            // sseEmitterMap.get(userId).send(message, MediaType.APPLICATION_JSON);
            sseEmitterMap.get(userId).send(message);
        } catch (IOException e) {
            log.error("用户[{}]推送异常:{}", userId, e.getMessage());
            removeUser(userId);
        }
    }

    /**
     * 群发消息
     */
    public static void batchSendMessage(String wsInfo, List<String> ids) {
        ids.forEach(userId -> sendMessage(wsInfo, userId));
    }

    /**
     * 群发所有人
     */
    public static void batchSendMessage(String wsInfo) {
        sseEmitterMap.forEach((k, v) -> {
            try {
                v.send(wsInfo, MediaType.APPLICATION_JSON);
            } catch (IOException e) {
                log.error("用户[{}]推送异常:{}", k, e.getMessage());
                removeUser(k);
            }
        });
    }

    /**
     * 移除用户连接
     */
    public static void removeUser(String userId) {
        sseEmitterMap.remove(userId);
        // 数量-1
        count.getAndDecrement();
        log.info("移除用户:{}", userId);
    }

    /**
     * 获取当前连接信息
     */
    public static List<String> getIds() {
        return new ArrayList<>(sseEmitterMap.keySet());
    }

    /**
     * 获取当前连接数量
     */
    public static int getUserCount() {
        return count.intValue();
    }

    private static Runnable completionCallBack(String userId) {
        return () -> {
            log.info("结束连接:{}", userId);
            removeUser(userId);
        };
    }

    private static Runnable timeoutCallBack(String userId) {
        return () -> {
            log.info("连接超时:{}", userId);
            removeUser(userId);
        };
    }

    private static Consumer<Throwable> errorCallBack(String userId) {
        return throwable -> {
            log.info("连接异常:{}", userId);
            removeUser(userId);
        };
    }
}

controller 中的写法

package com.demo.test.sse.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("sse")
public class SseRestController {

    /**
     * 用于创建连接
     */
    @GetMapping("/connect/{userId}")
    public SseEmitter connect(@PathVariable String userId) {
        return SseEmitterUtil.connect(userId);
    }

    /**
     * 推送给所有人
     *
     * @param message
     * @return
     */
    @GetMapping("/push/{message}")
    public String push(@PathVariable(name = "message") String message) {
        //获取连接人数
        int userCount = SseEmitterUtil.getUserCount();
        //如果无在线人数,返回
        if(userCount<1){
            return "无人在线!";
        }
        SseEmitterUtil.batchSendMessage(message);
        return "发送成功!";
    }

    /**
     * 发送给单个人
     *
     * @param message
     * @param userid
     * @return
     */
    @GetMapping("/push_one/{messsage}/{userid}")
    public String pushOne(@PathVariable(name = "message") String message, @PathVariable(name = "userid") String userid) {
        SseEmitterUtil.sendMessage(userid, message);
        return "推送消息给" + userid;
    }

    /**
     * 关闭连接
     */
    @GetMapping("/close/{userid}")
    public String close(@PathVariable("userid") String userid) {
        SseEmitterUtil.removeUser(userid);
        return "连接关闭";
    }
}

前端测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>消息推送</title>
</head>
<body>
    <div>
        <button onclick="closeSse()">关闭连接</button>
        <div id="message"></div>
    </div>
</body>

<script>
    let source = null;

    // 用时间戳模拟登录用户
    const userId = new Date().getTime();

    if (window.EventSource) {

        // 建立连接
        source = new EventSource('http://localhost:8080/sse/connect/' + userId);

        /**
         * 连接一旦建立,就会触发open事件
         * 另一种写法:source.onopen = function (event) {}
         */
        source.addEventListener('open', function (e) {
            setMessageInnerHTML("建立连接。。。");
        }, false);

        /**
         * 客户端收到服务器发来的数据
         * 另一种写法:source.onmessage = function (event) {}
         */
        source.addEventListener('message', function (e) {
            setMessageInnerHTML(e.data);
        });


        /**
         * 如果发生通信错误(比如连接中断),就会触发error事件
         * 或者:
         * 另一种写法:source.onerror = function (event) {}
         */
        source.addEventListener('error', function (e) {
            if (e.readyState === EventSource.CLOSED) {
                setMessageInnerHTML("连接关闭");
            } else {
                console.log(e);
            }
        }, false);

    } else {
        setMessageInnerHTML("你的浏览器不支持SSE");
    }

    // 监听窗口关闭事件,主动去关闭sse连接,如果服务端设置永不过期,浏览器关闭后手动清理服务端数据
    window.onbeforeunload = function () {
        closeSse();
    };

    // 关闭Sse连接
    function closeSse() {
        source.close();
        const httpRequest = new XMLHttpRequest();
        httpRequest.open('GET', 'http://localhost:8080/sse/close/' + userId, true);
        httpRequest.send();
        console.log("close");
    }

    // 将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }
</script>
</html>

注意
客户端关闭了连接,不管是调用了event.target.close() 还是关闭了网页,服务端不会触发任何回调。直到服务端调用 send 后才会触发onError和onCompletion回调。

服务端触发了onCompletion回调后,连接就断开了。

参考链接

 https://www.zhangbj.com/p/1262.html
 http://www.javashuo.com/article/p-uelldoqc-ds.html

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

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

相关文章

MySQL存储引擎MyISAM和InnoDB

1.1MySQL存储引擎 1.1.1概述 1、什么是存储引擎 MySQL中的数据用各种不同的技术存储在文件&#xff08;或内存&#xff09;中。这些技术中的每一种都使用不同的存储机制、索引技巧、锁定水平并且提供广泛的、不同的功能和能力&#xff1b;通过选择不同的技术&#xff0c;能够…

JavaScript - 如何安装和配置Yarn包管理器

作者&#xff1a;逍遥Sean 简介&#xff1a;一个主修Java的Web网站\游戏服务器后端开发者 主页&#xff1a;https://blog.csdn.net/Ureliable 觉得博主文章不错的话&#xff0c;可以三连支持一下~ 如有疑问和建议&#xff0c;请私信或评论留言&#xff01; 前言 Yarn是一个快速…

Langchain--如何使用大模型 2.0

【&#x1f34a;易编橙终身成长社群&#x1f34a;】 大家好&#xff0c;我是小森( &#xfe61;ˆoˆ&#xfe61; ) &#xff01; 易编橙终身成长社群创始团队嘉宾&#xff0c;橙似锦计划领衔成员、阿里云专家博主、腾讯云内容共创官、CSDN人工智能领域优质创作者 。 Langch…

集合竞价逐笔数据验证,level2行情接口验证

最近做集合竞价的策略&#xff0c;用的level2数据。集合竞价阶段推送数据量很大&#xff0c;但是不确定有没有因为网络原因的数据纰漏&#xff0c;所以需要验证一下。 把今天所有的数据记录了日志&#xff0c;其中筛选了09:25集合竞价的推送&#xff1a; grep 2024/07/29 09:2…

Linux服务器安装Zabbix7.0客户端实战记录和问题记录

1、获取最新的Zabbix客户端包&#xff0c;不同的linux的系统选择不同的安装包 阿里云镜像站 按照一下的格式惊醒编辑替换自己的安装包 openEuler&#xff1a;rpm -Uvh https://mirrors.aliyun.com/zabbix/zabbix/7.0/rhel/9/x86_64/zabbix-release-7.0-4.el9.noarch.rpm?sp…

Python 实现股票指标计算——DKX

DKX - 多空线 1 公式 MID&#xff1a;(3*CLOSELOWOPENHIGH)/6&#xff1b; DKX&#xff1a;(20*MID)19*REF(MID&#xff0c;1)18*REF(MID&#xff0c;2)17*REF(MID&#xff0c;3) 16*REF(MID&#xff0c;4)15*REF(MID&#xff0c;5)14*REF(MID&#xff0c;6) 13*REF(MID&a…

springboot高职院校毕业生信息管理系统-计算机毕业设计源码27889

摘 要 基于Java语言开发的高职院校毕业生信息管理系统旨在提供一个便捷、高效的方式来管理毕业生的相关信息。系统包括学生基本信息管理、成绩管理、就业信息管理等模块&#xff0c;通过界面友好、操作简单的设计&#xff0c;方便管理员快速查询和更新学生信息。系统还提供数据…

Vue2从基础到实战(指令修饰符)详解

什么是指令修饰符&#xff1f; 指令修饰符就是通过“.”指明一些指令后缀 不同的后缀封装了不同的处理操作 —> 简化代码 按键修饰符 keyup.enter —>当点击enter键的时候才触发 v-model修饰符 v-model.trim —>去除首位空格 v-model.number —>转数字 事件修…

使用docker在CentOS 7上安装php+mysql+nginx环境教程并运行WordPress

文章目录 一、安装docker1、切换yum源并更新系统2、卸载旧版docker3、配置Docker的yum库4、安装Docker5、启动和校验Docker6、配置镜像加速6.1、注册阿里云账号6.2、开通镜像服务6.3、配置镜像加速二、部署php+mysql+nginx环境1、准备目录结构2、拉取镜像3、运行容器并从中拷贝…

【Opencv】模糊

消除噪声 用该像素周围的平均值代替该像素值 4个函数 blur():最经典的 import os import cv2 img cv2.imread(os.path.join(.,dog.jpg)) k_size 7 #窗口大小&#xff0c;数字越大&#xff0c;模糊越强 img_blur cv2.blur(img,(k_size,k_size)) #窗口是正方形&#xff…

用依赖倒置和控制反转,突破Golang循环调用限制之后的思考

在软件开发中&#xff0c;随着项目规模的扩大和业务逻辑的复杂化&#xff0c;重构代码变得越来越重要。本文将介绍如何在既有代码基础上&#xff0c;通过依赖倒置&#xff08;DIP&#xff09;和控制反转&#xff08;IoC&#xff09;&#xff0c;实现新增加的代码可以循环引用到…

UI设计经验心得:优化设计流程与实战技巧分享

随着互联网的快速发展&#xff0c;UI 设计在中国也逐渐发展起来。UI 设计的目的不仅仅是让用户享受视觉享受&#xff0c;而是解决用户如何与互联网设备互动。因此&#xff0c;UI 设计是通过深入研究用户的使用习惯和操作逻辑来设计界面的互动和视觉效果。那么&#xff0c;UI 设…

邮箱API在CRM系统中如何高效的应用与集成?

邮箱API的高级功能和使用指南&#xff1f;怎么安全集成邮箱API&#xff1f; CRM系统已成为企业与客户保持联系的关键工具。通过集成邮箱API&#xff0c;企业可以大幅提升CRM系统的功能和效率。AokSend将探讨邮箱API在CRM系统中的高效应用与集成。 邮箱API&#xff1a;主要功能…

龙迅LT8642UXE 矩阵HDMI *4转HDMI *2输出切换芯片,支持HDMI 2.0,可带HDCP

LT8642UXE描述&#xff1a; LT8642UXE HDMI2.0/1.4交换机具有4&#xff1a;2的开关&#xff0c;符合HDMI2.0/1.4规格&#xff0c;最大6Gbps高速数据速率&#xff0c;自适应均衡RX输入和预先强调的TX输出&#xff0c;以支持长电缆应用程序。LT8642UXE HDMI2.0/1.4交换机自动检测…

JVM—运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 1、程序计数器—线程私有 字节码解释器工作时通过改变这个计数器的值&#xff0c;选取下一条执行的字节码指令。程序计数器是程序控制的指示器&#xff0c;分支、循环、跳转、异常处理、线…

适合证券公司的跨网传输解决方案,了解一下

证券公司由于其业务特性&#xff0c;涉及大量的敏感财务数据和交易信息&#xff0c;因此通常会在内部实施网络隔离措施。目的是为了保护数据免受未授权访问和网络攻击&#xff0c;确保数据的安全性和保密性&#xff0c;因此急需寻找安全可靠的跨网传输解决方案&#xff0c;实现…

拆解了数百个独立站,我们总结的高转化页面布局精华

高转化的独立站的页面布局都具备什么要素呢&#xff1f; 我们研究了几百个独立站&#xff0c;结合我们自己的项目经验&#xff0c;总结出转化率相对高的网站布局&#xff0c;都有以下注意的要点清单&#xff1a; 1 品牌Logo基本放在头部左边或者中间 2 首屏大banner 3 社交媒…

【可能是全网最丝滑的LangChain教程】十九、LangChain进阶之Agents

幸福&#xff0c;不是长生不老&#xff0c;不是大鱼大肉&#xff0c;不是权倾朝野。幸福是每一个微小的生活愿望达成。当你想吃的时候有得吃&#xff0c;想被爱的时候有人来爱你。 01 Agent介绍 在LangChain中&#xff0c;Agent 是一个核心概念&#xff0c;它代表了一种能够利…

CTF Web信息搜集 25000字详解

目录 前言信息收集常见信息分类域名信息whois备案CDN子域名解析记录 旁站C段服务器信息端口服务器类型数据库类型waf防火墙 网站信息备份文件备份文件常见的后缀名备份文件常见的文件名gedit备份文件vim备份文件收集方法 敏感目录CMS类型&#xff08;指纹识别&#xff09;探针泄…

牛客JS题(十二)列表动态渲染

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 忍者码风reduce注意事项 题干&#xff1a; 我的答案 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"></head><body><ul></ul><…