AimRT 中的基本概念

AimRT 框架包含的内容

参考 AimRT 源码中的 src 目录,AimRT 框架中包含的内容如下:

src
├── common --------------------------------- // 一些基础的、可以直接使用的通用组件,例如 string、log 接口、buffer 等
├── examples ------------------------------- // AimRT 官方示例
│   ├── cpp -------------------------------- // CPP 接口的示例
│   ├── py --------------------------------- // Python 接口的示例
│   └── plugins ---------------------------- // 一些各方插件的使用示例
├── interface ------------------------------ // AimRT 接口层
│   ├── aimrt_core_plugin_interface -------- // [CPP] 插件开发接口
│   ├── aimrt_module_c_interface ----------- // [C] 模块开发接口
│   ├── aimrt_module_cpp_interface --------- // [CPP] 模块开发接口,对 C 版本的封装
│   ├── aimrt_module_protobuf_interface ---- // [CPP] 与 protobuf 相关的模块开发接口,基于 CPP 版本接口
│   ├── aimrt_module_ros2_interface -------- // [CPP] 与 ROS2 相关的模块开发接口,基于 CPP 版本接口
│   ├── aimrt_pkg_c_interface -------------- // [C] Pkg 开发接口
│   └── aimrt_type_support_pkg_c_interface - // [C] Type support 包接口
├── plugins -------------------------------- // AimRT 官方插件
├── protocols ------------------------------ // 一些 AimRT 官方的标准协议
├── runtime -------------------------------- // AimRT 运行时
│   ├── core ------------------------------- // 运行时核心库
│   ├── main ------------------------------- // 基于 core 实现的一个主进程"aimrt_main"
│   └── python_runtime --------------------- // 基于 pybind11 封装的 python 版本运行时
└── tools ---------------------------------- // 一些配套工具

AimRT 中的 “Module” 概念

与大多数框架一样,AimRT 拥有一个用于标识独立逻辑单元的概念:ModuleModule是一个逻辑层面的概念,代表一个逻辑上内聚的块。Module之间可以在逻辑层通过ChannelRPC两种抽象的接口通信。可以通过实现几个简单的接口来创建一个Module

一个Module通常对应一个硬件抽象、或者是一个独立算法、一项业务功能。Module可以使用框架提供的句柄来调用各项运行时功能,例如配置、日志、执行器等。框架给每个Module提供的句柄也是独立的,来实现一些资源统计、管理方面的功能。

AimRT 中的 “Node” 概念

Node代表一个可以部署启动的进程,在其中运行了一个 AimRT 框架的 Runtime 实例。Node是一个部署、运行层面的概念,一个Node中可能存在多个ModuleNode在启动时可以通过配置文件来设置日志、插件、执行器等运行参数。

AimRT 中的 “Pkg” 概念

Pkg是 AimRT 框架运行Module的一种途径。Pkg代表一个包含了单个或多个Module的动态库,Node在运行时可以加载一个或多个Pkg。可以通过实现几个简单的模块描述接口来创建Pkg

Module的概念更侧重于代码逻辑层面,而Pkg则是一个部署层面的概念,其中不包含业务逻辑代码。一般来说,在可以兼容的情况下,推荐将多个Module编译在一个Pkg中,这种情况下使用 RPC、Channel 等功能时性能会有优化。

通常Pkg中的符号都是默认隐藏的,只暴露有限的纯 C 接口,不同Pkg之间不会有符号上的相互干扰。不同Pkg理论上可以使用不同版本的编译器独立编译,不同Pkg里的Module也可以使用相互冲突的第三方依赖,最终编译出的Pkg可以二进制发布。

AimRT 框架集成业务逻辑的两种方式

AimRT 框架可以通过两种方式来集成业务逻辑,分别是 App模式Pkg模式,实际采用哪种方式需要根据具体场景进行判断。两者的区别如下:

  • App模式:在开发者自己的 Main 函数中直接链接 AimRT 运行时库,编译时直接将业务逻辑代码编译进主程序:

    • 优势:没有 dlopen 这个步骤,没有 so,只会有最终一个 exe。

    • 劣势:可能会有第三方库的冲突;无法独立的发布Module,想要二进制发布只能直接发布 exe。

    • 使用场景:一般用于小工具、小型 Demo 场景,没有太大的模块解耦需求;

  • Pkg模式:使用 AimRT 提供的 aimrt_main 可执行程序,在运行时根据配置文件加载动态库形式的Pkg,导入其中的Module类:

    • 优势:编译业务Module时只需要链接非常轻量的 AimRT 接口层,不需要链接 AimRT 运行时库,减少潜在的依赖冲突问题;可以二进制发布 so;独立性较好。

    • 劣势:框架基于 dlopen 加载Pkg,极少数场景下会有一些兼容性问题。

    • 使用场景:一般用于中大型项目,对模块解耦、二进制发布等有较强烈需求时;

无论采用哪种方式都不影响业务逻辑,且两种方式可以共存,实际采用哪种方式需要根据具体场景进行判断。

注意,上述说的两种方式只是针对 Cpp 开发接口。如果是使用 Python 开发,则只支持App模式

AimRT 中的 “Protocol” 概念

Protocol意为协议,代表Module之间通信的数据格式,用来描述数据的字段信息以及序列化、反序列化方式,例如Channel的订阅者和发布者之间制定的数据格式、或者RPC客户端和服务端之间制定的请求包/回包的数据格式。通常由一种IDL( Interface description language )描述,然后由某种工具转换为各个语言的代码。

AimRT 目前官方支持两种 IDL:

  • Protobuf

  • ROS2 msg/srv

但 AimRT 并不限定协议与IDL的具体类型,使用者可以实现其他的 IDL,例如 Thrift IDL、FlatBuffers 等,甚至支持一些自定义的 IDL。

AimRT 中的 “Channel” 概念

Channel也叫数据通道,是一种典型的通信拓补概念,其通过Topic标识单个数据通道,由发布者Publisher和订阅者Subscriber组成,订阅者可以获取到发布者发布的数据。Channel是一种多对多的拓补结构,Module可以向任意数量的Topic发布数据,同时可以订阅任意数量的Topic。类似的概念如 ROS 中的 Topic、Kafka/RabbitMQ 等消息队列。

在 AimRT 中,Channel 由接口层后端两部分组成,两者相互解耦。接口层定义了一层抽象的 Api,表示逻辑层面上的Channel;而后端负责实际的 Channel 数据传输,可以有多种类型。AimRT 官方提供了一些 Channel 后端,例如 mqtt、ros2 等,使用者也可以自行开发新的 Channel 后端。

开发者在使用 AimRT 中的 Channel 功能时,先在业务逻辑层调用接口层的 API,往某个 Topic 中发布数据,或订阅某个 Topic 的数据。然后 AimRT 框架会根据一定的规则,选择一个或几个 Channel 后端进行处理,这些后端将数据通过一些特定的方式发送给其他节点,由其他节点上对应的 Channel 后端接收数据并传递给业务逻辑层。整个逻辑流程如下图所示:

AimRT 中的 “Rpc” 概念

RPC也叫远程过程调用,基于请求-回复模型,由客户端Client和服务端Server组成,Module可以创建客户端句柄,发起特定的 RPC 请求,由其指定的、或由框架根据一定规则指定的服务端来接收请求并回复。Module也可以创建服务端句柄,提供特定的 RPC 服务,接收处理系统路由过来的请求并回复。类似的概念如 ROS 中的 Services、GRPC/Thrift 等 RPC 框架。

在 AimRT 中,RPC 也由接口层后端两部分组成,两者相互解耦。接口层定义了 RPC 的抽象 Api,而后端则负责实际的 RPC 调用。AimRT 官方提供了一些 RPC 后端,例如 http、ros2 等,使用者也可以自行开发新的 RPC 后端。

开发者使用 AimRT 的 RPC 功能时,先在业务逻辑层调用接口层 API,通过 Client 发起一个 RPC 调用,AimRT 框架会根据一定的规则选择一个 RPC 后端进行处理,它将数据通过一些特定的方式发送给 Server 节点,由 Server 节点上对应的 Rpc 后端接收数据并传递给业务层,并将业务层的回包传递回 Client 端。整个逻辑流程如下图所示:

AimRT 中的 “Filter” 概念

AimRT 中提供了Filter功能,用以增强 RPC 或 Channel 的能力。Filter 是一层贴着 Interface 层的、用户可自定义的逻辑插接点,按照相对于 Interface 层的位置,分为框架侧 Filter(Framework Filter)和用户侧 Filter(User Filter)。按照服务的功能,又分为 RPC Filter 和 Channel Filter。

Filter 在 RPC 或 Channel 每次被调用时触发,以一种类似洋葱的运行结构,在 RPC 或 Channel 调用前后做一些自定义动作,例如计算耗时、上报监控等。以 RPC Filter 为例,其运行时流程如下图所示:

RPC Filter

AimRT 中的 “Executor” 概念

Executor,或者叫执行器,是指一个可以运行任务的抽象概念,一个执行器可以是一个 Fiber、Thread 或者 Thread Pool,我们平常写的代码也是默认的直接指定了一个执行器:Main 线程。一般来说,能提供以下接口的就可以算是一个执行器:

void Execute(std::function<void()>&& task);

还有一种Executor提供定时执行的功能,可以指定在某个时间点或某段时间之后再执行任务。其接口类似如下:

void ExecuteAt(std::chrono::system_clock::time_point tp, std::function<void()>&& task);
void ExecuteAfter(std::chrono::nanoseconds dt, std::function<void()>&& task);

在 AimRT 中,执行器功能由接口层实际执行器的实现两部分组成,两者相互解耦。接口层定义了执行器的抽象 Api,提供投递任务的接口。而实现层则负责实际的任务执行,根据实现类型的不同有不一样的表现。AimRT 官方提供了几种执行器,例如基于 Asio 的线程池、基于 Tbb 的无锁线程池、基于时间轮的定时执行器等。

开发者使用 AimRT 的执行器功能时,在业务层将任务打包成一个闭包,然后调用接口层的 API,将任务投递到具体的执行器内,而执行器会根据自己的调度策略,在一定时机执行投递过来的任务。具体逻辑流程如下图所示:

AimRT 中的 “Plugin” 概念

Plugin指插件,是指一个可以向 AimRT 框架注册各种自定义功能的动态库,可以被框架运行时加载,或在用户自定义的可执行程序中通过硬编码的方式注册到框架中。AimRT 框架暴露了大量插接点和查询接口,例如:

  • 日志后端注册接口

  • Channel/Rpc 后端注册接口

  • Channel/Rpc 注册表查询接口

  • 各组件启动 hook 点

  • RPC/Channel 调用过滤器

  • 模块信息查询接口

  • 执行器注册接口

  • 执行器查询接口

使用者可以直接使用一些 AimRT 官方提供的插件,也可以从第三方开发者处寻求一些插件,或者自行实现一些插件以增强框架的服务能力,满足特定需求。