当前位置: 首页 > news >正文

WebSocket的那些事(5-Spring STOMP支持之连接外部消息代理)

目录

  • 一、序言
  • 二、开启RabbitMQ外部消息代理
  • 三、代码示例
    • 1、Maven依赖项
    • 2、相关实体
    • 3、自定义用户认证拦截器
    • 4、Websocket外部消息代理配置
    • 5、ChatController
    • 6、前端页面chat.html
  • 四、测试示例
    • 1、群聊、私聊、后台定时推送测试
    • 2、登录RabbitMQ控制台查看队列信息
  • 五、结语

一、序言

上节我们在 WebSocket的那些事(4-Spring中的STOMP支持详解) 中详细说明了通过Spring内置消息代理结合STOMP子协议进行Websocket通信,以及相关注解的使用及原理。

但是Spring内置消息代理会有一些限制,比如只支持STOMP协议的一部分命令,像acksreceipts命令都是不支持的,还有由于内置消息代理把消息存储在内存,当应用不可用时,客户端也就订阅不到到后台推送的消息。

这节我们将会使用支持STOMP协议的外部消息代理(RabbitMQ)进行Websocket通信。


二、开启RabbitMQ外部消息代理

服务端路由发送消息以及客户端订阅消息都要通过STOMP协议与RabbitMQ进行交互,由于RabbitMQ默认没有启动STOMP插件,因此我们需要先启用该插件。

rabbitmq-plugins enable rabbitmq_stomp

启动该插件后,RabbitMQ中STOMP适配器默认会监听61613端口,如果是云服务器,需要把该端口在安全组中放开。

关于该插件说明请参考:RabbitMQ中STOMP插件说明。


三、代码示例

我们在 WebSocket的那些事(4-Spring中的STOMP支持详解)中写了一个简单的聊天Demo示例,下面我们对该聊天Demo示例进行改造,将Spring内置消息代理替换成RabbitMQ外部消息代理。

1、Maven依赖项

服务端和客户端与外部消息代理都是通过TCP进行通信,Spring底层默认使用的是NettyReactor,因此需要引入相关依赖项。

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

2、相关实体

(1) 请求消息参数

@Data
public class WebSocketMsgDTO {private String name;private String content;
}

(2) 响应消息内容

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketMsgVO {private String content;
}

(3) 自定义认证用户信息

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StompAuthenticatedUser implements Principal {/*** 用户唯一ID*/private String userId;/*** 用户昵称*/private String nickName;/*** 用于指定用户消息推送的标识* @return*/@Overridepublic String getName() {return this.userId;}}

3、自定义用户认证拦截器

@Slf4j
public class UserAuthenticationChannelInterceptor implements ChannelInterceptor {private static final String USER_ID = "User-ID";private static final String USER_NAME = "User-Name";@Overridepublic Message<?> preSend(Message<?> message, MessageChannel channel) {StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);// 如果是连接请求,记录userIdif (StompCommand.CONNECT.equals(accessor.getCommand())) {String userID = accessor.getFirstNativeHeader(USER_ID);String username = accessor.getFirstNativeHeader(USER_NAME);log.info("Stomp User-Related headers found, userID: {}, username:{}", userID, username);accessor.setUser(new StompAuthenticatedUser(userID, username));}return message;}}

4、Websocket外部消息代理配置

Spring中与外部消息代理通信的中间方被称之为Broker Relay,它会维护一个系统共享的单一TCP连接和外部消息代理进行通信,该TCP连接仅仅适用于服务端,用来发送消息,而不是接收消息,通过Broker RelaysystemLoginsystemPasscode属性可以设置该连接的认证信息。

Broker Relay也会为每个连接的Websocket客户端创建一个TCP连接,该连接用来接收消息,通过clientLoginclientPasscode属性可以设置连接的认证信息。

/*** Websocket连接外部消息代理配置* @author Nick Liu* @date 2023/9/6*/
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketExternalMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureClientInboundChannel(ChannelRegistration registration) {// 拦截器配置registration.interceptors(new UserAuthenticationChannelInterceptor());}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/websocket") // WebSocket握手端口.addInterceptors(new HttpSessionHandshakeInterceptor()).setAllowedOriginPatterns("*") // 设置跨域.withSockJS(); // 开启SockJS回退机制}@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.setApplicationDestinationPrefixes("/app") // 发送到服务端目的地前缀.enableStompBrokerRelay("/topic") // 开启外部消息代理,指定消息订阅前缀.setRelayHost("localhost") // 外部消息代理Host.setRelayPort(61613) // 外部消息代理STOMP端口.setSystemLogin("admin")  // 共享系统连接用户名,该连接主要用来发送消息.setSystemPasscode("admin") // 共享系统连接密码,该连接主要用来发送消息.setClientLogin("admin") // 客户端连接用户名,该连接主要用来接收消息.setClientPasscode("admin") // 客户端连接密码,该连接主要用来接收消息.setVirtualHost("/stomp"); // RabbitMQ虚拟主机}
}

备注:我们可以为服务端与客户端的连接设置不同的用户,针对客户端连接用户进行权限管控,保证系统的安全性,在这里为了方便测试我们统一用一个用户。

5、ChatController

STOMP协议并没有规定消息代理必须支持哪种类型的Destinations(目的地),但是RabbitMQ STOMP适配器只支持一些指定的目的地类型,如下图:
在这里插入图片描述

  • /exchange:指定交换机和路由key,发送和订阅来自队列的消息。
  • /queue:发送和订阅受STOMP网关管理的队列的消息,最多只有一个订阅者能到消息。
  • /amq/queue:发送和订阅不受STOMP网关管理的队列的消息。
  • /topic:发送和订阅来自临时或者持久Topic的消息,多个订阅者都能接收到消息。
  • /temp-queue/:发送和订阅来自临时队列的消息。

参考文档见:RabbitMQ中STOMP插件说明。

在下面的示例中,我们选用了/topic的开头的消息发送和订阅前缀,目的地格式只能为/topic/{routing-key}routing-key不能有斜杠,否则会报错。

@Slf4j
@Controller
@RequiredArgsConstructor
public class ChatController {private final SimpUserRegistry simpUserRegistry;private final SimpMessagingTemplate simpMessagingTemplate;/*** 模板引擎为Thymeleaf,需要加上spring-boot-starter-thymeleaf依赖,* @return*/@GetMapping("/page/chat")public ModelAndView turnToChatPage() {return new ModelAndView("chat");}/*** 群聊消息处理* 这里我们通过@SendTo注解指定消息目的地为"/topic/chat/group",如果不加该注解则会自动发送到"/topic" + "/chat/group"* @param webSocketMsgDTO 请求参数,消息处理器会自动将JSON字符串转换为对象* @return 消息内容,方法返回值将会广播给所有订阅"/topic/chat/group"的客户端*/@MessageMapping("/chat/group")@SendTo("/topic/chat-group")public WebSocketMsgVO groupChat(WebSocketMsgDTO webSocketMsgDTO) {log.info("Group chat message received: {}", FastJsonUtils.toJsonString(webSocketMsgDTO));String content = String.format("来自[%s]的群聊消息: %s", webSocketMsgDTO.getName(), webSocketMsgDTO.getContent());return WebSocketMsgVO.builder().content(content).build();}/*** 私聊消息处理* 这里我们通过@SendToUser注解指定消息目的地为"/topic/chat/private",发送目的地默认会拼接上"/user/"前缀* 实际发送目的地为"/user/topic/chat/private"* @param webSocketMsgDTO 请求参数,消息处理器会自动将JSON字符串转换为对象* @return 消息内容,方法返回值将会基于SessionID单播给指定用户*/@MessageMapping("/chat/private")@SendToUser("/topic/chat-private")public WebSocketMsgVO privateChat(WebSocketMsgDTO webSocketMsgDTO) {log.info("Private chat message received: {}", FastJsonUtils.toJsonString(webSocketMsgDTO));String content = "私聊消息回复:" + webSocketMsgDTO.getContent();return WebSocketMsgVO.builder().content(content).build();}/*** 定时消息推送,这里我们会列举所有在线的用户,然后单播给指定用户。* 通过SimpMessagingTemplate实例可以在任何地方推送消息。*/@Scheduled(fixedRate = 10 * 1000)public void pushMessageAtFixedRate() {log.info("当前在线人数: {}", simpUserRegistry.getUserCount());if (simpUserRegistry.getUserCount() <= 0) {return;}// 这里的Principal为StompAuthenticatedUser实例Set<StompAuthenticatedUser> users = simpUserRegistry.getUsers().stream().map(simpUser -> StompAuthenticatedUser.class.cast(simpUser.getPrincipal())).collect(Collectors.toSet());users.forEach(authenticatedUser -> {String userId = authenticatedUser.getUserId();String nickName = authenticatedUser.getNickName();WebSocketMsgVO webSocketMsgVO = new WebSocketMsgVO();webSocketMsgVO.setContent(String.format("定时推送的私聊消息, 接收人: %s, 时间: %s", nickName, LocalDateTime.now()));log.info("开始推送消息给指定用户, userId: {}, 消息内容:{}", userId, FastJsonUtils.toJsonString(webSocketMsgVO));simpMessagingTemplate.convertAndSendToUser(userId, "/topic/chat-push", webSocketMsgVO);});}}

6、前端页面chat.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>chat</title><script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script><style>#mainWrapper {width: 600px;margin: auto;}</style>
</head>
<body>
<div id="mainWrapper"><div><label for="username" style="margin-right: 5px">姓名:</label><input id="username" type="text"/></div><div id="msgWrapper"><p style="vertical-align: top">发送的消息:</p><textarea id="msgSent" style="width: 600px;height: 100px"></textarea><p style="vertical-align: top">收到的群聊消息:</p><textarea id="groupMsgReceived" style="width: 600px;height: 100px"></textarea><p style="vertical-align: top">收到的私聊消息:</p><textarea id="privateMsgReceived" style="width: 600px;height: 200px"></textarea></div><div style="margin-top: 5px;"><button onclick="connect()">连接</button><button onclick="sendGroupMessage()">发送群聊消息</button><button onclick="sendPrivateMessage()">发送私聊消息</button><button onclick="disconnect()">断开连接</button></div>
</div>
<script type="text/javascript">$(() => {$('#msgSent').val('');$("#groupMsgReceived").val('');$("#privateMsgReceived").val('');});let stompClient = null;// 连接服务器const connect = () => {const header = {"User-ID": new Date().getTime().toString(), "User-Name": $('#username').val()};const ws = new SockJS('http://localhost:8080/websocket');stompClient = Stomp.over(ws);stompClient.connect(header, () => subscribeTopic());}// 订阅主题const subscribeTopic = () => {alert("连接成功!");// 订阅广播消息stompClient.subscribe('/topic/chat-group', function (message) {console.log(`Group message received : ${message.body}`);const resp = JSON.parse(message.body);const previousMsg = $("#groupMsgReceived").val();$("#groupMsgReceived").val(`${previousMsg}${resp.content}\n`);});// 订阅单播消息stompClient.subscribe('/user/topic/chat-private', message => {console.log(`Private message received : ${message.body}`);const resp = JSON.parse(message.body);const previousMsg = $("#privateMsgReceived").val();$("#privateMsgReceived").val(`${previousMsg}${resp.content}\n`);});// 订阅定时推送的单播消息stompClient.subscribe(`/user/topic/chat-push`, message => {console.log(`Private message received : ${message.body}`);const resp = JSON.parse(message.body);const previousMsg = $("#privateMsgReceived").val();$("#privateMsgReceived").val(`${previousMsg}${resp.content}\n`);});};// 断连const disconnect = () => {stompClient.disconnect(() => {$("#msgReceived").val('Disconnected from WebSocket server');});}// 发送群聊消息const sendGroupMessage = () => {const msg = {name: $('#username').val(), content: $('#msgSent').val()};stompClient.send('/app/chat/group', {}, JSON.stringify(msg));}// 发送私聊消息const sendPrivateMessage = () => {const msg = {name: $('#username').val(), content: $('#msgSent').val()};stompClient.send('/app/chat/private', {}, JSON.stringify(msg));}
</script>
</body>
</html>

四、测试示例

1、群聊、私聊、后台定时推送测试

启动应用程序,日志打印显示系统连接建立成功,如下:

在这里插入图片描述

打开浏览器访问http://localhost:8080/page/chat可进入聊天页,同时打开两个窗口访问。
在这里插入图片描述


在这里插入图片描述

2、登录RabbitMQ控制台查看队列信息

在这里插入图片描述
可以看到所有消息都发送到了amq.topic交换机上(Topic类型), RabbitMQ会为每个连接的客户端创建3个队列。

因为我们在ChatController中定义了三个目的地,Routing Key分别是/topic/chat-group/topic/chat-private/topic/chat-push。群聊消息目的地/topic/chat-group绑定了两个队列,用于实现广播订阅,其它两个Routing Key分别绑定到了不同的队列上,实现唯一订阅。


五、结语

下一节我们将会详细说明RabbitMQ STOMP适配器支持的各种消息目的地类型的区别以及适用场景。

在这里插入图片描述

相关文章:

WebSocket的那些事(5-Spring STOMP支持之连接外部消息代理)

目录 一、序言二、开启RabbitMQ外部消息代理三、代码示例1、Maven依赖项2、相关实体3、自定义用户认证拦截器4、Websocket外部消息代理配置5、ChatController6、前端页面chat.html 四、测试示例1、群聊、私聊、后台定时推送测试2、登录RabbitMQ控制台查看队列信息 五、结语 一、…...

【数据结构】单链表详解

当我们学完顺序表的时候&#xff0c;我们发现了好多问题如下&#xff1a; 中间/头部的插入删除&#xff0c;时间复杂度为O(N)增容需要申请新空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不小的消耗。增容一般是呈2倍的增长&#xff0c;势必会有一定的空间浪费。例如当…...

dql的执行顺序

在 SQL 查询语言中&#xff0c;DQL&#xff08;Data Query Language&#xff09;是用于从数据库中检索数据的部分。SQL 查询的执行顺序通常按照以下步骤进行&#xff1a; FROM 子句&#xff1a;查询首先确定要从哪些表中检索数据。在 FROM 子句中列出的表格被称为源表&#xff…...

java的动态代理如何实现

一. JdkProxy jdkproxy动态代理必须基于接口(interface)实现 接口UserInterface.java public interface UserService {String getUserName(String userCde); }原始实现类&#xff1a;UseServiceImpl.java public class UserServiceImpl implements UserSerice {Overridepub…...

Java--日志管理

日志管理 作用&#xff1a; 设置日志级别&#xff0c;决定什么日志信息应该被输出、什么日志信息应该被忽略。 基本工具 见的日志管理用具有:JDK logging&#xff08;配置文件&#xff1a;logging.properties&#xff09; 和log4j(配置文件&#xff1a;log4j.properties) 。…...

Pygame中Sprite类的使用2

4 让僵尸动起来 让僵尸能够动起来&#xff0c;也就是让僵尸从屏幕右边走到屏幕左边&#xff0c;此时只需要使用while循环&#xff0c;改变僵尸图片的x轴坐标即可&#xff0c;代码如下所示。 while True:screen.fill((255,255,255))z1.rect.x - 5z1.draw(screen)z1.update()if…...

排队时延与流量强度

流量强度 设R为传输速率&#xff0c;a表示分组到达队列的平均速率&#xff0c;假定所有分组都是由L比特组成的&#xff0c;则比特到达队列的平均速率为La。比率 L a R \frac{La}{R} RLa​被成为流量强度。 根据流量强度的定义&#xff0c;我们可以很直观的得出以下结论&#x…...

mysql:如何设计互相关注业务场景

目录 业务场景 业务问题&#xff1a; 数据库表设计&#xff1a; like&#xff08;关注表&#xff09;&#xff1a; friend&#xff08;朋友表&#xff09; 并发场景下&#xff0c;SQL语句执行逻辑 比较 A 和 B 的大小&#xff0c;如果 A执行下面的逻辑&#xff1a;<&…...

AI伦理:科技发展中的人性之声

文章目录 AI伦理的关键问题1. 隐私问题2. 公平性问题3. 自主性问题4. 伦理教育问题 隐私问题的拓展分析数据收集和滥用隐私泄露和数据安全 公平性问题的拓展分析历史偏见和算法模型可解释性 自主性问题的拓展分析自主AI决策伦理框架 伦理教育的拓展分析伦理培训 结论 &#x1f…...

Direct3D光照

光照的组成 环境光&#xff1a;这种类型的光经其他表面反射到达物体表面&#xff0c;并照亮整个场景&#xff0c;要想以较低代价粗略模拟这类反射光&#xff0c;环境光是一个很好的选择 漫射光&#xff1a;这种类型光沿着特定的方向传播。当它到达某一表面时&#xff0c;将沿…...

编程语言排行榜

以下是2023年的编程语言排行榜&#xff08;按照流行度排序&#xff09;&#xff1a; Python&#xff1a;Python一直以来都是非常受欢迎的编程语言&#xff0c;它简洁、易读且功能强大。在数据科学、机器学习、人工智能等领域有广泛应用。 JavaScript&#xff1a;作为前端开发…...

基于语雀编辑器的在线文档编辑与查看

概述 语雀是一个非常优秀的文档和知识库工具&#xff0c;其编辑器更是非常好用&#xff0c;虽无开源版本&#xff0c;但有编译好的可以使用。本文基于语雀编辑器实现在线文档的编辑与文章的预览。 实现效果 实现 参考语雀编辑器官方文档&#xff0c;其实现需要引入以下文件&…...

开箱报告,Simulink Toolbox库模块使用指南(六)——S-Fuction模块(TLC)

文章目录 前言 Target Language Compiler&#xff08;TLC&#xff09; C MEX S-Function模块 编写TLC文件 生成代码 Tips 分析和应用 总结 前言 见《开箱报告&#xff0c;Simulink Toolbox库模块使用指南&#xff08;一&#xff09;——powergui模块》 见《开箱报告&am…...

Kafka详解

目录 一、消息系统 1、点对点的消息系统 2、发布-订阅消息系统 二、Apache Kafka 简介 三、Apache Kafka基本原理 3.1 分布式和分区&#xff08;distributed、partitioned&#xff09; 3.2 副本&#xff08;replicated &#xff09; 3.3 整体数据流程 3.4 消息传送机制…...

rabbitmq+springboot实现幂等性操作

文章目录 1.场景描述 1.1 场景11.2 场景2 2.原理3.实战开发 3.1 建表3.2 集成mybatis-plus3.3 集成RabbitMq 3.3.1 安装mq3.3.2 springBoot集成mq 3.4 具体实现 3.4.1 mq配置类3.4.2 生产者3.4.3 消费者 1.场景描述 消息中间件是分布式系统常用的组件&#xff0c;无论是异…...

ubuntu server 更改时区:上海

1. 打开终端&#xff0c;在命令行中以超级用户或具有sudo权限的用户身份运行以下命令&#xff1a; sudo dpkg-reconfigure tzdata 这会打开一个对话框&#xff0c;用于选择系统的时区设置。 2. 在对话框中&#xff0c;使用上下箭头键在地区列表中选择"Asia"&#x…...

java 整合 swagger-ui 步骤

1.在xml 中添加Swagger 相关依赖 <!-- springfox-swagger2 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><!-- springfox-swa…...

介绍两款生成神经网络架构示意图的工具:NN-SVG和PlotNeuralNet

对于神经网络架构的可视化是很有意义的&#xff0c;可以在很大程度上帮助到我们清晰直观地了解到整个架构&#xff0c;我们在前面的 PyTorch的ONNX结合MNIST手写数字数据集的应用(.pth和.onnx的转换与onnx运行时) 有介绍&#xff0c;可以将模型架构文件(常见的格式都可以)在线上…...

iOS IdiotAVplayer实现视频分片缓存

文章目录 IdiotAVplayer 实现视频切片缓存一 iOS视频边下边播原理一 分片下载的实现1 分片下载的思路2 IdiotAVplayer 实现架构 三 IdiotAVplayer 代码解析IdiotPlayerIdiotResourceLoaderIdiotDownLoader IdiotAVplayer 实现视频切片缓存 一 iOS视频边下边播原理 初始化AVUR…...

SpringBootWeb请求-响应

HTTP请求 前后端分离 在这种模式下&#xff0c;前端技术人员基于"接口文档"&#xff0c;开发前端程序&#xff1b;后端技术人员也基于"接口文档"&#xff0c;开发后端程序。 由于前后端分离&#xff0c;对我们后端技术人员来讲&#xff0c;在开发过程中&a…...

amsmath宏包完全使用手册:从解决符号显示问题到专业公式排版

amsmath宏包完全使用手册&#xff1a;从解决符号显示问题到专业公式排版 在科研论文、技术文档或数学教材的写作过程中&#xff0c;LaTeX作为专业的排版工具已经成为学术界的标准选择。而数学公式的排版&#xff0c;则是LaTeX最引以为傲的功能之一。然而&#xff0c;即使是经验…...

从‘能工作’到‘优秀’:手把手教你为你的Buck/Boost电路挑选和优化MOSFET驱动

从‘能工作’到‘优秀’&#xff1a;手把手教你为Buck/Boost电路挑选和优化MOSFET驱动 在开关电源设计中&#xff0c;MOSFET的选择和驱动优化往往是决定整体效率的关键因素。许多工程师能够设计出"能工作"的电路&#xff0c;但要达到"优秀"的性能指标&…...

GD32F30x串口DMA+空闲中断接收不定长数据,一个LED控制项目带你搞懂

GD32F30x串口DMA空闲中断实战&#xff1a;从零构建LED智能控制系统 在嵌入式开发中&#xff0c;串口通信就像设备的"嘴巴"和"耳朵"&#xff0c;而DMA技术则是解放CPU的"隐形助手"。想象一下这样的场景&#xff1a;你需要通过手机APP远程控制实验…...

突破透明动画性能瓶颈:VAP引擎实现移动端高效视觉体验

突破透明动画性能瓶颈&#xff1a;VAP引擎实现移动端高效视觉体验 【免费下载链接】vap VAP是企鹅电竞开发&#xff0c;用于播放特效动画的实现方案。具有高压缩率、硬件解码等优点。同时支持 iOS,Android,Web 平台。 项目地址: https://gitcode.com/gh_mirrors/va/vap …...

别再死记ResNet结构了!用PyTorch手搓一个ResNet-50,从零理解残差连接

从零构建ResNet-50&#xff1a;用PyTorch拆解残差网络的秘密 深度学习领域最令人着迷的突破之一&#xff0c;莫过于残差网络&#xff08;ResNet&#xff09;的诞生。2015年&#xff0c;何恺明团队提出的这一架构不仅横扫ImageNet竞赛&#xff0c;更彻底改变了我们对深度神经网络…...

OpenClaw技能分享:GLM-4.7-Flash驱动的邮件自动处理系统

OpenClaw技能分享&#xff1a;GLM-4.7-Flash驱动的邮件自动处理系统 1. 为什么需要自动化邮件处理 每天早晨打开邮箱&#xff0c;看到堆积如山的未读邮件总让人头皮发麻。作为一个小团队的负责人&#xff0c;我经常需要处理客户咨询、内部沟通、会议邀请等各种类型的邮件。最…...

CAN总线波特率计算器工具开发指南(Python+PyQt5)

CAN总线波特率计算器工具开发指南&#xff08;PythonPyQt5&#xff09; 在汽车电子工程领域&#xff0c;CAN总线作为车载网络的骨干&#xff0c;其通信质量直接影响整车系统的稳定性。而波特率作为CAN通信的基础参数&#xff0c;其配置精度直接决定了总线能否正常工作。传统的手…...

QLVideo:macOS视频管理效率提升的完整解决方案

QLVideo&#xff1a;macOS视频管理效率提升的完整解决方案 【免费下载链接】QuickLookVideo This package allows macOS Finder to display thumbnails, static QuickLook previews, cover art and metadata for most types of video files. 项目地址: https://gitcode.com/g…...

GF-1遥感影像水体提取实战:Unet++、Deeplabv3+、MANet模型对比与避坑指南

GF-1遥感影像水体提取实战&#xff1a;三大模型对比与避坑全攻略 当国产高分一号&#xff08;GF-1&#xff09;卫星数据遇上深度学习语义分割技术&#xff0c;水体提取这项传统遥感任务正在经历革命性变革。本文将带您深入Unet、Deeplabv3和MANet三大主流模型在GF-1影像上的实战…...

IDEA插件开发实战:手把手教你开发首个效率工具(附GitHub源码)

IDEA插件开发实战&#xff1a;从零打造你的专属效率工具 JetBrains系列IDE的强大之处不仅在于其核心功能&#xff0c;更在于其开放的插件生态系统。作为一名Java开发者&#xff0c;你是否曾想过为IDEA添加一个能提升自己工作效率的专属工具&#xff1f;本文将带你从零开始&…...