即时通讯增加Redis渠道
情况说明
在本地和服务器分别启动im服务,当本地发送消息时,会发现服务器上并没有收到消息


初版im只支持单机版,不支持分布式的情况。此次针对该情况对项目进行优化,文档中贴出的代码非完整代码,可自行查看参考资料[2]
代码结构调整
本次调整需要增加一个redis的渠道,为了方便后续再进行渠道的增加,对现有代码结构进行调整
- IBaseSendExecutor
渠道扩充接口,后续再增加渠道都可以实现该接口
package com.example.im.infra.executor.send;/*** @author PC* 通信处理*/
public interface IBaseSendExecutor {/*** 获取通信类型,预置的有默认和redis** @return 通讯类型*/String getCommunicationType();/*** 发送给指定人** @param sendUserName 发送人* @param message 消息*/void sendToUser(String sendUserName, String message);/*** 发送给全部人** @param sendUserName 发送人* @param message 消息*/void sendToAll(String sendUserName, String message);
}
- AbstractBaseSendExecutor
通信处理抽象类,将一些预定义的渠道所需要的公有方法提取出来
package com.example.im.infra.executor.send;import com.example.im.config.WebSocketProperties;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;/*** @author PC*/
public abstract class AbstractBaseSendExecutor implements IBaseSendExecutor {protected WebSocketProperties webSocketProperties;@Autowiredpublic void setWebSocketProperties(WebSocketProperties webSocketProperties) {this.webSocketProperties = webSocketProperties;}/*** 获取接收人信息** @param sendUserName 发送人* @param message 消息* @return 接收人列表*/protected List<String> getReceiverName(String sendUserName, String message) {if (!StringUtils.contains(message, webSocketProperties.getReceiverSeparator())) {return new ArrayList<>();}String[] names = StringUtils.split(message, webSocketProperties.getReceiverSeparator());return Stream.of(names).skip(1).filter(receiver ->!(webSocketProperties.getReceiverExcludesHimselfFlag() && StringUtils.equals(sendUserName, receiver))).collect(Collectors.toList());}/*** 根据配置处理发送的信息** @param message 原消息* @return 被处理后的消息*/protected String generatorMessage(String message) {return BooleanUtils.isTrue(webSocketProperties.getExcludeReceiverInfoFlag()) ?StringUtils.substringBefore(message, webSocketProperties.getReceiverSeparator()) : message;}
}
- DefaultSendExecutor
原有消息发送逻辑
package com.example.im.infra.executor.send;import com.example.im.endpoint.WebSocketEndpoint;
import com.example.im.infra.constant.ImConstants;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;/*** @author PC* 默认执行*/
@Component
public class DefaultSendExecutor extends AbstractBaseSendExecutor {private final static Logger logger = LoggerFactory.getLogger(DefaultSendExecutor.class);private TaskExecutor taskExecutor;@Autowiredpublic void setTaskExecutor(TaskExecutor taskExecutor) {this.taskExecutor = taskExecutor;}@Overridepublic String getCommunicationType() {return ImConstants.CommunicationType.DEFAULT;}@Overridepublic void sendToUser(String sendUserName, String message) {List<String> receiverNameList = getReceiverName(sendUserName, message);CountDownLatch countDownLatch = new CountDownLatch(receiverNameList.size());Set<String> notOnlineReceiverSet = ConcurrentHashMap.newKeySet();Set<String> finalNotOnlineReceiverSet = notOnlineReceiverSet;receiverNameList.forEach(receiverName -> taskExecutor.execute(() -> {try {if (WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.containsKey(receiverName)) {WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.get(receiverName).getSession().getBasicRemote().sendText(generatorMessage(message));} else {finalNotOnlineReceiverSet.add(receiverName);}} catch (IOException ioException) {logger.error("send error:" + ioException);} finally {countDownLatch.countDown();}}));try {countDownLatch.await();} catch (InterruptedException interruptedException) {logger.error("error.countDownLatch.await");}notOnlineReceiverSet = notOnlineReceiverSet.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toSet());if (CollectionUtils.isNotEmpty(notOnlineReceiverSet)) {logger.info("not online number is " + notOnlineReceiverSet.size());logger.info("The user : {} is not online", String.join(",", notOnlineReceiverSet));}}@Overridepublic void sendToAll(String sendUserName, String message) {for (Map.Entry<String, WebSocketEndpoint> webSocketEndpointEntry : WebSocketEndpoint.WEB_SOCKET_ENDPOINT_MAP.entrySet()) {taskExecutor.execute(() -> {if (webSocketProperties.getReceiverExcludesHimselfFlag() && StringUtils.equals(sendUserName, webSocketEndpointEntry.getKey())) {return;}try {webSocketEndpointEntry.getValue().getSession().getBasicRemote().sendText(generatorMessage(message));} catch (IOException ioException) {logger.error("send error:" + ioException);}});}}
}
- SendExecutorFactory
发送渠道工厂
package com.example.im.infra.executor.send;import com.example.im.config.WebSocketProperties;
import com.example.im.infra.executor.config.ExecutorConfiguration;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Optional;/*** @author PC* 发送逻辑工厂*/
@Component
public class SendExecutorFactory {private final WebSocketProperties webSocketProperties;private ExecutorConfiguration executorConfiguration;@Autowiredpublic SendExecutorFactory(WebSocketProperties webSocketProperties) {this.webSocketProperties = webSocketProperties;}@Autowiredpublic void setExecutorConfiguration(ExecutorConfiguration executorConfiguration) {this.executorConfiguration = executorConfiguration;}public void onMessage(String sendUserName, String message) {IBaseSendExecutor iBaseSendExecutor = Optional.ofNullable(executorConfiguration.getBaseSendExecutorMap().get(webSocketProperties.getCommunicationType())).orElse(new DefaultSendExecutor());//包含@发给指定人,否则发给全部人if (StringUtils.contains(message, webSocketProperties.getReceiverSeparator())) {iBaseSendExecutor.sendToUser(sendUserName, message);} else {iBaseSendExecutor.sendToAll(sendUserName, message);}}
}
- ExecutorConfiguration
加载
package com.example.im.infra.executor.config;import com.example.im.infra.executor.send.IBaseSendExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;/*** @author PC* Executor配置*/
@Component
public class ExecutorConfiguration implements ApplicationContextAware {private final static Logger logger = LoggerFactory.getLogger(ExecutorConfiguration.class);private Map<String, IBaseSendExecutor> baseSendExecutorMap = new HashMap<>(16);private static ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {ExecutorConfiguration.applicationContext = applicationContext;//加载IBaseSendExecutor实现类this.initBaseSendExecutor(applicationContext);}/*** 加载IBaseSendExecutor实现类* 如果一个服务的发送渠道是固定的,可以使用@Bean搭配@ConditionalOnProperty的方式* 但是考虑到后续可能会有一个服务不同发送渠道的场景,采用当前加载方式** @param applicationContext 上下文*/private void initBaseSendExecutor(ApplicationContext applicationContext) {logger.info("Start loading IBaseSendExecutor");Map<String, IBaseSendExecutor> baseSendExecutorMap = applicationContext.getBeansOfType(IBaseSendExecutor.class);for (Map.Entry<String, IBaseSendExecutor> iBaseSendExecutorEntry : baseSendExecutorMap.entrySet()) {String communicationType = iBaseSendExecutorEntry.getValue().getCommunicationType();this.baseSendExecutorMap.put(communicationType, iBaseSendExecutorEntry.getValue());logger.info("initBaseSendExecutor>>>>>>>communicationType:{},className:{}", communicationType, iBaseSendExecutorEntry.getValue().getClass().getName());}logger.info("IBaseSendExecutor loading is complete");}public static ApplicationContext getApplicationContext() {return applicationContext;}public Map<String, IBaseSendExecutor> getBaseSendExecutorMap() {return baseSendExecutorMap;}public void setBaseSendExecutorMap(Map<String, IBaseSendExecutor> baseSendExecutorMap) {this.baseSendExecutorMap = baseSendExecutorMap;}
}
添加redis通信渠道
- pom.xml
<dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
- application.yml
server:port: 18080
cus:ws:exclude-receiver-info-flag: truereceiver-excludes-himself-flag: truecommunication-type: redis
spring:redis:host: 127.0.0.1port: 6379username: rootpassword: rootdatabase: ${SPRING_REDIS_DATABASE:1}# Redis连接超时时间connect-timeout: ${SPRING_REDIS_CONNECT_TIMEOUT:2000}# Redis读取超时时间timeout: ${SPRING_REDIS_READ_TIMEOUT:5000}lettuce:pool:# 资源池中最大连接数# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}# 资源池运行最大空闲的连接数# 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}# 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)# 默认 -1 表示永不超时,设置5秒max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
- RedisSendExecutor
redis发送
package com.example.im.infra.executor.send.redis;import com.example.im.infra.constant.ImConstants;
import com.example.im.infra.executor.send.AbstractBaseSendExecutor;
import com.example.im.infra.executor.send.dto.MessageInfo;
import com.example.im.infra.executor.send.dto.ScopeOfSendingEnum;
import com.example.im.infra.util.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;/*** @author PC* redis执行*/
@Component
public class RedisSendExecutor extends AbstractBaseSendExecutor {private final static Logger logger = LoggerFactory.getLogger(RedisSendExecutor.class);private RedisTemplate<String, String> redisTemplate;@Autowiredpublic void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic String getCommunicationType() {return ImConstants.CommunicationType.REDIS;}@Overridepublic void sendToUser(String sendUserName, String message) {MessageInfo messageInfo = new MessageInfo();messageInfo.setSendUserName(sendUserName);messageInfo.setMessage(message);messageInfo.setScopeOfSending(ScopeOfSendingEnum.USER);logger.debug("send to user redis websocket, channel is " + "redis-websocket");redisTemplate.convertAndSend("redis-websocket-user", JsonUtils.toJson(messageInfo));}@Overridepublic void sendToAll(String sendUserName, String message) {MessageInfo messageInfo = new MessageInfo();messageInfo.setSendUserName(sendUserName);messageInfo.setMessage(message);messageInfo.setScopeOfSending(ScopeOfSendingEnum.ALL);logger.debug("send to all redis websocket, channel is " + "redis-websocket");redisTemplate.convertAndSend("redis-websocket-all", JsonUtils.toJson(messageInfo));}
}
- RedisMessageListener
redis监听
package com.example.im.infra.executor.send.redis;import com.example.im.infra.executor.send.DefaultSendExecutor;
import com.example.im.infra.executor.send.dto.MessageInfo;
import com.example.im.infra.util.JsonUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;import java.nio.charset.StandardCharsets;/*** @author PC* redis监听*/
@Component
public class RedisMessageListener implements MessageListener {private final static Logger logger = LoggerFactory.getLogger(RedisMessageListener.class);private DefaultSendExecutor defaultSendExecutor;@Autowiredpublic void setDefaultSendExecutor(DefaultSendExecutor defaultSendExecutor) {this.defaultSendExecutor = defaultSendExecutor;}@Overridepublic void onMessage(Message message, byte[] pattern) {//消息内容String messageJson = new String(message.getBody(), StandardCharsets.UTF_8);MessageInfo messageInfo = JsonUtils.toObjectByTypeReference(messageJson, new TypeReference<MessageInfo>() {});switch (messageInfo.getScopeOfSending()) {case USER:defaultSendExecutor.sendToUser(messageInfo.getSendUserName(), messageInfo.getMessage());break;case ALL:defaultSendExecutor.sendToAll(messageInfo.getSendUserName(), messageInfo.getMessage());break;default://一般来说不会出现该情况,除非用户覆盖了ScopeOfSending,后续可以开个扩展发送范围的口子logger.warn("invalid sending range:" + messageInfo.getScopeOfSending().getScopeCode());break;}}
}
测试
本地服务发送消息

服务器接收到了消息

常见问题
打包报错
执行mvn clean packages打包时出现以下错误
[ERROR] contextLoads Time elapsed: 0.001 s <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpoint' defined in class path resource [c
om/example/im/config/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
Caused by: java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
查看ServerContainer接口,发现其有两个接口实现类,其中有一个是test包的

将其排除后即可正常打包

jar包启动时no main manifest attribute问题
需将pom的plugin标签中的skip标签删除或设置为false

参考资料
[1].初版im文档
[2].im项目地址
相关文章:
即时通讯增加Redis渠道
情况说明 在本地和服务器分别启动im服务,当本地发送消息时,会发现服务器上并没有收到消息 初版im只支持单机版,不支持分布式的情况。此次针对该情况对项目进行优化,文档中贴出的代码非完整代码,可自行查看参考资料[2] 代码结构调…...
C++list
list简介 list是我们的链表,而且是带头双向循环链表,如下图 我们都知道,链表是由一个一个的节点组成的,它的成员由下面几个部分组成 通过对前面string,vector的学习,其实再来看我们的链表及其成员函数,是…...
设计模式 - 结构型
结构型 适配器模式,代理模式,桥接模式,装饰器模式,外观模式,组合模式,享元模式, 单一职责避免子类爆炸Bridge 模式对象的实现Decorator 模式对对象的职责,不生成子类接口隔离Adapt…...
STM32编码器接口
一、概述 1、Encoder Interface 编码器接口概念 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度每个高级定时器和通用…...
2024客户世界年度大会开幕,码号卫士赋能数字运营服务新升级
10月15日,2024年客户世界年度的大会在通州北投希尔顿酒店开幕。作为行业内的一个重要活动,本次大会以“数字运营支撑服务产业新升级”为主题,吸引了众多行业专家和企业代表。 据悉,本次大会以“数字运营支撑服务产业新升级”为主题…...
AcWing 802. 区间和(离散化算法,python)
本篇博客详细讲解一下离散化知识点,通过讲解和详细列题带大家掌握离散化。 题目: 原题链接:https://www.acwing.com/problem/content/description/804/ 假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。 现在,…...
【网页设计】CSS 盒子模型
目标 能够准确阐述盒子模型的 4 个组成部分能够利用边框复合写法给元素添加边框能够计算盒子的实际大小能够利用盒子模型布局模块案例能够给盒子设置圆角边框能够给盒子添加阴影能够给文字添加阴影 1. 盒子模型 页面布局要学习三大核心, 盒子模型, 浮动 和 定位. 学习好盒子模…...
如何通过构建对应的api服务器使Vue连接到数据库
一、安装数据库驱动 在后端安装 MySQL 数据库驱动,比如在 Node.js 环境中可以使用 mysql2 包来连接 MySQL 数据库。在项目目录下运行以下命令安装: npm install mysql2或者使用 yarn: yarn add mysql2二、创建数据库连接模块 创建一个专门…...
新手给视频加字幕的方法有哪些?4种加字幕方法推荐!
在视频制作中,字幕不仅是传递信息的重要手段,还能增强视频的观感和专业性。对于新手来说,如何给视频添加字幕可能是一个挑战。本文将介绍字幕的类型、推荐添加字幕的工具,以及详细添加字幕方法,帮助新手轻松掌握视频字…...
Oracle实际需要用到但常常被忽略的函数
1、Oracle中nvl()与nvl2()函数 函数nvl(expression1,expression2)根据参数1是否为null返回参数1或参数2的值; 函数nvl2(expression1,expression2,expression3)根据参数1是否为null返回参数2或参数3的值 【函数格式】:nvl(expression1,expression2) 若…...
代码随想录算法训练营Day23
局部最优——>全局最优&无反例,试试贪心 455.分发饼干 力扣题目链接:. - 力扣(LeetCode) class Solution {public int findContentChildren(int[] g, int[] s) {Arrays.sort(s);Arrays.sort(g);int gindex0;int count0;…...
vue使用table实现动态数据报表(行合并)
<template><div class"previewTable"><h2>***项目研发数据报告</h2><table id"previewTable" width"100%"><tr><th>项目名称</th><td colspan"6">{{ resultData.proName }}<…...
YARN调度原理详解
YARN(Yet Another Resource Negotiator)是 Hadoop 集群的资源管理和作业调度框架,它的设计旨在更好地管理和调度 Hadoop 集群中的资源。YARN 解决了传统 Hadoop MapReduce 中资源管理与作业调度紧耦合的问题,使得不同类型的计算任…...
Go-知识泛型
Go-知识泛型 1. 认识泛型1.1 不使用泛型1.2 使用泛型 2. 泛型的特点2.1 函数泛化2.2 类型泛化 3. 类型约束3.1 类型集合3.2 interface 类型集合3.2.1 内置interface类型集合3.2.2 自定义interface类型集合3.2.2.1 任意类型元素3.2.2.2 近似类型元素3.2.2.3 联合类型元素 3.2.3 …...
Qt 如何 发送与解析不定长报文以及数组不定长报文
文章目录 割方式一,采用QDataStream 解析,可直接设定大小端解析,无需自己转换方式二,采用结构体字节对齐方式解析发送接收方割 方式一,采用QDataStream 解析,可直接设定大小端解析,无需自己转换 需要注意的是结构体定义要去掉字节对齐,否则会崩溃,因为由自定义数据结…...
Rust默认使用UTF-8编码来解析源代码文件。如果在代码中包含无法用UTF-8编码表示的字符,编译器会报错!
文章目录 Rust默认编码示例在ANSI编码下中文显示正常的代码在UTF-8编码下将显示不正常在编译时,Rust使用UTF-8编码来解析代码,发现无法用UTF-8编码表示的字符,于是编译器报错 Rust默认编码 Rust 语言默认使用 UTF-8 编码来解析源代码文件。如…...
【jeston】torch相关环境安装
参考:玩转NVIDIA Jetson (25)— jetson 安装pytorch和torchvision 我的jeston信息: torch install 安装环境 conda create -n your_env python3.8 conda activate your_envpytorch_for_jeston 安装.whl文件 验证࿱…...
[CR]厚云填补_大型卫星影像去云数据集
AllClear: A Comprehensive Dataset and Benchmark for Cloud Removal in Satellite Imagery Abstract 卫星图像中的云对下游应用构成了重大挑战。当前云移除研究的一个主要挑战是缺乏一个全面的基准和一个足够大和多样化的训练数据集。为了解决这个问题,我们引入了…...
Langchain CharacterTextSplitter无法分割文档问题
在使用Langchain的文档分割器时,使用CharacterTextSplitter拆分文档是,发现返回的文档根本没有变化,即使设置了chunk_size,返回的大小也不符合参数设置。 CharacterTextSplitter设置了150,但是根本没有处理࿰…...
ros service不走是为什么
在ROS(Robot Operating System)中,如果ROS服务(Service)没有正常工作,可能有多种原因。你可以检查以下几点来排查问题: 服务是否正确启动 首先,确保服务节点已经启动并注册了相应的…...
将嵌套循环中的Java对象数组转换为HashMap以优化性能
本文旨在指导开发人员如何通过将嵌套循环转换为Hashmap来优化Java代码的性能,特别是当涉及到对象属性的相等性检查时。通过使用Hashmap的快速搜索特性,可以显著降低时间复杂性,提高代码执行效率。本文将提供详细的步骤和示例代码,…...
WarcraftHelper:魔兽争霸3终极优化指南 - 解锁现代硬件性能
WarcraftHelper:魔兽争霸3终极优化指南 - 解锁现代硬件性能 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper WarcraftHelper是一款专为魔兽…...
HarmonyOS 5 + UniApp 真机调试保姆级教程:从HBuilderX配置到ArkUI Inspector查错
HarmonyOS 5 UniApp 真机调试全流程实战指南 第一次在HarmonyOS设备上调试UniApp应用时,我盯着HBuilderX里那个灰色的"运行到鸿蒙设备"按钮整整半小时。设备明明连着USB线,开发者模式也开了,但工具就是识别不到我的MatePad Pro。…...
AD7124多通道配置实战:从寄存器映射到混合模式应用
1. AD7124多通道配置的核心价值 第一次接触AD7124时,我被它复杂的寄存器结构弄得晕头转向。这款24位Σ-Δ ADC芯片在工业测温、多路数据采集等场景表现优异,但想要充分发挥其性能,必须吃透通道与配置寄存器的映射关系。实际项目中,…...
终极指南:如何从零开始打造你的第一台六足机器人
终极指南:如何从零开始打造你的第一台六足机器人 【免费下载链接】hexapod 项目地址: https://gitcode.com/gh_mirrors/hexapod5/hexapod 你是否梦想过亲手制作一台能够灵活行走、稳定爬行的六足机器人?想要体验机器人制作的乐趣,却担…...
语义通信:从理论到6G落地的关键技术演进与挑战
1. 语义通信的理论基石 语义通信(Semantic Communication, SemCom)的核心思想与传统通信有着本质区别。传统通信追求的是"准确传输比特流",而语义通信关注的是"有效传递信息的意义"。这就像两个人对话:传统通…...
手把手教你用Matlab Simulink搭建闭环Buck电路:从PID调参到负载突变分析
从零构建闭环Buck电路:Simulink实战与PID调参全解析 电力电子工程师的日常工作中,Buck降压电路的设计与调试是基础中的基础。但真正让一个新手头疼的,往往不是电路拓扑本身,而是如何通过仿真快速验证设计,特别是当引入…...
如何突破Windows权限壁垒?系统管理专家的秘密武器
如何突破Windows权限壁垒?系统管理专家的秘密武器 【免费下载链接】NSudo [Deprecated, work in progress alternative: https://github.com/M2Team/NanaRun] Series of System Administration Tools 项目地址: https://gitcode.com/gh_mirrors/ns/NSudo 在W…...
保姆级教程:在OrangePi 5 Plus上从SSD启动Ubuntu 22.04,并配置ROS2 Humble环境
OrangePi 5 Plus开发板全栈配置指南:从SSD启动到ROS2 Humble环境搭建 拿到一块OrangePi 5 Plus开发板时,如何快速搭建一个稳定高效的开发环境?本文将手把手带你完成从系统烧录到ROS2环境配置的全过程,特别针对ARM64架构的优化方案…...
用MATLAB从零实现六足机器人步态:交替三角与波动步态代码详解
用MATLAB从零实现六足机器人步态:交替三角与波动步态代码详解 六足机器人因其卓越的稳定性和地形适应能力,在野外勘探、灾难救援等领域展现出巨大潜力。而步态规划作为机器人运动控制的核心,直接决定了机器人的移动效率和稳定性。本文将带您从…...
