• AimRT从入门到精通 - 04RPC客户端和服务器

AimRT从入门到精通 - 04RPC客户端和服务器

2025-05-06 21:00:07 0 阅读

一、ROS中的service通信机制

        服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即:一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人... 此时需要拍摄照片并留存。

在上述场景中,就使用到了服务通信。

  • 数据分析节点A需要向相机相关节点B发送图片存储请求,节点B处理请求,并返回处理结果。

与上述应用类似的,服务通信更适用于对实时性有要求、具有一定逻辑处理的应用场景。

概念

        服务通信是以请求响应的方式实现不同节点之间数据传输的通信模式。发送请求数据的对象称为客户端,接收请求并发送响应的对象称之为服务端,同话题通信一样,客户端和服务端也通过话题相关联,不同的是服务通信的数据传输是双向交互式的。

        服务通信中,服务端与客户端是一对多的关系,也即,同一服务话题下,存在多个客户端,每个客户端都可以向服务端发送请求。

二、 AimRT 中的RPC通讯机制

        RPC也叫远程过程调用,基于请求-回复模型,由客户端Client服务端Server组成,Module可以创建客户端句柄,发起特定的 RPC 请求,由其指定的、或由框架根据一定规则指定的服务端来接收请求并回复。其与ROS中的service通信中的服务器和客户端类似;

        与Channel类似,其在接口层和后端层也实现了解耦,分为两部分;

        在接口层主要使用Protocol支持的协议进行通信,例如protobuf / ROS2msg/srv;

        在后端支持的协议有http、ros2,开发者可以自己在配置文件中设置;

关于接口层使用的protocol,详细可以看上一篇的介绍;

这里就不再过多介绍; 

三、 基于Protobuf消息的客户端和服务器

这里关于protobuf的相关简单使用,大家可以参考我之前写的博客:

Protubuf入门 --- 01基本语法与编译使用-CSDN博客

接下来我们演示一个简单的案例:

编写.proto文件

common.proto

syntax = "proto3";

package aimrt.protocols.example;

message ExampleFoo {
  int32 code = 1;
  string data = 2;
}

message ExampleBar {
  int32 num = 1;
}

rpc.proto 

syntax = "proto3";

import "common.proto";

package aimrt.protocols.example;

message GetFooDataReq {
  string msg = 1;
}

message GetFooDataRsp {
  uint64 code = 1;
  string msg = 2;
  aimrt.protocols.example.ExampleFoo data = 3;
}


service ExampleService {
  rpc GetFooData(GetFooDataReq) returns (GetFooDataRsp);
}

其中:

  • rpc.proto:包含了公共的common.proto,所以这里我们improt;
  • package:这里会在对应的C++代码中,变成命名空间,如:aimrt::protocols::example;
  • GetFooDataReq:这里定义了一个消息的请求结构(客户端发送的数据类型);
  • GetFooDataRsp这里定义了一个消息的回复的结构(服务器回复的数据类型);
  • 接下来定义了一个服务方法:这里通过rpc架构,接收GetFooDataReq,回复GetFooDataRsp

.proto文件的编译(通信通信的框架下)

        对于正常情况下.proto文件的编译,大家可以参考我的之前写的博客;

        这里AimRT对于protobuf的编译提供了下面的编译指令,即我们可以在CMakeLists.txt进行相关文件的配置,需要注意的是,在实际使用时,需要先将其生成对应的C++代码,然后再生成C++对应的服务代码:

        add_ros2_aimrt_rpc_gencode_target_for_one_file:为单个 srv 文件生成 RPC 服务 C++ 代码,参数如下:

  • TARGET_NAME:生成的 CMake Target 名称;

  • PACKAGE_NAME:ROS2 协议 PKG 的名称;

  • PROTO_FILE:协议文件的路径;

  • GENCODE_PATH:生成的桩代码存放路径;

  • DEP_PROTO_TARGETS:依赖的协议 CMake Target;

  • OPTIONS:传递给工具的其他参数;

例如下面所示的代码例子:

# 添加对 pb 消息的代码生成
add_protobuf_gencode_target_for_proto_path(
    TARGET_NAME ${PROTO_NAME}_pb_gencode
    PROTO_PATH ${CMAKE_CURRENT_SOURCE_DIR}
    GENCODE_PATH ${CMAKE_CURRENT_BINARY_DIR})

# 添加对 RPC pb的代码生成
add_protobuf_aimrt_rpc_gencode_target_for_proto_files(
    TARGET_NAME ${PROTO_NAME}_aimrt_rpc_gencode
    PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/my_rpc.proto
    GENCODE_PATH ${CMAKE_CURRENT_BINARY_DIR}
    DEP_PROTO_TARGETS ${PROTO_NAME}_pb_gencode
)
  • 我们使用前面用到的add_protobuf_gencode_target_for_proto_path宏,将当前目录中的所有普通的 proto 消息进行代码生成,生成的目标名为:${PROTO_NAME}_pb_gencode;
  • 接着我们使用add_protobuf_aimrt_rpc_gencode_target_for_proto_files宏,将指定的 RPC proto消息进行生成,生成的target为${PROTO_NAME}_aimrt_rpc_gencode;
  • 在PROTO_FILES参数指明当前包含rpc的proto的文件名
  • GENCODE_PATH指明当前生成代码的路径,其中 AimRT 会自动将这个路径添加到库的头文件搜索路径;
  • 因为我们在RPC PB 中使用到了common.proto,所以需要给其添加普通pb消息的依DEP_PROTO_TARGETS ${PROTO_NAME}_pb_gencode

服务器模块

其中,服务器module包含下面三个模块,这里我们依次对其进行讲解:

logger.h/logger.cc

#pragma once

#include "aimrt_module_cpp_interface/logger/logger.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_server_module {

void SetLogger(aimrt::logger::LoggerRef);
aimrt::logger::LoggerRef GetLogger();

}

这里logger.h主要为服务器提供日志的设置,一共提供了两个函数:

  • 通过SetLogger: 这里我们传入对应的日志句柄,可以将其内部的管理的模块的日志句柄进行赋值;
  • 通过GetLogger:这里我们可以获取到我们之前设置的日志句柄;

service.h

#pragma once

#include "rpc.aimrt_rpc.pb.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_server_module {

class ExampleServiceSyncServiceImpl : public aimrt::protocols::example::ExampleServiceSyncService {
 public:
  ExampleServiceSyncServiceImpl() = default;
  ~ExampleServiceSyncServiceImpl() override = default;

  aimrt::rpc::Status GetFooData(
      aimrt::rpc::ContextRef ctx,
      const ::aimrt::protocols::example::GetFooDataReq& req,
      ::aimrt::protocols::example::GetFooDataRsp& rsp) override;

}

这里我们需要注意的是:

        AimRT 将每个 Service 的桩代码封装为一个基类,在程序中,我们只需要继承该基类,并重写基类中的 RPC 方法即可。例如在 pb 中,定义的 RPC 服务名为MyService,因此 AimRT 为我们生成以下桩代码类:

  • 异步服务端桩代码类名为: [服务名]CoServic;
  • 同步服务端桩代码类名为:[服务名]SyncService;

例如上面的,这里在看我们定义的.proto文件:

这里我们声明了下面的命名空间:

package aimrt.protocols.example;

所以生成的C++源文件中会生成对应的命名空间:aimrt::protocols::example这个命名空间!

除此之外,主要有的是我们定义了下面这个service:

service ExampleService {
  rpc GetFooData(GetFooDataReq) returns (GetFooDataRsp);

所以对应生成的服务会在 aimrt::protocols::example:ExampleService;

而AimRT会基于aimrt::protocols::example:ExampleService开发对应的异步/同步接口!

而这个异步/同步接口都是继承ServiceBase的!

用户如果要使用异步/同步的服务,只需要对其进行继承重写即可!

如上面所示,这里我们可以看到对其是基于继承基类模块的;

class ServiceBase {
 public:
  std::string_view RpcType() const;

  void SetServiceName(std::string_view service_name);
  std::string_view ServiceName() const;

  // ...
};

class XXXService : public aimrt::rpc::ServiceBase {
  // ...
}

详细的大家可以参考下面的文档:

Rpc — AimRT v0.10.0 documentation

因此再总结一下同步型服务接口的使用:

  • 引用桩代码头文件,例如xxx.aimrt_rpc.pb.h或者xxx.aimrt_rpc.srv.h,其中有同步接口的 Service 基类XXXSyncService;
  • 开发者实现一个 Impl 类,继承XXXSyncService,并实现其中的虚接口;
  • 解析 Req,并填充 Rsp;
  • 返回Status
  • Initialize阶段调用RpcHandleRefRegisterService方法注册 RPC Service;

这是一个固定的模板!大家可以参考;

service.cc

#include "normal_rpc_sync_server_module/service.h"
#include "aimrt_module_protobuf_interface/util/protobuf_tools.h"
#include "normal_rpc_sync_server_module/global.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_server_module {

aimrt::rpc::Status ExampleServiceSyncServiceImpl::GetFooData(
    aimrt::rpc::ContextRef ctx,
    const ::aimrt::protocols::example::GetFooDataReq& req,
    ::aimrt::protocols::example::GetFooDataRsp& rsp) {
  rsp.set_msg("echo " + req.msg());

  AIMRT_INFO("Server handle new rpc call. context: {}, req: {}, return rsp: {}",
             ctx.ToString(), aimrt::Pb2CompactJson(req), aimrt::Pb2CompactJson(rsp));

  return aimrt::rpc::Status();
}


}

实际上这里定义的这个函数就是服务端用来处理数据的回调函数!

这个函数包含3个参数,其中:

  • ctx:表示向RPC的后端传递一些特定的配置信息;
  • req:表示发送的proto类型的数据;
  • rsp:表示回复相应的数据;

这里函数体内实际上就是对回复数据做处理:前面+上打印"echo";

返回值是rpc对应的状态(如果这里我们设置的有错误码,这里返回可以获取到对应的错误码,从而查询获取到对应的错误类型;) 

service_module.h

#pragma once

#include 

#include "aimrt_module_cpp_interface/module_base.h"
#include "normal_rpc_sync_server_module/service.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_server_module {

class NormalRpcSyncServerModule : public aimrt::ModuleBase {
 public:
  NormalRpcSyncServerModule() = default;
  ~NormalRpcSyncServerModule() override = default;

  ModuleInfo Info() const override {
    return ModuleInfo{.name = "NormalRpcSyncServerModule"};
  }

  bool Initialize(aimrt::CoreRef core) override;

  bool Start() override;

  void Shutdown() override;

 private:
  aimrt::CoreRef core_;
  std::shared_ptr service_ptr_;

  std::string service_name_;
};

}

整体框架和之前的一致,这里采用共享指针的方式声明一个服务端;

service_module.cc

这里我们只对主模块进行讲解:

#include "normal_rpc_sync_server_module/normal_rpc_sync_server_module.h"
#include "normal_rpc_sync_server_module/global.h"

#include "yaml-cpp/yaml.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_server_module {

bool NormalRpcSyncServerModule::Initialize(aimrt::CoreRef core) {
  core_ = core;

  SetLogger(core_.GetLogger());

  try {
    // Read cfg
    std::string file_path = std::string(core_.GetConfigurator().GetConfigFilePath());
    if (!file_path.empty()) {
      YAML::Node cfg_node = YAML::LoadFile(file_path);
      if (cfg_node["service_name"]) {
        service_name_ = cfg_node["service_name"].as();
      }
    }

    // Create service
    service_ptr_ = std::make_shared();

    // Register service
    bool ret = false;
    if (service_name_.empty()) {
      ret = core_.GetRpcHandle().RegisterService(service_ptr_.get());
    } else {
      ret = core_.GetRpcHandle().RegisterService(service_name_, service_ptr_.get());
    }

    AIMRT_CHECK_ERROR_THROW(ret, "Register service failed.");

    AIMRT_INFO("Register service succeeded.");

  } catch (const std::exception& e) {
    AIMRT_ERROR("Init failed, {}", e.what());
    return false;
  }

  AIMRT_INFO("Init succeeded.");

  return true;
}

bool NormalRpcSyncServerModule::Start() { return true; }

void NormalRpcSyncServerModule::Shutdown() {}

}

上面的代码以同步通信为例子:

在初始化部分:

  1. 首先通过SetLogger设置全局的日志;
  2. 然后读取配置文件;
  3. 通过共享指针管理服务器模块;
  4. 接下来注册服务器的接口;
    // Register service
    bool ret = false;
    if (service_name_.empty()) {
      ret = core_.GetRpcHandle().RegisterService(service_ptr_.get());
    } else {
      ret = core_.GetRpcHandle().RegisterService(service_name_, service_ptr_.get());
    }

这里注册服务器的逻辑是:

  • 如果我们没有服务器的名字,那么此时就按照默认的名称进行注册;
  • 如果我们设置了对应的名字,此时就按照自定义的名字进行注册;

接下来分别开启开始模块和结束模块;

客户端模块

client_module.h

#pragma once

#include 
#include 
#include 

#include "aimrt_module_cpp_interface/module_base.h"

#include "rpc.aimrt_rpc.pb.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_client_module {

class NormalRpcSyncClientModule : public aimrt::ModuleBase {
 public:
  NormalRpcSyncClientModule() = default;
  ~NormalRpcSyncClientModule() override = default;

  ModuleInfo Info() const override {
    return ModuleInfo{.name = "NormalRpcSyncClientModule"};
  }

  bool Initialize(aimrt::CoreRef core) override;

  bool Start() override;

  void Shutdown() override;

 private:
  auto GetLogger() { return core_.GetLogger(); }

  void MainLoop();

 private:
  aimrt::CoreRef core_;
  aimrt::executor::ExecutorRef executor_;

  std::atomic_bool run_flag_ = false;
  std::promise stop_sig_;

  double rpc_frq_ = 1.0;
  std::string service_name_;

  std::shared_ptr proxy_;
};

}

这里的客户端模块的框架实际上与发布者publisher的框架一样;

其实这里的service的主要原因是channel与rpc的通信机制不一样,因为channel收到消息后不用再返回给客户端,而service这里收到消息后需要对消息做处理,然后再返回给client,所以就会麻烦很多,因此发送消息后客户端是异步还是同步等?考虑的问题更多,就显得更加复杂;

接下来我们依次对上面的代码模块进行分析:

  • 主要的模块(初始化、开始和结束)与我们之前进行的模块一样;
  •  除此之外,还有一个executor,这里是用于发送者将任务投递到执行器,然后执行器可以发送对应的任务;
  • run_flag:用来标识模块是否在运行;
  • stop_sig_n:当发送任务循环结束时,此时会给发送对应的信号给关闭模块,shutdown收到后此时就对模块进行关闭;
  • 除此之外,这里还定义了话题名、发布者和发布的频率;

client_module.cc

#include "normal_rpc_sync_client_module/normal_rpc_sync_client_module.h"
#include "aimrt_module_protobuf_interface/util/protobuf_tools.h"

#include "yaml-cpp/yaml.h"

namespace aimrt::examples::cpp::pb_rpc::normal_rpc_sync_client_module {

bool NormalRpcSyncClientModule::Initialize(aimrt::CoreRef core) {
  core_ = core;

  try {
    // Read cfg
    std::string file_path = std::string(core_.GetConfigurator().GetConfigFilePath());
    if (!file_path.empty()) {
      YAML::Node cfg_node = YAML::LoadFile(file_path);
      rpc_frq_ = cfg_node["rpc_frq"].as();

      if (cfg_node["service_name"]) {
        service_name_ = cfg_node["service_name"].as();
      }
    }

    // Get executor handle
    executor_ = core_.GetExecutorManager().GetExecutor("work_thread_pool");
    AIMRT_CHECK_ERROR_THROW(executor_, "Get executor 'work_thread_pool' failed.");

    // Get rpc handle
    auto rpc_handle = core_.GetRpcHandle();
    AIMRT_CHECK_ERROR_THROW(rpc_handle, "Get rpc handle failed.");

    // Register rpc client
    bool ret = false;
    if (service_name_.empty()) {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle);
    } else {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle, service_name_);
    }
    AIMRT_CHECK_ERROR_THROW(ret, "Register client failed.");

    // Create rpc proxy
    proxy_ = std::make_shared(rpc_handle);

    if (!service_name_.empty()) {
      proxy_->SetServiceName(service_name_);
    }

  } catch (const std::exception& e) {
    AIMRT_ERROR("Init failed, {}", e.what());
    return false;
  }

  AIMRT_INFO("Init succeeded.");

  return true;
}

bool NormalRpcSyncClientModule::Start() {
  try {
    run_flag_ = true;
    executor_.Execute(std::bind(&NormalRpcSyncClientModule::MainLoop, this));
  } catch (const std::exception& e) {
    AIMRT_ERROR("Start failed, {}", e.what());
    return false;
  }

  AIMRT_INFO("Start succeeded.");
  return true;
}

void NormalRpcSyncClientModule::Shutdown() {
  try {
    if (run_flag_) {
      run_flag_ = false;
      stop_sig_.get_future().wait();
    }
  } catch (const std::exception& e) {
    AIMRT_ERROR("Shutdown failed, {}", e.what());
    return;
  }

  AIMRT_INFO("Shutdown succeeded.");
}

// Main loop
void NormalRpcSyncClientModule::MainLoop() {
  try {
    AIMRT_INFO("Start MainLoop.");

    uint32_t count = 0;
    while (run_flag_) {
      // Sleep
      std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(1000 / rpc_frq_)));

      count++;
      AIMRT_INFO("Loop count : {} -------------------------", count);

      // Create req and rsp
      aimrt::protocols::example::GetFooDataReq req;
      aimrt::protocols::example::GetFooDataRsp rsp;
      req.set_msg("hello world foo, count " + std::to_string(count));

      // Create ctx
      auto ctx_ptr = proxy_->NewContextSharedPtr();
      ctx_ptr->SetTimeout(std::chrono::seconds(3));

      AIMRT_INFO("Client start new rpc call. req: {}", aimrt::Pb2CompactJson(req));

      // Call rpc
      auto status = proxy_->GetFooData(ctx_ptr, req, rsp);

      // Check result
      if (status.OK()) {
        AIMRT_INFO("Client get rpc ret, status: {}, rsp: {}", status.ToString(),
                   aimrt::Pb2CompactJson(rsp));
      } else {
        AIMRT_WARN("Client get rpc error ret, status: {}", status.ToString());
      }
    }

    AIMRT_INFO("Exit MainLoop.");
  } catch (const std::exception& e) {
    AIMRT_ERROR("Exit MainLoop with exception, {}", e.what());
  }

  stop_sig_.set_value();
}

}

接下来我们对上面的模块进行挨个分析:

#include "normal_rpc_sync_client_module/normal_rpc_sync_client_module.h"
#include "aimrt_module_protobuf_interface/util/protobuf_tools.h"

#include "yaml-cpp/yaml.h"

而当我们需要将二进制的protobuf文件转化为其他可读类型的数据时,此时就需要包含下面这个头文件:

#include "aimrt_module_protobuf_interface/util/protobuf_tools.h"

其他的头文件在 #include"normal_rpc_sync_client_module/normal_rpc_sync_client_module.h"包含的都有;

接下来我们再看初始化模块

bool NormalRpcSyncClientModule::Initialize(aimrt::CoreRef core) {
  core_ = core;

  try {
    // Read cfg
    std::string file_path = std::string(core_.GetConfigurator().GetConfigFilePath());
    if (!file_path.empty()) {
      YAML::Node cfg_node = YAML::LoadFile(file_path);
      rpc_frq_ = cfg_node["rpc_frq"].as();

      if (cfg_node["service_name"]) {
        service_name_ = cfg_node["service_name"].as();
      }
    }

    // Get executor handle
    executor_ = core_.GetExecutorManager().GetExecutor("work_thread_pool");
    AIMRT_CHECK_ERROR_THROW(executor_, "Get executor 'work_thread_pool' failed.");

    // Get rpc handle
    auto rpc_handle = core_.GetRpcHandle();
    AIMRT_CHECK_ERROR_THROW(rpc_handle, "Get rpc handle failed.");

    // Register rpc client
    bool ret = false;
    if (service_name_.empty()) {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle);
    } else {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle, service_name_);
    }
    AIMRT_CHECK_ERROR_THROW(ret, "Register client failed.");

    // Create rpc proxy
    proxy_ = std::make_shared(rpc_handle);

    if (!service_name_.empty()) {
      proxy_->SetServiceName(service_name_);
    }

  } catch (const std::exception& e) {
    AIMRT_ERROR("Init failed, {}", e.what());
    return false;
  }

  AIMRT_INFO("Init succeeded.");

  return true;
}

在初始化模块中,依次进行:

  • 读取配置文件中的内容,且如果配置文件中设置了话题名,我们就提取里面的话题名;
  • 获取我们对应的执行器的句柄;
  • 获得rpc通信的句柄;
  • 注册rcp客户端;
    // Register rpc client
    bool ret = false;
    if (service_name_.empty()) {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle);
    } else {
      ret = aimrt::protocols::example::RegisterExampleServiceClientFunc(rpc_handle, service_name_);
    }

        这里注册的逻辑依然是:如果我们没有设置服务话题名,就按照默认的名字进行注册;否则按照我们自定义的名字进行注册; 

除此之外,这里最主要的是创建了一个rpc的proxy;

问题:客户端当中我们如何传递信息给服务端?

在AimRT中,提供了四种类型的接口:

  • 同步型接口:名称一般为XXXSyncProxy;
  • 异步回调型接口:名称一般为XXXAsyncProxy;
  • 异步 Future 型接口:名称一般为XXXFutureProxy;
  • 无栈协程型接口:名称一般为XXXCoProxy;

其中,在头文件中,我们已经定义了一个共享指针来管理同步型的proxy接口:

std::shared_ptr proxy_;

这个接口会继承基类的一些共享接口:

class ProxyBase {
 public:
  std::string_view RpcType() const;

  void SetServiceName(std::string_view service_name);
  std::string_view ServiceName() const;

  std::shared_ptr NewContextSharedPtr(ContextRef ctx_ref = ContextRef()) const;

  void SetDefaultContextSharedPtr(const std::shared_ptr& ctx_ptr);
  std::shared_ptr GetDefaultContextSharedPtr() const;
};

class XXXProxy : public aimrt::rpc::CoProxyBase {
 public:
  explicit XXXProxy(aimrt::rpc::RpcHandleRef rpc_handle_ref);

  static bool RegisterClientFunc(aimrt::rpc::RpcHandleRef rpc_handle_ref);
  static bool RegisterClientFunc(aimrt::rpc::RpcHandleRef rpc_handle_ref, std::string_view service_name);

  // ...
}

详细的讲解大姐可以参考下面的链接:

Rpc — AimRT v0.10.0 documentation

接下来再回到我们的初始化模块关于proxy的部分:

  • 这里是相当于通过传入我们的rpc句柄,将proxy进行实例化;
  • 然后如果我们这里定义了话题名就设置话题名,相当于进行了一些预备操作;

开始start模块:

bool NormalRpcSyncClientModule::Start() {
  try {
    run_flag_ = true;
    executor_.Execute(std::bind(&NormalRpcSyncClientModule::MainLoop, this));
  } catch (const std::exception& e) {
    AIMRT_ERROR("Start failed, {}", e.what());
    return false;
  }

  AIMRT_INFO("Start succeeded.");
  return true;
}

开始模块的逻辑很简单:

  • 这里还是将我们对应的运行状态改为true;
  • 然后让执行器执行对应的MainLoop操作;(在MainLoop里面会进行一些数据的处理);

发送消息MainLoop模块:

// Main loop
void NormalRpcSyncClientModule::MainLoop() {
  try {
    AIMRT_INFO("Start MainLoop.");

    uint32_t count = 0;
    while (run_flag_) {
      // Sleep
      std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(1000 / rpc_frq_)));

      count++;
      AIMRT_INFO("Loop count : {} -------------------------", count);

      // Create req and rsp
      aimrt::protocols::example::GetFooDataReq req;
      aimrt::protocols::example::GetFooDataRsp rsp;
      req.set_msg("hello world foo, count " + std::to_string(count));

      // Create ctx
      auto ctx_ptr = proxy_->NewContextSharedPtr();
      ctx_ptr->SetTimeout(std::chrono::seconds(3));

      AIMRT_INFO("Client start new rpc call. req: {}", aimrt::Pb2CompactJson(req));

      // Call rpc
      auto status = proxy_->GetFooData(ctx_ptr, req, rsp);

      // Check result
      if (status.OK()) {
        AIMRT_INFO("Client get rpc ret, status: {}, rsp: {}", status.ToString(),
                   aimrt::Pb2CompactJson(rsp));
      } else {
        AIMRT_WARN("Client get rpc error ret, status: {}", status.ToString());
      }
    }

    AIMRT_INFO("Exit MainLoop.");
  } catch (const std::exception& e) {
    AIMRT_ERROR("Exit MainLoop with exception, {}", e.what());
  }

  stop_sig_.set_value();
}

接下来我们挨个分析上面的代码逻辑结构:

  • 首先,如果run_flag为true,说明代码module此时框架是start,所以我们需要发送任务;
  • 接下来,设置任务的执行频率;
  • 分别创建rep和rsp用来储存:发送的消息和接受的消息;
  • 接下来我们设置消息内容:
req.set_msg("hello world foo, count " + std::to_string(count));
  •  接下来我们创建对应的后端配置:
      auto ctx_ptr = proxy_->NewContextSharedPtr();
      ctx_ptr->SetTimeout(std::chrono::seconds(3));

      AIMRT_INFO("Client start new rpc call. req: {}", aimrt::Pb2CompactJson(req));
  • 在proxy的基类中,提供了管理context后端配置的共享指针;
  • 设置 RPC 调用的超时时间,如果超市超过3s,此时框架会在超时后自动取消等待响应;

接下来这里我们就可以通过proxy调用对应的回调函数进行信息的发送:

      // Call rpc
      auto status = proxy_->GetFooData(ctx_ptr, req, rsp);
  • 如果返回的start.OK()为true,说明此时获取rpc的过程中没有出现错误!所以接下来打印正常info信息;
  • 否则就打印warm警告信息; 
  • 服务器回复客户端的消息会保存到req当中;
stop_sig_.set_value();

最后,当循环结束后,此时说明不会再进行消息的发送,因此向shutdown发送对应的信号; 

结束shutdown模块:

void NormalRpcSyncClientModule::Shutdown() {
  try {
    if (run_flag_) {
      run_flag_ = false;
      stop_sig_.get_future().wait();
    }
  } catch (const std::exception& e) {
    AIMRT_ERROR("Shutdown failed, {}", e.what());
    return;
  }

  AIMRT_INFO("Shutdown succeeded.");
}

当信号发送后,此时shutdown会收到对应的信号,然后成功关闭!

CMake链接相关库 

在上面我们已经进行了.proto文件的相关编译,因此,此时如果我们想要在自己的源文件中进行链接,很简单,例如下面的例子(对应上面我们编译的.proto文件):

target_link_libraries(my_lib PUBLIC example_rpc_aimrt_rpc_gencode)

此时即可成功链接对应的库;

四、 基于ROS2 srv消息的客户端和服务器

编写.msg文件

byte[]  data
---
int64   code

        其中,以---来分割 Req 和 Rsp 的定义。然后直接通过 ROS2 提供的 CMake 方法rosidl_generate_interfaces,为 Req 和 Rsp 消息生成 C++ 代码和 CMake Target,例如:

rosidl_generate_interfaces(
  example_srv_gencode
  "srv/example.srv"
)

之后就可以引用相关的 CMake Target 来使用生成的 Req 和 Rsp 的消息结构 C++ 代码;

但是实际上,经过上面的操作,我们对于生成的消息还是不能直接进行调用;

在生成了 Req 和 Rsp 消息结构的 C++ 代码后,我们还需要使用 AimRT 提供的 Python 脚本工具,生成服务定义部分的 C++ 桩代码,例如:

python3 ARGS ./ros2_py_gen_aimrt_cpp_rpc.py --pkg_name=example_pkg --srv_file=./example.srv --output_path=./

此时即会生成对应的example.aimrt_rpc.srv.hexample.aimrt_rpc.srv.cc文件;

但是实际上,不需要这么麻烦的自己进行编译,AimRT内部也给我们提供了内置的CMake编译指令:

  • add_ros2_aimrt_rpc_gencode_target_for_one_file:为单个 srv 文件生成 RPC 服务 C++ 代码,参数如下:

    • TARGET_NAME:生成的 CMake Target 名称;

    • PACKAGE_NAME:ROS2 协议 PKG 的名称;

    • PROTO_FILE:协议文件的路径;

    • GENCODE_PATH:生成的桩代码存放路径;

    • DEP_PROTO_TARGETS:依赖的协议 CMake Target;

    • OPTIONS:传递给工具的其他参数;

例如下面这个例子:

# Generate C++ code for Req and Rsp message in `.srv` file
rosidl_generate_interfaces(
  example_srv_gencode
  "srv/example.srv"
)

# Generate RPC service C++ code for the example '.srv' file. It is necessary to rely on the CMake Target related to ROS2 messages, which is defined in '${ROS2_EXAMPLE_CMAKE_TARGETS}'
add_ros2_aimrt_rpc_gencode_target_for_one_file(
  TARGET_NAME example_ros2_rpc_aimrt_rpc_gencode
  PACKAGE_NAME example_pkg
  PROTO_FILE ${CMAKE_CURRENT_SOURCE_DIR}/srv/example.srv
  GENCODE_PATH ${CMAKE_CURRENT_BINARY_DIR}
  DEP_PROTO_TARGETS
    rclcpp::rclcpp
    ${ROS2_EXAMPLE_CMAKE_TARGETS})
  • rosidl_generate_interfaces:负责将消息结构体部分生成对应的C++代码;
  • add_ros2_aimrt_rpc_gencode_target_for_one_file:再将对应的C++代码生成C++的服务代码;
  • 二者缺一不可;

        其他的关于服务器和客户端的时候,与protobuf完全相同,不同的是将对应的消息类型改为ROS即可,因此这里我们不再进行过多的解释;

        截止到这里我们对AimRT的框架已经明白了不少,接下来有机会会发布关于AImRT中的rpc后端的相关介绍;

本文地址:https://www.vps345.com/8735.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 ssh javascript 前端 chrome edge 进程 操作系统 进程控制 Ubuntu 英语 nginx 负载均衡 开发环境 笔记 llama 算法 opencv 自然语言处理 神经网络 语言模型 阿里云 网络 网络安全 网络协议 ubuntu deepseek Ollama 模型联网 API CherryStudio php 人工智能 深度学习 计算机视觉 卷积神经网络 harmonyos 华为 开发语言 typescript 计算机网络 macos windows conda 经验分享 python MCP 数据库 centos oracle 关系型 安全 分布式 docker java 容器 数据分析 tomcat Dify vscode asm jellyfin nas openvpn server openvpn配置教程 centos安装openvpn C# MQTTS 双向认证 emqx tcp/ip react.js 前端面试题 node.js 持续部署 c++ 并查集 leetcode jenkins gitee spring boot mysql 学习方法 学习 RTSP xop RTP RTSPServer 推流 视频 adb android 科技 ai 个人开发 debian PVE 高级IO epoll 环境变量 fstab 机器学习 ip协议 大数据 spark hive json 进程优先级 调度队列 进程切换 智能路由器 外网访问 内网穿透 端口映射 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 Qwen2.5-coder 离线部署 matplotlib fonts-noto-cjk 架构 云原生 etcd 数据安全 RBAC redis pip dubbo 小程序 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 ui 华为云 华为od HarmonyOS Next numpy Alexnet qt QT 5.12.12 QT开发环境 Ubuntu18.04 DeepSeek-R1 API接口 eureka 串口服务器 YOLO 鸿蒙 cron crontab日志 maxkb ARG 程序员创富 nlp EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 思科 vue3 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 嵌入式 linux驱动开发 arm开发 嵌入式硬件 物联网 mcu iot 信息与通信 DNS 运维开发 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 Linux 进程信号 windows 服务器安装 github 创意 社区 媒体 websocket c# 单片机 protobuf 序列化和反序列化 安装 MacOS录屏软件 后端 集成学习 集成测试 面试 性能优化 jdk intellij-idea Flask FastAPI Waitress Gunicorn uWSGI Uvicorn mac 游戏程序 ios k8s kubernetes 自动化 spring WebUI DeepSeek V3 DeepSeek AI大模型 pycharm 虚拟机 VMware ue5 vr ide 互信 ip命令 新增网卡 新增IP 启动网卡 多线程 kylin pytorch transformer VMware安装mocOS macOS系统安装 Dell R750XS 产品经理 agi microsoft vim vnc Docker Hub docker pull 镜像源 daemon.json rust http xcode dify rabbitmq 计算机外设 电脑 软件需求 efficientVIT YOLOv8替换主干网络 TOLOv8 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK uni-app 网络药理学 生信 生物信息学 gromacs 分子动力学模拟 MD 动力学模拟 llm prometheus 监控k8s集群 集群内prometheus git elasticsearch 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 gcc centos 7 vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 数据结构 c语言 链表 burp suite 抓包 DevEco Studio powerpoint gnu sublime text 编辑器 ssl spring cloud kafka 大模型技术 本地部署大模型 fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse 前端框架 目标检测 java-ee ollama 私有化 本地部署 聚类 unix 开源 实时音视频 实时互动 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 ip 深度求索 私域 知识库 threejs 3D apache tcpdump 系统架构 微服务 设计模式 软件工程 golang rocketmq AIGC C 进程地址空间 chatgpt 监控 自动化运维 gateway Clion Nova ResharperC++引擎 Centos7 远程开发 机器人 AI编程 visual studio code 大模型 AI Agent 大数据平台 主从复制 pillow 交互 腾讯云大模型知识引擎 Deepseek html5 firefox yum docker-compose docker compose sqlserver kamailio sip VoIP 报错 https rust腐蚀 docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 iperf3 带宽测试 WSL win11 无法解析服务器的名称或地址 alias unalias 别名 云原生开发 K8S k8s管理系统 代码调试 ipdb JAVA Java 僵尸进程 开发 list docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos svn gitlab VMware安装Ubuntu Ubuntu安装k8s Reactor C++ bash 课程设计 虚拟现实 ux 视觉检测 程序人生 YOLOv12 VMware创建虚拟机 Hive环境搭建 hive3环境 Hive远程模式 gpu算力 Windsurf ubuntu20.04 ros ros1 Noetic 20.04 apt 安装 .net flash-attention milvus 键盘 web安全 firewalld 读写锁 中间件 iis 多线程服务器 Linux网络编程 远程工作 FTP 服务器 远程控制 远程看看 远程协助 腾讯云 jar gradle burpsuite 安全工具 mac安全工具 burp安装教程 渗透工具 Ubuntu Server Ubuntu 22.04.5 live555 rtsp rtp 目标跟踪 OpenVINO 推理应用 KylinV10 麒麟操作系统 Vmware perf linux内核 maven rag ragflow ragflow 源码启动 硬件工程 1024程序员节 ESP32 camera Arduino 电子信息 Hyper-V WinRM TrustedHosts Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer virtualenv ue4 着色器 虚幻 mongodb DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 iBMC UltraISO Kali Linux 黑客 渗透测试 信息收集 postgresql cuda cudnn anaconda springcloud 安卓模拟器 udp 本地环回 bind 域名服务 DHCP 符号链接 配置 网易邮箱大师 OpenManus iftop 网络流量监控 mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 压测 ECS grafana CLion 远程连接 IDE Deepseek-R1 私有化部署 推理模型 指令 源码剖析 rtsp实现步骤 流媒体开发 文件系统 路径解析 Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 ollama下载加速 webstorm 区块链 设置代理 实用教程 cpu 内存 实时 使用 go 代理模式 权限 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 C语言 缓存 ipython 强制清理 强制删除 mac废纸篓 flutter 飞牛nas fnos jvm log4j Chatbox xml bcompare Beyond Compare YOLOv8 NPU Atlas800 A300I pro asi_bench minicom 串口调试工具 测试工具 fd 文件描述符 mybatis 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 统信UOS 麒麟 bonding 链路聚合 迁移指南 ffmpeg 音视频 stm32 mq linux安装配置 rpc kali 共享文件夹 ddos stm32项目 llama3 Chatglm 开源大模型 5G 3GPP 卫星通信 windows日志 unity ping++ sql zotero WebDAV 同步失败 selenium 安防软件 Ubuntu共享文件夹 共享目录 Linux共享文件夹 安卓 WSL2 Ubuntu22.04 虚拟化 开发人员主页 Linux的权限 中兴光猫 换光猫 网络桥接 自己换光猫 系统开发 binder 车载系统 framework 源码环境 Samba NAS 工业4.0 NFS 监控k8s 监控kubernetes axure 富文本编辑器 客户端 arm WLAN 温湿度数据上传到服务器 Arduino HTTP fpga开发 鸿蒙系统 dell服务器 Linux PID 爬虫 数据集 ansible playbook 剧本 file server http server web server 云桌面 微软 AD域控 证书服务器 博客 Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 AI-native gpt LLM CrewAI edge浏览器 rime html bug KingBase 基础环境 CPU 主板 电源 网卡 UOS 统信操作系统 oceanbase rc.local 开机自启 systemd 深度优先 图论 并集查找 换根法 树上倍增 智能手机 Termux 微信 微信分享 Image wxopensdk 系统安全 ssh漏洞 ssh9.9p2 CVE-2025-23419 MQTT协议 消息服务器 代码 RAGFLOW database docker run 数据卷挂载 交互模式 vmware 卡死 postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 Cline 自动化编程 .netcore openwrt tar ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 selete 隐藏文件 隐藏目录 管理器 通配符 IIS服务器 IIS性能 日志监控 ros2 moveit 机器人运动 rancher 银河麒麟服务器操作系统 系统激活 向日葵 环境配置 通信工程 毕业 wireshark 显示过滤器 ICMP Wireshark安装 网络工程师 网络管理 软考 2024 2024年上半年 下午真题 答案 linux环境变量 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 数据库系统 AI写作 低代码 数据挖掘 网络用户购物行为分析可视化平台 大数据毕业设计 命名管道 客户端与服务端通信 nftables 防火墙 linux上传下载 豆瓣 追剧助手 迅雷 express p2p 无人机 银河麒麟 kylin v10 麒麟 v10 远程 命令 执行 sshpass 操作 WSL2 上安装 Ubuntu aws googlecloud vSphere vCenter 软件定义数据中心 sddc flask ESXi LDAP 话题通信 服务通信 虚拟局域网 Cursor compose jmeter 软件测试 政务 分布式系统 监控运维 Prometheus Grafana 模拟实现 HCIE 数通 intellij idea cocoapods OpenSSH code-server MQTT mosquitto 消息队列 交叉编译 okhttp gpt-3 文心一言 SenseVoice android studio 计算机 程序员 7z LORA 大语言模型 NLP django sqlite 其他 华为认证 交换机 kernel 可信计算技术 安全架构 网络攻击模型 MS Materials Mac内存不够用怎么办 显卡驱动 openssl 密码学 智能电视 HarmonyOS OpenHarmony 真机调试 ELF加载 业界资讯 鲲鹏 压力测试 模拟退火算法 matlab EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 sdkman 编程 图形化界面 序列化反序列化 pdf jina echarts 信息可视化 网页设计 IPv4 子网掩码 公网IP 私有IP SSH 密钥生成 SSH 公钥 私钥 生成 wsl 版本 镜像下载 freebsd 企业微信 Linux24.04 deepin 图形渲染 金融 oracle fusion oracle中间件 硬件架构 seatunnel yolov8 AISphereButler shell pygame 小游戏 五子棋 Kali 渗透 硅基流动 ChatBox zabbix 华为证书 HarmonyOS认证 华为证书考试 相机 国产操作系统 ukui 麒麟kylinos openeuler composer npm 统信 虚拟机安装 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? Google pay Apple pay 框架搭建 产测工具框架 IMX6ULL 管理框架 CosyVoice ruby 游戏引擎 Linux的基础指令 线程 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 vue css less nvidia 大模型入门 大模型教程 remote-ssh TCP WebServer 混合开发 环境安装 JDK 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 微信小程序 trea idea RustDesk自建服务器 rustdesk服务器 docker rustdesk mysql离线安装 ubuntu22.04 mysql8.0 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 excel 源码 毕业设计 web3.py fork wait waitpid exit web Cookie 云服务器 VPS su sudo pyqt frp MI300x 孤岛惊魂4 WebRTC lio-sam SLAM centos-root /dev/mapper yum clean all df -h / du -sh uv 考研 恒源云 devops ci/cd onlyoffice 在线office 工具 京东云 open Euler dde vite RTMP 应用层 jupyter tcp 命令行 基础入门 db big data opensearch helm 服务器主板 AI芯片 飞书 多进程 游戏服务器 TrinityCore 魔兽世界 HiCar CarLife+ CarPlay QT RK3588 python2 ubuntu24.04 sysctl.conf vm.nr_hugepages Node-Red 编程工具 流编程 mamba adobe 传统数据库升级 银行 LLMs wps 双系统 Python 网络编程 聊天服务器 套接字 Socket webrtc 服务器扩容没有扩容成功 ubuntu24.04.1 搜索引擎 IPMITOOL BMC 硬件管理 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 opcua opcda KEPServer安装 生活 oneapi open webui 自学笔记 小米 澎湃OS Android c/c++ 串口 sqlite3 n8n 工作流 workflow 嵌入式系统开发 curl wget 图片增强 增强数据 群晖 飞牛 asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 minio 文件分享 办公自动化 自动化生成 pdf教程 VSCode 移动云 rnn 视频平台 录像 视频转发 性能测试 视频流 存储 MySql 云服务 springsecurity6 oauth2 授权服务器 token sas 安全威胁分析 fast ecmascript KVM 飞牛NAS 飞牛OS MacBook Pro visualstudio harmonyOS面试题 银河麒麟操作系统 国产化 cnn 邮件APP 免费软件 arcgis nfs 3d 数学建模 服务器部署ai模型 计算生物学 生物信息 基因组 部署 SSL 域名 rsyslog Anolis nginx安装 linux插件下载 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 redhat USB转串口 CH340 pyautogui Trae AI 原生集成开发环境 Trae AI ShenTong DBeaver 开机自启动 驱动开发 嵌入式实习 conda配置 conda镜像源 k8s二次开发 集群管理 三级等保 服务器审计日志备份 国产数据库 瀚高数据库 数据迁移 下载安装 Kylin-Server 服务器安装 ruoyi 毕设 idm EasyConnect yum源切换 更换国内yum源 Vmamba nextjs react reactjs QQ bot Docker 黑客技术 流式接口 URL x64 SIGSEGV SSE xmm0 api 稳定性 看门狗 banner 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 代理 Typore make命令 makefile文件 ssrf 失效的访问控制 小智 单例模式 Web服务器 多线程下载工具 PYTHON DeepSeek r1 Open WebUI cd 目录切换 树莓派 VNC hadoop 宝塔 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 网工 xrdp 远程桌面 蓝桥杯 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 镜像 miniapp 调试 debug 断点 网络API请求调试方法 kotlin iphone Unity Dedicated Server Host Client 无头主机 SSL证书 apt 国内源 Docker Desktop Web应用服务器 elk tensorflow eclipse 热榜 图像处理 NLP模型 策略模式 yolov5 xpath定位元素 职场和发展 崖山数据库 YashanDB bat 服务器繁忙 Ubuntu 24.04.1 轻量级服务器 ROS 自动驾驶 跨域 odoo 服务器动作 Server action NPS 雨云服务器 雨云 边缘计算 CentOS 能力提升 面试宝典 技术 IT信息化 环境迁移 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 ebpf uprobe IIS .net core Hosting Bundle .NET Framework vs2022 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 springboot 直播推流 ROS2 VMware Tools vmware tools安装 vmwaretools安装步骤 vmwaretools安装失败 vmware tool安装步骤 vm tools安装步骤 vm tools安装后不能拖 vmware tools安装步骤 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 大模型部署 离线部署dify mariadb 相差8小时 UTC 时间 联网 easyconnect linux 命令 sed 命令 netty 代码托管服务 GameFramework HybridCLR Unity编辑器扩展 自动化工具 kvm 无桌面 gitea 微信公众平台 risc-v 串口驱动 CH341 uart 485 自动化任务管理 Carla 智能驾驶 prompt easyui langchain RAG 检索增强生成 文档解析 大模型垂直应用 状态管理的 UDP 服务器 Arduino RTOS SSH 服务 SSH Server OpenSSH Server openEuler 开机黑屏 联想开天P90Z装win10 Invalid Host allowedHosts 医疗APP开发 app开发 bootstrap 功能测试 自动化测试 模拟器 教程 Ark-TS语言 推荐算法 宝塔面板无法访问 蓝耘科技 元生代平台工作流 ComfyUI Xinference RAGFlow 虚拟显示器 Dell HPE 联想 浪潮 cmos 硬件 云电竞 云电脑 todesk 宕机切换 服务器宕机 opengl 执法记录仪 智能安全帽 smarteye webdav 软件构建 llama.cpp SysBench 基准测试 samba 服务器时间 ecm bpm xfce .net mvc断点续传 ubuntu24 vivado24 Minecraft DOIT 四博智联 r语言 数据可视化 免费域名 域名解析 重启 排查 系统重启 日志 原因 docker命令大全 MCP server C/S DocFlow 服务器管理 宝塔面板 配置教程 网站管理 thingsboard k8s集群资源管理 iDRAC R720xd HarmonyOS NEXT 原生鸿蒙 矩阵 灵办AI Helm k8s集群 代码规范 XFS xfs文件系统损坏 I_O error 测试用例 磁盘监控 服务器配置 UOS1070e 单元测试 实习 浏览器自动化 本地部署AI大模型 网站搭建 serv00 c 大模型面经 大模型学习 换源 Debian FunASR ASR AnythingLLM AnythingLLM安装 Mermaid 可视化图表 微信开放平台 微信公众号配置 zip unzip 远程登录 telnet 软链接 硬链接 muduo X11 Xming top Linux top top命令详解 top命令重点 top常用参数 Linux 维护模式 visual studio rdp 实验 王者荣耀 Wi-Fi qemu libvirt wsl2 WebVM 网络爬虫 trae 流水线 脚本式流水线 GCC crosstool-ng 毕昇JDK 多层架构 解耦 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 设备 GPU PCI-Express CentOS Stream Zoertier 内网组网 SSH jetty undertow 金仓数据库 2025 征文 数据库平替用金仓 grub 版本升级 扩容 DIFY can 线程池 信号处理 vu大文件秒传跨域报错cors stable diffusion AI作画 半虚拟化 硬件虚拟化 Hypervisor Erlang OTP gen_server 热代码交换 事务语义 java-rabbitmq MNN Qwen ssh远程登录 分析解读 virtualbox 视频编解码 SWAT 配置文件 服务管理 网络共享 输入法 IO gaussdb pgpool DeepSeek行业应用 Heroku 网站部署 端口测试 信号 田俊楠 notepad 游戏机 hugo Netty 即时通信 NIO outlook micropython esp32 mqtt 机柜 1U 2U 思科模拟器 Cisco etl nuxt3 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 Redis Desktop 技能大赛 scikit-learn Qualcomm WoS QNN AppBuilder MacMini Mac 迷你主机 mini Apple next.js 部署next.js 聊天室 宠物 免费学习 宠物领养 宠物平台 小艺 Pura X ocr 算力 Jellyfin dity make rclone AList fnOS eNSP 网络规划 VLAN 企业网络 TrueLicense W5500 OLED u8g2 TCP服务器 网页服务器 web服务器 Nginx k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm 实战案例 searxng Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 export import save load 迁移镜像 社交电子 React Next.js 开源框架 智慧农业 开源鸿蒙 团队开发 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 磁盘清理 hibernate 云耀服务器 跨平台 VR手套 数据手套 动捕手套 动捕数据手套 rustdesk IPMI 网络结构图 拓扑图 SEO OD机试真题 华为OD机试真题 服务器能耗统计 chfs ubuntu 16.04 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 漏洞 显示管理器 lightdm gdm kind firewall 同步 备份 建站 safari 系统 Linux权限 权限命令 特殊权限 av1 电视盒子 机顶盒ROM 魔百盒刷机 vscode 1.86 历史版本 下载 cfssl Docker Compose 昇腾 npu 直流充电桩 充电桩 P2P HDLC nac 802.1 portal neo4j 数据仓库 数据库开发 HP Anyware 智能音箱 智能家居 健康医疗 互联网医院 GRUB引导 Linux技巧 弹性计算 裸金属服务器 弹性裸金属服务器 unity3d TRAE Playwright keepalived 网络穿透 sonoma 自动更新 火绒安全 MAC SecureCRT Nuxt.js Xterminal XCC Lenovo dns RAID RAID技术 磁盘 uniapp 繁忙 解决办法 替代网站 汇总推荐 AI推理 powerbi chrome devtools chromedriver dba vpn 致远OA OA服务器 服务器磁盘扩容 ubuntu 18.04 CORS 程序 性能分析 ArcTS 登录 ArcUI GridItem arkUI skynet ceph nvm IM即时通讯 剪切板对通 HTML FORMAT AI代码编辑器 saltstack lb 协议 embedding AutoDL 强化学习 用户缓冲区 yaml Ultralytics 可视化 大模型微调 支付 微信支付 开放平台 ArkTs ArkUI 鸿蒙开发 移动开发 VPN wireguard 阿里云ECS 语法 wpf 大模型应用 Apache Beam 批流统一 案例展示 数据分区 容错机制 容器技术 sequoiaDB wsgiref Web 服务器网关接口 flink 匿名管道 捆绑 链接 谷歌浏览器 youtube google gmail 华为机试 netlink libnl3 cmake 自定义客户端 SAS 黑苹果 glibc 计算机科学与技术 底层实现 我的世界服务器搭建 minecraft 回显服务器 UDP的API使用 java-rocketmq 做raid 装系统 armbian u-boot seleium 负载测试 prometheus数据采集 prometheus数据模型 prometheus特点 对比 meld DiffMerge regedit 开机启动 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 端口聚合 windows11 PX4 MAVROS 四旋翼无人机 达梦 DM8 项目部署到linux服务器 项目部署过程 ftp 升级 CVE-2024-7347 本地化部署 web3 vscode1.86 1.86版本 ssh远程连接 Isaac Sim 虚拟仿真 LLM Web APP Streamlit AI员工 webgl tidb GLIBC IMM IO模型 软负载 玩机技巧 软件分享 软件图标 环境搭建 Maven 单一职责原则 信创 信创终端 中科方德 搭建个人相关服务器 内核 端口 查看 ss 高效I/O HistoryServer Spark YARN jobhistory Headless Linux deployment daemonset statefulset cronjob wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 代理服务器 csrutil mac恢复模式进入方法 SIP 恢复模式 授时服务 北斗授时 zookeeper 极限编程 错误代码2603 无网络连接 2603 安装MySQL g++ g++13 移动魔百盒 word clickhouse 僵尸世界大战 游戏服务器搭建 IMX317 MIPI H265 VCU Reactor反应堆 GIS 遥感 WebGIS 远程服务 服务器部署 本地拉取打包 nosql 运维监控 大大通 第三代半导体 碳化硅 rpa Windows ai工具 v10 软件 ldap IDEA Docker快速入门 deepseak 豆包 KIMI 腾讯元宝 CNNs 图像分类 欧标 OCPP 多个客户端访问 IO多路复用 TCP相关API 内网环境 lua h.264 网络文件系统 deepseek r1 Linux find grep 钉钉 aarch64 编译安装 HPC 架构与原理 AzureDataStudio 音乐库 查看显卡进程 fuser 网卡的名称修改 eth0 ens33 ArtTS 浪潮信息 AI服务器 抓包工具 System V共享内存 进程通信 cpp-httplib SRS 流媒体 直播 NAT转发 NAT Server whistle docker desktop image vmamba qt5 客户端开发 网络建设与运维 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 常用命令 文本命令 目录命令 python3.11 sudo原理 su切换 navicat dash 正则表达式 李心怡 Linux环境 远程过程调用 Windows环境 docker部署Python Attention 技术共享 sentinel deep learning es MDK 嵌入式开发工具 论文笔记 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 midjourney Qwen2.5-VL vllm WINCC Mac软件 企业网络规划 华为eNSP k8s部署 MySQL8.0 高可用集群(1主2从) 佛山戴尔服务器维修 佛山三水服务器维修 swoole FTP服务器 iNode Macos 加解密 Yakit yaklang 干货分享 黑客工具 密码爆破 我的世界 我的世界联机 数码 DevOps 软件交付 数据驱动 应用场景 hosts ranger MySQL8.0 win服务器架设 windows server lvm 磁盘挂载 磁盘分区 cursor tailscale derp derper 中转 triton 模型分析 线性代数 电商平台 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 perl 嵌入式Linux IPC EMUI 回退 降级 MobaXterm 文件传输 沙盒 音乐服务器 Navidrome 音流 多端开发 智慧分发 应用生态 鸿蒙OS diskgenius H3C mcp服务器 client close ArkTS 移动端开发 状态模式 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 Python基础 Python教程 Python技巧 VS Code 前后端分离 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 知识图谱 影刀 #影刀RPA# 小智AI服务端 xiaozhi TTS uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 元服务 应用上架 Claude AD 域管理 LInux yashandb 个人博客 hexo 反向代理 EVE-NG DenseNet shell脚本免交互 expect linux免交互 figma react native Spring Security 大屏端 增强现实 沉浸式体验 技术实现 案例分析 AR ISO镜像作为本地源 游戏开发 GoogLeNet scapy deekseek 电视剧收视率分析与可视化平台 阻塞队列 生产者消费者模型 服务器崩坏原因 问题解决 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 USB网络共享 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 虚幻引擎 浏览器开发 AI浏览器 HTTP 服务器控制 ESP32 DeepSeek dns是什么 如何设置电脑dns dns应该如何设置 银河麒麟桌面操作系统 Kylin OS xss 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 Sealos 论文阅读 pppoe radius 烟花代码 烟花 元旦 AI agent 性能调优 安全代理 本地知识库部署 DeepSeek R1 模型 vasp安装 分布式训练 proxy模式 软件卸载 系统清理 免密 公钥 私钥 Radius 网络搭建 神州数码 神州数码云平台 云平台 qt项目 qt项目实战 qt教程 copilot 国标28181 视频监控 监控接入 语音广播 流程 SDP 银河麒麟高级服务器 外接硬盘 Kylin 物联网开发 PPI String Cytoscape CytoHubba 根服务器 mysql安装报错 windows拒绝安装 超融合 glm4 人工智能生成内容 Tabs组件 TabContent TabBar TabsController 导航页签栏 滚动导航栏 kerberos EMQX 通信协议 计算虚拟化 弹性裸金属 junit 多路转接 openstack Xen laravel TCP协议 抗锯齿 内网服务器 内网代理 内网通信 xshell termius iterm2 北亚数据恢复 oracle数据恢复 需求分析 规格说明书 es6 qt6.3 g726 Logstash 日志采集 语音识别 iTerm2 高德地图 鸿蒙接入高德地图 HarmonyOS5.0 MVS 海康威视相机 ECT转485串口服务器 ECT转Modbus485协议 ECT转Modbus串口服务器 CDN 备选 网站 调用 示例 UEFI Legacy MBR GPT U盘安装操作系统 项目部署 AD域 deepseek-r1 大模型本地部署 服务网格 istio 笔灵AI AI工具 合成模型 扩散模型 图像生成 MacOS 查询数据库服务IP地址 SQL Server dock 加速 开源软件 CPU 使用率 系统监控工具 linux 命令 蓝桥杯C++组 桌面快捷方式 retry 重试机制 ardunio BLE pyicu 工具分享 Ardupilot 服务器正确解析请求体 autodl Office initramfs Linux内核 Grub AI Agent 字节智能运维 solidworks安装 WireGuard 异地组网 动静态库 多产物 lighttpd安装 Ubuntu配置 Windows安装 服务器优化 粘包问题 解决方案 nohup后台启动 弹性服务器 显示器 端口号 开放端口 访问列表 接口优化 js css3 管道 macOS 配置原理 服务器ssl异常解决 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 Pyppeteer UDP GeneCards OMIM TTD 华为OD 可以组成网络的服务器 qps 高并发 流量运营 桌面环境 玩游戏 iventoy VmWare OpenEuler fiddler ShapeFile GeoJSON Unity插件 搜狗输入法 中文输入法 archlinux kde plasma 零售 beautifulsoup yum换源 VGG网络 卷积层 池化层 ufw gunicorn 带外管理 充电桩平台 充电桩开源平台 电脑桌面出现linux图标 电脑桌面linux图标删除不了 电脑桌面Liunx图标删不掉 linux图标删不掉 大模型推理 zerotier 进程程序替换 execl函数 execv函数 execvp函数 execvpe函数 putenv函数 风扇控制软件 锁屏不生效 NFC 近场通讯 智能门锁 env 变量 用户管理 蓝牙 ftp服务 文件上传 ABAP 进程间通信 SVN Server tortoise svn 存储维护 NetApp存储 EMC存储 Linux Vim ajax HAProxy 输入系统 录音麦克风权限判断检测 录音功能 录音文件mp3播放 小程序实现录音及播放功能 RecorderManager 解决录音报错播放没声音问题 一切皆文件 西门子PLC 通讯 联机 僵尸毁灭工程 游戏联机 开服 内网渗透 靶机渗透 日志分析 系统取证 数字证书 签署证书 ollama api ollama外网访问 linuxdeployqt 打包部署程序 appimagetool c/s AimRT massa sui aptos sei 大版本升 升级Ubuntu系统 sublime text3 子系统 umeditor粘贴word ueditor粘贴word ueditor复制word ueditor上传word图片 ueditor导入word ueditor导入pdf ueditor导入ppt 7-zip nacos IPv6 IPv6测试 IPv6测速 IPv6检测 IPv6查询 分子对接 autodock mgltools PDB PubChem 空间 查错 网络原理 实时内核 Echarts图表 折线图 柱状图 异步动态数据 可视化效果 行情服务器 股票交易 速度慢 切换 股票量化接口 股票API接口 内存管理 电路仿真 multisim 硬件工程师 硬件工程师学习 电路图 电路分析 仪器仪表 显卡驱动持久化 GPU持久化 终端 安全漏洞 信息安全 LVM lvresize 磁盘扩容 pvcreate SystemV 动态规划 chromium dpi 大模型训练/推理 推理问题 mindie 程序化交易 量化交易 高频交易 智能体开发 全文检索 图搜索算法 分布式账本 智能合约 信任链 共识算法 IP配置 netplan 终端工具 远程工具 llamafactory 微调 考试 Xshell 脚本 打不开xxx软件 无法检查其是否包含恶意软件 autoware openjdk ubuntu安装 linux入门小白 Bug解决 Qt platform OpenCV 券商 股票交易接口api 类型 特点 GRE 金仓数据库概述 金仓数据库的产品优化提案 webview element-ui 上传视频并预览视频 vue上传本地视频及进度条功能 vue2选择视频上传到服务器 upload上传视频组件插件 批量上传视频 限制单个上传视频 区块链项目 PyQt PySide6 哈希算法 机械臂 servlet 免费 跨域请求 软件开发 源代码管理