SpringBoot集成WebSocket实现后端向前端推送数据

news/2024/9/18 23:56:05/文章来源:https://www.cnblogs.com/fragmentary/p/18401906

SpringBoot集成WebSocket实现后端向前端推送数据

这里最好了解一定 websocket 参考地址:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications

在此之前可以了解一下【轮询(Polling)、长轮询(Long Polling)、服务器发送事件(Server-Sent Events, SSE)、 WebSocket】

零、需求场景(了解可以直接跳过)

​ 什么时候我们需要后端给前端主动推送数据?(直接通过询问AI获得一些常用场景,让我们快速了解到这个需求场景)

  1. 实时数据更新:当应用需要实时展示最新数据时,如实时聊天应用、股票行情、在线游戏状态更新、实时通知等。在这些场景下,前端页面需要即时反映后端数据的变化,而传统的HTTP请求-响应模式可能因为轮询(Polling)导致数据更新延迟或服务器资源浪费。
  2. 低延迟的交互体验:对于需要极低延迟的交互体验的应用,如在线游戏、实时协作编辑工具等,后端需要能够立即向前端推送数据更新,以保证用户操作的即时反馈。
  3. 长连接和事件驱动的应用:在需要维持长时间连接的应用中,如WebSocket连接,后端可以在有数据更新时直接通过这条连接推送给前端,而不需要前端不断发起请求。这适用于需要实时数据交换的场景,如实时地图应用、远程监控等。
  4. 服务器推送通知:在需要向用户发送推送通知的应用中,如新闻应用、社交媒体、邮件客户端等,后端可以在有新消息或更新时主动向前端推送通知,即使前端页面没有打开或处于非活动状态。
  5. 资源状态监控:在需要监控服务器或应用资源状态的应用中,如服务器监控工具、云管理平台等,后端可以实时向前端推送资源状态的变化,以便管理员能够及时了解并响应。
  6. 实时数据分析:在需要实时分析数据的场景中,如大数据分析平台、实时数据仪表盘等,后端可以将分析结果实时推送给前端,以便用户能够即时看到数据的变化趋势和分析结果。

​ 这里看到第三点就可以补充一下,比如我们常见的扫码登录,用户通过扫码进行登录,比如我们将二维码转码为一个请求向服务端发起。这是我们的前端就处于一个等待状态,此时前端为了确定此次登录是否有效,要么就是后端可以直接给其反馈,要不就是前端不断重复发起请求向后端确定即轮询

一、构建项目

​ 这里为了简单我们假定实现一个简单需求,在一个单页上通过一个表单添加数据,然后通过 WebSocket 进行数据推送更新。

(零)、数据库准备

​ 一个简单的user表,除了id就是名称和积分。

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (`id` bigint(0) NOT NULL AUTO_INCREMENT,`nickname` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1831327964131840002 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三', '1000');
INSERT INTO `user` VALUES (1831327914701967362, '李四', '500');
INSERT INTO `user` VALUES (1831327964131840001, '王五', '1500');
(一)、前端工程

​ 合理使用AI工具,对于不懂的代码位置可以通过AI了解。如果对vue熟悉可以自行改为vue代码或者直接AI修改,然后如果想要美化可以直接:http://datav.jiaminghi.com/guide/scrollRankingBoard.html 使用DataV 的排行轮播表。

效果(-_-)朴实无华:

效果图

代码:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>单页排行榜测试</title><style>/*表单样式开始*/.flex-container {display: flex;flex-wrap: nowrap;background-color: DodgerBlue;height: 500px;}.form {background-color: #f1f1f1;width: 40%;margin: 10px;text-align: center;line-height: 75px;}.rankinglist {background-color: #f1f1f1;width: 60%;margin: 10px;text-align: center;line-height: 75px;}/*表单样式结束*//*排行榜样式开始*/table {width: 100%;margin: 0 auto;border-collapse: collapse;}th,td {padding: 8px;text-align: left;border-bottom: 1px solid #ddd;}th {background-color: #f2f2f2;}/*排行榜样式结束*/</style><script src="https://unpkg.com/axios/dist/axios.min.js"></script><!-- WebSocket --><!-- 引入 SockJS --><script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script><!-- 引入 Stomp.js --><script src="https://cdn.jsdelivr.net/npm/stompjs@latest/lib/stomp.min.js"></script></head><body><div class="flex-container"><div class="form"><!-- <form action="http://localhost:9999/rankinglists/add" method="POST" onsubmit="return  false"> --><form onsubmit="return  false">昵称:<input type="text" name="nickname" id="nickname"><br>得分:<input type="text" name="grade" id="grade"><br><input type="submit" value="提交" onclick="onclickSubmit()"></form></div><div class="rankinglist"><table id="rankingTable"><thead><tr><th>排名</th><th>昵称</th><th>得分</th></tr></thead><tbody></tbody></table></div></div><script>let isConnected = false;let stompClient = null;let socket = null;// 基地址const API_BASE_URL = "http://localhost:9999";const wsHost = "http://localhost:9999/websocket"const wsTopic = "/topic/receiver"//##################表单处理逻辑开始##################const sendData = async (data) => {try {const response = await axios({method: 'post',url: `${API_BASE_URL}/rankinglists/add`,data: data});console.log("查看返回的数据", response);if (response.data.code === 200) {}} catch (error) {console.error('用户数据添加失败', error);} finally {const nicknameInput = document.getElementById('nickname');const gradeInput = document.getElementById('grade');nicknameInput.value = '';gradeInput.value = '';}};// 按钮点击后操作function onclickSubmit() {const nicknameInput = document.getElementById('nickname');const gradeInput = document.getElementById('grade');const nickname = nicknameInput.value;const grade = gradeInput.value;// 检查输入是否为空if (!nickname || !grade) {alert('昵称和得分不能为空!');return;}// 将表单数据转换为JSON对象const formData = { nickname, grade };console.log("表单中的json数据: ", formData);// 发送数据sendData(formData);}//##################表单处理逻辑结束##################//##################排行榜处理逻辑开始##################const fetchRankingData = async () => {try {const response = await axios.get(`${API_BASE_URL}/rankinglists/info`);return response.data;} catch (error) {console.error('排行榜数据获取失败', error);return [];}};// 渲染排行榜数据const renderRankingList = (data) => {const tbody = document.querySelector('#rankingTable tbody');tbody.innerHTML = ''; // 清空现有数据console.log("看看传递过来的排行榜数据:", data);data.forEach((entry, index) => {const row = `<tr><td>${index + 1}</td><td>${entry.nickname}</td><td>${entry.grade}</td></tr>`;tbody.insertAdjacentHTML('beforeend', row);});};// 初始化数据加载document.addEventListener('DOMContentLoaded', async () => {const rankingData = await fetchRankingData();renderRankingList(rankingData);});//##################排行榜处理逻辑结束##################//##################websocket开始####################// 初始化SockJS连接的函数  function initSockJs() {// 创建一个新的SockJS客户端实例,连接到指定的WebSocket服务器地址(wsHost应该是一个变量,包含服务器的URL)  socket = new SockJS(wsHost);console.log("查看一下:", socket); // 在控制台打印SockJS实例,用于调试  // 使用Stomp协议在SockJS连接上创建一个客户端  stompClient = Stomp.over(socket);// 尝试连接到WebSocket服务器  stompClient.connect({}, function (frame) {// 连接成功时的回调函数  console.log('WebSocket 连接成功:' + frame); // 打印连接成功的帧信息  isConnected = true; // 设置连接状态为已连接  subscribeToTopic(); // 订阅主题,开始接收消息  }, function (error) {// 连接失败时的回调函数  console.error('WebSocket 连接失败:' + error); // 打印连接失败的错误信息  });}// 订阅WebSocket服务器上的特定主题的函数  function subscribeToTopic() {// 使用Stomp客户端订阅一个主题(wsTopic应该是一个变量,包含要订阅的主题名)  // 当收到来自该主题的消息时,会调用回调函数并传入消息对象  stompClient.subscribe(wsTopic, function (res) {// 打印从服务器接收到的消息体(未解析的JSON字符串)  // 注意:如果消息体是JSON格式,你可能需要先使用JSON.parse()进行解析  console.log("查看一下后端推送过来的数据:" + res.body);data = JSON.parse(res.body);console.log("排行数据json转对象后: ", data);renderRankingList(data.rankinglist); //传递更新排行榜});}// 销毁SockJS连接和Stomp客户端的函数  function destroySockJs() {if (stompClient !== null) { // 检查Stomp客户端是否存在,以避免空引用错误  // 断开Stomp客户端连接  stompClient.disconnect(function () {console.log('WebSocket 断开成功!'); // 连接断开时打印信息  });// 如果SockJS连接(socket)仍然存在且未关闭,则尝试关闭它  if (socket && socket.readyState !== WebSocket.CLOSED) {socket.close(); // 关闭SockJS连接  }// 将Stomp客户端和SockJS连接设置为null,释放资源  stompClient = null;socket = null;isConnected = false; // 设置连接状态为未连接  }}// 初始化WebSocket连接  initSockJs();// 添加窗口关闭事件监听器,以确保在窗口关闭前正确断开WebSocket连接  window.addEventListener('beforeunload', destroySockJs);//##################websocket结束####################</script></body></html>
(二)、后端构建

​ 分析前端页面和需求,本项目我们需要提供一个新增数据的接口和一个获取排行数据的接口,同时需要实现一个后端主动向前端推送数据的功能。先搞定前两个让数据显示出来。

0、pom依赖参考
    <dependencies><!-- 数据源 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.6</version></dependency><!-- 持久层技术 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><!-- 数据库 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- fastjson JSON与Java类的转换 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><!-- 依赖websocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!--  简化类书写  --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.28</version></dependency><!-- springboot依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies>
1、yaml配置 (注意数据库库名、账号密码改为你自己的)
server:port: 9999spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/rankinglist?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: rootmybatis-plus:configuration:# 在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射# address_book --> AddressBookmap-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: ASSIGN_ID  #设置id根据数据库一样自增# mapper映射文件路径mapperLocations: classpath:mybatis/mapper/*.xml
2、控制器参考

​ 项目最开始没有使用数据库,直接造数据测试的,所以保留了一些注释代码。

/*** @Package: com.yanjiali.controller* @Author: yanjiali* @Created: 2024/8/31 21:32* 操作排行榜的控制器* 127.0.0.1:9999/rankinglists/*/
@RestController
@RequestMapping("/rankinglists")
public class RankingListController {@Autowiredprivate UserService userService;//TODO 注意使用 APIFOX 时需要使用路径参数写法才能识别
//    @GetMapping("/add/{nickname}/{grade}")
//    public void addUserInfo(@PathVariable("nickname") String nickname,
//                            @PathVariable("grade") String grade) {/*** 添加一个用户信息* @param user*/@PostMapping("/add")public void addUserInfo(@RequestBody User user) {//TODO 校验数据System.out.println("收到了来自前端的消息:nickname = " + user.getNickname() + ", grade = " +user.getGrade());//TODO 操作数据库userService.save(user);}/*** 获取用户排行信息* @return*/@GetMapping("/info")public List<User> getAllUserInfo() {List<User> userList = userService.list();//        //TODO 模式数据库获取数据
//        User user1 = new User();
//        user1.setNickname("张三");
//        user1.setGrade("1000");
//        userList.add(user1);
//
//        User user2 = new User();
//        user2.setNickname("李四");
//        user2.setGrade("2000");
//        userList.add(user2);
//
//        User user3 = new User();
//        user3.setNickname("王五");
//        user3.setGrade("1500");
//        userList.add(user3);if(CollectionUtils.isEmpty(userList)) {return null;    //用户数据为空}userList.sort(new Comparator<User>() {@Overridepublic int compare(User o1, User o2) {//此处使用 Long.compare() 是考虑到如果这里的数值范围较大return Long.compare(Long.valueOf(o2.getGrade()), Long.valueOf(o1.getGrade()));}});return userList;}
}
3、说明

跨域处理:

@Configuration  //表示这是一个配置类
public class WebMvcConfig extends WebMvcConfigurationSupport {/*** SpringBoot处理跨域** @param registry*/@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*") // 使用模式匹配// 是否允许cookie.allowCredentials(true)	//TODO 此处注意,后续会改// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}

​ 相信有了以上内容基本构建一个项目就可以了,这里我就不写实体类,mapper,service的代码了。至于是否需要统一返回给前端数据的结构就看个人意愿了,此处重点不在于此。

效果(可以测试添加数据接口是否可行):

效果图

(三)、WebSocket部分讲解
0、导入依赖 此处上面的依赖文件中已经有了这个关于websocket的依赖了
        <!-- 依赖websocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
1、编写配置类

关于跨域问题(重点) 参考文章:https://cloud.tencent.com.cn/developer/article/1883429

// 前面提到的跨域配置的这里修改为false
allowCredentials(false)	//TODO 此处注意,后续会改

websocket的配置类,这里直接使用配置类编写端点连接,同时定义了一个订阅地址。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {// 注册一个 /notification 端点,前端通过这个端点进行连接registry.addEndpoint("/websocket").setAllowedOriginPatterns("*").withSockJS();}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 定义了一个客户端订阅地址的前缀信息,也就是客户端接收服务端发送消息的前缀信息registry.enableSimpleBroker("/topic");}
}
2、编写对应定义任务进行推送

我们先在 event 包(放在service包同级就是了)下编写下面接口、实现类

事件接口:

public interface Event {/*** 对应事件触发所调用的方法*/void handle();
}

事件实现类(可以放在impl包下):

@Component
public class RankingListUpdateEvent implements Event {@Autowiredprivate SimpMessagingTemplate wsTemplate;@Autowiredprivate UserService userService;/*** 触发事件所调用的方法*/@Overridepublic void handle() {//TODO 查询数据库List<User> userList = userService.list();JSONObject jsonObject = new JSONObject();   //格式处理一下,对应前端jsonObject.put("rankinglist", userList);//TODO 将查询到的数据发送给前端 (这里使用websocket)wsTemplate.convertAndSend("/topic/receiver", jsonObject);}
}

然后在 task 包下编写定义任务类(别忘了给启动类加上:@EnableScheduling  注解哦 【-_-】 )

@Component
public class RankingListUpdateTask {@Autowiredprivate RankingListUpdateEvent rankingListUpdateEvent;/*** 推送交易对信息 1s*/@Scheduled(fixedRate = 1000)public void pushMarkets() {rankingListUpdateEvent.handle();}
}
3、说明

​ 这里这种事件,定时任务编写的方式是为了后续更好的扩展,并不是强制的,简单来说你直接写一个定时任务给前端发,不写这一个结构也没什么。

二、总结:

​ 这里就是通过快速实现这个效果来到达理解的目的,并没有细讲,当你成功实现后添加数据就会自动更新排行数据,没错数据显示多了页面样式还有点不好看如:
效果图

​ 不要在意这个细节,关键在于你能够知道我们想要实现这样一个效果应该怎么办,那么这个效果怎么用在其他地方你自然就知道。只是注意:

首先:去了解websocket基本知识,还有配置类为什么能够实现这个效果需要去了解。当然你突然冒出这样的想法也可以理解——我直接添加数据后再去调用一下获取排行数据的接口不就 -,哈哈哈哈。当然对于你添加后更新数据这样看似没有什么问题,但是首先如果我们希望没有做出某个行为(特指你不是第一次进入页面,点击按钮等)就可以更新呢?如你并没有更新数据或者你就没有权限修改(如一些只显示的场景——可视化大屏、股票市场趋势),你总不能一直刷新页面吧 -_- ,还有就是你觉得你发请求在获取响应 和 后端直接给你谁快?这里不细说。

其次:这个代码最好处理一下端点和订阅的写法,因为在一些系统中websocket除了推送,可能还承担聊天功能,所以细节一定有待考究。

最后:这里推荐使用一个开源产品tio:https://www.tiocloud.com/1/product/tio.html 当然也可以试试netty:https://netty.io/index.html 直接看文档看的困难就去找文章视频 [0_0]

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

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

相关文章

扫码获取微信公众号用户的openid,向某个用户推送消息

1. 生成二维码: 生成二维码比较简单的方法是直接使用phpqrcode程序包(可在网上下载得到)。 若想获得ThinkPHP支持,需将程序包放在ThinkPHP/Extend/Vendor目录下,让后在程序中引用,代码如下: vendor("phpqrcode.phpqrcode"); //要生成二维码的数据 $text="…

京东h5st参数js逆向

扣代码的环节挺简单的就不讲了 直接到重点 发现许多包都会有一个h5st的加密参数 那么我们就要看这个参数是怎么生成的我们可以根据请求堆栈 找到h5st的入口 当然还有一种更简单的方法 就是直接全局搜索h5st 这里采用后者这里sign函数就是h5st生成的地方 但是这是个promise异步函…

GenRec论文阅读笔记

GenRec: Large Language Model for Generative Recommendation论文阅读笔记 Abstract ​ 本文提出了一种使用大型语言模型(LLM)的创新推荐系统方法,该方法纯粹基于原始文本数据,即使用项目名称或标题作为项目 ID,而不是创建精心设计的用户或项目 ID。更具体地说,我们提出…

XGBoost模型 0基础小白也能懂(附代码)

XGBoost 是 eXtreme Gradient Boosting 的缩写称呼,它是一个非常强大的 Boosting 算法工具包,优秀的性能(效果与速度)让其在很长一段时间内霸屏数据科学比赛解决方案榜首,现在很多大厂的机器学习方案依旧会首选这个模型。XGBoost模型 0基础小白也能懂(附代码) 原文链接 …

编程技术开发105本经典书籍推荐分享

最近整理了好多的技术书籍,对于提高自己能力来说还是很有用的,当然要有选择的看,不然估计退休了都不一定看得完,分享给需要的同学。 编程技术开发105本经典书籍推荐:https://zhangfeidezhu.com/?p=753 分享截图本文来自博客园,作者:张飞的猪,转载请注明原文链接:http…

ArcMap批量附色操作,并保存mxd

ArcMap批量附色操作,并保存mxd 1、对单文件操作 1、保存当前ArcMap中打开的shp文件为mxd文件 打开label_shp_root中的任意一个shp文件夹保存成mxd文件2、对当前在arcmap中打开的shp文件应用color配色 color配色是手动设置好一个shp文件夹的配色方案并保存成mxd文件应用color.m…

Linux 忘记密码

最近需要搞几台虚拟机,之前的vm密码进不去 找了几个方法 不是很贴切, Centos7 重启页面 e--> grub ,在linux16 行 修改ro 为rw,最后加上 init=/bin/sh F10 或Ctrl+x进入这里的 rw代替 进os之后 mount -o remount,rw /passwd touch /.autorelabel exec /sbin/init

使用 `Roslyn` 分析器和修复器对.cs源代码添加头部注释

之前写过两篇关于Roslyn源生成器生成源代码的用例,今天使用Roslyn的代码修复器CodeFixProvider实现一个cs文件头部注释的功能, 代码修复器会同时涉及到CodeFixProvider和DiagnosticAnalyzer, 实现FileHeaderAnalyzer 首先我们知道修复器的先决条件是分析器,比如这里,如果要对代…

线性dp:LeetCode516 .最长回文子序列

LeetCode516 .最长回文子序列 题目叙述: 力扣题目链接(opens new window) 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 示例 1: 输入:s = "bbbab&…

202409071506,开始写代码,从0开始 验证基本架子

由于视频教程里面 用的VS2105 所以 照抄。开发环境是VS2015 ,WIN10. VS2015 在今天看来是一个很古老的开发环境了,估计都很难找到安装包。(各种安装包:https://www.cnblogs.com/zjoch/p/5694013.html) 用:vs2015.ent_chs.iso (3.88 GB (4,172,560,384 字节))这个安装…

PR出现冲突无法直接解决

举例:存在p-dev 分支,申请合入 master 分支,产生pr 无法直接自动将pr 合入到master中 需要在本地解决 解决:git checkout p-dev,切换分支dev git pull ,更新到最新的 git merge origin master, 此时会出现冲突,通过vscode 或者smartgit 去解决 解决完冲突的文件,需要…

彻底理解字节序

1.基本理论计算机发送数据从内存低地址开始. 计算机接收数据的保存从低地址开始.2.非数值型网络数据传输如上图例子所示,发送端发送了四个字节内容,分别为0x12,0x34,0x56,0x78,假设这四个字节不表示数值例如unsigned int,而是图片内容数据。发送端从低内存地址开始发送四个…