ROS2
ROS2 学习笔记:节点、工作空间、DDS 通信中间件,以及话题、服务、参数和动作的使用。
前言
在学习DRL的同时补一下ROS通信的知识,目标是掌握ROS2常用工具使用,并可以通过C++或Python调用ROS2的API进行通信,使用的教程是鱼香ROS的 动手学习ROS2。
ROS
ROS虽然名为机器人操作系统但其实并不是一个我们通常所谓的os,而是为了简化机器人的开发,设计的软件库和工具集,包括一整套通信机制(话题、服务、参数、动作)。一些ROS和ROS2的区分我不打算过多了解,ROS2的改进大致是做了跨平台而不只是linux,支持更新的编译标准提高效率,舍弃master节点让各node直接互相通信,ROS2自身只做个进程间的通信,节点和节点之间的通信采用的是DDS服务进行等等,后续笔记中的ROS都是指ROS2。
ROS系统架构

DDS
Data Distribution Service(数据分发服务)是国际标准组织 OMG(Object Management Group,对象管理组织)于2004年发布的一个分布式实时通信标准,专门用于解决分布式系统中低延迟、高可靠、可扩展的高性能数据交换问题。它采用发布/订阅(Publish/Subscribe)体系架构,以数据为中心(不关心是谁发布的),按照主题进行分发和接收,以中间件的形式提供通信服务,并提供丰富的 QoS(服务质量)策略,保障关键任务数据的分发,满足不同应用场景下的信息交互需求。
首先对各种DDS实现封装为统一接口,然后再抽象一层,进行大量的设置和配置(分区,主题名称,发现模式,消息创建),封装为ROS中间件接口rmw,再往上就是各种语言的客户端库,提供api来操作ROS2的节点、话题、服务、动作、参数等。
ROS节点
一个节点就是可实现某一模块化功能的可执行文件,
节点相关常用指令:
-
运行节点:
ros2 run <package_name> <executable_name> -
查看节点列表:
ros2 node list <package_name> -
查看节点信息:
ros2 node info <node_name> -
重映射节点名称
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle -
运行节点时设置参数
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10
ROS功能包与工作空间
工作空间就是存放若干个功能包的目录,功能包里存放许多节点,
功能包目录结构:
.
├── build
├── install
├── log
└── src
获取功能包有apt 获取和手动编译两种方式,前者不需要source,后者需要记得source。
source 是指将包所在路径即工作空间的位置加入环境变量,让ros能找到。
功能包相关常用指令
-
创建功能包
ros2 pkg create <package-name> --build-type {cmake,ament_cmake,ament_python} --dependencies <依赖名字> --node-name <node_name> --license Apache-2.0功能包根据编译分为三种类型,cmake、ament_cmake、ament_python,前两者用于cpp程序,ament_cmake是cmake的增强版,ament_python用于python程序。
-
列出可执行文件
ros2 pkg executables -
列出所有的包
ros2 pkg list -
输出某个包所在路径的前缀
ros2 pkg prefix <package-name>
编写ROS2节点的一般步骤
- 导入库文件 2. 初始化客户端库 3. 新建节点对象 4. spin循环节点 5. 关闭客户端库
编写完节点文件后还需要修改CmakeLists.txt 或 setup.py 文件,最后colcon build 编译节点 source install/setup.sh source 环境。
ROS_DOMAIN_ID
ROS 2用于通讯的默认中间件是DDS。在DDS中,不同逻辑网络共享物理网络的主要机制称为域(Domain) ID。同一域上的ROS 2节点可以自由地相互发现并发送消息,而不同域上的ROS 2节点则不能。所有ROS 2节点默认使用域ID为0。为了避免在同一网络上运行ROS 2的不同机器人之间互相干扰,应为每组设置不同的域ID。
根据 OMG 标准的 RTPS (实时发布订阅协议) 规范,系统为每个 ROS_DOMAIN_ID 预留的 UDP 端口范围是有限的,默认每个域(Domain)间隔 250 个端口。对于计算机上运行的每个ROS 2进程,将创建一个DDS “participant” 。由于每个DDS参与者占用计算机上的两个端口,因此在一台计算机上运行120个以上的ROS 2进程可能会溢出到其他域ID或临时端口。
- 端口基准公式:
Base Port=7400+250×Domain_ID- 对于特定的 Domain,它的单播端口计算公式为:
- 发现单播端口:
Base Port+10+2×Participant_Index- 用户数据单播端口:
Base Port+11+2×Participant_Index这里的 Participant_Index 就是该域内在这台电脑上启动的第几个 ROS 2 进程(从 0 开始累加)默认情况下,linux内核使用端口32768-60999作为临时端口。这意味着域ID 0-101 和 215-232 可以安全使用,而不会与临时端口发生冲突。
- 发现端口:专门用来跑后台管理流。比如接收新节点上线、节点下线、QoS 协商、心跳包(Liveliness)等元数据。
- 用户数据端口:专门用来接收在 ROS 2 里发布的 Topic 消息(如雷达点云、控制指令等)。
- 优势:当网络中突然有大量节点频繁启动或发生网络震荡(导致产生海量“发现包”)时,高负载的元数据只会在“发现端口”排队,而不会挤占或延迟“用户数据端口”上的实时控制指令。这保证了机器人控制的确定性与低延迟。也方便了网络分析与调试,在使用 Wireshark 等抓包工具调试机器人网络时,开发者可以直接根据 UDP 端口,把“握手报文”与“高频数据报文”清晰地过滤开,极大地方便了网络诊断
通信中间件
- 作为ROS2 默认底座的FastDDS、CycloneDDS 在局域网下表现优秀,但在跨网段、无线网络、弱网环境下表现很差。
- 由 ZettaScale 维护的开源协议 Zenoh,在 ROS 2 Jazzy 中已经被官方深度集成(rmw_zenoh)正在成为替代 DDS 的下一代大势所趋。支持不稳定的无线网和广域网,能通过路由代理消除局域网发现风暴。
- ZeroMQ (ZMQ) 一个轻量级、极致速度的异步 Socket 库,延迟极低(20~50 微秒级)但没有内置的动态发现(Discovery)机制,必须手动指定 IP 端口或自己写发现服务,没有开箱即用的消息管理,需要配合 Protobuf/Flatbuffers 使用自己封装序列化。
话题与服务
消息接口
ROS2在消息传递时会帮我们做好序列化与反序列化,并借助消息接口和底层DDS可以做到跨编程语言、跨平台和跨设备。当我们使用已有消息接口或定义好消息接口后,ROS2会根据消息接口内容生成不同语言的接口类,在不同编程语言中调用相同的类即可实现无感的消息序列化和反序列化。
ROS 2 定义消息接口时使用一种简化的描述语言,接口定义语言(the interface definition language, IDL),来描述这些接口。 这种描述语言使得 ROS 工具能够轻松地为接口在多种目标编程语言中自动生成源代码。ROS2消息接口有如下三种类型: topics、 services 或 actions。
- msg:
.msg文件是简单的文本文件,描述了 ROS 消息(message)的字段。它们用于在不同的语言中生成消息的源代码。 - srv:
.srv文件描述了一个服务。它们由两部分组成:请求(request)和响应(response)。请求和响应都是消息声明。 - action:
.action文件描述了动作。它们由三部分组成:目标(goal)、结果(result)和反馈(feedback)。每一部分都是一个消息声明。
话题的实现
首先创建一个工作空间,功能包,然后创建发布节点和订阅节点,导入消息接口。
//在构造函数中
//创建发布者
command_publisher_ = this->create_publisher<std_msgs::msg::String>("command", 10);
// 创建定时器,500ms为周期,定时发布
timer_ = this->create_wall_timer(std::chrono::milliseconds(500),
std::bind(&TopicPublisher01::timer_callback, this));
//创建订阅者
command_subscribe_ = this->create_subscription<std_msgs::msg::String>("command", 10, std::bind(&TopicSubscribe01::command_callback, this, std::placeholders::_1));
//std::placeholders::_1是参数占位符,等函数被调用时再把传入的第一个参数放这
//并在private中声明为成员变量,避免随函数结束销毁
// 声名定时器指针
rclcpp::TimerBase::SharedPtr timer_;
// 声明话题发布者指针
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr command_publisher_;
// 声明一个订阅者
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr command_subscribe_;
SharedPtr 智能指针
auto node = std::make_shared<rclcpp::Node>("node_01");
在堆上创建一个Node对象,并返回一个指向该对象的 shared_ptr ,当指向对象的最后一个智能指针销毁后,所管理的对象自动销毁。
ament_cmake 类型功能包导入消息接口分为三步:
- 在
CMakeLists.txt中导入,具体是先find_packages再ament_target_dependencies。 - 在
packages.xml中导入,具体是添加depend标签并将消息接口写入。 - 在代码中导入,C++中是
#include"消息功能包/xxx/xxx.hpp"。
CMakeLists.txt
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(topic_publisher_01 src/topic_publisher_01.cpp)
ament_target_dependencies(topic_publisher_01 rclcpp std_msgs)
packages.xml
<depend>rclcpp</depend>
<depend>std_msgs</depend>
代码文件topic_publisher_01.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
如果是自己实现接口功能包需要注意构建类型一定是 ament_cmake。
话题相关指令
- 返回可用话题列表
ros2 topic list
- 在话题后增加显示消息类型
ros2 topic list -t
- 打印实时话题内容
ros2 topic echo /chatter
- 查看主题信息
ros2 topic info /chatter
- 查看消息类型
ros2 interface show std_msgs/msg/String
- 手动发布消息
ros2 topic pub /chatter std_msgs/msg/String 'data: "123"'
服务的实现
区别于话题大量的单向数据传递,服务通过客户端发送请求,服务端响应请求进行双向数据传递。
// 创建服务
add_ints_server_ = this->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints_srv", std::bind(&ServiceServer01::handle_add_two_ints, this, std::placeholders::_1, std::placeholders::_2));
// 声明一个服务
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr add_ints_server_;
// 收到请求的处理函数
void handle_add_two_ints(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
RCLCPP_INFO(this->get_logger(), "收到a: %ld b: %ld", request->a,request->b);
response->sum = request->a + request->b;
};
// 创建客户端
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints_srv");
void send_request(int a, int b) {
RCLCPP_INFO(this->get_logger(), "计算%d+%d", a, b);
// 1.等待服务端上线
while (!client_->wait_for_service(std::chrono::seconds(1))) {
//等待时检测rclcpp的状态
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "等待服务的过程中被打断...");
return;
}
RCLCPP_INFO(this->get_logger(), "等待服务端上线中");
}
// 2.构造请求的
auto request = std::make_shared<example_interfaces::srv::AddTwoInts_Request>();
request->a = a;
request->b = b;
// 3.发送异步请求,然后等待返回,返回时调用回调函数
client_->async_send_request(request, std::bind(&ServiceClient01::result_callback_, this,std::placeholders::_1));
}
// 声明客户端
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
void result_callback_(rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture result_future) {
auto response = result_future.get();
RCLCPP_INFO(this->get_logger(), "计算结果:%ld", response->sum);
}
SharedFuture
// 启动异步任务,得到 future, share 把异步状态的所有权从 future 转移给了 shared_future
auto result_future = std::async(std::launch::async, some_function).share();
auto response = result_future.get();
shared_future 可以复制,每个持有者都能访问同一结果。
服务相关指令
- 查看服务列表
ros2 service list
- 手动调用服务
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
- 查看服务接口类型
ros2 service type /add_two_ints
- 查找使用某一接口的服务
ros2 service find example_interfaces/srv/AddTwoInts
参数
参数用于方便地动态调整节点的设置,而不需要重新编译节点,参数的读写操作也可以通过服务实现,实际上当我们启动节点后查看服务列表就会发现 ros2的参数操作就是通过服务通信方式实现的。
参数相关指令
- 查看所有节点的参数列表
ros2 param list
- 查看参数的详细信息
ros2 param describe <node_name> <param_name>
- 获取参数的值
ros2 param get <node_name> <param_name>
- 修改参数的值
ros2 param set <node_name> <parameter_name> <value>
- 保存节点参数为yaml文件
ros2 param dump <node_name>
- 加载参数快照
//启动节点时
ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>
//节点运行中
ros2 param load <node_name> ./xxx.yaml
动作
对于复杂的机器人控制,单个话题和服务不能满足实际应用中的异步请求、暂停、恢复的需求。动作这一通信方式由话题和服务共同构建,由目标(客户端告诉服务端要做什么,服务端针对该目标要有响应)、反馈(服务端告诉客户端此时做的进度如何)和结果(服务端最终告诉客户端其执行结果)组成,三个服务是目标传递服务、结果传递服务、取消执行服务,两个话题是反馈话题(服务发布,客户端订阅)、状态话题(服务端发布,客户端订阅)。动作的消息类型有三种,一个用于请求,一个用于响应,一个用于反馈。
动作相关指令
- 获取action列表及其消息接口
ros2 action list -t
- 查看action的info包括客户端、服务端数量及名字
ros2 action info <action_name>
- 向服务端发送action请求
ros2 send_goal <action_name> <interface_name> "{key: value}"
参考资料: