ROS2----组件(Components)

机器人系统中多模块(如相机采集、视觉检测、运动控制、激光SLAM)的耦合性、通信效率、动态部署性是核心痛点。ROS2的组件(Components) 机制正是为解决这些问题而生------它允许将多个独立的功能模块(节点)封装为可动态加载的共享库,运行在同一个进程中,既保留了模块化的灵活性,又消除了多进程间通信(IPC)的开销,尤其适合CV这类高数据吞吐量、低延迟要求的场景。

一、ROS2组件的核心定位与设计初衷

1.1 传统ROS2节点的痛点

ROS2默认的节点运行方式是单节点单进程 :每个rclcpp::Node实例对应一个独立的操作系统进程。这种方式在CV/机器人开发中存在显著缺陷:

  • 通信开销大 :CV场景中频繁传输的sensor_msgs/Image(动辄几MB/帧)通过IPC(DDS)传输时,会产生序列化/反序列化、进程间拷贝的延迟,导致视觉算法实时性下降;
  • 资源占用高:每个进程有独立的内存空间、线程池、DDS客户端,多传感器(相机、激光雷达、IMU)+多算法(检测、跟踪、分割)的系统会占用大量内存和CPU上下文切换资源;
  • 部署灵活性差:节点启动后无法动态加载/卸载模块(比如机器人运行中切换视觉检测模型),只能重启进程;
  • 调试复杂度高:多进程调试需要附加多个gdb实例,难以追踪跨节点的内存/线程问题。

1.2 组件的核心价值

ROS2组件(正式名称为Composable Nodes)本质是封装为共享库(.so)的节点,核心优势:

  • 单进程多组件:多个组件运行在同一个进程(组件容器)中,组件间通信直接通过内存指针传递(无需DDS序列化),CV图像数据传输延迟降低90%以上;
  • 动态加载/卸载:运行时可通过命令行/API加载、卸载组件,支持机器人在线升级视觉算法模块;
  • 模块化复用:CV算法(如YOLO检测、ORB特征提取)封装为组件后,可直接复用在不同机器人项目中;
  • 统一调试:单个进程内的所有组件可通过一次gdb附加完成调试,便于定位CV算法的内存泄漏、线程死锁问题;
  • 资源隔离:组件容器支持命名空间隔离,多机器人的视觉组件可运行在同一个容器中,避免参数冲突。

二、ROS2组件的底层核心原理

2.1 核心依赖:rclcpp_components与Class Loader

ROS2组件的底层实现依赖两个核心模块:

  • rclcpp_components:ROS2官方提供的C++组件框架,封装了组件的注册、加载、生命周期管理接口;
  • Class Loader :ROS2基于Poco::SharedLibrary(替代旧版Boost.DLL)实现的动态库加载库,支持在运行时加载.so文件并实例化其中的类对象。

核心逻辑:

  1. 开发者将节点封装为继承rclcpp::Node(或rclcpp_lifecycle::LifecycleNode)的类,并通过宏RCLCPP_COMPONENTS_REGISTER_NODE将类注册到Class Loader的全局注册表;
  2. 组件容器(component_container)启动时,初始化rclcpp::Executor并监听组件加载请求;
  3. 加载组件时,容器通过Class Loader加载组件的.so库,从注册表中获取类实例,将其添加到Executor中运行;
  4. 组件的回调函数(如图像订阅回调)由容器的Executor统一调度,共享同一个线程池(或多线程池)。

2.2 组件的生命周期模型

ROS2组件分为两类,对应不同的生命周期:

(1)普通组件(Basic Component)

继承rclcpp::Node,生命周期与容器强绑定:

  • 加载:容器调用组件类的构造函数,初始化节点、订阅/发布器、参数等;
  • 运行:Executor调度回调函数;
  • 卸载:容器调用组件类的析构函数,释放资源;
  • 缺陷:无显式的"初始化/激活/停用"状态,CV组件加载模型(如YOLO权重)失败时会直接导致容器崩溃。
(2)生命周期组件(Lifecycle Component)

继承rclcpp_lifecycle::LifecycleNode,遵循ROS2生命周期节点的状态机规范,是CV/机器人开发的首选:

核心状态(按执行顺序):

  • Unconfigured:组件加载完成,未初始化;
  • Inactive:完成初始化(如加载CV模型、初始化相机参数),但未激活回调;
  • Active:激活所有订阅/发布器、定时器,执行核心逻辑;
  • Finalized:释放资源(如销毁CV模型、关闭相机流)。

状态切换接口:

  • configure():Unconfigured → Inactive(CV模型加载、参数解析);
  • activate():Inactive → Active(启动图像订阅回调);
  • deactivate():Active → Inactive(暂停回调,保留模型内存);
  • cleanup():Inactive → Unconfigured(释放模型内存);
  • shutdown():任意状态 → Finalized(析构)。

这种模型完美适配CV场景:比如机器人启动时先加载视觉组件(Unconfigured),待相机硬件就绪后再configure(加载模型)、activate(开始检测),遇到异常时可deactivate(暂停检测)而不销毁组件。

三、C++组件开发全流程(CV场景实战)

以"机器人视觉目标检测组件"为例,完整讲解从代码编写到运行的全流程,适配humble/iron/jazzy版本(ROS2主流LTS版本)。

要充分体现components的优势,需要多个功能节点打包成为容器比较好,但是受制于记录博客载体,这里给出一个组件的写法作为示例。

3.1 环境准备

确保已安装核心依赖:

bash 复制代码
# 安装rclcpp_components、cv_bridge(CV必备)、sensor_msgs(图像消息)
sudo apt install ros-humble-rclcpp-components ros-humble-cv-bridge ros-humble-sensor-msgs
# 安装OpenCV(ROS2集成版)
sudo apt install ros-humble-opencv-vendor

3.2 创建功能包

创建名为cv_detection_components的功能包,依赖需包含CV/组件核心库:

bash 复制代码
ros2 pkg create --build-type ament_cmake --dependencies rclcpp rclcpp_components rclcpp_lifecycle cv_bridge sensor_msgs std_msgs opencv_vendor -- cv_detection_components

3.3 编写生命周期组件代码

src/目录下创建yolo_detection_component.cpp,实现一个加载YOLO简化版、订阅图像、发布检测结果的组件(核心逻辑适配CV开发):

cpp 复制代码
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_lifecycle/lifecycle_node.hpp>
#include <rclcpp_lifecycle/lifecycle_publisher.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <std_msgs/msg/string.hpp>
#include <cv_bridge/cv_bridge.h>
#include <opencv2/opencv.hpp>

// 生命周期组件核心类(CV目标检测)
class YoloDetectionComponent : public rclcpp_lifecycle::LifecycleNode {
public:
    // 构造函数:初始化节点基本信息,暂不加载模型(生命周期Unconfigured)
    explicit YoloDetectionComponent(const rclcpp::NodeOptions &options)
        : LifecycleNode("yolo_detection_node", options) {
        // 声明参数(CV算法参数:置信度阈值、模型路径)
        this->declare_parameter("confidence_threshold", 0.5);
        this->declare_parameter("yolo_model_path", "/opt/models/yolov8n.onnx");
        
        RCLCPP_INFO(this->get_logger(), "Detection component created (Unconfigured)");
    }

    // 状态切换:Unconfigured → Inactive(加载CV模型、初始化资源)
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_configure(const rclcpp_lifecycle::State &previous_state) {
        RCLCPP_INFO(this->get_logger(), "Configuring detection component...");
        
        // 1. 获取参数
        confidence_threshold_ = this->get_parameter("confidence_threshold").as_double();
        model_path_ = this->get_parameter("yolo_model_path").as_string();
        
        // 2. 加载YOLO模型(CV核心操作,模拟实际场景)
        try {
            yolo_model_ = cv::dnn::readNet(model_path_)
            if (yolo_model_.empty()) {
                RCLCPP_ERROR(this->get_logger(), "Failed to load YOLO model: %s", model_path_.c_str());
                return CallbackReturn::FAILURE;
            }
        } catch (const std::exception &e) {
            RCLCPP_ERROR(this->get_logger(), "Model load exception: %s", e.what());
            return CallbackReturn::FAILURE;
        }

        // 3. 创建生命周期发布器(检测结果)
        detection_pub_ = this->create_lifecycle_publisher<std_msgs::msg::String>("detection_result", 10);
        
        RCLCPP_INFO(this->get_logger(), "Component configured (Inactive)");
        return CallbackReturn::SUCCESS;
    }

    // 状态切换:Inactive → Active(激活图像订阅、启动核心逻辑)
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_activate(const rclcpp_lifecycle::State &previous_state) {
        RCLCPP_INFO(this->get_logger(), "Activating detection component...");
        
        // 激活发布器(仅Active状态下发布消息)
        detection_pub_->on_activate();
        
        // 创建图像订阅器(CV核心:订阅相机图像)
        image_sub_ = this->create_subscription<sensor_msgs::msg::Image>(
            "camera/image_raw", 10, 
            std::bind(&YoloDetectionComponent::image_callback, this, std::placeholders::_1)
        );

        RCLCPP_INFO(this->get_logger(), "Component activated (Active)");
        return CallbackReturn::SUCCESS;
    }

    // 状态切换:Active → Inactive(暂停订阅、保留模型)
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_deactivate(const rclcpp_lifecycle::State &previous_state) {
        RCLCPP_INFO(this->get_logger(), "Deactivating detection component...");
        
        // 停用发布器
        detection_pub_->on_deactivate();
        
        // 销毁订阅器(暂停图像处理)
        image_sub_.reset();

        RCLCPP_INFO(this->get_logger(), "Component deactivated (Inactive)");
        return CallbackReturn::SUCCESS;
    }

    // 状态切换:Inactive → Unconfigured(释放模型资源)
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_cleanup(const rclcpp_lifecycle::State &previous_state) {
        RCLCPP_INFO(this->get_logger(), "Cleaning up detection component...");
        
        // 释放CV模型内存
        yolo_model_.release();
        detection_pub_.reset();

        RCLCPP_INFO(this->get_logger(), "Component cleaned up (Unconfigured)");
        return CallbackReturn::SUCCESS;
    }

    // 状态切换:任意状态 → Finalized(析构)
    rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
    on_shutdown(const rclcpp_lifecycle::State &previous_state) {
        RCLCPP_INFO(this->get_logger(), "Shutting down detection component...");
        yolo_model_.release();
        image_sub_.reset();
        detection_pub_.reset();
        return CallbackReturn::SUCCESS;
    }

private:
    // CV核心变量
    double confidence_threshold_;  // 检测置信度阈值
    std::string model_path_;       // YOLO模型路径
    cv::dnn::Net yolo_model_;      // YOLO模型实例

    // ROS2通信变量
    rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr image_sub_;
    rclcpp_lifecycle::LifecyclePublisher<std_msgs::msg::String>::SharedPtr detection_pub_;

    // 图像回调函数(CV核心逻辑:处理图像、执行检测)
    void image_callback(const sensor_msgs::msg::Image::SharedPtr msg) {
        try {
            // 1. 将ROS图像消息转换为OpenCV格式(CVBridge核心用法)
            cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, sensor_msgs::image_encodings::BGR8);
            cv::Mat frame = cv_ptr->image;

            // 2. 执行YOLO检测(模拟核心CV逻辑)
            std::string detection_result = "Detected 3 objects (conf > " + std::to_string(confidence_threshold_) + ")";
            RCLCPP_DEBUG(this->get_logger(), "Detection result: %s", detection_result.c_str());

            // 3. 发布检测结果(仅Active状态下有效)
            auto result_msg = std_msgs::msg::String();
            result_msg.data = detection_result;
            detection_pub_->publish(result_msg);

        } catch (cv_bridge::Exception &e) {
            RCLCPP_ERROR(this->get_logger(), "CVBridge exception: %s", e.what());
        } catch (const std::exception &e) {
            RCLCPP_ERROR(this->get_logger(), "Image process exception: %s", e.what());
        }
    }
};

// 注册组件到Class Loader(核心宏,必须有)
#include "rclcpp_components/register_node_macro.hpp"
RCLCPP_COMPONENTS_REGISTER_NODE(YoloDetectionComponent)

3.4 配置CMakeLists.txt(核心)

ROS2组件的编译配置与普通节点差异较大,需重点关注共享库编译组件注册

cmake 复制代码
cmake_minimum_required(VERSION 3.8)
project(cv_detection_components)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic -O3)  # O3优化CV算法
endif()

# 查找依赖
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(rclcpp_lifecycle REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(OpenCV REQUIRED)

# 编译组件为共享库(核心:SHARED)
add_library(yolo_detection_component SHARED
  src/yolo_detection_component.cpp
)

# 链接依赖库(CV/ROS2核心)
ament_target_dependencies(yolo_detection_component
  rclcpp
  rclcpp_components
  rclcpp_lifecycle
  cv_bridge
  sensor_msgs
  std_msgs
  OpenCV
)

# 注册组件(关键:让ROS2识别组件)
rclcpp_components_register_nodes(yolo_detection_component "YoloDetectionComponent")

# 设置共享库输出路径(可选,便于查找)
set_target_properties(yolo_detection_component PROPERTIES
  LIBRARY_OUTPUT_DIRECTORY lib
)

# 安装共享库(必须安装,否则容器无法找到)
install(TARGETS
  yolo_detection_component
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

3.5 配置package.xml

补充依赖声明(确保编译/运行时依赖完整):

xml 复制代码
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>cv_detection_components</name>
  <version>0.0.0</version>
  <description>CV detection component for ROS2</description>
  <maintainer email="your_email@xxx.com">Your Name</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>rclcpp_components</depend>
  <depend>rclcpp_lifecycle</depend>
  <depend>cv_bridge</depend>
  <depend>sensor_msgs</depend>
  <depend>std_msgs</depend>
  <depend>opencv_vendor</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

3.6 编译与运行

(1)编译组件
bash 复制代码
cd ~/ros2_ws
colcon build --packages-select cv_detection_components --cmake-args -DCMAKE_BUILD_TYPE=Release  # Release模式优化CV性能
source install/setup.bash
(2)启动组件容器

ROS2提供两种默认容器:

  • component_container:单线程执行器(适合轻量CV逻辑);
  • component_container_mt:多线程执行器(适合耗时CV算法,如YOLO推理)。

启动多线程容器(CV场景首选):

bash 复制代码
# 启动容器并自定义节点名
ros2 run rclcpp_components component_container_mt --ros-args -r __node:=cv_component_container
(3)动态加载组件

在新终端执行加载命令(指定容器名、组件类名、组件库路径):

bash 复制代码
# 加载生命周期组件
ros2 component load /cv_component_container cv_detection_components YoloDetectionComponent --ros-args -p confidence_threshold:=0.6 -p yolo_model_path:=/home/user/models/yolov8n.onnx

# 查看已加载的组件
ros2 component list /cv_component_container
# 输出:/yolo_detection_node (cv_detection_components::YoloDetectionComponent) [Unconfigured]
(4)控制组件生命周期(CV场景核心操作)

通过ros2 lifecycle命令切换状态:

bash 复制代码
# 1. 配置组件(加载YOLO模型)
ros2 lifecycle set /yolo_detection_node configure
# 2. 激活组件(开始处理图像)
ros2 lifecycle set /yolo_detection_node activate
# 3. 查看组件状态
ros2 lifecycle get /yolo_detection_node  # 输出:active
# 4. 停用组件(暂停处理)
ros2 lifecycle set /yolo_detection_node deactivate
# 5. 卸载组件
ros2 component unload /cv_component_container 1  # 1是组件ID(从list中查)

四、组件高级特性

4.1 组件命名空间与重映射

机器人多传感器场景中(如左/右相机),需通过命名空间隔离组件:

bash 复制代码
# 加载组件时指定命名空间
ros2 component load /cv_component_container cv_detection_components YoloDetectionComponent -n /left_camera/detection --ros-args -p confidence_threshold:=0.5

# 重映射图像订阅话题(适配不同相机话题名)
ros2 component load /cv_component_container cv_detection_components YoloDetectionComponent --ros-args -r camera/image_raw:=/right_camera/image_raw

4.2 组件参数动态调整

CV算法常需在线调整参数(如置信度阈值),组件支持动态参数更新:

bash 复制代码
# 设置动态参数
ros2 param set /yolo_detection_node confidence_threshold 0.7
# 获取参数
ros2 param get /yolo_detection_node confidence_threshold

代码中监听参数变化(CV场景优化):

cpp 复制代码
// 在on_configure中添加参数回调
param_callback_handle_ = this->add_on_set_parameters_callback(
    std::bind(&YoloDetectionComponent::on_parameter_change, this, std::placeholders::_1)
);

// 参数变化回调函数
rcl_interfaces::msg::SetParametersResult on_parameter_change(const std::vector<rclcpp::Parameter> &params) {
    rcl_interfaces::msg::SetParametersResult result;
    result.successful = true;
    for (const auto &param : params) {
        if (param.get_name() == "confidence_threshold") {
            confidence_threshold_ = param.as_double();
            RCLCPP_INFO(this->get_logger(), "Updated confidence threshold to: %.2f", confidence_threshold_);
        }
    }
    return result;
}

4.3 组件间进程内通信优化

同一容器内的组件通信无需通过DDS,可直接使用IntraProcessComm(进程内通信),CV图像传输效率提升10倍以上:

cpp 复制代码
// 创建订阅/发布器时启用进程内通信
image_sub_ = this->create_subscription<sensor_msgs::msg::Image>(
    "camera/image_raw", 
    rclcpp::QoS(10).intra_process_shared(),  // 启用进程内共享
    std::bind(&YoloDetectionComponent::image_callback, this, std::placeholders::_1)
);

detection_pub_ = this->create_lifecycle_publisher<std_msgs::msg::String>(
    "detection_result", 
    rclcpp::QoS(10).intra_process_shared()
);

4.4 自定义组件容器

默认容器满足大部分场景,但机器人CV开发中常需自定义容器(如添加全局日志、内存监控):

cpp 复制代码
#include <rclcpp_components/component_container.hpp>

// 自定义容器类
class CvComponentContainer : public rclcpp_components::ComponentContainer {
public:
    explicit CvComponentContainer(const rclcpp::NodeOptions &options)
        : ComponentContainer("cv_custom_container", options) {
        // 添加CV全局监控逻辑(如内存使用、模型加载状态)
        RCLCPP_INFO(this->get_logger(), "Custom CV component container started");
    }
};

// 注册自定义容器为可执行文件
int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<CvComponentContainer>(rclcpp::NodeOptions());
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

CMakeLists.txt中添加可执行文件编译:

cmake 复制代码
add_executable(cv_custom_container src/cv_container.cpp)
ament_target_dependencies(cv_custom_container rclcpp rclcpp_components)
install(TARGETS cv_custom_container DESTINATION lib/${PROJECT_NAME})

运行自定义容器:

bash 复制代码
ros2 run cv_detection_components cv_custom_container

4.5 组件的异常处理与容错

CV算法易因图像异常、模型错误崩溃,需在组件中添加容错逻辑:

  • 在图像回调中捕获所有异常,避免单个组件崩溃导致整个容器挂掉;
  • 使用rclcpp::GuardCondition处理紧急停止信号;
  • 生命周期组件的on_shutdown中确保释放所有CV资源(如GPU内存)。

五、CV/机器人场景性能优化策略

5.1 进程内vs进程间通信对比

通信方式 延迟(1080p图像) 内存占用 适用场景
进程间(DDS) ~5ms/帧 高(多份图像拷贝) 跨机器人/跨主机通信
进程内(IntraProcess) ~0.1ms/帧 低(共享内存) 单机器人内CV组件通信

5.2 线程模型选择

  • 单线程执行器:适合轻量CV逻辑(如图像缩放),无线程安全问题;
  • 多线程执行器:适合耗时CV算法(如YOLO推理、SLAM),建议线程数=CPU核心数-1(预留1核给系统);
  • 自定义执行器:CV算法使用GPU时,可将CPU回调与GPU推理拆分到不同线程。

5.3 组件拆分原则

  • 按"数据流向"拆分:图像采集组件 → 预处理组件 → 检测组件 → 结果发布组件;
  • 按"资源占用"拆分:GPU密集型组件(如推理)与CPU密集型组件(如数据解析)分开;
  • 避免"大组件":单个组件代码不超过2000行,便于维护和复用。

5.4 CV内存优化

  • 复用OpenCV矩阵(cv::Mat),避免频繁创建/销毁;
  • 使用cv::UMat利用OpenCL加速,减少CPU-GPU数据拷贝;
  • 组件卸载时显式释放GPU内存(如cudaFree),避免内存泄漏。

六、常见问题与排错方案(CV开发高频踩坑)

6.1 组件加载失败:"Component not found"

  • 原因:组件未安装到install/lib目录,或类名拼写错误;
  • 解决方案:
    1. 检查CMakeLists.txt中的install(TARGETS)是否正确;
    2. 确认RCLCPP_COMPONENTS_REGISTER_NODE的类名与加载命令一致;
    3. 执行ls install/cv_detection_components/lib/查看.so文件是否存在。

6.2 CVBridge转换失败:"Unsupported encoding"

  • 原因:图像消息的编码格式与CVBridge转换格式不匹配;
  • 解决方案:
    1. 查看图像消息编码:ros2 topic echo /camera/image_raw --noarr
    2. 转换时指定正确编码(如MONO8/BGR8/RGB8);
    3. 增加编码容错逻辑:cv_bridge::toCvCopy(msg, cv_bridge::getCvType(msg->encoding))

6.3 生命周期组件激活后无输出

  • 原因:发布器未激活,或订阅话题名不匹配;
  • 解决方案:
    1. 检查detection_pub_->on_activate()是否调用;
    2. ros2 topic list确认话题存在;
    3. ros2 topic echo查看是否有消息发布。

6.4 GPU内存泄漏

  • 原因:CV算法使用GPU后未释放内存,组件卸载时仅释放CPU资源;
  • 解决方案:
    1. on_cleanup中显式释放GPU资源;
    2. 使用nvidia-smi监控GPU内存,定位泄漏组件;
    3. 避免在回调函数中频繁创建GPU对象。

每天都是一个小小的生命周期;每一次醒来、每一次起床都是一次小小的诞生。 ---阿图尔·叔本华

相关推荐
野犬寒鸦1 小时前
WebSocket协同编辑:高性能Disruptor架构揭秘及项目中的实战应用
java·开发语言·数据库·redis·后端
藦卡机器人2 小时前
国产包装机器人品牌推荐
大数据·人工智能·机器人
阿猿收手吧!2 小时前
【C++】Ranges 工厂视图与投影机制
开发语言·c++
.小墨迹2 小时前
局部规划中的TEB,DWA,EGOplanner等算法在自动驾驶中应用?
开发语言·c++·人工智能·学习·算法·机器学习·自动驾驶
哈基咩2 小时前
从零搭建校园活动平台:go-zero 微服务实战完整指南
开发语言·微服务·golang
前端程序猿i2 小时前
第 3 篇:消息气泡组件 —— 远比你想的复杂
开发语言·前端·javascript·vue.js
一晌小贪欢2 小时前
Python在物联网(IoT)中的应用:从边缘计算到云端数据处理
开发语言·人工智能·python·物联网·边缘计算
你的冰西瓜2 小时前
C++中的priority_queue容器详解
开发语言·c++·stl
H Corey2 小时前
Java字符串操作全解析
java·开发语言·学习·intellij-idea