springboot 微信消息推送 springboot sse推送

目录

一、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// 依赖H5var 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` 作为响应的 ContentTyperesponse.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秒,超过时间未完成会抛出异常:AsyncRequestTimeoutExceptionSseEmitter sseEmitter = new SseEmitter();// 注册回调sseEmitter.onCompletion(completionCallBack(userId));sseEmitter.onError(errorCallBack(userId));sseEmitter.onTimeout(timeoutCallBack(userId));// 缓存sseEmitterMap.put(userId, sseEmitter);// 数量+1count.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);// 数量-1count.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://xiahunao.cn/news/3280601.html

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

相关文章

Android 自定义圆形进度条样式

效果 代码 主要是设置属性indeterminateDrawable <ProgressBarandroid:id"id/iv_progress"android:layout_width"20dp"android:layout_height"20dp"android:layout_gravity"center"android:layout_marginStart"15dp"and…

清爽简洁!这可能是开源界功能最强大的项目开发管理系统

&#x1f482; 个人网站: IT知识小屋&#x1f91f; 版权: 本文由【IT学习日记】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 文章目录 写在前面项目简介项目特点设计思想技术栈项目展示项目获取 写在前…

❤️‍FlyFlow:新增表格布局表单

FlyFlow 介绍 官网地址&#xff1a;www.flyflow.cc ElementPlus演示网址&#xff1a;pro.flyflow.cc AntDesign演示网址&#xff1a;ant.flyflow.cc FlyFlow 借鉴了钉钉与飞书的界面设计理念&#xff0c;致力于打造一款用户友好、快速上手的工作流程工具。相较于传统的基于 …

21. Hibernate 性能之数据库连接池

1. 前言 从本节课程开始&#xff0c;和大家一起聊聊 Hibernate 中的性能问题&#xff0c;面对开发者&#xff0c;Hibernate 表现出卓越的数据库操作能力。 使用框架最大的优势就是带来操作的快捷、便利。同时&#xff0c;因为框架的封装性&#xff0c;其性能往往比原生开发要…

【PostGresql】---- pgSql 将列中合并字符串拆分为多行 实例代码

-- 将 AQY_ID,AQY 中的字符串拆分为多行 SELECT"ID","AQY_ID","AQY",UNNEST ( string_to_array( "AQY_ID", , ) ) AS "AQY_ID_1",UNNEST ( string_to_array( "AQY", , ) ) AS "AQY_1" FROM"JF_SGC…

30.jdk源码阅读之ReentrantReadWriteLock

1.写在前面 ReentrantReadWriteLock 是 Java 并发包中的一个读写锁实现&#xff0c;它允许多个读线程同时访问共享资源&#xff0c;但在写线程访问时&#xff0c;所有的读线程和其他写线程都会被阻塞。不知道大家在日常工作中这个类使用的多不多&#xff0c;对于它的底层实现有…

Tooltip 文字提示

在偶然维护前端开发时,遇到页面列表中某个字段内容太长,且该字段使用了组件显示,导致不能使用纯文本得那个省略号代替显示得css样式效果,如下 所以只能另辟溪路了, 1、最开始想到是使用横向滚动得效果来实现,但是实现后,感觉还是不太理想,因为用户注意不到你这里有滚动…

【基础篇】Docker 容器操作 FOUR

嘿&#xff0c;小伙伴们&#xff01;我是小竹笋&#xff0c;一名热爱创作的工程师。在上一篇文章中&#xff0c;我们探讨了 Docker 镜像管理的相关知识。今天&#xff0c;让我们一起深入了解一下 Docker 容器的操作吧&#xff01; &#x1f4e6; 运行、停止和删除容器 Docker…

一个私有化的中文笔记工具个人知识库,极空间Docker部署中文版『Trilium Notes』

一个私有化的中文笔记工具&个人知识库&#xff0c;极空间Docker部署中文版『Trilium Notes』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 最近被很多小伙伴问到NAS上的笔记工具&#xff0c;虽说之前也出过Memos&#xff0c;刚开始用起来还不错&#xff0c;但是用了一段时间…

【JKI SMO】框架讲解(六)

接JKI SMO 框架讲解&#xff08;五&#xff09;&#xff0c;现在对代码进行一个扩展&#xff0c;当前代码仅有一路电压采集&#xff0c;现在需要扩展一路电流采集通道。 下面是对应的步骤&#xff1a; 1.打开项目&#xff0c;在工具里打开SMO Editor。 2.之前创建的SMO会自动加…

快速收集地图商户信息_百度|高德|腾讯|google

数字化营销中企业名录和商家电话号码的采集已成为营销人员日常工作的首要一环。地图平台以其海量的商家信息和实时更新的特性&#xff0c;成为我们获取数据的宝贵渠道。如何快速利用百度、高德、腾讯这三大地图平台高效采集商家联系方式是每个营销人员的必备技能。 我们整理了…

记录某次“有趣的“挖矿木马排查

挖矿木马是什么&#xff1f; 挖矿木马是一种恶意软件&#xff0c;它在用户不知情或未经同意的情况下&#xff0c;利用受害者的计算机资源进行加密货币挖矿。这类软件通过执行大量运算来挖掘数字货币&#xff0c;如比特币或门罗币等。挖矿木马通常通过漏洞利用、弱口令爆破或非…

微软蓝屏事件:全球网络安全与系统稳定性的警示

文章目录 每日一句正能量前言探讨软件更新流程中的风险管理和质量控制机制软件更新的风险风险管理策略质量控制措施测试流程缺陷识别实施质量控制结论 提供预防类似大规模故障的最佳方案或应急响应对策设计冗余系统实施灾难恢复计划建立高可用架构应急响应对策利用自动化工具和…

豆瓣9.9分!大名鼎鼎的深度学习入门书“鱼书”更新第二版了!带你深刻理解神经网络!

大家应该都知道这本非常有名的著作吧&#xff1a;《深度学习入门&#xff0c;基于python的理论与实现》。 这是更新的第二版&#xff0c;是由日本作者斋藤康毅所著的一本关于深度学习的书籍。这本书的出版社是人民邮电出版社&#xff0c;出品方是图灵教育。这本书的原名是“ゼロ…

vue给数组对象赋值改变对象里面的数据,数据没有更新this.$set

替换数组startTime的值&#xff1a; 原数据 this.serviceTimeList.push({serviceTimeName: 服务时间段,startTime: this.startTime,endTime: this.endTime,currentDateStart: this.currentDate,currentDateEnd: this.currentDate}) this.$set(this.array, index, newValue); …

Day12--Servlet实现前后端交互(案例:学生信息管理系统登录页面)

&#xff08;在一个完整的项目架构中&#xff0c;servlet的角色和位置&#xff09; Servlet、GenericServlet和HttpServlet三者之间的关系是Java Web开发中的一个重要概念&#xff0c;它们共同构成了基于Java的服务器端程序的基础。以下是具体分析&#xff1a; 1. Servlet接口…

AI技术革命对未来就业的影响

一、引言 AI技术革命带来的就业结构变化是双面的&#xff0c;既存在着挑战也蕴含着机遇。过去的几次技术革命例如工业革命和信息技术革命都对就业结构产生了深刻的影响&#xff0c;使一些工作消失&#xff0c;也催生了许多新的工作类型。当前和未来的AI技术发展也必将推动类似…

【HadoopShuffle原理剖析】基础篇二

Shuffle原理剖析 Shuffle&#xff0c;是指对Map输出结果进行分区、排序、合并等处理并交给Reduce的过程。分为Map端的操作和Reduce端的操作。 Shuffle过程 Map端的Shuffle Map的输出结果首先被缓存到内存&#xff0c;当缓存区容量到达80%&#xff08;缓冲区默认100MB&#xff…

基于FPGA的数字信号处理(20)--半加器和全减器

1、前言 既然有半加器和全加器&#xff0c;那自然也有半减器和全减器了。尽管在电路中减法的实现基本都是 补码 加法 的形式&#xff0c;但是正所谓技多不压身&#xff0c;了解一下半减器和全减器还是有一定作用的&#xff0c;至少能扩宽知识面嘛。 2、半减器 最简单的减法器叫…

RFID分体式天线如何选购?

分体式读写器天线具有可与RFID读写器分离部署&#xff0c;在远距离读写、集成应用等场景下拥有十分广泛的应用。下面我们就跟大家一起来了解一下&#xff0c;分体式天线选购的要点有哪些? 分体式天线的选购要点主要包括以下几个方面&#xff1a; 一、明确使用需求 应用场景…