ROS2 DDS中间件(图文并茂+超详细)
ROS2 DDS中间件(图文并茂+超详细)
ROS2(Robot Operating System 2)作为新一代机器人操作系统,其底层通信框架采用了DDS(Data Distribution Service)。本文将详细介绍DDS的原理、功能以及在ROS2中的具体应用,帮助读者深入理解这一重要的中间件技术。
DDS简介
DDS(Data Distribution Service)是由Object Management Group (OMG)最早于2004年发布的一种基于发布-订阅(publish-subscribe)模式的端到端网络通信中间件,它可用于实时系统中实现可靠、高性能、可扩展、可互操作的数据传输。
DDS本身只是一个标准以及一套API定义,各厂商可以根据该标准实现自己的DDS。比较有名的DDS实现包括:
- eProsima FastDDS
- Eclipse Cyclone DDS
- RTI Context
- ADLINK Opensplice
ROS2对它们的支持情况如下表所示:
DDS Vendor | Support Status |
---|---|
eProsima FastDDS | Fully Supported |
Eclipse Cyclone DDS | Partial Support |
RTI Context | Partial Support |
ADLINK Opensplice | Partial Support |
可以根据需要配置ROS2使用不同的DDS实现。
DDS标准定义见About the Data Distribution Service Specification Version 1.4。DDS C++ API定义见About the ISO/IEC C++ 2003 Language DDS PSM Specification Version 1.0
DDS是完全分布式的,没有中心节点,任何两个节点之间都是直接通信的,示意图如下:
为了实现这样的系统,首先要解决的是各节点的互相发现问题。DDS本身并没有定义应该如何发现各节点,而是在RTPS(Real-time Publish-Subscribe Protocol)中定义该行为。RTPS用来支撑DDS的实现,是DDS底层使用的协议,用来保证各种DDS的实现之间可以互操作。在RTPS中定义了两个独立的发现协议,分别是PDP(Participant Discovery Protocol)和EDP(Endpoint Discovery Protocol),PDP用于发现各参与节点,EDP用于发现每个节点提供的所有端点(Endpoints)。PDP的实现可以通过单播列表或者组播实现。RTPS允许实现者实现不同的PDP和EDP协议,但是至少要实现SPDP(Simple PDP)和SEDP(Simple EDP)协议。关于协议的具体规定见RTPS规范。
在ROS2的实现中,默认采用的DDS实现为eProsima FastDDS。eProsima FastDDS是Apache2的开源协议,代码托管在github上eProsima/Fast-DDS。文档见DDS API — Fast DDS 2.0.0 documentation。eProsima提供的一张图可以很好地来表示DDS的实现原理:
可以看到,DDS基于Topic来实现发布-订阅模式,而且没有中心节点。是一种很好的端到端分布式通信中间件。ROS2使用它提供了面向机器人领域的开发框架。
DDS仅提供给予发布-订阅模式的通信,有些框架对它进行了包装,在发布-订阅的基础上提供了RPC的能力,比如同样来自OMG的DDS-RPC,以及来自eProsima的eProsima RPC over DDS。
将基于发布订阅模式的DDS包装成RPC的方法如下:
每个RPC调用都会是用2个Topic来实现,一个用于发送请求,一个用于接收回复。更详细的信息可以参考官方文档。
总结
DDS的目标场景在端上,致力于使端上的应用可以更容易地进行通信,由于它的完全分布式架构,可以方便地将端上的应用拆分为多进程架构,从而提高端的容错能力。从产品定位角度来讲,它和Chromium中的mojo都是端上的通信中间件,只不过mojo不强调分布式,内部集成了更多的特定于端上的机制,比如句柄的传输,消息的缓存等。但由于mojo生于Chromium,目前官方并没有把它独立出来,所以用起来比较不方便。而DDS或许可以在简单的场景下作为mojo的替代品。
一些代码实现
以下是使用ROS2标准方式(基于.msg文件+rclcpp)重构的代码示例,保留了核心功能但完全遵循ROS2抽象层的最佳实践:
1. 创建ROS2包和消息定义
1.1 创建工作空间
mkdir -p ros2_ws/src && cd ros2_ws/src
ros2 pkg create helloworld_demo --build-type ament_cmake
1.2 定义消息类型
在ros2_ws/src/helloworld_demo/msg/HelloWorld.msg
中添加:
uint32 id
string message
1.3 修改package.xml
<!-- 在 package.xml 中添加以下依赖 -->
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
1.4 修改CMakeLists.txt
# 在 CMakeLists.txt 中添加以下内容
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/HelloWorld.msg"
)
# 发布者节点
add_executable(publisher src/publisher.cpp)
target_include_directories(publisher PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_cpp)
ament_target_dependencies(publisher
rclcpp
std_msgs
rosidl_default_runtime
)
# 订阅者节点
add_executable(subscriber src/subscriber.cpp)
target_include_directories(subscriber PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_cpp)
ament_target_dependencies(subscriber
rclcpp
std_msgs
rosidl_default_runtime
)
# 安装和导出
install(TARGETS publisher subscriber
DESTINATION lib/${PROJECT_NAME}
)
rosidl_export_interfaces(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
ament_package()
2. 实现发布者节点
src/publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "helloworld_demo/msg/hello_world.hpp"
#include <chrono>
using namespace std::chrono_literals;
class PublisherNode : public rclcpp::Node {
public:
PublisherNode() : Node("hello_publisher"), count_(0) {
publisher_ = this->create_publisher<helloworld_demo::msg::HelloWorld>(
"hello_topic", // 话题名称
10 // QoS历史深度
);
timer_ = this->create_wall_timer(
1000ms, // 1秒周期
[this]() {
auto message = helloworld_demo::msg::HelloWorld();
message.id = ++count_;
message.message = "Hello from ROS2 publisher";
RCLCPP_INFO(this->get_logger(), "Publishing: [%d] '%s'",
message.id, message.message.c_str());
publisher_->publish(message);
});
}
private:
rclcpp::Publisher<helloworld_demo::msg::HelloWorld>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
uint32_t count_;
};
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<PublisherNode>());
rclcpp::shutdown();
return 0;
}
3. 实现订阅者节点
src/subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "helloworld_demo/msg/hello_world.hpp"
class SubscriberNode : public rclcpp::Node {
public:
SubscriberNode() : Node("hello_subscriber") {
subscriber_ = this->create_subscription<helloworld_demo::msg::HelloWorld>(
"hello_topic", // 与发布者相同的话题名称
10, // QoS历史深度
[this](const helloworld_demo::msg::HelloWorld::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "Received: [%d] '%s'",
msg->id, msg->message.c_str());
});
}
private:
rclcpp::Subscription<helloworld_demo::msg::HelloWorld>::SharedPtr subscriber_;
};
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SubscriberNode>());
rclcpp::shutdown();
return 0;
}
4. 编译与运行
4.1 编译
cd ros2_ws
colcon build --packages-select helloworld_demo
source install/setup.bash
4.2 运行(两个终端)
# 终端1:订阅者
ros2 run helloworld_demo subscriber
# 终端2:发布者
ros2 run helloworld_demo publisher