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

news2024/12/29 19:06:38

本示例实现一个功能,前端通过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;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 继承DefaultHandshakeHandler并重写determineUser方法,可以自定义如何确定用户
        // 添加方法:registry.addEndpoint("/ws").setHandshakeHandler(handshakeHandler)
        registry.addEndpoint("/ws")
                .setAllowedOrigins(allowOrigins)
                .withSockJS();
    }

    /**
     * 配置消息代理
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");

        if (brokerConfig.isUseSimpleBroker()) {
            // 使用 SimpleBroker
            // 配置前缀, 有这些前缀的消息会路由到broker
            registry.enableSimpleBroker("/topic", "/queue")
                    //配置stomp协议里, server返回的心跳
                    .setHeartbeatValue(new long[]{10000L, 10000L})
                    //配置发送心跳的scheduler
                    .setTaskScheduler(new DefaultManagedTaskScheduler());
        } else {
            // 使用外部 Broker
            // 指定前缀,有这些前缀的消息会路由到broker
            registry.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());
    }

    @Override
    public 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://www.coloradmin.cn/o/1449552.html

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

相关文章

半导体通讯SECS-I是什么?

SECS-I&#xff08;Semi Equipment Communications Standard 1 Message Transfer&#xff09;是一个定义如何发送和接收通信内容&#xff08;Content&#xff09;的协议。此标准定义了通过RS-232C传输介质进行通信内容的发送和接收规约。 其主要特点如下&#xff1a; 1.使用RS2…

ATCoder Beginner Contest 340 A~G

A.Arithmetic Progression&#xff08;模拟&#xff09; 题意&#xff1a; 输出首项为 a a a&#xff0c;末项为 b b b&#xff0c;公差为 d d d的等差数列。 分析&#xff1a; 按照要求模拟。 代码&#xff1a; #include <bits/stdc.h>using namespace std;int ma…

CTF-web 之 burp suite 使用

burp suite 使用 一般其是作为一个辅助工具&#xff0c;直接使用来解题的部分是少数&#xff0c;我们可以使用它来观察请 求和响应&#xff0c;并且可以反复的提交&#xff0c;关键的是他还带有很多其他的功能&#xff0c;在我们做题的过程中&#xff0c; 使用的关键点包括&…

算法沉淀——栈(leetcode真题剖析)

算法沉淀——栈 01.删除字符串中的所有相邻重复项02.比较含退格的字符串03.基本计算器 II04.字符串解码05.验证栈序列 栈&#xff08;Stack&#xff09;是一种基于先进后出&#xff08;Last In, First Out&#xff0c;LIFO&#xff09;原则的数据结构。栈具有两个主要的操作&am…

统一数据返回格式 及 可能遇到的问题;统一异常处理

统一数据返回格式 统一数据返回格式就像我们寄快递一样&#xff0c;不管你需要寄的东西具体是什么都需要将它打包到统一的快递箱中。 此时我们需要一个“快递箱”用来将返回的数据“装”在里面。这个类是根据业务情况来自行定义的。 Data public class Resp<T> {//200…

【算法随想录03】相交链表

题目&#xff1a;160. 相交链表 难度&#xff1a;EASY 思路 主要难点在于如何进行节点之间的对应。两条链表长度不定长&#xff0c;如何找到需要对比的节点至关重要。 我们从后往前看&#xff0c;我们需要对比的节点有什么特点。一个最大的特点就是后面的节点数相同。这就…

Python算法题集_对称二叉树

Python算法题集_对称二叉树 题101&#xff1a;对称二叉树1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【DFS递归】2) 改进版一【BFS迭代】3) 改进版二【BFS迭代deque】 4. 最优算法 本文为Python算法题集之一的代码示例 题101&#xff1a;对…

软件架构设计--操作系统

常见I/O类型:缓冲I/O和直接I/O 缓冲I/O是C语言提供的库函数 直接I/O是Linux系统API 应用程序内存:代码中用malloc/free,new/delete 等分配出来的内存 用户缓冲区:C语言的FILE结构体里面的bufer. 内核缓冲区:Linux操作系统的Page Cache. 1Page 一般为4K 缓冲I/O的读操作有3次数…

JVM(4)原理篇

1 栈上的数据存储 在Java中有8大基本数据类型&#xff1a; 这里的内存占用&#xff0c;指的是堆上或者数组中内存分配的空间大小&#xff0c;栈上的实现更加复杂。 以基础篇的这段代码为例&#xff1a; Java中的8大数据类型在虚拟机中的实现&#xff1a; boolean、byte、char…

【贪心算法】代码随想录算法训练营第三十二天 |122.买卖股票的最佳时机II,55.跳跃游戏,45.跳跃游戏II(待补充)

122.买卖股票的最佳时机II&#xff08;未观看&#xff0c;动态待补充&#xff09; 1、题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 2、文章讲解&#xff1a;代码随想录 3、题目&#xff1a; 给定一个数组&#xff0c;它的…

云备份项目:在云端保护您的数据【一、初识】

桃李不言&#xff0c;下自成蹊 文章目录 项目简介项目设计方案服务端功能划分客户端功能划分 项目环境搭建环境准备第三方库JsonCppbundle数据压缩库httplib 总结 ☘️项目源代码&#xff1a;云备份 ☘️云备份专栏&#xff1a;云备份 项目简介 云备份系统是一个自动化的备份解…

vue3 之 商城项目—结算模块

路由配置 chekout/index.vue <script setup> const checkInfo {} // 订单对象 const curAddress {} // 地址对象 </script> <template><div class"xtx-pay-checkout-page"><div class"container"><div class"w…

Linux platform tree下的单总线驱动程序设计(DHT11)

目录 概述 1 认识DHT11 1.1 DHT11特性 1.2 DHT11数据格式 1.3 DHT11与MCU通信 1.4 DHT11信号解析 1.4.1 起始信号 1.4.2 解析信号0 1.4.3 解析信号1 2 驱动开发 2.1 硬件接口 2.2 更新设备树 2.2.1 添加驱动节点 2.2.2 编译.dts 2.2.3 更新板卡中的.dtb 2.3 驱…

【开源】SpringBoot框架开发企业项目合同信息系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 合同审批模块2.3 合同签订模块2.4 合同预警模块2.5 数据可视化模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 合同审批表3.2.2 合同签订表3.2.3 合同预警表 四、系统展示五、核心代码5.1 查询合同…

蓝桥杯嵌入式学习记录——按键的使用

目录 一、按键原理简介 二、cubeMX的配置 三、按键的短按代码 四、按键的长按代码 一、按键原理简介 在STM32中&#xff0c;按键连接通常使用GPIO&#xff08;通用输入/输出&#xff09;端口来实现。当按键未被按下时&#xff0c;GPIO端口处于高电平状态&#xff08;即1&am…

【Go语言】第一个Go程序

第一个 Go 程序 1 安装 Go Go语言官网&#xff1a;Download and install - The Go Programming Language&#xff0c;提供了安装包以及引导流程。 以 Windows 为例&#xff0c;进入windows安装包下载地址&#xff1a;All releases - The Go Programming Language&#xff0c…

【Midjourney】解密Midjourney付费订阅:畅享全新体验!(详细流程与各版本一览)

一、Midjourney 付费订阅流程 1、在首页点击Purchase plan 2、进入到midjourney年月选择页面 3、这里续费一个最便宜的版本 , 按年付费 8 , 按月 10 4、输入银行卡信息 , 用的WildCard虚拟信用卡 &#xff0c;打开 5、填写完银行卡信息就订阅成功 二、Midjourney 各版本介绍…

洛谷_P1923 【深基9.例4】求第 k 小的数_python写法

哪位大佬可以出一下这个的题解&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;话说蓝桥杯可以用numpy库吗&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f; 这道题有一个很简单的思路就是排序完成之后再访问。 but有很大的问题&…

SAP PP学习笔记- 豆知识01 - 怎么查询既存品目

SAP系统当中已经有哪些品目要怎么查询呢&#xff1f; 1&#xff0c;MM60 品目一览 这里可以输入Plant&#xff0c;然后可以查询该工厂的所有品目。 2&#xff0c;SE16 > MARA MARA 品目一般データ&#xff0c;存放的是品目基本信息。 要查询该品目属于哪个Plant&#xff…

如何在30天内使用python制作一个卡牌游戏

如何在30天内使用python制作一个卡牌游戏 第1-5天&#xff1a;规划和设计第6-10天&#xff1a;搭建游戏框架第11-20天&#xff1a;核心游戏机制开发第21-25天&#xff1a;游戏界面和用户体验第26-30天&#xff1a;测试和发布附加建议游戏类型游戏规则设计界面设计技术选型第6-…