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

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...

【网络安全】开源系统getshell漏洞挖掘
审计过程: 在入口文件admin/index.php中: 用户可以通过m,c,a等参数控制加载的文件和方法,在app/system/entrance.php中存在重点代码: 当M_TYPE system并且M_MODULE include时,会设置常量PATH_OWN_FILE为PATH_APP.M_T…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...

深入解析光敏传感技术:嵌入式仿真平台如何重塑电子工程教学
一、光敏传感技术的物理本质与系统级实现挑战 光敏电阻作为经典的光电传感器件,其工作原理根植于半导体材料的光电导效应。当入射光子能量超过材料带隙宽度时,价带电子受激发跃迁至导带,形成电子-空穴对,导致材料电导率显著提升。…...
AWS vs 阿里云:功能、服务与性能对比指南
在云计算领域,Amazon Web Services (AWS) 和阿里云 (Alibaba Cloud) 是全球领先的提供商,各自在功能范围、服务生态系统、性能表现和适用场景上具有独特优势。基于提供的引用[1]-[5],我将从功能、服务和性能三个方面进行结构化对比分析&#…...