【stomp实战】Springboot+Stomp协议实现聊天功能

本示例实现一个功能,前端通过websocket发送消息给后端服务,后端服务接收到该消息时,原样将消息返回给前端。前端技术栈html+stomp.js,后端SpringBoot

前端代码

关于stomp客户端的开发,如果不清楚的,可以看一下上一篇文章
这里我对Stomp.js进行了一个简单的封装,写在stomp-client.js里面

/*** 对 stomp 客户端进行封装*/var client;
var subscribes = [];
var errorTimes = 0;var endpoint = "/ws";/*** 建立websocket连接* @param {Function} onConnecting 开始连接时的回调* @param {Function} onConnected 连接成功回调* @param {Function} onError 连接异常或断开回调*/
function connect(onConnecting, onConnected, onError) {onConnecting instanceof Function && onConnecting();var sock = new SockJS(endpoint);client = Stomp.over(sock);console.log("ws: start connect to " + endpoint);client.connect({}, function (frame) {errorTimes = 0;console.log('connected: ' + frame);// 连接成功后重新订阅subscribes.forEach(function (item) {client.subscribe(item.destination, function (resp) {console.debug("ws收到消息: ", resp);item.cb(JSON.parse(resp.body));});});onConnected instanceof Function && onConnected();}, function (err) {errorTimes = errorTimes > 8 ? 0 : errorTimes;var nextTime = ++errorTimes * 3000;console.warn("与服务器断开连接," + nextTime + " 秒后重新连接", err);setTimeout(function () {console.log("尝试重连……");connect(onConnecting, onConnected, onError);}, nextTime);onError instanceof Function && onError();});
}/*** 订阅消息,若当前未连接,则会在连接成功后自动订阅** 注意,为防止重连导致重复订阅,请勿使用匿名函数做回调** @param {String} destination 目标* @param {Function} cb 回调*/
function subscribe(destination, cb) {var exist = subscribes.filter(function (sub) {return sub.destination === destination && sub.cb === cb});// 防止重复订阅if (exist && exist.length) {return;}// 记录所有订阅,在连接成功时统一处理subscribes.push({destination: destination,cb: cb});if (client && client.connected) {client.subscribe(destination, function (resp) {console.debug("ws收到消息: ", resp);cb instanceof Function && cb(JSON.parse(resp.body));});} else {console.warn("ws未连接,暂时无法订阅:" + destination)}
}/*** 发送消息* @param {String} destination 目标* @param {Object} msg 消息体对象*/
function send(destination, msg) {if (!client) {console.error("客户端未连接,无法发送消息!")}client.send(destination, {}, JSON.stringify(msg));
}window.onbeforeunload = function () {// 当窗口关闭时断开连接if (client && client.connected) {client.disconnect(function () {console.log("websocket disconnected ");});}
};

前端的html页面index.html如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>STOMP</title>
</head>
<body>
<h1 id="tip">Welcome!</h1>
<p>状态: <span id="status"></span></p>
<input type="text" id="content" placeholder="请输入要发送的消息"> <br>
<button onclick="sendTextMsg()">发送</button>
<ul id="ul">
</ul>
<script th:src="@{lib/sockjs.min.js}"></script>
<script th:src="@{lib/stomp.min.js}"></script>
<script th:src="@{stomp-client.js}"></script>
<script>connect(function () {statusChange("连接中...");}, function () {statusChange("在线");// 注意,为防止重连导致重复订阅,请勿使用匿名函数做回调subscribe("/user/topic/subNewMsg", onNewMsg);}, function () {statusChange("离线");});function onNewMsg(msg) {var li = document.createElement("li");li.innerText = msg.content;document.getElementById("ul").appendChild(li);}function sendTextMsg() {var content = document.getElementById("content").value;var msg = {msgType: 1,content: content};send("/app/echo", msg);}function statusChange(status) {document.getElementById("status").innerText = status;}
</script>
</body>
</html>

后端代码

依赖引入,主要引入下面的包,其它的包略过

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>

配置类

@Slf4j
@Setter
@Configuration
@EnableWebSocketMessageBroker
@ConfigurationProperties(prefix = "websocket")
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer, ApplicationListener<BrokerAvailabilityEvent> {private final BrokerConfig brokerConfig;private String[] allowOrigins;@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {// 继承DefaultHandshakeHandler并重写determineUser方法,可以自定义如何确定用户// 添加方法:registry.addEndpoint("/ws").setHandshakeHandler(handshakeHandler)registry.addEndpoint("/ws").setAllowedOrigins(allowOrigins).withSockJS();}/*** 配置消息代理*/@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.setApplicationDestinationPrefixes("/app");if (brokerConfig.isUseSimpleBroker()) {// 使用 SimpleBroker// 配置前缀, 有这些前缀的消息会路由到brokerregistry.enableSimpleBroker("/topic", "/queue")//配置stomp协议里, server返回的心跳.setHeartbeatValue(new long[]{10000L, 10000L})//配置发送心跳的scheduler.setTaskScheduler(new DefaultManagedTaskScheduler());} else {// 使用外部 Broker// 指定前缀,有这些前缀的消息会路由到brokerregistry.enableStompBrokerRelay("/topic", "/queue")// 广播用户目标,如果要推送的用户不在本地,则通过 broker 广播给集群的其他成员.setUserDestinationBroadcast("/topic/log-unresolved-user")// 用户注册广播,一旦有用户登录,则广播给集群中的其他成员.setUserRegistryBroadcast("/topic/log-user-registry")// 虚拟地址.setVirtualHost(brokerConfig.getVirtualHost())// 用户密码.setSystemLogin(brokerConfig.getUsername()).setSystemPasscode(brokerConfig.getPassword()).setClientLogin(brokerConfig.getUsername()).setClientPasscode(brokerConfig.getPassword())// 心跳间隔.setSystemHeartbeatSendInterval(10000).setSystemHeartbeatReceiveInterval(10000)// 使用 setTcpClient 以配置多个 broker 地址,setRelayHost/Port 只能配置一个.setTcpClient(createTcpClient());}}/*** 创建 TcpClient 工厂,用于配置多个 broker 地址*/private ReactorNettyTcpClient<byte[]> createTcpClient() {return new ReactorNettyTcpClient<>(// BrokerAddressSupplier 用于获取中继地址,一次只使用一个,如果该中继出错,则会获取下一个client -> client.addressSupplier(brokerConfig.getBrokerAddressSupplier()),new StompReactorNettyCodec());}@Overridepublic void onApplicationEvent(BrokerAvailabilityEvent event) {if (!event.isBrokerAvailable()) {log.warn("stomp broker is not available!!!!!!!!");} else {log.info("stomp broker is available");}}
}

消息处理

@Slf4j
@Controller
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class StompController {private final SimpMessageSendingOperations msgOperations;private final SimpUserRegistry simpUserRegistry;/*** 回音消息,将用户发来的消息内容加上 Echo 前缀后推送回客户端*/@MessageMapping("/echo")public void echo(Principal principal, Msg msg) {String username = principal.getName();msg.setContent("Echo: " + msg.getContent());msgOperations.convertAndSendToUser(username, "/topic/subNewMsg", msg);int userCount = simpUserRegistry.getUserCount();int sessionCount = simpUserRegistry.getUser(username).getSessions().size();log.info("当前本系统总在线人数: {}, 当前用户: {}, 该用户的客户端连接数: {}", userCount, username, sessionCount);}
}

实现效果

在这里插入图片描述

报文分析

开启调试模式,我们根据报文来分析一下前后端互通的报文
在这里插入图片描述

握手

客户端请求报文如下

GET ws://localhost:8025/ws/035/5hy4avgm/websocket HTTP/1.1
Host: localhost:8025
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.289 Safari/537.36
Upgrade: websocket
Origin: http://localhost:8025
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: 略
Sec-WebSocket-Key: PlMHmdl2JRzDAVk3feOaeA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

服务端响应握手请求

HTTP/1.1 101
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 9CKY8n1j/cHoKsWmpmX4pNlQuZg=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Date: Thu, 08 Feb 2024 06:58:28 GMT

stomp报文分析

在浏览器消息一栏,我们可以看到长连接过程中通信的报文
在这里插入图片描述
下面来简单分析一下stomp的报文
在这里插入图片描述

客户端请求连接

其中\n表示换行

["CONNECT\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000"
]

可以看到请求连接的命令是CONNECT,连接报文里面还包含了心跳的信息

服务端返回连接成功

["CONNECTED\nversion:1.1\nheart-beat:10000,10000\nuser-name:admin\n\n\u0000"
]

CONNECTED是服务端连接成功的命令,报文中也包含了心跳的信息

客户端订阅

订阅的目的地是:/user/topic/subNewMsg

["SUBSCRIBE\nid:sub-0\ndestination:/user/topic/subNewMsg\n\n\u0000"]

客户端发送消息

发送的目的地是:/app/echo

["SEND\ndestination:/app/echo\ncontent-length:35\n\n{\"msgType\":1,\"content\":\"你好啊\"}\u0000"
]

服务端响应消息

响应的目的地是:/user/topic/subNewMsg,当订阅了这个目的地的,方法,将会被回调

["MESSAGE\ndestination:/user/topic/subNewMsg\ncontent-type:application/json;charset=UTF-8\nsubscription:sub-0\nmessage-id:5hy4avgm-1\ncontent-length:41\n\n{\"content\":\"Echo: 你好啊\",\"msgType\":1}\u0000"
]

心跳报文

可以看到,约每隔10S,客户端和服务端都有一次心跳报文,发送的报文内容为一个回车。

[\n]

在这里插入图片描述

项目链接:https://gitee.com/syk1234/stomp-demo.git

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

2.6日学习打卡----初学RabbitMQ(一)

2.6日学习打卡 初识RabbitMQ、 一. MQ 消息队列 MQ全称Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保 存消息的容器。多用于系统之间的异步通信。 同步通信相当于两个人当面对话&#xff0c;你一言我一语。必须及时回复 异步通信相当于通…

Office 2010下载安装教程,保姆级教程,附安装包和工具

前言 Microsoft Office是由Microsoft(微软)公司开发的一套基于 Windows 操作系统的办公软件套装。常用组件有 Word、Excel、PowerPoint、Access、Outlook等。 准备工作 1、Win7 及以上系统 2、提前准备好 Office 2010 安装包 安装步骤 1.鼠标右击【Office2010(64bit)】压缩…

数据结构第十二天(队列)

目录 前言 概述 源码&#xff1a; 主函数&#xff1a; 运行结果&#xff1a; 前言 今天和大家共享一句箴言&#xff1a;我本可以忍受黑暗&#xff0c;如果我不曾见过太阳。 概述 队列&#xff08;Queue&#xff09;是一种常见的数据结构&#xff0c;遵循先进先出&#…

华为机考入门python3--(10)牛客10-字符个数统计

分类&#xff1a;字符 知识点&#xff1a; 字符的ASCII码 ord(char) 题目来自【牛客】 def count_unique_chars(s): # 创建一个空集合来保存不同的字符 unique_chars set() # 遍历字符串中的每个字符 for char in s: # 将字符转换为 ASCII 码并检查是否在范围内 #…

C#上位机与三菱PLC的通信05--MC协议之QnA-3E报文解析

1、MC协议回顾 MC是公开协议 &#xff0c;所有报文格式都是有标准 &#xff0c;MC协议可以在串口通信&#xff0c;也可以在以太网通信 串口&#xff1a;1C、2C、3C、4C 网口&#xff1a;4E、3E、1E A-1E是三菱PLC通信协议中最早的一种&#xff0c;它是一种基于二进制通信协…

教师如何找答案? #知识分享#职场发展

当今社会&#xff0c;随着信息技术的迅猛发展&#xff0c;大学生们在学习过程中面临着各种各样的困难和挑战。而在这些挑战中&#xff0c;面对繁重的作业和复杂的题目&#xff0c;大学生搜题软件应运而生 1.快解题 这是一个网站 是一款服务于职业考证的考试搜题软件,拥有几千…

从基建发力,CESS 如何推动 RWA 发展?

2023 年 11 月 30 日&#xff0c;Web3 基金会&#xff08;Web3 Foundation&#xff09;宣布通过 Centrifuge 将部分资金投资于 RWA&#xff08;Real World Assets&#xff0c;真实世界资产&#xff09;&#xff0c;试点投资为 100 万美元。Web3 基金会旨在通过支持专注于隐私、…

倒计时59天

(来源&#xff1a;b站左程云up 099&#xff09; 一&#xff1a;求逆元&#xff1a; 1&#xff09;要保证a可以整除b 2)要保证mod的是一个质数 3&#xff09;b和mod互质 题目2&#xff09;3&#xff09;一般都满足&#xff0c;主要是1) 方法&#xff1a;如求1.…

骨科器械行业分析:市场规模为360亿元

骨科器械一般指专门用于骨科手术用的专业医疗器械。按国家食品药品监督局的分类划分常分为&#xff1a;一类;二类和三类。按照使用用途和性能主要分为骨科用刀、骨科用剪、骨科用钳、骨科用钩、骨科用针、骨科用刮、骨科用锥、骨科用钻、骨科用锯、骨科用凿、骨科用锉/铲、骨科…

【C语言】一道相当有难度的指针某大厂笔试真题(超详解)

这是比较复杂的题目&#xff0c;但是如果我们能够理解清楚各个指针代表的含义&#xff0c;画出各级指针的关系图&#xff0c;这道题就迎刃而解了。 学会这道笔试题&#xff0c;相信你对指针的理解&#xff0c;对数组&#xff0c;字符串的理解都会上一个档次。 字符串存储使用的…

UDP是什么,UDP协议及优缺点

UDP&#xff0c;全称 User Datagram Protocol&#xff0c;中文名称为用户数据报协议&#xff0c;主要用来支持那些需要在计算机之间传输数据的网络连接。 UDP 协议从问世至今已经被使用了很多年&#xff0c;虽然目前 UDP 协议的应用不如 TCP 协议广泛&#xff0c;但 UDP 依然是…

JAVA设计模式之代理模式详解

代理模式 1 代理模式介绍 在软件开发中,由于一些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称为"代理"的第三者来实现间接访问.该方案对应的设计模式被称为代理模式. 代理模式(Proxy Design Pattern ) 原始定义是&#xff1a;让你能够提供对象的替代…

OpenEuler20.03LTS SP2 上安装 OpenGauss3.0.0 单机部署过程(二)

开始安装 OpenGauss 数据库 3.1.7 安装依赖包 (说明:如果可以联网,可以通过网络 yum 安装所需依赖包,既可以跳过本步骤。如果网络无法连通,请把本文档所在目录下的依赖包上传到服务器上,手工安装后,即无需通过网络进行 Yum 安装了): 上传:libaio-0.3.111-5.oe1.x8…

【机器学习】合成少数过采样技术 (SMOTE)处理不平衡数据(附代码)

1、简介 不平衡数据集是机器学习和人工智能中普遍存在的挑战。当一个类别中的样本数量明显超过另一类别时&#xff0c;机器学习模型往往会偏向大多数类别&#xff0c;从而导致性能不佳。 合成少数过采样技术 (SMOTE) 已成为解决数据不平衡问题的强大且广泛采用的解决方案。 …

Webshell一句话木马

一、webshell介绍&#xff08;网页木马&#xff09; 分类&#xff1a; 大马&#xff1a;体积大、隐蔽性差、功能多 小马&#xff1a;体积小&#xff0c;隐蔽强&#xff0c;功能少 一句话木马&#xff1a;代码简短&#xff0c;灵活多样 二、一句话木马&#xff1a; &#xff1a;…

文件查找和解压缩

一、文件搜索查找 1、按照名字搜索 &#xff08;1&#xff09;查找software目录下名字为1.txt的文件 [rootmaster opt]# find software/ -name 1.txt software/1.txt&#xff08;2&#xff09;查找software目录下所有以.txt结尾的文件 [rootmaster opt]# find software/ -n…

新春满满的祝福,春晚文字版节目单,养生篮球与吃喝玩乐——早读

新年快乐都是祝福 引言代码第一篇&#xff08;跳&#xff09; 人民日报 “兔兔&#xff0c;这一年辛苦了&#xff0c;接下来就交给我吧&#xff01;”第三篇 人民日报 【夜读】新年三愿&#xff1a;家人安康&#xff0c;生活美满&#xff0c;心怀希望第四篇 人民日报&#xff0…

【OrangePi Zero2的系统移植】OrangePi Zero2 SDK说明

一、使用环境要求 二、获取Linux SDK 三、首次编译完整SDK 基于OrangePi Zero2的系统移植 之前我们讲解香橙派的使用时&#xff0c; 都是直接在香橙派上进行代码编译&#xff0c; 但在实际的项目开发过程中&#xff0c;更多 的还是使用交叉编译环境进行代码的编译。再编译完成…

VUE学习之路——列表渲染

<p v-for"item in items">{{ item }}</p>使用v-for进行列表的渲染。 这仅仅是一个简单的demo&#xff0c;使用v-for可以用来遍历数组和对象&#xff0c;具体如下&#xff1a; 注意&#xff1a;遍历数组或对象的时候&#xff0c;&#xff08;&#xff09;…

Kafka集群安装与部署

集群规划 准备工作 安装 安装包下载&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1BtSiaf1ptLKdJiA36CyxJg?pwd6666 Kafka安装与配置 1、上传并解压安装包 tar -zxvf kafka_2.12-3.3.1.tgz -C /opt/moudle/2、修改解压后的文件名称 mv kafka_2.12-3.3.1/ kafka…