Spring Boot整合STOMP实现实时通信

目录

引言

代码实现

配置类WebSocketMessageBrokerConfig

 DTO

工具类

Controller

common.html

stomp-broadcast.html

运行效果 

完整代码地址


引言

STOMP(Simple Text Oriented Messaging Protocol)作为一种简单文本导向的消息传递协议,提供了一种轻量级且易于使用的方式来实现实时通信。本篇博客将讲解如何使用Spring Boot创建一个基于STOMP的WebSocket应用程序,并展示相关的配置类。同时,还会介绍如何使用Thymeleaf模板引擎生成动态的HTML页面,以展示实时通信的效果。

代码实现

配置类WebSocketMessageBrokerConfig

package com.wsl.websocket.config;import org.springframework.context.annotation.Configuration;
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;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/topic");config.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {// it is OK to leave it hereregistry.addEndpoint("/broadcast");//registry.addEndpoint("/broadcast").withSockJS();// custom heartbeat, every 60 sec//registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000);}
}

 DTO

package com.wsl.websocket.dto;import com.wsl.websocket.util.StringUtils;
import lombok.Getter;
import lombok.Setter;@Getter
@Setter
public class ChatMessage {private String from;private String text;private String recipient;private String time;public ChatMessage() {}public ChatMessage(String from, String text, String recipient) {this.from = from;this.text = text;this.recipient = recipient;this.time = StringUtils.getCurrentTimeStamp();}
}

工具类

package com.wsl.websocket.util;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;public class StringUtils {private static final String TIME_FORMATTER= "HH:mm:ss";public static String getCurrentTimeStamp() {DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIME_FORMATTER);return LocalDateTime.now().format(formatter);}
}

Controller

package com.wsl.websocket.controller;import com.wsl.websocket.dto.ChatMessage;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;@Controller
public class WebSocketBroadcastController {@GetMapping("/stomp-broadcast")public String getWebSocketBroadcast() {return "stomp-broadcast";}@GetMapping("/sockjs-broadcast")public String getWebSocketWithSockJsBroadcast() {return "sockjs-broadcast";}@MessageMapping("/broadcast")@SendTo("/topic/broadcast")public ChatMessage send(ChatMessage chatMessage) {return new ChatMessage(chatMessage.getFrom(), chatMessage.getText(), "ALL");}
}

common.html

src/main/resources/templates/common.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headerfiles"><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/4.4.1/css/bootstrap.css}"/><link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/><script th:src="@{/webjars/jquery/3.4.1/jquery.js}"></script>
</head>
<body>
<footer th:fragment="footer" class="my-5 text-muted text-center text-small"><p class="mb-1">© 2020 Dariawan</p><ul class="list-inline"><li class="list-inline-item"><a href="https://www.dariawan.com">Homepage</a></li><li class="list-inline-item"><a href="#">Articles</a></li></ul>
</footer>
</body>
</html>

stomp-broadcast.html

src/main/resources/templates/stomp-broadcast.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>WebSocket With STOMP Broadcast Example</title><th:block th:include="common.html :: headerfiles"></th:block>
</head>
<body>
<div class="container"><div class="py-5 text-center"><a href="/"><h2>WebSocket</h2></a><p class="lead">WebSocket Broadcast - with STOMP</p></div><div class="row"><div class="col-md-6"><div class="mb-3"><div class="input-group"><input type="text" id="from" class="form-control" placeholder="Choose a nickname"/><div class="btn-group"><button type="button" id="connect" class="btn btn-sm btn-outline-secondary" onclick="connect()">Connect</button><button type="button" id="disconnect" class="btn btn-sm btn-outline-secondary"onclick="disconnect()" disabled>Disconnect</button></div></div></div><div class="mb-3"><div class="input-group" id="sendmessage" style="display: none;"><input type="text" id="message" class="form-control" placeholder="Message"><div class="input-group-append"><button id="send" class="btn btn-primary" onclick="send()">Send</button></div></div></div></div><div class="col-md-6"><div id="content"></div><div><span class="float-right"><button id="clear" class="btn btn-primary" onclick="clearBroadcast()"style="display: none;">Clear</button></span></div></div></div>
</div><footer th:insert="common.html :: footer"></footer><script th:src="@{/webjars/stomp-websocket/2.3.3-1/stomp.js}" type="text/javascript"></script>
<script type="text/javascript">var stompClient = null;var userName = $("#from").val();function setConnected(connected) {$("#from").prop("disabled", connected);$("#connect").prop("disabled", connected);$("#disconnect").prop("disabled", !connected);if (connected) {$("#sendmessage").show();} else {$("#sendmessage").hide();}}function connect() {userName = $("#from").val();if (userName == null || userName === "") {alert('Please input a nickname!');return;}/*<![CDATA[*/var url = /*[['ws://'+${#httpServletRequest.serverName}+':'+${#httpServletRequest.serverPort}+@{/broadcast}]]*/ 'ws://localhost:8080/broadcast';/*]]>*/stompClient = Stomp.client(url);stompClient.connect({}, function () {stompClient.subscribe('/topic/broadcast', function (output) {showBroadcastMessage(createTextNode(JSON.parse(output.body)));});sendConnection(' connected to server');setConnected(true);}, function (err) {alert('error' + err);});}function disconnect() {if (stompClient != null) {sendConnection(' disconnected from server');stompClient.disconnect(function () {console.log('disconnected...');setConnected(false);});}}function sendConnection(message) {var text = userName + message;sendBroadcast({'from': 'server', 'text': text});}function sendBroadcast(json) {stompClient.send("/app/broadcast", {}, JSON.stringify(json));}function send() {var text = $("#message").val();sendBroadcast({'from': userName, 'text': text});$("#message").val("");}function createTextNode(messageObj) {return '<div class="row alert alert-info"><div class="col-md-8">' +messageObj.text +'</div><div class="col-md-4 text-right"><small>[<b>' +messageObj.from +'</b> ' +messageObj.time +']</small>' +'</div></div>';}function showBroadcastMessage(message) {$("#content").html($("#content").html() + message);$("#clear").show();}function clearBroadcast() {$("#content").html("");$("#clear").hide();}
</script>
</body>
</html>

运行效果 

http://localhost:8080/stomp-broadcast

完整代码地址

 GitHub - wangsilingwsl/springboot-stomp: springboot integrates stomp

指定消息的目标用户

此功能基于Spring Boot,和上面代码分隔开,没有关联关系。请结合实际情况参考下列代码。

配置类

package com.twqc.config.websocket;import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
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消息代理配置类* <p>* 注解@EnableWebSocketMessageBroker表示启用WebSocket消息代理功能。不开启不能使用@MessageMapping和@SendTo注解。* 注意:@MessageMapping和@SendTo的使用方法:* <p>* 注解@MessageMapping的方法用于接收客户端发送的消息,方法的参数用于接收消息内容,方法的返回值用于发送消息内容。* 注解@SendTo的方法用于发送消息内容,方法的返回值用于发送消息内容。* <p>* 示例:@MessageMapping("/example")注解的方法用于接收客户端发送的消息,@SendTo("/topic/example")注解的方法用于发送消息内容。* 3.1 对应的客户端连接websocket的路径为:ws://localhost:8080/example* 3.2 对应的客户端发送消息的路径为:/app/example* 3.3 对应的客户端接收消息的路径为:/topic/example* 3.4 app和topic在WebSocketMessageBrokerConfigurer.configureMessageBroker方法中配置* 3.5 具体的路径需要自己定义,上文仅为示例,与本项目中使用的路径无关。* <p>** @author wsl* @date 2024/2/29* @see WebSocketMessageBrokerConfigurer* @see MessageMapping* @see SendTo* @see <a href="https://www.dariawan.com/tutorials/spring/spring-boot-websocket-stomp-tutorial/">Spring Boot WebSocket with STOMP Tutorial</a>*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {/*** 配置消息代理** @param config 消息代理配置注册器*/@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {// 设置消息代理的目的地前缀,所有以"/websocket/topic"开头的消息都会被代理发送给订阅了该目的地的客户端config.enableSimpleBroker("/websocket/topic");// 设置应用的目的地前缀,所有以"/websocket/app"开头的消息都会被路由到带有@MessageMapping注解的方法中进行处理config.setApplicationDestinationPrefixes("/websocket/app");// 设置用户的目的地前缀,所有以"/user"开头的消息都会被代理发送给订阅了该目的地的用户config.setUserDestinationPrefix("/user");}/*** 注册消息端点* 可以注册多个消息端点,每个消息端点对应一个URL路径。** @param registry 消息端点注册器*/@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {// 注册消息端点,参数为消息端点的URL路径(对应@MessageMapping注解的路径,也是客户端连接的路径)registry.addEndpoint("/websocket/bpm/runFlow")// 设置允许的跨域请求来源.setAllowedOrigins("*");}/*** 配置客户端入站通道** @param registration 客户端入站通道注册器*/@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) {// 设置客户端入站通道的自定义拦截器registration.interceptors(new MyWebSocketInterceptor());}
}

拦截器

package com.twqc.config.websocket;import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;import java.util.List;/*** WebSocket拦截器** @author wsl* @date 2024/3/4*/
@Slf4j
public class MyWebSocketInterceptor implements ChannelInterceptor {@Overridepublic Message<?> preSend(Message<?> message, MessageChannel channel) {StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);if (StompCommand.CONNECT.equals(accessor.getCommand())) {List<String> nativeHeaders = accessor.getNativeHeader("username");if (nativeHeaders != null) {String username = nativeHeaders.get(0);// 存入Principalaccessor.setUser(() -> username);log.info("用户:{}发起了stomp连接", username);} else {log.info("未提供用户名的stomp连接");return message;}}return message;}
}

用户的消息推送有两种实现方式

@SendToUser

    @MessageMapping("/websocket/bpm/runFlow")@SendToUser("/websocket/topic/websocket/bpm/runFlow")public String runFlow2(Principal principal) {if (principal == null || principal.getName() == null) {log.error("未提供用户名的stomp连接,无法运行流程");}String username = principal.getName();return "success" + username;}

SimpMessagingTemplate

    @MessageMapping("/websocket/bpm/runFlow")public void runFlow(FlowRequest request, Principal principal) {if (principal == null || principal.getName() == null) {log.error("未提供用户名的stomp连接,无法运行流程");}String username = principal.getName();flowService.runFlow(request, username);}

 flowService

    @Autowiredprivate SimpMessagingTemplate messagingTemplate;private void sendNodeResult(FlowResponse response, String username) {messagingTemplate.convertAndSendToUser(username, BpmConstant.Flow.TOPIC_FLOW_RESULTS, response);}

前端(客户端)

<template><div id="app"><!-- 发送消息表单 --><van-form @submit="onSubmit"><van-fieldv-model="content"name="内容"label="内容"rows="3"autosizetype="textarea"placeholder="请输入内容"/><div style="margin: 16px"><van-button round block type="info" native-type="submit">提交</van-button></div></van-form><!-- 消息回复体 --><van-cell-group><van-cellv-for="(msgItem, msgIndex) in msgList":key="msgIndex":title="'回复消息' + (msgIndex + 1)"value="":label="msgItem"/></van-cell-group></div>
</template><script>
import Stomp from "stompjs";
let socketTimer = null;export default {name: "App",created() {this.username = "admin";this.initWebsocket();},data() {return {content: "",stompClient: null,msgList: [],username: "admin",};},methods: {initWebsocket() {this.stompClient = Stomp.client("ws://192.168.1.109:7010/websocket/bpm/runFlow");this.stompClient.debug = null;const headers = {username: this.username,};this.stompClient.connect(headers, // 自定义请求头() => {this.stompClient.subscribe("/user/websocket/topic/websocket/bpm/runFlow",(res) => {this.msgList.push(JSON.stringify(res));});},(err) => {console.log("err", err);this.$toast("连接失败:" + JSON.stringify(err));// 监听报错信息,手动发起重连if (socketTimer) {clearInterval(socketTimer);}// 10s后重新连接一次socketTimer = setTimeout(() => {this.initWebsocket();}, 10000);});// this.stompClient.heartbeat.outgoing = 10000;// 若使用STOMP 1.1 版本,默认开启了心跳检测机制(默认值都是10000ms)// this.stompClient.heartbeat.incoming = 0;// 客户端不从服务端接收心跳包},closeWebsocket() {if (this.stompClient !== null) {this.stompClient.disconnect(() => {console.log("关闭连接");});}},onSubmit() {// 发送信息// 转成json对象this.stompClient.send("/websocket/app/websocket/bpm/runFlow",{},// JSON.stringify({ content: this.content })this.content);},},destroyed() {// 页面销毁后记得关闭定时器clearInterval(socketTimer);this.closeWebsocket();},
};
</script><style>
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;color: #2c3e50;
}
</style>

 请注意,客户端接收信号时,需要在订阅地址前加上/app,否则接收失败。

完整代码地址

GitHub - wangsilingwsl/vue-stomp

参考:Spring Boot + WebSocket With STOMP Tutorial | Dariawan 

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

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

相关文章

sqllab第二十七A关通关笔记

知识点&#xff1a; 双引号闭合union select 大小写绕过 Union Select这里不能进行错误注入&#xff0c;无回显 经过测试发现这是一个双引号闭合 构造payload:id1"%09and%091"1 页面成功回显 构造payload:id0"%09uNion%09SElect%091,2,3%09"1 页面成功…

在雄安新区买新房要注意什么?有哪些注意事项?

雄安新区新建住宅均价每平方米11735元起&#xff0c;二手房每平方米8950元起。 整体价格非常有优势。 雄安新区房价走势与区域发展直接相关。 而且&#xff0c;雄安新区已经成立五周年了。 2022年&#xff0c;雄安新区多项重点项目将陆续竣工。 雄安新区城市基础设施建设已初具…

Linux之shell循环

华子目录 for循环带列表的for循环格式分析示例shell允许用户指定for语句的步长&#xff0c;格式如下示例 不带列表的for循环示例 基于C语言风格的for循环格式示例注意 while循环格式示例 until循环作用格式示例 循环控制breakcontinue详细语法示例 循环嵌套示例 for循环 for循…

深度学习——SAM(Segment-Anything)代码详解

目录 引言代码目录segment-anything 代码详解build_sam.pypredictor.pyautomatic_mask_generator.py 引言 从去年年初至今&#xff0c;SAM(Segment Anything )已经问世快一年了&#xff0c;SAM凭借其强大而突出的泛化性能在各项任务上取得了优异的表现&#xff0c;广大的研究者…

源码编译部署LAMP

编译部署LAMP 配置apache [rootzyq ~]#: wget https://downloads.apache.org/apr/apr-1.7.4.tar.gz --2023-12-11 14:35:57-- https://downloads.apache.org/apr/apr-1.7.4.tar.gz Resolving downloads.apache.org (downloads.apache.org)... 88.99.95.219, 135.181.214.104…

BUUCTF-WEB1

[ACTF2020 新生赛]Exec1 1.打开靶机 是一个ping命令 2.利用管道符“|” ping一下本地主机并查看ls ping 127.0.0.1 | ls 可以看到回显的内容是一个文件 127.0.0.1 | cat index.php #查看主机下index.php 127.0.0.1 | ls / #查看主机根目录下的文件 看的一个flag文件 …

数据仓库数据分层详解

数据仓库中的数据分层是一种重要的数据组织方式&#xff0c;其目的是为了在管理数据时能够对数据有一个更加清晰的掌控。以下是数据仓库中的数据分层详解&#xff1a; 原始数据层&#xff08;Raw Data Layer&#xff09;&#xff1a;这是数仓中最底层的层级&#xff0c;用于存…

jupyter闪退和自动跳转问题

1.闪退问题 当我们点击jupyter时&#xff0c;它会闪一下&#xff0c;然后无法进入&#xff0c;这个时候我们可以去prompt命令行输入jupyter notebook启动试试&#xff0c;如果还不行&#xff0c;我们可以根据报错去解决&#xff0c;一般csdn上都有对应情况&#xff0c;直接搜索…

Linux-新手小白速秒Hadoop集群全生态搭建(图文混编超详细)

在之前的文章中&#xff0c;我教会大家如何一步一步搭建一个Hadoop集群&#xff0c;但是只提供了代码&#xff0c;怕有些朋友会在一些地方产生疑惑&#xff0c;今天我来以图文混排的方式&#xff0c;一站式交给大家如何搭建一个Hadoop高可用集群包括&#xff08;HadoopHA&#…

el-select使用filterable下拉无法关闭得问题

这里推荐一个前端框架 sakuya / SCUI&#xff0c;他里面有个formTable&#xff0c;可以解决很多订单明细保存得问题。基本沿用element-plus的前端使用模式&#xff0c;让表单表格变的非常容易。 这个的供应商插件&#xff0c;当使用filterable后&#xff0c;点击表格重的选项&…

Redis Desktop Manager:一站式Redis数据库管理与优化

Redis Desktop Manager是一款功能强大的Redis桌面管理工具&#xff0c;也被称作Redis可视化工具。以下是其主要的功能特色&#xff1a; 连接管理&#xff1a;Redis Desktop Manager支持连接多个Redis服务器&#xff0c;用户可以在同一界面下管理多个数据库&#xff0c;大大提高…

记录一下在Pycharm中虚拟环境的创建

如果在Pycharm中要新建一个虚拟环境&#xff0c;那你可以在Terminal中选择Command Prompt&#xff0c;在这里面执行相关命令 一、安装了Anaconda&#xff0c;创建虚拟环境 当你使用解释器是Anaconda提供的时&#xff0c;你可以使用conda命令执行&#xff0c;见以下操作&#x…

前端Vue与uni-app中的九宫格、十二宫格和十五宫格菜单组件实现

在前端 Vue 开发中&#xff0c;我们经常会遇到需要开发九宫格、十二宫格和十五宫格菜单按钮的需求。这些菜单按钮通常用于展示不同的内容或功能&#xff0c;提供给用户快速访问和选择。 一、引言 在前端开发中&#xff0c;九宫格、十二宫格和十五宫格菜单按钮是一种常见的布局…

202206 CSP认证 | 角色授权

角色授权 fine&#xff0c;又是一道acwing上TLE但是平台通过了的&#xff0c;那就酱吧… 直接跟着题目来模拟的…先找到每个用户授予的所有角色&#xff0c;包括用户本身和它所属的用户组。 然后遍历这个角色集合&#xff0c;看是否有操作权限&#xff0c;种类权限以及资源名称…

SVN修改已提交版本的注释

目录 一、需求分析 二、问题分析 三、解决办法 一、需求分析 ​开发过程中&#xff0c;在SVN提交文件后&#xff0c;发现注释写的不完整或不够明确&#xff0c;想再修改之前的注释文字​。 使用环境&#xff1a; SVN服务器操作系统&#xff1a;Ubuntu 20.04.6 LTS SVN版本&…

JVM实战篇

内存调优 内存溢出和内存泄漏 内存泄漏&#xff1a;在java中如果不再使用一个对象&#xff0c;但是该对象依然在GC ROOT的引用链上&#xff0c;这个对象就不会被垃圾回收器回收。 内存泄漏绝大多数情况都是由堆内存泄漏引起的&#xff0c;所以后续没有特别说明则讨论的都是堆…

Linux-centos如何搭建yum源仓库

1.本地搭建&#xff08;无需连接外网&#xff09; 1.1检查网络配置&#xff0c;及网络连接 打开虚拟机&#xff0c;点击【编辑——虚拟网络编辑器】 点击【仅主机模式】查看子网段是否和局内IP匹配 进入局内&#xff0c;查看网络IP是否在你上述设置的网段内&#xff0c;如果不…

Chapter 13 Techniques of Design-Oriented Analysis: The Feedback Theorem

Chapter 13 Techniques of Design-Oriented Analysis: The Feedback Theorem 从这一章开始讲负反馈Control系统和小信号建模. 13.2 The Feedback Theorem 首先介绍 Middlebrook’s Feedback Theorem 考虑下面负反馈系统 传输函数 Guo/ui G ( s ) u o u i G ∞ T 1 T G…

1.实用Qt:解决绘制圆角边框时,圆角锯齿问题

目录 问题描述 解决方案 方案1&#xff1a; 方案2&#xff1a; 结果示意图 问题描述 做UI的时候&#xff0c;我们很多时候需要给绘制一个圆角边框&#xff0c;初识Qt绘制的童鞋&#xff0c;可能绘制出来的圆角边框很是锯齿&#xff0c;而且粗细不均匀&#xff0c;如下图&…

Vue | 使用 ECharts 绘制折线图

目录 一、安装和引入 ECharts 二、使用 ECharts 2.1 新增 div 盒子 2.2 编写画图函数 2.3 完整代码结构 三、各种小问题 3.1 函数调用问题 3.2 数据格式问题 3.3 坐标轴标签问题 3.4 间隔显示标签 参考博客&#xff1a;Vue —— ECharts实现折线图 本文是在上…