SpringBoot2.0集成WebSocket,多客户端
适用于单客户端,一个账号登陆一个客户端,登陆多个客户端会报错
The remote endpoint was in state [TEXT_FULL_WRITING]
这是因为此时的session是不同的,只能锁住一个session,解决此问题的方法把全局静态对象锁住,因为账号是唯一的
/*** @Description 开启springboot对websocket的支持* @Author WangKun* @Date 2023/8/14 17:21* @Version*/
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev")
@Configuration
public class WebSocketConfig{/*** @Description 注入一个ServerEndpointExporter, 会自动注册使用@ServerEndpoint注解* @param* @Throws* @Return org.springframework.web.socket.server.standard.ServerEndpointExporter* @Date 2023-08-14 17:26:31* @Author WangKun*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
/*** @Description websocket服务,不考虑分组* @Author WangKun* @Date 2023/8/14 17:29* @Version*/
@ConditionalOnClass(value = WebSocketConfig.class)
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocket {private static final long SESSION_TIMEOUT = 60000;//存放每个客户端对应的WebSocket对象。private static final ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocket>> WEB_SOCKET_MAP = new ConcurrentHashMap<>();//与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;private String userId;/*** @param o* @Description 重写防止session重复* @Throws* @Return boolean* @Date 2023-09-01 10:02:51* @Author WangKun*/@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}WebSocket that = (WebSocket) o;return Objects.equals(session, that.session);}@Overridepublic int hashCode() {return Objects.hash(session);}/*** @param session* @param userId* @Description 建立连接* @Throws* @Return void* @Date 2023-08-14 17:52:08* @Author WangKun*/@SneakyThrows@OnOpenpublic void onOpen(final Session session, @PathParam("userId") String userId) {this.session = session;this.userId = userId;session.setMaxIdleTimeout(SESSION_TIMEOUT);//先查找是否有uniCodeCopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(userId);if (users == null) {//处理多个同时连接并发synchronized (WEB_SOCKET_MAP) {if (!WEB_SOCKET_MAP.contains(userId)) {users = new CopyOnWriteArraySet<>();WEB_SOCKET_MAP.put(userId, users);}}}users.add(this);sendMessage(String.valueOf(ResponseCode.CONNECT_SUCCESS.getCode()));log.info("用户--->{} 连接成功,当前在线人数为--->{}", userId, WEB_SOCKET_MAP.size());}/*** @param message* @Description 向客户端发送消息 session.getBasicRemote()与session.getAsyncRemote()的区别* @Throws* @Return void* @Date 2023-08-14 17:51:07* @Author WangKun*/@SneakyThrowspublic void sendMessage(String message) {// 加锁避免阻塞// 如果有多个客户端的话,亦或者同一个用户,或者打开了多个浏览器(同一个用户打开多个客户端或者多个界面),开了多个页面,此时Session是不同的,只能锁住一个session,所以锁住全局静态对象
// synchronized(session) {
// this.session.getBasicRemote().sendText(message);
// }synchronized (WEB_SOCKET_MAP) {CopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(userId);if (users != null) {for (WebSocket user : users) {// 判断当前客户端的用户是否打开连接if (user.session.isOpen()) {user.session.getBasicRemote().sendText(message);log.info("向客户端发送数据--->{} 数据为--->{}", userId, message);}}}}}/*** @param* @Description 关闭连接* @Throws* @Return void* @Date 2023-08-14 17:52:30* @Author WangKun*/@SneakyThrows@OnClosepublic void onClose(Session session) {// 避免多人同时在线直接关闭通道。CopyOnWriteArraySet<WebSocket> copyOnWriteArraySet = WEB_SOCKET_MAP.get(this.userId);if (!copyOnWriteArraySet.isEmpty()) {Object[] objects = copyOnWriteArraySet.toArray();for (Object object : objects) {if (((WebSocket) object).session.equals(session)) {//删除当前用户WEB_SOCKET_MAP.get(this.userId).remove((WebSocket) object);// 如果有一个客户端登陆 下线清除用户if (WEB_SOCKET_MAP.get(this.userId).isEmpty()) {WEB_SOCKET_MAP.remove(this.userId);}CloseReason close = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "关闭客户端,下线!");session.close(close);log.info("用户--->{} 关闭连接!", userId);}}}}/*** @param message* @param session* @Description 收到客户端消息* @Throws* @Return void* @Date 2023-08-15 10:54:55* @Author WangKun*/@SneakyThrows@OnMessagepublic void onMessage(String message, Session session) {//枷锁避免多个资源互抢//这一块可以操作数据,比如存到数据// 同一个用户,多个地方登录(多个session),循环发送消息,// 如果有多个客户端的话,亦或者同一个用户,或者打开了多个浏览器,开了多个页面,此时Session是不同的,只能锁住一个session,所以锁住全局静态对象synchronized (WEB_SOCKET_MAP) {CopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(userId);if (users != null) {for (WebSocket user : users) {if (user.session.isOpen()) {user.session.getBasicRemote().sendText("pong");log.info("收到客户端发送的心跳的数据--->{} 数据为--->{}", userId, message);}}}}}/*** @param session* @param error* @Description 发生错误时* @Throws* @Return void* @Date 2023-08-15 10:55:27* @Author WangKun*/@SneakyThrows@OnErrorpublic void onError(Session session, Throwable error) {CopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(userId);if (!users.isEmpty()) {Object[] objects = users.toArray();for (Object object : objects) {if (((WebSocket) object).session.equals(session)) {//删除当前用户WEB_SOCKET_MAP.get(this.userId).remove((WebSocket) object);// 如果有一个客户端登陆 下线清除用户if (WEB_SOCKET_MAP.get(this.userId).isEmpty()) {WEB_SOCKET_MAP.remove(this.userId);}CloseReason close = new CloseReason(CloseReason.CloseCodes.UNEXPECTED_CONDITION, "异常,下线!");session.close(close);log.error("用户--->{} 错误!" + userId, "原因--->{}" + error.getMessage(), error);}}}
// WEB_SOCKET_MAP.remove(userId);
// log.error("用户--->{} 错误!" + userId, "原因--->{}" + error.getMessage(), error);}/*** @param userId* @param message* @Description 通过userId向客户端发送消息(指定用户发送)* @Throws* @Return void* @Date 2023-08-14 18:01:35* @Author WangKun*/public static void sendTextMessageByUserId(String userId, String message) {CopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(userId);if (users != null) {for (WebSocket user : users) {user.sendMessage(message);log.info("服务端发送消息到用户{},消息:{}", userId, message);}}}/*** @param message* @Description 群发自定义消息* @Throws* @Return void* @Date 2023-08-14 18:03:38* @Author WangKun*/public static void sendTextMessage(String message) {// 如果在线一个就广播if (!WEB_SOCKET_MAP.isEmpty()) {for (String item : WEB_SOCKET_MAP.keySet()) {CopyOnWriteArraySet<WebSocket> users = WEB_SOCKET_MAP.get(item);if (users != null) {for (WebSocket user : users) {user.sendMessage(message);log.info("服务端发送消息到用户{},消息:{}", item, message);}}}}}
}
相关文章:
SpringBoot2.0集成WebSocket,多客户端
适用于单客户端,一个账号登陆一个客户端,登陆多个客户端会报错 The remote endpoint was in state [TEXT_FULL_WRITING] 这是因为此时的session是不同的,只能锁住一个session,解决此问题的方法把全局静态对象锁住,因…...

华为OD机试 - 等和子数组最小和 - 深度优先搜索(Java 2022 Q4 100分)
目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中,刷题点这里 专栏导读 本专栏收录于《华为OD机试(JAVA)真题(A卷B卷)》…...
浏览器会因为什么样的脚本而崩溃
浏览器可能因为以下几种情况而崩溃: 无限循环:如果JavaScript脚本包含一个无限循环,浏览器将无法停止脚本的执行,导致浏览器不响应甚至崩溃。例如,以下代码会导致无限循环: while (true) {// 无限循环 } 内…...

生成与调用C++动态链接库(so文件)
文章目录 前言生成C动态链接库步骤1:编写C源码步骤2:生成共享库步骤3:验证生成的SO文件 调用C动态链接库步骤1:修改原来makefile步骤2:编译调用程序步骤3:运行调用程序 总结 前言 动态链接库是代码重用和模…...

韶音的耳机怎么样,韶音骨传导耳机值得入手吗
韶音关于骨传导耳机的产品在质量方面还是有着不错的表现,其最具代表性的骨传导耳机就是韶音OpenRun Pro,在国产骨传导耳机中是具备了一定的知名度,有着自主研发的声学技术。 最突出的点就在于颜色上多样化,有着经典的黑色…...

STM32G030F6 (SOP-20)Cortex ® -M0+, 32KB Flash, 8KB RAM, 17 GPIOs
淘宝淘了一批 STM32G030F6P6 SOP20.先备注一下, 还没想到能干嘛用. 手上的 STM32F103C6T6还剩一些. 一堆 “淘宝原厂STM32F103C8T6”, 还烫着手. 理解信息: ( 逐步补充 ) System Clock GPIOs GPIOs 17 PA[7:0] : 8bits USART Timer ADC I2…...
常用的字符集和字符编码
基础概念 字符 字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等 字符集 一个操作系统支持的字符的集合。 字符编码和解码 将每个字符都设置一个唯一编号,编码就是将字符集中的字符编号以一定形式转化为字节存储下来,…...
容器技术简介
引言 随着云计算、大数据、人工智能等技术的不断发展,容器技术作为一种新兴的虚拟化技术,正逐渐成为IT领域的热点。容器技术可以帮助开发者更好地管理、部署和扩展应用程序,提高开发效率和应用程序的可靠性。本文将深入探讨容器技术的概念、…...

数据分享|R语言用lme4多层次(混合效应)广义线性模型(GLM),逻辑回归分析教育留级调查数据...
全文链接:http://tecdat.cn/?p22813 本教程为读者提供了使用频率学派的广义线性模型(GLM)的基本介绍。具体来说,本教程重点介绍逻辑回归在二元结果和计数/比例结果情况下的使用,以及模型评估的方法(点击文末“阅读原文…...

macos 不支持svn安装
macos 10.13可能不支持svn命令,所以要安装 xcode-select --install 弹窗在线安装失败的话只能手动下载安装 打开:Sign In - Apple 搜索Command Line Tools (macOS 10.13) 下载9.4.1版本直接安装后即可...
如何通过实际操作来加深对Linux命令和概念的理解?
作为一个新手,你一定不要被Linux那堆命令吓到。其实,它们就像你的“超能力”,只要你掌握它们,你就能成为Linux世界的超级英雄! 首先,我们要了解的是,Linux命令其实就像你的“魔法咒语”&#x…...

【开发语言】C语言与Python的互操作详解
博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持! 博主链接 本人就职于国际知名终端厂商,负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。 博客…...
华为配置聚合vlan(Super vlan--Sub vlan)
聚合vlan,Aggregation vlan,也称Super vlan,可以实现用Sub vlan二层隔离广播域,但又将这些Sub vlan聚合使用同一IP子网和网关的情况。 这样,多个Sub-VLAN共享一个网关地址,节约了子网号、子网定向广播地址、…...

CentOS7安装时直接跳过了安装信息摘要页面的解决方法
最近在配置Hadoop虚拟机的时候,创建的centos7虚拟机在安装信息摘要时直接自动跳过,直接跳到设置用户名和密码,在重复多次的重新删除安装后发现了问题所在: 在进行到选择操作系统来源时,注意是否出现“该操作系统将使用…...

python基础运用例子
python基础运用例子 1、⼀⾏代码交换 a , b :a, b b, a2、⼀⾏代码反转列表 l[::-1]3、合并两个字典 res {**dict1, **dict2}**操作符合并两个字典for循环合并dict(a, **b) 的方式dict(a.items() b.items()) 的方式dict.update(other_dict) 的方式 4、⼀⾏代码列…...

k8s基本概念
一、什么是Kubernetes二:Kubernetes部署方式的演变三、为什么要用K8S四、K8S的特性五、Kubernetes 集群架构与组件5.1 Master 组件① Kube-apiserver② Kube-controller-manager③ Kube-scheduler④ AUTH 认证模块 5.2 配置存储中心5.3 Node 组件① Kubelet② Kube-…...
Python exp() 函数
描述 exp() 方法返回x的指数,ex。 语法 以下是 exp() 方法的语法: import mathmath.exp( x ) 注意:exp()是不能直接访问的,需要导入 math 模块,通过静态对象调用该方法。 参数 x -- 数值表达式。 返回值 返回x的指数,ex。 实例 以下展…...
Day 34 贪心算法 part03 : 1005.K次取反后最大化的数组和 134. 加油站 135. 分发糖果
134. 加油站 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 给定两个整数数组 gas…...

气象站的构成及功能应用
气象站是一种用于观测、记录和报告天气数据的设备。它是由数据采集系统、通讯系统、供电系统和立杆支架构成。 一、气象站的构成: 数据采集系统:用于测量气温、湿度、风速、风向、气压、降雨量、雪深等气象参数。 通讯系统:收集和处理传感…...

Qt中布局管理使用总结
目录 1. 五大布局 1.1 QVBoxLayout垂直布局 1.2 QHBoxLayout水平布局 1.3 QGridLayout网格布局 1.4 QFormLayout表单布局 1.5 QStackedLayout分组布局 1.6 五大布局综合应用 2. 分割窗口 3. 滚动区域 4. 停靠区域 1. 五大布局 1.1 QVBoxLayout垂直布局 #include <…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

《基于Apache Flink的流处理》笔记
思维导图 1-3 章 4-7章 8-11 章 参考资料 源码: https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

R 语言科研绘图第 55 期 --- 网络图-聚类
在发表科研论文的过程中,科研绘图是必不可少的,一张好看的图形会是文章很大的加分项。 为了便于使用,本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中,获取方式: R 语言科研绘图模板 --- sciRplothttps://mp.…...