Java Springboot SSE如何判断客户端能否正常接收消息

目录

  • 背景
  • 解决方案
    • 思路
    • 代码
    • 代码解释
  • Java反射知识点补充

背景

当新建一个 emitter 对象的时候, 它的默认超时时间是 30s.

SseEmitter emitter = new SseEmitter(); 

但是很多情况下, 默认30s的时间太短, 需要把 emitter 对象的超时时间设置成不超时, 也就是永久有效.

private static long DEFAULT_TIMEOUT = 0L;......SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); 

这样也会带来一个问题, 就是永久有效的 emitter 对象如果没有调用关闭连接的接口的话 (比如用户直接关闭浏览器了) , 这个 emitter 对象就会一直存在.

解决方案

思路

sseEmitter 有下面的几个属性:

在这里插入图片描述

注意一下 sendFailed 这个属性, 我们可以利用这个属性来判断客户端能否正常接到消息.

当客户端无法接受消息时,SseEmitter对象在send一次之后sendFailed状态会变为True,这时候就可以剔除。同时在订阅时用此判断可以减少重复创建的机会

还有一个 complete 属性, 这个属性是与 sendFailed 有关的, 也就是消息发送成功的时候 complete 为 true, 失败的时候 complete 为 false. 我们可以用这个属性当做一个辅助.

请添加图片描述

拿到客户端是否能够正常接收消息这个状态以后, 我们就可以建立一个定时器,固定时间发送消息用来检测客户端是否离线.

代码

package com.example.demo.utils;import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class SSEUtils {public static Map<String, SseEmitter> subscribeMap = new ConcurrentHashMap<>();/**** 添加订阅* @param id 客户id* @return*/public static SseEmitter addSubscribe(String id) {SseEmitter sseEmitter = subscribeMap.get(id);if (sseEmitter == null) {sseEmitter = new SseEmitter(0L); // 永久有效sseEmitter.onTimeout(() -> {subscribeMap.remove(id);});sseEmitter.onError(throwable -> {subscribeMap.remove(id);});SseEmitter finalSseEmitter = sseEmitter;sseEmitter.onCompletion(() -> {subscribeMap.put(id, finalSseEmitter);});}return sseEmitter;}/**** 给单个用户发消息* @param id* @param msg* @return*/public static boolean sendSingleClientMsg(String id,Object msg) {SseEmitter sseEmitter = subscribeMap.get(id);if (sseEmitter == null) {return false;}try {sseEmitter.send(msg, MediaType.APPLICATION_JSON);return true;} catch (IOException e) {e.printStackTrace();return false;}}/**** 关闭订阅* @param id* @return*/public static boolean closeSubscribe(String id) {SseEmitter sseEmitter = subscribeMap.get(id);if (sseEmitter == null) {return true;}try {sseEmitter.complete();subscribeMap.remove(id);return true;} catch (Exception e) {e.printStackTrace();return false;}}/**** 检测客户端连接状态* @param sseEmitter* @return true代表还连接, false代表失去连接*/public static boolean checkSseConnectAlive(SseEmitter sseEmitter) {if (sseEmitter == null) {return false;}// 返回true代表还连接, 返回false代表失去连接return !(Boolean) getField(sseEmitter,sseEmitter.getClass(), "sendFailed") &&!(Boolean) getField(sseEmitter,sseEmitter.getClass(), "complete");}public static Object getField(Object obj, Class<?> clazz, String fieldName) {for (; clazz != Object.class; clazz = clazz.getSuperclass()) {try {Field field;field = clazz.getDeclaredField(fieldName);field.setAccessible(true);return field.get(obj);} catch (Exception e) {}}return null;}/**** 给所有客户端发消息* @param msg*/public void sendAllClientMsg(Object msg) {if (subscribeMap != null && !subscribeMap.isEmpty()) {for (String key : subscribeMap.keySet()) {// 发送检测消息sendSingleClientMsg(key,msg);// 判断客户端是否能接收到消息boolean isAlive = checkSseConnectAlive(subscribeMap.get(key));if (!isAlive) {// 断开连接的业务代码}}}}/**** 定时判断所有客户端状态*/@Async("threadPoolTaskExecutor")@Scheduled(fixedDelay = 1000*60*10) // 10minpublic void checkAlive() {sendAllClientMsg("CHECK_ALIVE");}
}

使用 @Scheduled 定时器, 不要忘记在启动类上面加这两个注解:

@SpringBootApplication
@EnableAsync
@EnableScheduling
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}}

代码解释

重点部分是下面这段代码:

    /**** 检测客户端连接状态* @param sseEmitter* @return*/public static boolean checkSseConnectAlive(SseEmitter sseEmitter) {if (sseEmitter == null) {return false;}// 返回true代表还连接, 返回false代表失去连接return !(Boolean) getField(sseEmitter,sseEmitter.getClass(), "sendFailed") &&!(Boolean) getField(sseEmitter,sseEmitter.getClass(), "complete");}public static Object getField(Object obj, Class<?> clazz, String fieldName) {for (; clazz != Object.class; clazz = clazz.getSuperclass()) {try {Field field;field = clazz.getDeclaredField(fieldName);field.setAccessible(true);return field.get(obj);} catch (Exception e) {}}return null;}

1. 循环找 SseEmitter 和它的父类中是否存在 sendFailed 这个属性, 直到找到.

这是因为 sendFailed 这个属性是私有的, 不供外部访问, 这属性还正好在父类里, 所以要循环父类.

在这里插入图片描述

在这里插入图片描述

2. 通过 getDeclaredField() 方法拿到传入的 fieldName 的属性 (也就是 "sendFailed""complete" ), 接着使用 setAccessible(true) 把这个值设置为可访问的.

3. 最后通过 field.get(obj) 拿到这个属性的值, 也就是"sendFailed""complete" 的值是 true/false

思路和代码参考: Java Springboot SSE 解决永久存活 判断客户端离线问题. 关于 SSE utils的一些工具类的方法在这个博客里面也有.

Java反射知识点补充

Java 反射是指在运行时动态地获取一个类的信息,并且可以操作它的属性、方法和构造方法等。Java 反射机制提供了一种在运行时检查、创建和操作对象的能力,这使得 Java 程序可以实现动态性和灵活性。

Java 反射机制主要包括以下三个类:

  • java.lang.Class 类:代表一个类,在运行时动态获取一个类的信息。
  • java.lang.reflect.Method 类:代表类的方法,在运行时可以使用 Method.invoke() 方法调用一个方法。
  • java.lang.reflect.Field 类:代表类的属性,在运行时可以使用 Field.get() 和 Field.set() 方法获取或设置一个属性的值。

以下是一个简单的 Java 反射示例,演示如何使用反射获取一个类的信息:

import java.lang.reflect.*;public class MyClass {private String name;private int age;public MyClass(String name, int age) {this.name = name;this.age = age;}public void sayHello() {System.out.println("Hello, " + name + "!");}public static void main(String[] args) throws Exception {// 获取 MyClass 类的 Class 对象Class<?> myClass = MyClass.class;// 创建一个 MyClass 对象MyClass obj = new MyClass("Bob", 20);// 获取 MyClass 类的构造方法,并使用它创建一个新的 MyClass 对象Constructor<?> constructor = myClass.getConstructor(String.class, int.class);MyClass newObj = (MyClass) constructor.newInstance("Alice", 30);// 获取 MyClass 类的属性,并使用它获取 obj 对象的 name 属性值Field field = myClass.getDeclaredField("name");field.setAccessible(true);String name = (String) field.get(obj);// 获取 MyClass 类的方法,并使用它调用 obj 对象的 sayHello 方法Method method = myClass.getMethod("sayHello");method.invoke(obj);System.out.println(name);         // 输出:BobSystem.out.println(newObj.name);  // 输出:Alice}
}

在上述示例中,我们首先获取了 MyClass 类的 Class 对象。然后,我们创建了一个 MyClass 对象,并使用 getConstructor() 方法获取了 MyClass 类的构造方法,并使用 newInstance() 方法创建了一个新的 MyClass 对象。

接着,我们使用 getDeclaredField() 方法获取了 MyClass 类的 name 属性,并使用 setAccessible() 方法设置该属性可访问性为 true,然后使用 get() 方法获取了 obj 对象中 name 属性的值。

最后,我们使用 getMethod() 方法获取了 MyClass 类的 sayHello() 方法,并使用 invoke() 方法调用了 obj 对象的 sayHello() 方法。

需要注意的是,在使用反射机制时,应该尽量避免使用硬编码的字符串来表示类名、方法名和属性名等信息,这样会使代码更加灵活和可维护。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

@PostConstruct 注解分析

一、简介 如果需要在生成对象时就完成某些初始化操作&#xff0c;而且这些初始化操作又依赖于依赖注入Autowired&#xff0c;那么就无法在构造函数中实现。为此&#xff0c;可以使用PostConstruct注解一个方法来完成初始化。PostConstruct注解的方法将会在依赖注入完成后被自动…

lenovo联想笔记本电脑拯救者Legion Y7000 2019 PG0(81T0)原装出厂Windows10系统

链接&#xff1a;https://pan.baidu.com/s/1fn0aStc4sfAfgyOKtMiCCA?pwdas1l 提取码&#xff1a;as1l 联想拯救者原厂Win10系统自带所有驱动、出厂主题壁纸、系统属性联机支持标志、系统属性专属LOGO标志、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;…

【AIGC】IP-Adapter:文本兼容图像提示适配器,用于文本到图像扩散模型

前言 IPAdapter能够通过图像给Stable Diffusion模型以内容提示&#xff0c;让其生成参考该图像画风&#xff0c;可以免去Lora的训练&#xff0c;达到参考画风人物的生成效果。 摘要 通过文本提示词生成的图像&#xff0c;往往需要设置复杂的提示词&#xff0c;通常设计提示词变…

Python多线程爬虫——数据分析项目实现详解

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 ChatGPT体验地址 文章目录 前言爬虫获取cookie网站爬取与启动CSDN爬虫爬虫启动将爬取内容存到文件中 多线程爬虫选择要爬取的用户 线程池 爬虫 爬虫是指一种自动化程序&#xff0c;能够模…

【动态规划】23子数组系列_等差数列划分_C++

题目链接&#xff1a;等差数列划分 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求数组 nums 中所有为等差数组的 子数组 个数。 由题可得&#xff1a; 一个等差数列 至少…

mybatis分页、延迟加载、立即加载、一级缓存、二级缓存

mybatis分页、延迟加载、立即加载、一级缓存、二级缓存 分页延迟加载和立即加载缓存一级缓存二级缓存 分页 分类&#xff1a; 使用Limit&#xff0c;来进行分页&#xff1b;物理分页使用RowBounds集合来保存分页需要数据&#xff0c;来进行分页;逻辑分页&#xff1b;本质是全…

【ArcGIS微课1000例】0088:计算城市建筑物朝向(矩形角度)

文章目录 一、实验描述二、实验数据三、角度计算1. 添加字段2. 计算角度四、方向计算一、实验描述 矩形要素具有长轴和短轴,其长轴方向也称为矩形面的主角度,可用于确定面要素的走向趋势。根据该方向参数,可以对具有矩形特征的地理对象进行方向分析,且适用于很多应用场景,…

Chrome 开发者工具

Chrome 开发者工具 介绍控制面板时间线下载信息概要请求列表单个请求时间线优化时间线上耗时项 lighthouse 插件Performance&#xff08;性能指标&#xff09;Accessibility&#xff08;可访问性&#xff09;Best Practices&#xff08;最佳实践&#xff09;SEO&#xff08;搜索…

hanlp,pkuseg,jieba,cutword分词实践

总结&#xff1a;只有jieba,cutword,baidu lac成功将色盲色弱成功分对,这两个库字典应该是最全的 hanlp[持续更新中] https://github.com/hankcs/HanLP/blob/doc-zh/plugins/hanlp_demo/hanlp_demo/zh/tok_stl.ipynb import hanlp # hanlp.pretrained.tok.ALL # 语种见名称最…

【计算机网络】TCP原理 | 可靠性机制分析(四)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】 本专栏旨在分享学习计算机网络的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 这里写目录标题 &#x1…

解决springboot启动报Failed to start bean ‘subProtocolWebSocketHandler‘;

解决springboot启动报 Failed to start bean subProtocolWebSocketHandler; nested exception is java.lang.IllegalArgumentException: No handlers 问题发现问题解决 问题发现 使用springboot整合websocket&#xff0c;启动时报错&#xff0c;示例代码&#xff1a; EnableW…

pxe高效批量网络装机 以及安装教程

系统装机的三种引导模式 1.pe 2光驱 3.网卡 打开本机桌面 可以看见背景图片 查看配置文件内容 文件时引导选项的功能 pxe原理&#xff1a; 先根据dhcp找到IP地址、和引导程序的地址&#xff0c;还提供客户机tftp地址&#xff0c;因为tftp是小文件&#xff0c;容量小&#…