Springboot整合websocket(附详细案例代码)
文章目录
- WebSocket简述
- WebSocket是什么?
- WebSocket 的特点
- WebSocket 的工作流程
- WebSocket的消息(帧)格式
- WebSocket 与 HTTP
- springboot中整合WebSocket
- pom依赖
- 实体类
- 配置类
- 握手配置类
- WebSocket配置类
- 自定义异常类
- webSocket服务类
- websocket中Session的 getBasicRemote() 和 getAsyncRemote() 方法的区别
- 前端代码
- 心跳机制
WebSocket简述
WebSocket是什么?
WebSocket 是一种网络通信协议,它提供了全双工(full-duplex)的通信渠道,允许客户端和服务端之间的双向数据交换。WebSocket 协议是在 HTTP 协议的基础上构建的,它通过一个初始的 HTTP 握手过程来建立连接,之后转换到二进制帧传输数据。
WebSocket 的特点
- 全双工通信:WebSocket 支持客户端和服务端之间的双向通信,这意味着服务端可以主动向客户端发送信息,而不必等待客户端的请求。
- 持久连接:WebSocket 连接一旦建立,就会保持打开状态,直到任何一方关闭连接。
- 低延迟:相比轮询等其他实时通信方案,WebSocket 通信具有更低的延迟。
- 安全性:WebSocket 可以通过 SSL/TLS 加密(wss://)来保证通信的安全性。
- 实时性:WebSocket 支持实时数据推送,减少了轮询带来的延迟。
- 性能:相比轮询,WebSocket 减少了请求次数,降低了带宽消耗。
- 灵活性:WebSocket 可以用于多种类型的数据传输,包括文本、二进制数据等。
WebSocket 的工作流程
握手:客户端首先通过 HTTP 协议发起一个特殊的 GET 请求到服务端,这个请求包含了特定的升级头(Upgrade: websocket 和 Connection: Upgrade)以及其他必要的信息。
响应:服务端响应这个请求,并通过一个特定的算法生成一个接受密钥(accept key),然后发送回客户端。
建立连接:一旦握手成功,客户端和服务端之间的 WebSocket 连接就建立起来了。
数据交换:通过这个连接,客户端和服务端可以相互发送数据帧。
WebSocket的消息(帧)格式
WebSocket 的消息格式与 HTTP 请求和响应的消息格式有所不同。WebSocket 的消息格式可以是文本或二进制数据,并且 WebSocket 消息的传输是在一个已经建立的连接上进行的,因此不需要再进行 HTTP 请求和响应的握手操作。
WebSocket 消息格式由两个部分组成:消息头(帧头)和消息体(负载)。
消息头包含以下信息:
- FIN: 表示这是一条完整的消息,一般情况下都是1。
- RSV1、RSV2、RSV3: 暂时没有使用,一般都是0。
- Opcode: 表示消息的类型,包括文本消息、二进制消息等。
- Mask: 表示消息是否加密。
- Payload length: 表示消息体的长度。
- Masking key: 仅在消息需要加密时出现,用于对消息进行解密。
消息体就是实际传输的数据,可以是文本或二进制数据。
WebSocket 与 HTTP
HTTP 是一种基于请求/响应模式的应用层协议,用于在 Web 浏览器和 Web 服务器之间传输数据。它是一个无状态协议,意味着每次请求都是独立的,服务器不会记住之前的请求。
而WebSocket 是一种网络通信协议,它提供了全双工(full-duplex)的通信渠道,允许客户端和服务端之间的双向数据交换。WebSocket 协议是在 HTTP 协议的基础上构建的,它通过一个初始的 HTTP 握手过程来建立连接,之后转换到二进制帧传输数据。
springboot中整合WebSocket
pom依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>4.6.3</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
实体类
用于存储客户端的相关信息
import lombok.Data;import javax.websocket.Session;
import java.time.LocalDateTime;/*** 客户端实体类** @author qf* @since 2024/08/29 19:50*/
@Data
public class ClientInfoEntity {/*** 客户端唯一标识*/private String token;/*** 客户端连接的session*/private Session session;/*** 连接存活时间*/private LocalDateTime existTime;
}
配置类
握手配置类
在和客户端连接时需要用到代表每个客户端的唯一的连接标识,可以在握手配置类中为每个客户端生成一个唯一的连接标识,也可以让客户端传一个唯一的连接标识。本文使用让客户端传一个唯一的连接标识。
import org.springframework.context.annotation.Configuration;import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Map;
import java.util.UUID;/*** 主要用于WebSocket的握手配置* @author qf* @since 2024/08/29 19:55*/
@Configuration
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {/*** 注意: 每一个客户端发起握手,端点就有一个新的实列,那么引用的这个配置也是新的实列,这里sec的用户属性也不同就不会产生冲突。* 修改握手机制 就是第一次http发送过来的握手* @param sec 服务器websocket端点的配置* @param request* @param response*/@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
// 将从握手的请求中获取httpsessionHttpSession httpSession =(HttpSession) request.getHttpSession();/*** 一般会在请求头中添加token 解析出来id作为键值对*/Map<String, Object> properties = sec.getUserProperties();/*** 一个客户端和和服务器发起一次请求交互 就有一个唯一session* 设置唯一标识:为每个客户端生成一个唯一的UUID作为连接标识,并将其存储在UserProperties中,便于后续跟踪与管理*/
// properties.put(HttpSession.class.getName(),httpSession);String sessionKey = UUID.randomUUID().toString().replaceAll("-", "");properties.put("Connected",sessionKey);}
}
WebSocket配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** WebSocketConfig类的主要功能是配置和管理WebSocket端点,* 确保它们在应用程序启动时被正确初始化和注册,以便能够处理WebSocket连接和通信。** @author qf* @since 2024/08/29 20:02*/
@Configuration
public class WebSocketConfig {/*** 该方法用来创建并返回一个ServerEndpointExporter实例。* 这个实例的作用是扫描并自动配置所有使用@ServerEndpoint注解标记的WebSocket端点** @return ServerEndpointExporter:这是一个用于自动检测和管理WebSocket端点的类。* 通过将其实例化并配置为Spring管理的Bean,可以确保所有WebSocket端点在应用程序启动时被自动初始化和注册。*/@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}
自定义异常类
public class ServiceException extends RuntimeException {private Integer code;private String msg;public ServiceException(Integer code, String msg) {super();this.msg = msg;this.code = code;}public ServiceException(String msg) {this(1, msg);}@Overridepublic String getMessage() {return msg;}/*** @return code*/public Integer getCode() {return code;}
}
webSocket服务类
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。** @author qf* @since 2024/08/29 19:50*/
@Slf4j
@Component
@CrossOrigin(origins = "*")
@ServerEndpoint(value = "/webSocket/{token}", configurator = GetHttpSessionConfig.class)
public class ChatEndpoint2 {//key:客户端连接唯一标识(token)//value:ClientInfoEntityprivate static final Map<String, ClientInfoEntity> uavWebSocketInfoMap = new ConcurrentHashMap<String, ClientInfoEntity>();private static final int EXIST_TIME_HOUR = 6;/*** 连接建立成功调用的方法** @param session 第一个参数必须是session* @param sec* @param token 代表客户端的唯一标识*/@OnOpenpublic void onOpen(Session session, EndpointConfig sec, @PathParam("token") String token) {if (uavWebSocketInfoMap.containsKey(token)) {throw new ServiceException("token已建立连接");}//开始进入页面默认监听所有无人机//把成功建立连接的会话在实体类中保存ClientInfoEntity entity = new ClientInfoEntity();entity.setToken(token);entity.setSession(session);//默认连接6个小时entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));uavWebSocketInfoMap.put(token, entity);//之所以获取http session 是为了获取获取httpsession中的数据 (用户名 /账号/信息)log.info("WebSocket 连接建立成功: " + token);}/*** 当断开连接时调用该方法** @param session*/@OnClosepublic void onClose(Session session, @PathParam("token") String token) {// 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除if (ObjectUtil.isNotEmpty(token) && uavWebSocketInfoMap.containsKey(token)) {uavWebSocketInfoMap.remove(token);log.info("WebSocket 连接关闭成功: " + token);}}/*** 接受消息* 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑,可能包括广播消息给所有连接的用户。**/@OnMessagepublic void onMessage(Session session, @PathParam("token") String token, String message) throws IOException {log.info("接收到消息:" + message);ClientInfoEntity entity = uavWebSocketInfoMap.get(token);//如果是心跳包if("heartbeat".equals(message)){//只要接受到客户端的消息就进行续命(时间)entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));if (entity.getSession().isOpen()) {entity.getSession().getBasicRemote().sendText("{\"msg\": \"success\", \"code\": 0}");}return;}//业务逻辑//只要接受到客户端的消息就进行续命(时间)entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));if (entity.getSession().isOpen()) {entity.getSession().getBasicRemote().sendText("{\"msg\": \"success\", \"code\": 0}");}}/*** 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。*/@OnErrorpublic void onError(Throwable error) {log.error("报错信息:" + error.getMessage());error.printStackTrace();}private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");/*** 发生消息定时器*/@PostConstruct@Scheduled(cron = "0/1 * * * * ? ")public void refreshDate() {//开启定时任务,1秒一次向前台发送当前时间//当没有客户端连接时阻塞等待if (!uavWebSocketInfoMap.isEmpty()) {//超过存活时间进行删除Iterator<Map.Entry<String, ClientInfoEntity>> iterator = uavWebSocketInfoMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, ClientInfoEntity> entry = iterator.next();if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {log.info("WebSocket " + entry.getKey() + " 已到存活时间,自动断开连接");try {entry.getValue().getSession().close();} catch (IOException e) {log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());}//过期则进行移除iterator.remove();}}sendMessage(FORMAT.format(new Date()));}}/*** 群发信息的方法** @param message 消息*/public void sendMessage(String message) {System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+ "发送全体消息:" + message);//循环客户端map发送消息uavWebSocketInfoMap.values().forEach(item -> {//向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,// 一种是getBasicRemote,一种是getAsyncRemote//区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。// 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息// 则避免尽量一次发送全部消息,使用部分消息来发送,可以看到下面sendMessageToTarget方法内就用的getBasicRemote,因为这个方法是根据用户id来私发的,所以不是全部一起发送。item.getSession().getAsyncRemote().sendText(message);});}
}
websocket中Session的 getBasicRemote() 和 getAsyncRemote() 方法的区别
websocket中Session的 getBasicRemote() 和 getAsyncRemote() 方法有以下主要区别
- 同步 vs 异步
getBasicRemote():同步发送消息的方式。发送消息时会阻塞当前线程,直到消息发送完成。
getAsyncRemote():异步发送消息的方式。发送消息后立即返回,不阻塞当前线程。 - 性能和并发
getBasicRemote():由于同步特性,可能会导致性能瓶颈,尤其是在高并发场景下。
getAsyncRemote():异步特性使得它更适合高并发场景,提高系统的响应速度和吞吐量。 - 使用场景
getBasicRemote():适用于消息量较小且对实时性要求较高的场景。
getAsyncRemote():适用于大量消息发送或者对实时性要求相对较低的场景。 - 总之,getBasicRemote():同步发送,适合小量消息和高实时性需求。
getAsyncRemote():异步发送,适合大量消息和高并发场景。
前端代码
代码为.uve后缀文件,直接将代码复制到uve项目中即可。
<template>
<!-- websocketceshi --><div class="layout"><div class="msgBody">{{ msg }}</div><input v-model="sendMsg" style="width:200px;height:30px;margin-top:20px"/><button @click="sendMessage" style="width:100px;height:30px;">发送</button><button @click="close" style="width:100px;height:30px;">断开链接</button><button @click="init" style="width:100px;height:30px;">建立链接</button></div>
</template><script>
export default {name: "LayOut",data() {return {msg: "",sendMsg: "",//后台的地址,只需要动localhost:8089部分,改成你后端的地址。//后面webSocket是后台设定的接口地址,uuid_test001是你前台的客户端唯一id(可以使用uuid生成)。//用于区分不同的客户端,比如你多个客户端连接后台,后台推送数据的时候需要根据这个id不同,给对应的人推送,不然就推送到所有建立链接的网页上了path: "ws://localhost:8089/qf/webSocket/uuid_test001",//存websocket实例化的socket: "",};},methods: {//用于前台发送数据到后台,调用websocket中的send方法把数据发过去。sendMessage() {this.socket.send(this.sendMsg);},//初始化建立前后台链接init() {if (typeof WebSocket === "undefined") {alert("您的浏览器不支持socket");} else {// 实例化socketthis.socket = new WebSocket(this.path);// 监听socket连接this.socket.onopen = this.open;// 监听socket错误信息this.socket.onerror = this.error;// 监听socket消息this.socket.onmessage = this.getMessage;this.socket.onclose = this.close;}},//链接成功时的回调函数open() {console.log("socket连接成功");},//链接错误时的回调error(err) {console.log("连接错误" + err);},//后台消息推送过来,接收的函数,参数为后台推过来的数据。getMessage(msg) {this.msg = msg.data;},//链接关闭的回调close(event) {//socket是链接的实例,close就是关闭链接this.socket.close()console.log("断开链接成功");},},created() {//开局初始化建立链接this.init();},
};
</script>
<style scoped>
.layout {position: relative;width: 100%;height: 100%;
}
.msgBody {width: 500px;height: 300px;border: 1px solid rgb(95, 79, 79);
}
</style>
心跳机制
在WebSocket中使用心跳机制是为了保持 WebSocket 连接的活跃状态而设计的一种机制。在长时间没有数据传输的情况下,WebSocket 连接可能会被中间的网络设备(如路由器或防火墙)认为已经断开,从而导致连接失效。心跳机制通过定期发送小的数据包来保持连接的活动状态,从而避免这种情况的发生。
心跳机制的作用:
- 保持连接活跃:在网络设备中,长时间没有数据传输的连接可能会被视为断开,心跳机制可以防止这种情况。
- 检测连接状态:心跳机制可以帮助检测连接是否仍然有效,及时发现并处理连接中断的情况。
- 优化资源使用:心跳机制可以确保无效的连接被及时关闭,释放资源。
实现心跳机制的方式
心跳机制可以通过多种方式实现,常见的方法包括定时发送心跳包、响应心跳请求以及超时重试等。
添加心跳机制的前端代码
<template><!-- websocketceshi --><div class="layout"><div class="msgBody">{{ msg }}</div><inputv-model="sendMsg"style="width: 200px; height: 30px; margin-top: 20px"/><button @click="websocketsend(sendMsg)" style="width: 100px; height: 30px">发送</button><button @click="websocketclose" style="width: 100px; height: 30px">断开链接</button><button @click="initWebSocket" style="width: 100px; height: 30px">建立链接</button></div></template><script>export default {name: "LayOut",data() {return {websock: null, //建立的连接lockReconnect: false, //是否真正建立连接timeout: 20 * 1000, //20秒一次心跳timeoutObj: null, //心跳心跳倒计时serverTimeoutObj: null, //心跳倒计时timeoutnum: null, //断开 重连倒计时msg: "", //显示的值sendMsg: "", //输入框的值};},created() {// //页面刚进入时开启长连接this.initWebSocket();},destroyed() {//页面销毁时关闭长连接this.websocketclose();},methods: {//建立连接,初始化weosocketinitWebSocket() {//后台地址,前面的ws不动,后面是后台地址,我是本地运行的所以填的本地,自行更改。再后面webSocket是后端的接口地址,uuid_test002是参数const wsuri = "ws://localhost:8089/qf/webSocket/uuid_test000";//建立连接this.websock = new WebSocket(wsuri);//连接成功this.websock.onopen = this.websocketonopen;//连接错误this.websock.onerror = this.websocketonerror;//接收信息this.websock.onmessage = this.websocketonmessage;//连接关闭this.websock.onclose = this.websocketclose;},reconnect() {//重新连接var that = this;//判断链接状态,true就是链接,false是断开,这里如果是链接状态就不继续执行了,跳出来。if (that.lockReconnect) {return;}//把链接状态改为truethat.lockReconnect = true;//没连接上会一直重连,设置延迟避免请求过多that.timeoutnum && clearTimeout(that.timeoutnum);that.timeoutnum = setTimeout(function () {//初始化新连接that.initWebSocket();//把链接状态改为falsethat.lockReconnect = false;}, 5000);},reset() {//重置心跳var that = this;//清除时间clearTimeout(that.timeoutObj);clearTimeout(that.serverTimeoutObj);//重启心跳that.start();},start() {//开启心跳var self = this;//有延迟时间的就清除掉self.timeoutObj && clearTimeout(self.timeoutObj);self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);//从新创建计时器self.timeoutObj = setTimeout(function () {//这里发送一个心跳,后端收到后,返回一个心跳消息if (self.websock.readyState == 1) {//如果连接正常发送信息到后台self.websock.send("ping");} else {//否则重连self.reconnect();}self.serverTimeoutObj = setTimeout(function () {//超时关闭self.websock.close();}, self.timeout);}, self.timeout);},//链接成功时执行的方法websocketonopen() {//连接成功事件this.websocketsend("heartbeat");//提示成功console.log("连接成功", 3);//开启心跳this.start();},//连接失败事件websocketonerror(e) {//错误console.log("WebSocket连接发生错误");//重连this.reconnect();},//连接关闭事件websocketclose(e) {this.websock.close();//提示关闭console.log("连接已关闭");//重连this.reconnect();},//接收服务器推送的信息websocketonmessage(event) {//打印收到服务器的内容console.log("收到服务器信息", event.data);this.msg = event.data;//收到服务器信息,心跳重置this.reset();},websocketsend(msg) {//向服务器发送信息this.websock.send(msg);},},};</script><style scoped>.layout {position: relative;width: 100%;height: 100%;}.msgBody {width: 500px;height: 300px;border: 1px solid rgb(95, 79, 79);}</style>
参考文章:
websocket前后端交互
相关文章:

Springboot整合websocket(附详细案例代码)
文章目录 WebSocket简述WebSocket是什么?WebSocket 的特点WebSocket 的工作流程WebSocket的消息(帧)格式WebSocket 与 HTTP springboot中整合WebSocketpom依赖实体类配置类握手配置类WebSocket配置类 自定义异常类webSocket服务类websocket中Session的 getBasicRemo…...
微信小程序:navigateTo跳转无效
关于 navigateTo 跳转无效问题,在IOS、模拟器上面都能正常跳转,但是在安卓上面不能跳转,过了一段时间IOS也不能跳转了。仔细找了下问题结果是要跳转的页面是tab,不能使用navigateTo 取跳转修改为: wx.switchTab({url:…...

C++ 设计模式——解释器模式
目录 C 设计模式——解释器模式1. 主要组成成分2. 逐步构建解释器模式步骤1: 定义抽象表达式步骤2: 实现终结符表达式步骤3: 实现非终结符表达式步骤4: 构建语法树步骤5: 实现内存管理步骤6: 创建上下文和客户端 3. 解释器模式 UML 图UML 图解析 4. 解释器模式的优点5. 解释器模…...

【开源大模型生态6】生态大咖与产品布局
上图是基础设施、大模型、行业应用构成大模型开源生态体系。 这里一一给大家介绍以下。 金融 Qwen:阿里云推出的一种大型语言模型,具有强大的对话能力和多模态理解能力。天工:通常指的是阿里云的一套物联网(IoT)解决…...

虚拟机苹果系统的QT安装体验
前言 苹果系统MacOS中除了安装XCode,完全可以安装QT。本质上来讲,苹果系统就是Linux改装版本,实际上和Ubuntu非常的接近。 1、Mac对应的QT安装包的下载 安装参考链接:MacOS下Qt 5开发环境安装与配置_macos qt-CSDN博客 苹果系统…...

多路转接之poll(接口介绍,struct pollfd介绍,实现原理,实现非阻塞网络通信代码)
目录 poll 引入 介绍 函数原型 fds struct pollfd 特点 nfds timeout 取值 返回值 原理 如何实现关注多个fd? 如何确定哪个fd上有事件就绪? 如何区分事件类型? 判断某事件是否就绪的方法 代码 示例 总结 为什么说它解决了fd上限问题? 缺点 poll 引入…...

两个月冲刺软考——位示图题型的例题讲解与分析;索引文件的详细解读
1. 位示图 位示图(Bitmap)是一种数据结构,用于表示和存储图像信息。在计算机科学中,位示图通常指的是一个二维的数组,每个元素称为一个像素,每个像素可以存储一个颜色值。 可以将位示图类比为电影院选座操作…...

SprinBoot+Vue校园数字化图书馆系统的设计与实现
目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍:CSDN认证博客专家,CSDN平台Java领域优质…...

python如何加速计算密集型任务?
问题描述: 在python中,有一个函数,其功能是进行某种计算,需要传入一些参数,计算完成后传回结果,调用其一次大概要1s的时间,现在需要通过for循环调用其350次,保存每次调用结果&#…...
握手的方式展现人的性格及行为倾向
握手是人际交往中最常见的礼节之一,同时通过和对方握手,可以感知他的内心,进一步得知对方的性格及行为倾向。 心理学家认为,最好的握手方式是力度适中,动作沉稳,自然注视对方的眼睛,这种握手方…...
Java 排序算法详解
排序是计算机科学中的基本操作,Java 提供了多种排序算法来满足不同的需求。常见的排序算法包括冒泡排序、选择排序、插入排序、归并排序、快速排序和堆排序。本文将逐一介绍这些排序算法及其 Java 实现。 1. 冒泡排序 (Bubble Sort) 冒泡排序是一种简单的排序算法…...
vue3实现拖拽移动位置,拖拽过程中鼠标松开后元素还吸附在鼠标上并随着鼠标移动
发现问题 拖拽元素移动的时候,偶尔会出现拖拽过程中鼠标松开后元素还吸附在鼠标上并随着鼠标移动,要再按一下元素才会被放置下来。但是有时就正常。 问题分析 出现该问题的原因是:这个过程会触发H5原生的拖拽事件,并且不会监听…...
没有屋檐的房子-011
棺材 (下) 时过境迁这个成语描述了前因后果的两种概念的变化,时间推延和环境的变迁。问题是,时间是什么呢?是环境变化表征了时间的推延,还是时间推延导致了环境的变化?人在多数时候,…...
Puppeteer-Cluster:并行处理网页操作的新利器
在现代Web开发和自动化测试领域,高效地处理多个网页操作任务成为了许多开发者和测试工程师的迫切需求。传统的Puppeteer工具虽然功能强大,但在处理大量并发任务时可能会显得力不从心。为此,Puppeteer-Cluster应运而生,作为一个基于…...
使用Protocol Buffers传输数据
使用 Google Protocol Buffers(ProtoBuf)与 Kafka 结合来定义和传输数据,可以确保传输数据的结构性、可扩展性和高效性。以下是一个简单的步骤指南,帮助你实现生产者和消费者。 1. 定义 ProtoBuf 消息格式 首先,你需…...
chmod修改文件权限
0 Preface/Foreword 1 chmod使用方法 1.1 修改单个文件 命令如下: sudo chmod xyz fileName xyz: x, y, z分别代表一个8进制数字(0-7) 1.2 修改文件夹 命令如下: sudo chmod -R xyz folderName...
二叉树--python
二叉树 一、概述 1、介绍 是一种非线性数据结构,将数据一分为二,代表根与叶的派生关系,和链表的结构类似,二叉树的基本单元是结点,每个节点包括值和左右子节点引用。 每个节点都有两个引用(类似于双向链…...

matlab数据批量保存为excel,文件名,行和列的名称设置
Excel文件内数据保存结果如下: Excel文件保存结果如下: 代码如下: clear;clc; for jjjj1:10 %这个可以改 jname(jjjj-1)*10; %文件名中变数 这是EXCEL文件名字的一部分 根据自己需要改 jkkkk_num2str(jname); for …...

Pygame中Sprite类实现多帧动画3-2
3.2.3 设置帧的宽度、高度、范围及列数 通过如图6所示的代码设置帧的宽度、高度、范围及列数。 图6 设置帧的宽度、高度、范围及列数的代码 其中,frame_width、frame_height、rect和columns都是MySprite类的属性,在其__init__()方法中定义,…...

C#发送正文带图片带附件的邮件
1,开启服务,获取授权码。以QQ邮箱为例: 点击管理服务,进入账号与安全页面 2,相关设置参数,以QQ邮箱为例: 登录时,请在第三方客户端的密码输入框里面填入授权码进行验证。࿰…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的
修改bug思路: 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑:async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...