使用WebSocket、SockJS、STOMP实现消息实时通讯功能
客户端
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head><title>websocket client</title><script src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script> <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script> <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script> <script type="text/javascript">var stompClient = null; //加载完浏览器后 调用connect(),打开双通道 $(function(){ //打开双通道 connect() }) //强制关闭浏览器 调用websocket.close(),进行正常关闭 window.onunload = function() { disconnect() } function connect(){ // 当前登录的用户idvar userId="4a85f897fc48360be1f8e6abeec40c16"; //连接SockJS的endpoint名称为"webSocket" var socket = new SockJS('http://xxx.xxx.xxx.xxx:8080/webSocket');//使用STMOP子协议的WebSocket客户端 stompClient = Stomp.over(socket);// socket 链接传递 Authorization:token 信息var headers={"Authorization": "token"}// var testMsg = JSON.stringify({'message':'Hello WebSocket!','userId':userId});//连接WebSocket服务端 stompClient.connect(headers,function(frame){ console.log('Connected:' + frame); //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息 stompClient.subscribe('/user/'+userId+'/single/ip',function(response){ console.log("点对点消息");console.log(response);var message=JSON.parse(response.body); console.log(message); showResponse(message); }); stompClient.subscribe('/topic',function(response){ console.log("订阅消息");console.log(response);var message=JSON.parse(response.body); console.log(message); }); // 客户端给服务端发送消息stompClient.send("/app/testSendMsg",{},testMsg); }); } //关闭双通道 function disconnect(){ if(stompClient != null) { stompClient.disconnect(); } console.log("Disconnected"); } function showResponse(message){ var response = $("#response"); var msg = JSON.parse(message.responseMessage); response.append("<p>只有userID为"+msg.userId+"的人才能收到</p>"); } </script>
</head>
<body><pre id="response"></pre>
</body>
</html>
服务端
pom
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
初始化config
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;/*** webSocket 初始化类*/
@Configuration
// @EnableWebSocketMessageBroker 注解用于开启使用 STOMP 协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)
// 开始支持@MessageMapping,就像是使用 @requestMapping 一样。
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Lazy@Autowiredprivate WebsocketUserInterceptor websocketUserInterceptor;@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {//注册一个名为 /oauth/ws 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。registry.addEndpoint("/webSocket").setAllowedOrigins("*").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {//点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理registry.enableSimpleBroker("/topic","/user");//客户端向服务端发起请求时,需要以/app为前缀。registry.setApplicationDestinationPrefixes("/app");//点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/registry.setUserDestinationPrefix("/user");}/*** 采用自定义拦截器,获取connect时候传递的参数* @param registration*/@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) {registration.interceptors(websocketUserInterceptor);}
}
拦截器
@Slf4j
@Component
public class WebsocketUserInterceptor implements ChannelInterceptor {@Autowiredprivate WebSocketServer webSocketServ;@Overridepublic Message<?> preSend(Message<?> message, MessageChannel channel) {StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);if (StompCommand.CONNECT.equals(accessor.getCommand())) {log.info("【webSocket】 --- 连接success");Object raw = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS);if (raw instanceof Map) {Object tokenObj = ((Map) raw).get(JwtTokenUtil.AUTH_HEADER_KEY);if (tokenObj instanceof LinkedList) {String token = ((LinkedList) tokenObj).get(0).toString();//设置当前访问器的认证用户String userId = this.getUserIdFromToken(token);accessor.setUser(new WebsocketUserVO(userId));webSocketServ.pushOnlineUser(userId);log.info("【webSocket】 --- userId:{} 上线了,在线数量:{}",userId,webSocketServ.getOnlineUserSize());}}}return message;}/*** 发送消息调用后立即调用 一般用于监听上下线*/@Overridepublic void postSend(Message<?> message, MessageChannel channel, boolean sent) {try {StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);//消息头访问器if (accessor.getCommand() == null){ return;}// 避免非stomp消息类型,例如心跳检测switch(accessor.getCommand()){case DISCONNECT://点击断开连接,这里会执行两次,第二次执行的时候,message.getHeaders.size()=5,第一次是6。直接关闭浏览器,只会执行一次,size是5。log.info("【webSocket】 --- 断开连接");MessageHeaders headers = message.getHeaders();if(ObjectUtil.isNotNull(headers)) {Object header = headers.get(SimpMessageHeaderAccessor.USER_HEADER);log.info("【webSocket】 --- header:{}", header.toString());if (ObjectUtil.isNotNull(header)) {WebsocketUserVO vo = (WebsocketUserVO) header;webSocketServ.removeOnlineUser(vo.getName());log.info("【webSocket】 断开连接 --- userId:{} 下线了,在线数量:{}", vo.getName(), webSocketServ.getOnlineUserSize());}break;}case MESSAGE:break;case SEND:break;}} catch (Exception e) {log.info("【webSocket】 断开连接 - 异常:{}", e.getMessage());}}// 解析token获取userIdprivate String getUserIdFromToken(String token){token = token.replace(JwtTokenUtil.TOKEN_PREFIX,"");LoginUser user = JSON.parseObject(JWT.decode(token).getSubject(),LoginUser.class);String userId = user.getUserId();log.info("【webSocket】解析token获取userId:{}",userId);return userId;}
}
WebSocketServer
@Component
public class WebSocketServer {public static String WEB_SOCKET_KEY="webSocket:register:";@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 用户上线public void pushOnlineUser(String id) {stringRedisTemplate.opsForValue().set(WEB_SOCKET_KEY+id,id);}// 用户下线public void removeOnlineUser(String id) {stringRedisTemplate.delete(WEB_SOCKET_KEY+id);}// 获取在线用户数量public Integer getOnlineUserSize() {return this.getOnlineUserList().size();}// 获取在线用户集合public List<String> getOnlineUserList() {Set<String> keys = stringRedisTemplate.keys(WEB_SOCKET_KEY + "*");return stringRedisTemplate.opsForValue().multiGet(keys);}
}
实体bean
public class WebsocketUserVO implements Principal {private String name;public WebsocketUserVO(String name) {this.name = name;}@Overridepublic String getName() {return name;}
}
/*** 服务器向浏览器发送消息用这个类*/
public class Server2ClientMessage {private String responseMessage;public Server2ClientMessage(String responseMessage) {this.responseMessage = responseMessage;}public String getResponseMessage() {return responseMessage;}public void setResponseMessage(String responseMessage) {this.responseMessage = responseMessage;}
}
/*** 浏览器向服务器发送消息用这个类*/
public class Client2ServerMessage {private String userId;private String message;public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}
}
测试controller
@Controller
public class SockertController {@Autowiredprivate SimpMessagingTemplate simpMessagingTemplate;@MessageMapping("/testSendMsg")public BaseResponse testSendMsg(Client2ServerMessage socketBean) {System.out.println("收到客户端消息:"+ JSON.toJSON(socketBean));System.out.println("开始发送服务器端消息");Server2ClientMessage message = new Server2ClientMessage("{\"userId\":\""+socketBean.getUserId()+"\"}");// 点对点消息simpMessagingTemplate.convertAndSendToUser(socketBean.getUserId(),"/single/ip",message);// 广播消息simpMessagingTemplate.convertAndSend("/topic",message);System.out.println("结束发送服务器端消息");return BaseResponse.success("操作成功");}
}
测试效果
nginx配置协议升级
proxy_http_version 1.1
proxy_set_header Upgrade $http_upgrade
proxy_set_header Connection 'upgrade'
相关文章:

使用WebSocket、SockJS、STOMP实现消息实时通讯功能
客户端 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>websocket client</title><script src"http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script>…...

C++回顾(十一)—— 动态类型识别和抽象类
11.1 动态类识别 11.1.1 自定义类型 C中的多态根据实际的对象类型调用对应的函数 (1)可以在基类中定义虚函数返回具体的类型信息 (2)所有的派生类都必须实现类型相关的虚函数 (3)每个类中的类型虚函数都需…...

雷电模拟器安卓7以上+Charles抓包APP最新教程
一、工具准备: 证书安装工具全局代理工具下载: https://download.csdn.net/download/weixin_51111267/87536481 二、Charles设置 (一)电脑上证书安装 (二)安卓模拟器上系统证书安装(RooT权限打…...
vsvode 配置sftp,连接远程linux全过程
在本地安装sftp插件,配置参数https://blog.csdn.net/u011119817/article/details/106630599在linux机台安装vscode-service服务https://zhuanlan.zhihu.com/p/294933020连接超时,将配置文件添加超时时间遇到的错误处理:(272条消息) 【vscode插…...
C++类转换为蓝图、打印日志、蓝图关卡、删除C++文件
蓝图宏 UCLASS(Blueprintable)//c脚本可转换为蓝图 UPROPERTY(BlueprintReadWrite)//蓝图中可创建set,get节点 UFUNCTION(BlueprintCallable)//可创建函数节点 UPROPERTY(BlueprintReadWrite,Category”My Variables”)//节点进行分类打印日志 UE_LOG(LogTemp, Lo…...

elasticsearch高级篇:核心概念和实现原理
1.elasticsearch核心概念1.1 索引(index)一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母&#…...

部署安装Nginx服务实例
其他服务: 搭建zabbix4.0监控服务实例 普罗米修斯监控mysql数据库实战 Linux安装MySQL数据库步骤 一. Nginx概念介绍 1.介绍Nginx程序 Nginx (engine x) 是一款开源且高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。主要特点是占用…...

云原生架构设计原则及典型技术
云原生是面向云应用设计的一种思想理念,充分发挥云效能的最佳实践路径,帮助企业构建弹性可靠、松耦合、易管理可观测的应用系统,提升交付效率,降低运维复杂度。代表技术包括不可变基础设施、服务网格、声明式 API 及 Serverless 等…...

【Linux】-- 工具介绍 vim_gcc/g++_gdb
目录 Linux中的软件管理工具 – yum 在Linux下安装软件的方式 认识yum 查找软件包 安装 卸载 lrzsz.x86_64 rz sz Linux中的编辑器 – vim vim的基本概念 vim各模式切换 vim命令模式命令 vim底行模式命令 gcc / g gcc / g的作用 gcc / g语法 预处理 编译 汇…...

JAVA SE: IO流
一、Java流式输入输出原理Java对于输入输出是以流(Stream)的方式进行的,JDK提供各种各样的“流”类,以获取不同类型的数据。可以理解为将管道插入到文件中,然后从管道获取数据。这个管道外边还可以套管道,外边的管道对数据进行处理…...

打破原来软件开发模式的无代码开发平台
前言传统的系统开发是需要大量的时间和成本的,如今无代码开发平台的出现就改变了这种状况。那么你知道什么是无代码开发平台?无代码开发对企业来说有什么特殊的优势么?什么是无代码平台无代码平台指的是:使用者无需懂代码或手写代码,只需通…...
06-redux中的hook
知识点06-redux的hook 在函数组件中要和redux连接,分为两个步骤 前提状态机已经主备就绪 注入store到根组件 在函数组件中,使用Provider包裹根组件,并将store注入这一步,依旧是不能少的 import store from "./redux/store…...
watch监听不到数组对象的变化
watch监听不到数组对象的变化一、利用索引直接改变arr的值二、修改数组的长度arr.length三、添加和修改对象属性和值Vue不能监听到数组和对象值的变化其实和双向绑定的原理有关。Vue双向绑定原理是利用js中的Object.defineproperty重定义对象的GET和SET方法,而同时这…...
言语理解与表达之语句表达
考点一语句填空提问方式:填入划横线处最恰当的一句是( )1.横线在结尾:总结前文提出对策2.横线在开头:需概括文段的中心内容3.横线在中间:注意与上下文联系把握好主题词,保证文段话题一致实例1和…...
2023年全国最新食品安全管理员精选真题及答案14
百分百题库提供食品安全管理员考试试题、食品安全员考试预测题、食品安全管理员考试真题、食品安全员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 131.食品生产企业在一年内()次因违反《中华人民共…...

【MySQL】约束
文章目录1. 约束2. 非空约束 NOT NULL3. 唯一性约束 UNIQUE4. 主键约束 PRIMARY KEY5. 自增约束 AUTO_INCREMENT6. 外键约束FOREIGN KEY7. 默认值约束 DEFAULT8. 小结1. 约束 为了保证数据的完整性,SQL规范以约束的方式对表数据进行额外的条件限制。从以下四个方面…...

C语言学习(三)
#include <stdio.h> int main(void){int a; scanf("%d",&a); printf("%d",a); return 0;} 正在上传…重新上传取消正在上传…重新上传取消&符号作用是把键盘中输入的值给变量a,使用scanf()时输入数值,需要按一下enter…...

TOUGH系列软件建模及在地下水、CO2地质封存、水文地球化学、地热等多相多组分系统多过程耦合
TOUGH2系列软件传统地下水模拟软件Feflow和Modflow不同,TOUGH2系列软件采用模块化设计和有限积分差网格剖分方法,通过配合不同EOS模块,软件可以处理各种复杂地质条件下,诸如地热能开发,非饱和带水气运移、油气运移&…...

k8s学习之路 | k8s 工作负载 ReplicaSet
文章目录1. ReplicaSet 基础概念1.1 RS 是什么?1.2 RS 工作原理1.3 什么时候使用 RS1.4 RS 示例1.5 非模板 Pod 的获得1.6 编写 RS1.7 使用 RS1.8 RS 替代方案2. ReplicaSet 与 ReplicationController2.1 关于 RS、RC2.2 两者的选择器区别2.3 总结1. ReplicaSet 基础…...

python实现半色调技术图像转换
半色调技术 半色调技术是一种将灰度图像转换为黑白图像的技术。它是通过将灰度图像的像素值映射到黑白像素值上来实现的。 比如说,在一块只能显示纯黑或纯白的屏幕上,如何将一张灰度图显示出灰度的效果,这时就可以用半色调技术实现。 如下…...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...

Sklearn 机器学习 缺失值处理 获取填充失值的统计值
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 使用 Scikit-learn 处理缺失值并提取填充统计信息的完整指南 在机器学习项目中,数据清…...
从实验室到产业:IndexTTS 在六大核心场景的落地实践
一、内容创作:重构数字内容生产范式 在短视频创作领域,IndexTTS 的语音克隆技术彻底改变了配音流程。B 站 UP 主通过 5 秒参考音频即可克隆出郭老师音色,生成的 “各位吴彦祖们大家好” 语音相似度达 97%,单条视频播放量突破百万…...

高保真组件库:开关
一:制作关状态 拖入一个矩形作为关闭的底色:44 x 22,填充灰色CCCCCC,圆角23,边框宽度0,文本为”关“,右对齐,边距2,2,6,2,文本颜色白色FFFFFF。 拖拽一个椭圆,尺寸18 x 18,边框为0。3. 全选转为动态面板状态1命名为”关“。 二:制作开状态 复制关状态并命名为”开…...
用js实现常见排序算法
以下是几种常见排序算法的 JS实现,包括选择排序、冒泡排序、插入排序、快速排序和归并排序,以及每种算法的特点和复杂度分析 1. 选择排序(Selection Sort) 核心思想:每次从未排序部分选择最小元素,与未排…...
RLHF vs RLVR:对齐学习中的两种强化方式详解
在语言模型对齐(alignment)中,强化学习(RL)是一种重要的策略。而其中两种典型形式——RLHF(Reinforcement Learning with Human Feedback) 与 RLVR(Reinforcement Learning with Ver…...
关于疲劳分析的各种方法
疲劳寿命预测方法很多。按疲劳裂纹形成寿命预测的基本假定和控制参数,可分为名义应力法、局部应力一应变法、能量法、场强法等。 1名义应力法 名义应力法是以结构的名义应力为试验和寿命估算的基础,采用雨流法取出一个个相互独立、互不相关的应力循环&…...