【ServerSentEvents】服务端单向消息推送
依赖库:
Springboot 不需要而外支持,WebMVC组件自带的类库
浏览器要检查内核是否支持EventSource库
Springboot搭建SSE服务:
这里封装一个服务Bean, 用于管理SSE的会话,推送消息
package cn.hyite.amerp.common.sse; import cn.hyite.amerp.common.log.HyiteLogger;
import cn.hyite.amerp.common.log.LogFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; /**
* 服务单向消息传递,SeverSentEvents web端自动重连
*
* @author Cloud9
* @version 1.0
* @project amerp-server
* @date 2022年11月10日 10:04
*/
@Service
public class SeverSentEventService { private static final HyiteLogger logger = LogFactory.getLogger(SeverSentEventService.class); /**
* SSE客户端管理容器
*/
private static final Map<String, SseEmitter> SESSION_MAP = new ConcurrentHashMap<>(); /**
* 连接超时时限1小时 (客户端不活跃的时长上限?)
*/
private static final Long TIME_OUT = 1000L * 60L * 60L; /**
* 根据客户端标识创建SSE连接
*
* @param clientId
* @return SseEmitter
* @author Cloud9
* @date 2022/11/10 10:17
*/
public SseEmitter createConnection(String clientId) {
SseEmitter emitter = new SseEmitter(TIME_OUT);
emitter.onTimeout(() -> {
logger.info(" - - - - SSE连接超时 " + clientId + " - - - - ");
SESSION_MAP.remove(clientId);
});
emitter.onCompletion(() -> {
logger.info(" - - - - - SSE会话结束 " + clientId + " - - - - ");
SESSION_MAP.remove(clientId);
});
emitter.onError(exception -> {
logger.error("- - - - - SSE连接异常 " + clientId + " - - - - -", exception);
SESSION_MAP.remove(clientId);
}); SESSION_MAP.put(clientId, emitter);
return emitter;
} /**
* 根据客户端令牌标识,向目标客户端发送消息
*
* @param clientId
* @param message
* @author Cloud9
* @date 2022/11/10 10:17
*/
public void sendMessageToClient(String clientId, String message) {
SseEmitter sseEmitter = SESSION_MAP.get(clientId); /* 如果客户端不存在,不执行发送 */
if (Objects.isNull(sseEmitter)) return;
try {
SseEmitter.SseEventBuilder builder = SseEmitter
.event()
.data(message);
sseEmitter.send(builder);
} catch (Exception e) {
logger.error("- - - - Web连接会话中断, ClientId:" + clientId + "已经退出 - - - - ", e);
/* 结束服务端这里的会话, 释放会话资源 */
sseEmitter.complete();
SESSION_MAP.remove(clientId);
}
} /**
* 给所有客户端发送消息
*
* @param message
* @author Cloud9
* @date 2022/11/10 10:18
*/
public void sendMessageToClient(String message) {
SseEmitter.SseEventBuilder builder = SseEmitter
.event()
.data(message); for (String clientId : SESSION_MAP.keySet()) {
SseEmitter emitter = SESSION_MAP.get(clientId);
if (Objects.isNull(emitter)) continue;
try {
emitter.send(builder);
} catch (Exception e) {
logger.error("- - - - Web连接会话中断, ClientId:" + emitter + "已经退出 - - - - ", e);
emitter.complete();
SESSION_MAP.remove(clientId);
}
}
} }
接口和写普通接口有小小区别:
package cn.hyite.amerp.approve.controller; import cn.hyite.amerp.approve.dto.ApproveDTO;
import cn.hyite.amerp.common.Constant;
import cn.hyite.amerp.common.activiti.ActivitiUtils;
import cn.hyite.amerp.common.activiti.ProcessInstanceQueryDTO;
import cn.hyite.amerp.common.activiti.TaskQueryDTO;
import cn.hyite.amerp.common.annotation.IgnoreLoginCheck;
import cn.hyite.amerp.common.config.mybatis.page.PageResult;
import cn.hyite.amerp.common.dingtalk.DingDingUtils;
import cn.hyite.amerp.common.security.LoginUserContext;
import cn.hyite.amerp.common.security.UserContext;
import cn.hyite.amerp.common.sse.SeverSentEventService;
import cn.hyite.amerp.common.util.PageUtil;
import cn.hyite.amerp.system.dingtalk.todotask.dto.SysDiTodotaskDTO;
import cn.hyite.amerp.system.dingtalk.todotask.service.ISysDiTodotaskService;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.task.TaskQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; /**
*
* @author cloud9
* @version 1.0
* @project
* @date 2022-09-27
*/
@RestController
@RequestMapping("${api.path}/approve")
public class ApproveController { @Resource
private SeverSentEventService severSentEventService; /**
* web连接的SSE目标地址, 保管凭证并返回这个会话资源给web,注意produces属性必须是文本事件流形式
* 凭证可以使用@PathVariable或者Url参数获取
* /approve/sse/message
* @param token
* @return SseEmitter
* @author cloud9
* @date 2022/11/10 09:57
*
*/
@IgnoreLoginCheck
@GetMapping(value = "/sse/message", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter message(@RequestParam("clientToken") String token) {
return severSentEventService.createConnection(token);
} /**
* 提供给POSTMAN测试调用,方便给web发送消息调试
* /approve/sse/message/send
* @param map
* @author cloud9
* @date 2022/11/10 10:25
*
*/
@IgnoreLoginCheck
@PostMapping("/sse/message/send")
public void send(@RequestBody Map<String, String> map) {
String client = map.get("client");
String txt = map.get("txt");
severSentEventService.sendMessageToClient(client, txt);
} }
检测SSE服务是否正常,可以直接浏览器访问目标接口:
发送消息,查看是否能够接收:
编写SSE客户端EventSource
使用JS原生EventSource编写:
三个钩子函数:open, error, message
message 数据在event对象的data属性
initialSseConnection() {
const isSupportSse = 'EventSource' in window
if (!isSupportSse) {
console.log('不支持SSE')
return
}
const clientId = 1001 // 假设这是客户端标识令牌,根据实际情况动态设置
const url = `${window._config.default_service}approve/sse/message/?clientToken=${clientId}`
const eventSource = new EventSource(url, { withCredentials: true })
console.log(`url ${eventSource.url}`)
this.sseConnection = eventSource
eventSource.onopen = function(event) {
console.log(`SSE连接建立 ${eventSource.readyState}`)
} const that = this
eventSource.onmessage = function(event) {
console.log('id: ' + event.lastEventId + ', data: ' + event.data)
++that.unreadDeliverTotal
}
eventSource.onerror = function(error) {
console.log('connection state: ' + eventSource.readyState + ', error: ' + error)
}
},
在创建实例周期函数时调用:
created() {
this.initialSseConnection()
},
在实例销毁时,释放连接资源:
beforeDestroy() {
if (this.sseConnection) this.sseConnection.close()
}
NPM仓库提供了一个封装库:
https://www.npmjs.com/package/vue-sse
默认npm我这下不动,改用cnpm就可以了,按文档CV就可以直接用
这里不赘述了
实践效果:
初始化时,open方法触发:
HTTP接口状态可以查看服务发送的信息:
PostMan调用开放的接口:
Post请求
http://localhost:8091/amerp-server/api/approve/sse/message/send JsonBody:
{
"client": "1001",
"txt": "{ 'id': 1001, 'msg': 'success qsadasdasd' }"
}
注意客户端不要混用令牌标识,这将导致前一个客户端的连接资源被新的客户端覆盖
SseEmitterBuilder发射器对象API
SseEmitter.SseEventBuilder builder = SseEmitter.event();
/* 目前不知道干嘛用的 */
builder.comment("注释数据...");
/* 设置一个消息ID,要有唯一性意义,客户端可以通过event.lastEventId 获取这个ID */
builder.id(UUID.randomUUID(true).toString());
/* 设置这个发射器的名称,也就是事件的名称,如果设置了,客户端则调用这个名字的处理方法,不会执行onmessage */
builder.name("自定义处理器名");
/* 设置消息数据 */
builder.data(message); /* 发射器可以不设置上述内容发送消息 */
emitter.send(builder);
参考来源:
https://oomake.com/question/9520150
自定义事件处理案例:
这里我设置的是customHandler
public void sendMessageToClient(String message) {
for (String clientId : SESSION_MAP.keySet()) {
final SseEmitter emitter = SESSION_MAP.get(clientId);
if (Objects.isNull(emitter)) continue; SseEmitter.SseEventBuilder builder = SseEmitter.event();
builder.comment("注释数据...");
builder.name("customHandler");
builder.id(UUID.randomUUID(true).toString());
builder.data(message);
try {
emitter.send(builder);
} catch (Exception e) {
log.error("- - - - - 发送给客户端:{} 消息失败,异常原因:{}", clientId, e.getMessage());
log.error("- - - - - 客户端已经下线,开始删除会话连接 - - - - - ");
emitter.completeWithError(e);
SESSION_MAP.remove(clientId);
}
}
}
客户端的编写是这样的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE客户端连接测试</title>
</head>
<body> <script>
const clientId = 'admin-' + new Date().getTime()
const url = `http://127.0.0.1:8080/test/sse/release?clientId=${clientId}`
const connection = new EventSource(url, { withCredentials: true }) connection.onopen = () => {
console.log(`连接已建立,状态: ${connection.readyState}`)
console.log(`连接地址: ${connection.url}`) } connection.onerror = event => {
console.log(JSON.stringify(event))
} connection.onmessage = event => {
console.log(`- - - - 收到服务器消息 - - - - - -`)
console.log(`event.lastEventId -> ${event.lastEventId}`)
console.log(`event.data -> ${event.data}`)
} customHandler = event => {
debugger
console.log(`- - - - 收到服务器消息(自定义处理器) - - - - - -`)
console.log(`event.lastEventId -> ${event.lastEventId}`)
console.log(`event.data -> ${JSON.stringify(event)}`)
} connection.addEventListener('customHandler', customHandler, true); </script> </body>
</html>
可以检查event对象,类型变为我们对发射器对象设置的name值了
思路场景:
可以通过url参数传入后台设置动态的接收器对象?
一般来说好像用不上
最后测试一个JSON解析:
connection.onmessage = event => {
console.log(`- - - - 收到服务器消息 - - - - - -`)
console.log(`event.lastEventId -> ${event.lastEventId}`)
console.log(`event.data -> ${event.data}`) // "{ \"a\": 100, \"b\": \"javascript\", \"c\": true }"
const obj = JSON.parse( JSON.parse(event.data))
console.log(typeof obj)
console.log(obj.a)
}
【ServerSentEvents】服务端单向消息推送的更多相关文章
- spring集成webSocket实现服务端向前端推送消息
原文:https://blog.csdn.net/ya_nuo/article/details/79612158 spring集成webSocket实现服务端向前端推送消息 1.前端连接webso ...
- java服务端的 极光推送
项目中用到了极光推送 下面写下笔记 首先引入jar包 下载地址https://docs.jiguang.cn/jpush/resources/(非maven项目的下载地址) <depend ...
- 服务端向客户端推送消息技术之websocket的介绍
websocket的介绍 在讲解WebSocket前,我们先来看看下面这种场景,在HTTP协议下,怎么实现. 需求: 在网站中,要实现简单的聊天,这种情况怎么实现呢?如下图: 当发送私信的时候,如果要 ...
- C#服务端通过Socket推送数据到Android端App中
需求: 描述:实时在客户端上获取到哪些款需要补货. 要求: 后台需要使用c#,并且哪些需要补货的逻辑写在公司框架内,客户端采用PDA(即Android客户端 版本4.4) . 用户打开了补货通知页面时 ...
- iOS 消息推送原理
一.消息推送原理: 在实现消息推送之前先提及几个于推送相关概念,如下图: 1. Provider:就是为指定IOS设备应用程序提供Push的服务器,(如果IOS设备的应用程序是客户端的话,那么Prov ...
- iOS 消息推送原理及实现总结
在实现消息推送之前先提及几个于推送相关概念,如下图:1. Provider:就是为指定IOS设备应用程序提供Push的服务器,(如果IOS设备的应用程序是客户端的话,那么Provider可以理解为服务 ...
- iOS 消息推送原理及实现总结 分类: ios技术 2015-03-01 09:22 70人阅读 评论(0) 收藏
在实现消息推送之前先提及几个于推送相关概念,如下图: 1. Provider:就是为指定IOS设备应用程序提供Push的服务器,(如果IOS设备的应用程序是客户端的话,那么Provider可以理解为服 ...
- iOS消息推送原理和实现总结
一.消息推送原理: 在实现消息推送之前先提及几个于推送相关概念,如下图:1. Provider:就是为指定IOS设备应用程序提供Push的服务器,(如果IOS设备的应用程序是客户端的话,那么Provi ...
- 结合实际需求,在webapi内利用WebSocket建立单向的消息推送平台,让A页面和服务端建立WebSocket连接,让其他页面可以及时给A页面推送消息
1.需求示意图 2.需求描述 原本是为了给做unity3d客户端开发的同事提供不定时的消息推送,比如商城购买道具后服务端将道具信息推送给客户端. 本篇文章简化理解,用“相关部门开展活动,向全市人民征集 ...
- SignalR Self Host+MVC等多端消息推送服务(2)
一.概述 上次的文章中我们简单的实现了SignalR自托管的服务端,今天我们来实现控制台程序调用SignalR服务端来实现推送信息,由于之前我们是打算做审批消息推送,所以我们的demo方向是做指定人发 ...
随机推荐
- SpringBoot系列(七) jpa的使用,以增删改查为例
JPA是Java Persistence API的简称,Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中. 它是SUN公司推出的一套基 ...
- VIVO IQOO 5G 开关
VIVO IQOO 5G 开关 在拨号盘输入*#*#2288#*#*,然后点击网络模式选择. -
- ftl生成模板并从前台下载
1.生成模板的工具类 package com.jesims.busfundcallnew.util; import freemarker.template.Configuration; import ...
- 贝壳找房: 为 AI 平台打造混合多云的存储加速底座
贝壳机器学习平台的计算资源,尤其是 GPU,主要依赖公有云服务,并分布在不同的地理区域.为了让存储可以灵活地跟随计算资源,存储系统需具备高度的灵活性,支持跨区域的数据访问和迁移,同时确保计算任务的连续 ...
- Charles抓不到包常见原因排查
Charles抓不到包常见原因排查 1.1.1配置代理端口 1.wifi设置代理 2.Charles客户端安装证书 3.Charles 配置抓取域名或IP 4.配置域名 Focus 重点
- FTP传输PORT、PASV模式
FTP FTP是File Transfer Protocol(文件传输协议)的缩写,用来在两台计算机之间互相传送文件.相比于HTTP,FTP协议要复杂得多.复杂的原因,是因为FTP协议要用到两个TCP ...
- iOS11之后刷新tableview会出现漂移的现象解决办法
首先要注意这只是在iOS11下会出现的bug,如果iOS10以及以下也有问题的情况不属于此列 问题的动图如下所示,如果要做每隔一段短时间就刷新一个section甚至整个tableview的操作的时候会 ...
- 【读论文】LLaMA: Open and Efficient Foundation Language Models
论文:LLaMA: Open and Efficient Foundation Language Models 模型代码:https://github.com/facebookresearch/lla ...
- 关于 KL 散度和变分推断的 ELBO
01 KL 散度 Kullback-Leibler (KL) 散度,是一种描述 一个概率分布 \(P\) 相对于另一个概率分布 \(Q\) 的非对称性差异的概念. KL 散度是非负的:当且仅当两个分布 ...
- Apline部署K3s的Agent
之前我们在Ubuntu上部署了K3s的Server节点(传送门),这次我们加入两台K3s的Agent节点搭建一个K3s的3节点工作环境. 需要准备好网络环境,确保三台VM之间是可以ping通的,设置好 ...