当前位置: 首页 > news >正文

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,多客户端

适用于单客户端&#xff0c;一个账号登陆一个客户端&#xff0c;登陆多个客户端会报错 The remote endpoint was in state [TEXT_FULL_WRITING] 这是因为此时的session是不同的&#xff0c;只能锁住一个session&#xff0c;解决此问题的方法把全局静态对象锁住&#xff0c;因…...

华为OD机试 - 等和子数组最小和 - 深度优先搜索(Java 2022 Q4 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》…...

浏览器会因为什么样的脚本而崩溃

浏览器可能因为以下几种情况而崩溃&#xff1a; 无限循环&#xff1a;如果JavaScript脚本包含一个无限循环&#xff0c;浏览器将无法停止脚本的执行&#xff0c;导致浏览器不响应甚至崩溃。例如&#xff0c;以下代码会导致无限循环&#xff1a; while (true) {// 无限循环 } 内…...

生成与调用C++动态链接库(so文件)

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

韶音的耳机怎么样,韶音骨传导耳机值得入手吗

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

STM32G030F6 (SOP-20)Cortex ® -M0+, 32KB Flash, 8KB RAM, 17 GPIOs

淘宝淘了一批 STM32G030F6P6 SOP20&#xff0e;先备注一下, 还没想到能干嘛用&#xff0e; 手上的 STM32F103C6T6还剩一些&#xff0e; 一堆 “淘宝原厂STM32F103C8T6”, 还烫着手. 理解信息: ( 逐步补充 ) System Clock GPIOs GPIOs 17 PA[7:0] : 8bits USART Timer ADC I2…...

常用的字符集和字符编码

基础概念 字符 字符是各种文字和符号的总称&#xff0c;包括各国家文字、标点符号、图形符号、数字等 字符集 一个操作系统支持的字符的集合。 字符编码和解码 将每个字符都设置一个唯一编号&#xff0c;编码就是将字符集中的字符编号以一定形式转化为字节存储下来&#xff0c…...

容器技术简介

引言 随着云计算、大数据、人工智能等技术的不断发展&#xff0c;容器技术作为一种新兴的虚拟化技术&#xff0c;正逐渐成为IT领域的热点。容器技术可以帮助开发者更好地管理、部署和扩展应用程序&#xff0c;提高开发效率和应用程序的可靠性。本文将深入探讨容器技术的概念、…...

数据分享|R语言用lme4多层次(混合效应)广义线性模型(GLM),逻辑回归分析教育留级调查数据...

全文链接:http://tecdat.cn/?p22813 本教程为读者提供了使用频率学派的广义线性模型&#xff08;GLM&#xff09;的基本介绍。具体来说&#xff0c;本教程重点介绍逻辑回归在二元结果和计数/比例结果情况下的使用&#xff0c;以及模型评估的方法&#xff08;点击文末“阅读原文…...

macos 不支持svn安装

macos 10.13可能不支持svn命令,所以要安装 xcode-select --install 弹窗在线安装失败的话只能手动下载安装 打开:Sign In - Apple 搜索Command Line Tools (macOS 10.13) 下载9.4.1版本直接安装后即可...

如何通过实际操作来加深对Linux命令和概念的理解?

作为一个新手&#xff0c;你一定不要被Linux那堆命令吓到。其实&#xff0c;它们就像你的“超能力”&#xff0c;只要你掌握它们&#xff0c;你就能成为Linux世界的超级英雄&#xff01; 首先&#xff0c;我们要了解的是&#xff0c;Linux命令其实就像你的“魔法咒语”&#x…...

【开发语言】C语言与Python的互操作详解

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G算力网络技术标准研究。 博客…...

华为配置聚合vlan(Super vlan--Sub vlan)

聚合vlan&#xff0c;Aggregation vlan&#xff0c;也称Super vlan&#xff0c;可以实现用Sub vlan二层隔离广播域&#xff0c;但又将这些Sub vlan聚合使用同一IP子网和网关的情况。 这样&#xff0c;多个Sub-VLAN共享一个网关地址&#xff0c;节约了子网号、子网定向广播地址、…...

CentOS7安装时直接跳过了安装信息摘要页面的解决方法

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

python基础运用例子

python基础运用例子 1、⼀⾏代码交换 a , b &#xff1a;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二&#xff1a;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 ) 注意&#xff1a;exp()是不能直接访问的&#xff0c;需要导入 math 模块&#xff0c;通过静态对象调用该方法。 参数 x -- 数值表达式。 返回值 返回x的指数,ex。 实例 以下展…...

Day 34 贪心算法 part03 : 1005.K次取反后最大化的数组和 134. 加油站 135. 分发糖果

134. 加油站 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发&#xff0c;开始时油箱为空。 给定两个整数数组 gas…...

气象站的构成及功能应用

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

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 <…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

React Native在HarmonyOS 5.0阅读类应用开发中的实践

一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强&#xff0c;React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 &#xff08;1&#xff09;使用React Native…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

.Net Framework 4/C# 关键字(非常用,持续更新...)

一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

ABAP设计模式之---“简单设计原则(Simple Design)”

“Simple Design”&#xff08;简单设计&#xff09;是软件开发中的一个重要理念&#xff0c;倡导以最简单的方式实现软件功能&#xff0c;以确保代码清晰易懂、易维护&#xff0c;并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计&#xff0c;遵循“让事情保…...

windows系统MySQL安装文档

概览&#xff1a;本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容&#xff0c;为学习者提供全面的操作指导。关键要点包括&#xff1a; 解压 &#xff1a;下载完成后解压压缩包&#xff0c;得到MySQL 8.…...

面试高频问题

文章目录 &#x1f680; 消息队列核心技术揭秘&#xff1a;从入门到秒杀面试官1️⃣ Kafka为何能"吞云吐雾"&#xff1f;性能背后的秘密1.1 顺序写入与零拷贝&#xff1a;性能的双引擎1.2 分区并行&#xff1a;数据的"八车道高速公路"1.3 页缓存与批量处理…...