ROS教程07:从 ros::start() 顺着源码读懂 Master、XML-RPC 与 Topic 注册发现

ROS教程07:从 ros::start() 顺着源码读懂 Master、XML-RPC 与 Topic 注册发现

摘要:沿 ros::start() 的真实调用顺序逐层阅读 Poll、XML-RPC、Topic、Service 与 callback 源码,再回看 Publisher/Subscriber 的注册发现全过程。

文章目录

  • [ROS教程07:从 ros::start() 顺着源码读懂 Master、XML-RPC 与 Topic 注册发现](#ROS教程07:从 ros::start() 顺着源码读懂 Master、XML-RPC 与 Topic 注册发现)
    • [1. 从 `ros::start()` 开始:先把启动顺序看清](#1. 从 ros::start() 开始:先把启动顺序看清)
    • [2. `param::param("/tcp_keepalive", ...)`:`start()` 一开始为什么先读参数](#2. param::param("/tcp_keepalive", ...)start() 一开始为什么先读参数)
    • [3. `PollManager::addPollThreadListener()`:先把回调挂上去,再启动 Poll 线程](#3. PollManager::addPollThreadListener():先把回调挂上去,再启动 Poll 线程)
      • [3.1 `VoidSignal` 到底是什么](#3.1 VoidSignal 到底是什么)
      • [3.2 `return poll_signal_.connect(func);` 在做什么](#3.2 return poll_signal_.connect(func); 在做什么)
      • [3.3 `checkForShutdown` 为什么先注册,线程却后启动](#3.3 checkForShutdown 为什么先注册,线程却后启动)
    • [4. `XMLRPCManager::bind("shutdown", ...)`:Node API 在 server 启动之前就能先注册](#4. XMLRPCManager::bind("shutdown", ...):Node API 在 server 启动之前就能先注册)
    • [5. `TopicManager::start()`:这里一次绑定了六个 Node API](#5. TopicManager::start():这里一次绑定了六个 Node API)
      • [5.1 `boost::bind(... _1, _2)` 到底在绑定什么](#5.1 boost::bind(... _1, _2) 到底在绑定什么)
      • [5.2 六个 Node API 分别干什么](#5.2 六个 Node API 分别干什么)
      • [5.3 `processPublishQueues` 为什么也挂到 Poll 线程](#5.3 processPublishQueues 为什么也挂到 Poll 线程)
    • [6. `ServiceManager::start()`:它本身没有再创建线程](#6. ServiceManager::start():它本身没有再创建线程)
    • [7. `ConnectionManager::start()`:把 TCPROS/UDPROS 的监听入口真正建起来](#7. ConnectionManager::start():把 TCPROS/UDPROS 的监听入口真正建起来)
      • [7.1 把"已掉线连接清理"注册到 Poll 线程](#7.1 把“已掉线连接清理”注册到 Poll 线程)
      • [7.2 创建 TCPROS server transport](#7.2 创建 TCPROS server transport)
      • [7.3 UDP 监听也在这里准备](#7.3 UDP 监听也在这里准备)
    • [8. `PollManager::start()`:100 ms 到底是什么意思](#8. PollManager::start():100 ms 到底是什么意思)
      • [8.1 当前 `poll_signal_` 上已经注册了哪些回调](#8.1 当前 poll_signal_ 上已经注册了哪些回调)
      • [8.2 `PollSet::update(100)` 又做了什么](#8.2 PollSet::update(100) 又做了什么)
    • [9. `XMLRPCManager::start()`:当前 Node 的 XML-RPC server 真正开始监听](#9. XMLRPCManager::start():当前 Node 的 XML-RPC server 真正开始监听)
      • [9.1 `getPid` 也是 Node API](#9.1 getPid 也是 Node API)
      • [9.2 `bindAndListen(0)` 为什么传 0](#9.2 bindAndListen(0) 为什么传 0)
      • [9.3 `serverThreadFunc()` 才真正持续处理 Node API 请求](#9.3 serverThreadFunc() 才真正持续处理 Node API 请求)
      • [9.4 为什么里面还有 `ASyncXMLRPCConnection`](#9.4 为什么里面还有 ASyncXMLRPCConnection)
    • [10. `ROSOutAppender`:`ROS_INFO` 为什么最终能变成 `/rosout` Topic](#10. ROSOutAppenderROS_INFO 为什么最终能变成 /rosout Topic)
      • [10.1 `ROSOutAppender` 构造时直接 advertise `/rosout`](#10.1 ROSOutAppender 构造时直接 advertise /rosout)
      • [10.2 `ROSOutAppender::log()` 先把日志放入自己的 queue](#10.2 ROSOutAppender::log() 先把日志放入自己的 queue)
      • [10.3 `logThread()` 再真正调用 TopicManager 发布](#10.3 logThread() 再真正调用 TopicManager 发布)
    • [11. `~get_loggers`、`~set_logger_level` 为什么在 `start()` 里自动出现](#11. ~get_loggers~set_logger_level 为什么在 start() 里自动出现)
      • [11.1 `ServiceManager::advertiseService()` 逐层看](#11.1 ServiceManager::advertiseService() 逐层看)
    • [12. `g_internal_callback_queue` 到底干嘛:不是"发布 start 成功通知"](#12. g_internal_callback_queue 到底干嘛:不是“发布 start 成功通知”)
      • [12.1 内部 queue 是懒创建的](#12.1 内部 queue 是懒创建的)
      • [12.2 `start()` 里已经有明确使用者](#12.2 start() 里已经有明确使用者)
      • [12.3 独立线程怎样处理它](#12.3 独立线程怎样处理它)
    • [13. 回到业务代码:`nh.advertise()` 从模板 API 一直走到 `registerPublisher`](#13. 回到业务代码:nh.advertise() 从模板 API 一直走到 registerPublisher)
      • [13.1 `AdvertiseOptions::init<M>()` 先把消息类型信息补全](#13.1 AdvertiseOptions::init<M>() 先把消息类型信息补全)
      • [13.2 `NodeHandle::advertise(AdvertiseOptions&)`](#13.2 NodeHandle::advertise(AdvertiseOptions&))
        • [第一步:Topic 名先解析](#第一步:Topic 名先解析)
        • [第二步:确定 Publisher status callback 用哪条 queue](#第二步:确定 Publisher status callback 用哪条 queue)
        • [第三步:构造 `Publisher`](#第三步:构造 Publisher)
      • [13.3 `getLastMessageCallback()`:latch 是怎样接进来的](#13.3 getLastMessageCallback():latch 是怎样接进来的)
      • [13.4 `TopicManager::advertise()`:先建立 Publication,再向 Master 注册](#13.4 TopicManager::advertise():先建立 Publication,再向 Master 注册)
      • [13.5 为什么 `advertise()` 里还检查"自己是否也订阅了同一个 Topic"](#13.5 为什么 advertise() 里还检查“自己是否也订阅了同一个 Topic”)
      • [13.6 最后才是 `registerPublisher`](#13.6 最后才是 registerPublisher)
      • [13.7 `master::execute()` 里面又做了什么](#13.7 master::execute() 里面又做了什么)
    • [14. `publisher.publish(msg)`:从用户 API 到 Publication,再交给 Poll 线程](#14. publisher.publish(msg):从用户 API 到 Publication,再交给 Poll 线程)
      • [14.1 当前 `hello_node` 实际走的是 `publish(const M&)`](#14.1 当前 hello_node 实际走的是 publish(const M&))
      • [14.2 非模板 `Publisher::publish()` 很薄](#14.2 非模板 Publisher::publish() 很薄)
      • [14.3 `TopicManager::publish()` 决定要不要序列化、要不要走 nocopy](#14.3 TopicManager::publish() 决定要不要序列化、要不要走 nocopy)
      • [14.4 `Publication::publish()` 为什么没有立即遍历所有 TCP socket 写出去](#14.4 Publication::publish() 为什么没有立即遍历所有 TCP socket 写出去)
      • [14.5 同进程时:`IntraProcessSubscriberLink -> IntraProcessPublisherLink -> Subscription`](#14.5 同进程时:IntraProcessSubscriberLink -> IntraProcessPublisherLink -> Subscription)
      • [14.6 `Subscription::handleMessage()` 最终不是直接调用业务 callback](#14.6 Subscription::handleMessage() 最终不是直接调用业务 callback)
    • [15. Subscriber:从 `nh.subscribe()` 一直走到 `requestTopic`](#15. Subscriber:从 nh.subscribe() 一直走到 requestTopic)
      • [15.1 `SubscribeOptions::init<M>()` 做什么](#15.1 SubscribeOptions::init<M>() 做什么)
      • [15.2 `NodeHandle::subscribe(SubscribeOptions&)`](#15.2 NodeHandle::subscribe(SubscribeOptions&))
      • [15.3 `TopicManager::subscribe()`:先建立本地 Subscription](#15.3 TopicManager::subscribe():先建立本地 Subscription)
      • [15.4 `registerSubscriber()`:向 Master 注册,同时拿回已有 Publisher](#15.4 registerSubscriber():向 Master 注册,同时拿回已有 Publisher)
      • [15.5 `Subscription::pubUpdate()`:比较"新列表"和"当前连接"](#15.5 Subscription::pubUpdate():比较“新列表”和“当前连接”)
      • [15.6 `negotiateConnection()`:从这里开始直接找 Publisher,不再找 Master](#15.6 negotiateConnection():从这里开始直接找 Publisher,不再找 Master)
      • [15.7 Publisher 收到 `requestTopic`](#15.7 Publisher 收到 requestTopic)
      • [15.8 Subscriber 的 `pendingConnectionDone()` 再建立 TCPROS](#15.8 Subscriber 的 pendingConnectionDone() 再建立 TCPROS)
    • [16. `spinOnce()` 与 `spin()`:为什么消息到了,业务 callback 还需要 Spinner](#16. spinOnce()spin():为什么消息到了,业务 callback 还需要 Spinner)
      • [16.1 `spinOnce()`](#16.1 spinOnce())
      • [16.2 `spin()`](#16.2 spin())
      • [16.3 `hello_listener` 的完整 callback 路径](#16.3 hello_listener 的完整 callback 路径)
    • [17. ROS Master 是否属于普通 roscpp Node:启动进程与线程模型](#17. ROS Master 是否属于普通 roscpp Node:启动进程与线程模型)
      • [17.1 `roscore` 本身先进入 Python `roslaunch`](#17.1 roscore 本身先进入 Python roslaunch)
      • [17.2 roslaunch 再启动独立 `rosmaster --core` 进程](#17.2 roslaunch 再启动独立 rosmaster --core 进程)
      • [17.3 `rosmaster` 进程内部怎样启动 XML-RPC server](#17.3 rosmaster 进程内部怎样启动 XML-RPC server)
      • [17.4 Parameter Server 就在同一个 `rosmaster` 进程里](#17.4 Parameter Server 就在同一个 rosmaster 进程里)
      • [17.5 `/rosout` 才是一个普通 core Node](#17.5 /rosout 才是一个普通 core Node)
    • [18. 名称解析、namespace、remap 与 private namespace:注册前已经完成](#18. 名称解析、namespace、remap 与 private namespace:注册前已经完成)
      • [18.1 全局名](#18.1 全局名)
      • [18.2 相对名](#18.2 相对名)
      • [18.3 private namespace](#18.3 private namespace)
      • [18.4 remap 在注册前就完成](#18.4 remap 在注册前就完成)
    • [19. Parameter 在本章掌握到 XML-RPC 机制边界](#19. Parameter 在本章掌握到 XML-RPC 机制边界)
      • [19.1 普通 `pnh.param()` 的读取路径](#19.1 普通 pnh.param() 的读取路径)
      • [19.2 为什么 `rosparam set` 不会自动改已经构造好的 `ros::Rate`](#19.2 为什么 rosparam set 不会自动改已经构造好的 ros::Rate)
      • [19.3 `getParamCached()` 为什么又会出现 `paramUpdate`](#19.3 getParamCached() 为什么又会出现 paramUpdate)
      • [19.4 这一章 Parameter 掌握到这里就够](#19.4 这一章 Parameter 掌握到这里就够)
    • [20. 完整机制回看第一步:先分清 Master URI、Node XML-RPC URI 与 TCPROS 端口](#20. 完整机制回看第一步:先分清 Master URI、Node XML-RPC URI 与 TCPROS 端口)
      • [20.1 Master XML-RPC URI](#20.1 Master XML-RPC URI)
      • [20.2 每个 Node 自己的 XML-RPC URI](#20.2 每个 Node 自己的 XML-RPC URI)
      • [20.3 TCPROS 监听端口](#20.3 TCPROS 监听端口)
    • [21. 完整机制回看:两种启动顺序为什么会走不同的首次发现路径](#21. 完整机制回看:两种启动顺序为什么会走不同的首次发现路径)
      • [21.1 情况 A:Publisher 先启动](#21.1 情况 A:Publisher 先启动)
      • [21.2 情况 B:Subscriber 先启动](#21.2 情况 B:Subscriber 先启动)
      • [21.3 Master 端 `registerSubscriber()` / `registerPublisher()` 实际做了什么](#21.3 Master 端 registerSubscriber() / registerPublisher() 实际做了什么)
      • [21.4 `publisherUpdate` 和 `requestTopic` 的边界](#21.4 publisherUpdaterequestTopic 的边界)
      • [21.5 Master 为什么不是 Topic 数据中转站](#21.5 Master 为什么不是 Topic 数据中转站)
    • [22. 完整机制回看:Master API、Node API、Parameter Server API 与数据面的边界](#22. 完整机制回看:Master API、Node API、Parameter Server API 与数据面的边界)
    • [23. 源码主线与完整机制已经闭合:第 08 章从 TCPROS 入口继续](#23. 源码主线与完整机制已经闭合:第 08 章从 TCPROS 入口继续)
    • 参考源码与资料

本章继续使用现有工程:

text 复制代码
ros_ws/src/ros1_hello/src/hello_node.cpp
ros_ws/src/ros1_hello/src/hello_listener.cpp
ros_debug_ws/src/ros_comm

第 06 章已经确认第一条启动链:

text 复制代码
ros::init()
    ↓
第一个 ros::NodeHandle
    ↓
NodeHandle::construct()
    ↓
ros::start()

第 07 章直接从 ros::start() 的源码入口继续向下阅读。阅读顺序遵循真实调用关系:先认识 roscpp 在启动阶段创建或注册的 Manager、线程、监听端口和 callback,再进入业务代码的 advertise()publish()subscribe()spin()。完成这些源码路径后,再统一回看 Master、Node API、registerPublisherpublisherUpdaterequestTopic 与 TCPROS 入口之间的关系。

本章追到 TransportTCP::connect() 和 TCPROS 入口为止。TCPROS connection header、序列化格式和 socket 数据收发留到第 08 章继续展开。


1. 从 ros::start() 开始:先把启动顺序看清

源码:

text 复制代码
ros_comm/clients/roscpp/src/libros/init.cpp

先只看 ros::start() 的主干:

cpp 复制代码
void start()
{
    ...

    param::param("/tcp_keepalive",
                 TransportTCP::s_use_keepalive_,
                 TransportTCP::s_use_keepalive_);

    PollManager::instance()->addPollThreadListener(checkForShutdown);
    XMLRPCManager::instance()->bind("shutdown", shutdownCallback);

    initInternalTimerManager();

    TopicManager::instance()->start();
    ServiceManager::instance()->start();
    ConnectionManager::instance()->start();
    PollManager::instance()->start();
    XMLRPCManager::instance()->start();

    ...

    if (!(no_rosout || (g_init_options & init_options::NoRosout)))
    {
        g_rosout_appender = new ROSOutAppender;
        ros::console::register_appender(g_rosout_appender);
    }

    ...

    // 注册 roscpp 内部 Service
    // 处理 /use_sim_time 与 /clock

    ...

    g_internal_queue_thread =
        boost::thread(internalCallbackQueueThreadFunc);

    ...

    getGlobalCallbackQueue()->enable();
}

读取主干后,先记录 start() 的真实执行顺序。该流程节点较多,使用独立技术图表示:

图中需要区分三个后台执行面:

  1. Poll 线程:网络 fd 轮询、发送队列推进、连接清理、shutdown 检查;
  2. Node XML-RPC server 线程 :处理 publisherUpdaterequestTopicshutdown 等 Node API;
  3. roscpp 内部 callback 线程 :处理 roscpp 自己的 Service、/clock 等内部 callback。

随后还会出现 ROSOutAppender 自己的日志线程。

所以第 06 章看到的:

text 复制代码
ros::start()
  -> TopicManager::start()
  -> ServiceManager::start()
  -> ConnectionManager::start()
  -> PollManager::start()
  -> XMLRPCManager::start()

继续向下阅读时,需要回答以下问题:

text 复制代码
start() 里面到底注册了什么?
哪些组件真正创建线程?
哪些组件只保存对象引用?
callback 最后在哪个线程执行?

以下章节严格按照该顺序进入对应函数。


2. param::param("/tcp_keepalive", ...)start() 一开始为什么先读参数

ros::start() 先执行:

cpp 复制代码
param::param("/tcp_keepalive",
             TransportTCP::s_use_keepalive_,
             TransportTCP::s_use_keepalive_);

ros::param::param() 是一个"读取参数,失败则使用默认值"的模板辅助函数:

cpp 复制代码
template<typename T>
bool param(const std::string& param_name,
           T& param_val,
           const T& default_val)
{
    if (has(param_name))
    {
        if (get(param_name, param_val))
        {
            return true;
        }
    }

    param_val = default_val;
    return false;
}

这里第三个参数仍然传:

cpp 复制代码
TransportTCP::s_use_keepalive_

意思不是"固定默认 true/false",而是:

text 复制代码
如果 Parameter Server 中存在 /tcp_keepalive
    -> 用参数值覆盖 TransportTCP::s_use_keepalive_

否则
    -> 保持当前默认值

这一行同时说明:Parameter Server 在 Node 建立 Topic 连接之前就可能被 roscpp 内部使用。

不过参数机制不是这一章的主线,后面会单独说明本阶段应该掌握到什么深度。


3. PollManager::addPollThreadListener():先把回调挂上去,再启动 Poll 线程

接下来:

cpp 复制代码
PollManager::instance()->addPollThreadListener(checkForShutdown);

这里同时需要把相关 Boost 类型与连接机制说明清楚。

3.1 VoidSignal 到底是什么

poll_manager.h

cpp 复制代码
typedef boost::signals2::signal<void(void)> VoidSignal;
typedef boost::function<void(void)> VoidFunc;

class PollManager
{
    ...

    VoidSignal poll_signal_;
};

这一句:

cpp 复制代码
typedef boost::signals2::signal<void(void)> VoidSignal;

可以拆成:

text 复制代码
boost::signals2::signal<函数签名>

函数签名:void(void)
    -> 无参数
    -> 无返回值

因此:

cpp 复制代码
VoidSignal poll_signal_;

可以把它先理解成一个"可以挂多个 void() 回调的事件广播器"。

和普通函数指针最大的区别是,它不是只能保存一个函数,而是可以:

text 复制代码
signal
  ├─ slot A
  ├─ slot B
  └─ slot C

当代码执行:

cpp 复制代码
poll_signal_();

当前关系是单向广播,使用短流程表示:
#mermaid-svg-awZe0k0OTBiLZa7U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-awZe0k0OTBiLZa7U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-awZe0k0OTBiLZa7U .error-icon{fill:#552222;}#mermaid-svg-awZe0k0OTBiLZa7U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-awZe0k0OTBiLZa7U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-awZe0k0OTBiLZa7U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-awZe0k0OTBiLZa7U .marker.cross{stroke:#333333;}#mermaid-svg-awZe0k0OTBiLZa7U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-awZe0k0OTBiLZa7U p{margin:0;}#mermaid-svg-awZe0k0OTBiLZa7U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-awZe0k0OTBiLZa7U .cluster-label text{fill:#333;}#mermaid-svg-awZe0k0OTBiLZa7U .cluster-label span{color:#333;}#mermaid-svg-awZe0k0OTBiLZa7U .cluster-label span p{background-color:transparent;}#mermaid-svg-awZe0k0OTBiLZa7U .label text,#mermaid-svg-awZe0k0OTBiLZa7U span{fill:#333;color:#333;}#mermaid-svg-awZe0k0OTBiLZa7U .node rect,#mermaid-svg-awZe0k0OTBiLZa7U .node circle,#mermaid-svg-awZe0k0OTBiLZa7U .node ellipse,#mermaid-svg-awZe0k0OTBiLZa7U .node polygon,#mermaid-svg-awZe0k0OTBiLZa7U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-awZe0k0OTBiLZa7U .rough-node .label text,#mermaid-svg-awZe0k0OTBiLZa7U .node .label text,#mermaid-svg-awZe0k0OTBiLZa7U .image-shape .label,#mermaid-svg-awZe0k0OTBiLZa7U .icon-shape .label{text-anchor:middle;}#mermaid-svg-awZe0k0OTBiLZa7U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-awZe0k0OTBiLZa7U .rough-node .label,#mermaid-svg-awZe0k0OTBiLZa7U .node .label,#mermaid-svg-awZe0k0OTBiLZa7U .image-shape .label,#mermaid-svg-awZe0k0OTBiLZa7U .icon-shape .label{text-align:center;}#mermaid-svg-awZe0k0OTBiLZa7U .node.clickable{cursor:pointer;}#mermaid-svg-awZe0k0OTBiLZa7U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-awZe0k0OTBiLZa7U .arrowheadPath{fill:#333333;}#mermaid-svg-awZe0k0OTBiLZa7U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-awZe0k0OTBiLZa7U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-awZe0k0OTBiLZa7U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-awZe0k0OTBiLZa7U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-awZe0k0OTBiLZa7U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-awZe0k0OTBiLZa7U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-awZe0k0OTBiLZa7U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-awZe0k0OTBiLZa7U .cluster text{fill:#333;}#mermaid-svg-awZe0k0OTBiLZa7U .cluster span{color:#333;}#mermaid-svg-awZe0k0OTBiLZa7U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-awZe0k0OTBiLZa7U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-awZe0k0OTBiLZa7U rect.text{fill:none;stroke-width:0;}#mermaid-svg-awZe0k0OTBiLZa7U .icon-shape,#mermaid-svg-awZe0k0OTBiLZa7U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-awZe0k0OTBiLZa7U .icon-shape p,#mermaid-svg-awZe0k0OTBiLZa7U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-awZe0k0OTBiLZa7U .icon-shape .label rect,#mermaid-svg-awZe0k0OTBiLZa7U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-awZe0k0OTBiLZa7U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-awZe0k0OTBiLZa7U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-awZe0k0OTBiLZa7U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} poll_signal_
checkForShutdown
TopicManager::processPublishQueues
ConnectionManager::removeDroppedConnections

这些已经连接到 signal 上的 slot 会在调用 poll_signal_() 的当前线程里被同步触发。

这里不要把它理解成:

text 复制代码
connect() 之后自动创建一个线程执行 func

boost::signals2 本身不会在这里自动创建 Poll 线程。线程是后面的:

cpp 复制代码
PollManager::start()

创建的。

3.2 return poll_signal_.connect(func); 在做什么

实现:

cpp 复制代码
boost::signals2::connection
PollManager::addPollThreadListener(const VoidFunc& func)
{
    boost::recursive_mutex::scoped_lock lock(signal_mutex_);
    return poll_signal_.connect(func);
}

其中:

cpp 复制代码
poll_signal_.connect(func)

表示:

text 复制代码
把 func 注册到 poll_signal_ 的 slot 列表

返回值:

cpp 复制代码
boost::signals2::connection

是这次连接的"句柄"。以后可以:

cpp 复制代码
c.disconnect();

取消这一条回调连接。

ConnectionManager 就保存了这个返回值:

cpp 复制代码
poll_conn_ = poll_manager_->addPollThreadListener(
    boost::bind(&ConnectionManager::removeDroppedConnections, this));

shutdown 时再:

cpp 复制代码
poll_manager_->removePollThreadListener(poll_conn_);

3.3 checkForShutdown 为什么先注册,线程却后启动

现在:

cpp 复制代码
PollManager::instance()->addPollThreadListener(checkForShutdown);

只是:

text 复制代码
把 checkForShutdown 挂到 poll_signal_

这时 PollManager 的线程还没有启动。

后面 start() 执行到:

cpp 复制代码
PollManager::instance()->start();

才真正创建线程。

所以顺序是:

text 复制代码
先注册 listener
    ↓
再启动 Poll 线程
    ↓
Poll 线程每轮执行 poll_signal_()
    ↓
checkForShutdown() 才开始被周期性触发

这和第 06 章的 shutdown 路径正好接起来:

text 复制代码
SIGINT
  -> basicSigintHandler()
  -> requestShutdown()
  -> g_shutdown_requested = true

Poll 线程下一轮
  -> poll_signal_()
  -> checkForShutdown()
  -> ros::shutdown()

4. XMLRPCManager::bind("shutdown", ...):Node API 在 server 启动之前就能先注册

下一行:

cpp 复制代码
XMLRPCManager::instance()->bind("shutdown", shutdownCallback);

这里很容易产生一个疑问:

text 复制代码
XMLRPCManager::start() 明明还没执行,为什么现在就能 bind?

因为 bind() 做的不是"开始监听端口",而是先往 XmlRpcServer 对象中登记:

text 复制代码
XML-RPC 方法名
    -> 对应的 C++ callback

核心实现:

cpp 复制代码
bool XMLRPCManager::bind(const std::string& function_name,
                         const XMLRPCFunc& cb)
{
    boost::mutex::scoped_lock lock(functions_mutex_);

    if (functions_.find(function_name) != functions_.end())
    {
        return false;
    }

    FunctionInfo info;
    info.name = function_name;
    info.function = cb;
    info.wrapper.reset(
        new XMLRPCCallWrapper(function_name, cb, &server_));

    functions_[function_name] = info;
    return true;
}

XMLRPCFunc 的类型是:

cpp 复制代码
typedef boost::function<
    void(XmlRpc::XmlRpcValue&, XmlRpc::XmlRpcValue&)
> XMLRPCFunc;

也就是说所有 roscpp Node API callback 都统一成:

cpp 复制代码
void callback(XmlRpcValue& params,
              XmlRpcValue& result);

XMLRPCCallWrapper 再把这个 boost::function 包装成 xmlrpcpp 能识别的 XmlRpcServerMethod

cpp 复制代码
class XMLRPCCallWrapper : public XmlRpcServerMethod
{
public:
    ...

    void execute(XmlRpcValue& params,
                 XmlRpcValue& result)
    {
        func_(params, result);
    }
};

XmlRpcServerMethod 构造时会直接:

cpp 复制代码
_server->addMethod(this);

因此:

text 复制代码
XMLRPCManager::bind("shutdown", shutdownCallback)
    ↓
创建 XMLRPCCallWrapper
    ↓
XmlRpcServerMethod 构造
    ↓
server_.addMethod(...)
    ↓
shutdown 方法已经进入 server 的 method table

但这时还没有 socket 监听。

监听端口要等:

cpp 复制代码
XMLRPCManager::start()

5. TopicManager::start():这里一次绑定了六个 Node API

ros::start() 接下来进入:

cpp 复制代码
TopicManager::instance()->start();

源码:

cpp 复制代码
void TopicManager::start()
{
    boost::mutex::scoped_lock shutdown_lock(shutting_down_mutex_);
    shutting_down_ = false;

    poll_manager_ = PollManager::instance();
    connection_manager_ = ConnectionManager::instance();
    xmlrpc_manager_ = XMLRPCManager::instance();

    xmlrpc_manager_->bind(
        "publisherUpdate",
        boost::bind(&TopicManager::pubUpdateCallback,
                    this,
                    boost::placeholders::_1,
                    boost::placeholders::_2));

    xmlrpc_manager_->bind(
        "requestTopic",
        boost::bind(&TopicManager::requestTopicCallback,
                    this,
                    boost::placeholders::_1,
                    boost::placeholders::_2));

    xmlrpc_manager_->bind("getBusStats", ...);
    xmlrpc_manager_->bind("getBusInfo", ...);
    xmlrpc_manager_->bind("getSubscriptions", ...);
    xmlrpc_manager_->bind("getPublications", ...);

    poll_manager_->addPollThreadListener(
        boost::bind(&TopicManager::processPublishQueues, this));
}

这一段非常关键,因为本章后面出现的:

text 复制代码
publisherUpdate
requestTopic

并不是 Master 进程里的函数,而是每个 roscpp Node 自己在这里暴露出去的 Node API

5.1 boost::bind(... _1, _2) 到底在绑定什么

先看:

cpp 复制代码
boost::bind(&TopicManager::pubUpdateCallback,
            this,
            boost::placeholders::_1,
            boost::placeholders::_2)

&TopicManager::pubUpdateCallback 是一个成员函数指针。

this 表示:

text 复制代码
将来调用这个成员函数时,固定使用当前 TopicManager 对象

而:

cpp 复制代码
boost::placeholders::_1
boost::placeholders::_2

不是两个实际参数,它们是"将来调用这个函数对象时,第 1、第 2 个实参放到这里"的占位符。

可以把它近似理解成 C++11 lambda:

cpp 复制代码
[this](XmlRpc::XmlRpcValue& params,
       XmlRpc::XmlRpcValue& result)
{
    this->pubUpdateCallback(params, result);
}

因此:

cpp 复制代码
boost::bind(&TopicManager::pubUpdateCallback,
            this, _1, _2)

最终生成的是一个符合:

cpp 复制代码
void(XmlRpcValue&, XmlRpcValue&)

签名的可调用对象,正好能装进:

cpp 复制代码
XMLRPCFunc

5.2 六个 Node API 分别干什么

这里先只建立职责:

Node API 主要作用
publisherUpdate Master 通知 Subscriber:某 Topic 的 Publisher 列表变化了
requestTopic Subscriber 向 Publisher 协商实际数据传输协议和地址
getBusStats 查询当前通信统计
getBusInfo 查询当前连接信息
getSubscriptions 查询当前 Node 的订阅列表
getPublications 查询当前 Node 的发布列表

其中这一章重点追:

text 复制代码
publisherUpdate
requestTopic

其它四个知道它们是 Node API 即可。

5.3 processPublishQueues 为什么也挂到 Poll 线程

最后:

cpp 复制代码
poll_manager_->addPollThreadListener(
    boost::bind(&TopicManager::processPublishQueues, this));

这意味着以后 Poll 线程每轮执行:

cpp 复制代码
poll_signal_();

时,会调用:

cpp 复制代码
TopicManager::processPublishQueues()

实现:

cpp 复制代码
void TopicManager::processPublishQueues()
{
    boost::recursive_mutex::scoped_lock lock(advertised_topics_mutex_);

    for (...)
    {
        const PublicationPtr& pub = *it;
        pub->processPublishQueue();
    }
}

也就是说 Publisher 跨进程发送路径中的"待发送消息推进",并不是只靠用户调用 Publisher::publish() 的线程完成。

后面讲 Publication::publish() 时会重新回到这里。


6. ServiceManager::start():它本身没有再创建线程

下一步:

cpp 复制代码
ServiceManager::instance()->start();

实现其实很短:

cpp 复制代码
void ServiceManager::start()
{
    shutting_down_ = false;

    poll_manager_ = PollManager::instance();
    connection_manager_ = ConnectionManager::instance();
    xmlrpc_manager_ = XMLRPCManager::instance();
}

它主要是把后面需要用到的 Manager 单例保存下来。

这里要注意:

text 复制代码
ServiceManager::start()

并不等于:

text 复制代码
新建一个 ServiceManager 线程

Service 的网络连接还是依赖:

text 复制代码
ConnectionManager
PollManager

而 Service 注册发现仍然要通过:

text 复制代码
Master

真正注册内部 Service 的动作要等 ros::start() 后半段。


7. ConnectionManager::start():把 TCPROS/UDPROS 的监听入口真正建起来

接下来:

cpp 复制代码
ConnectionManager::instance()->start();

实现:

cpp 复制代码
void ConnectionManager::start()
{
    poll_manager_ = PollManager::instance();

    poll_conn_ = poll_manager_->addPollThreadListener(
        boost::bind(&ConnectionManager::removeDroppedConnections,
                    this));

    tcpserver_transport_ = boost::make_shared<TransportTCP>(
        &poll_manager_->getPollSet());

    tcpserver_transport_->listen(
        network::getTCPROSPort(),
        MAX_TCPROS_CONN_QUEUE,
        boost::bind(&ConnectionManager::tcprosAcceptConnection,
                    this,
                    boost::placeholders::_1));

    udpserver_transport_ = boost::make_shared<TransportUDP>(
        &poll_manager_->getPollSet());

    udpserver_transport_->createIncoming(0, true);
}

这里做三件事。

7.1 把"已掉线连接清理"注册到 Poll 线程

cpp 复制代码
poll_conn_ = poll_manager_->addPollThreadListener(
    boost::bind(&ConnectionManager::removeDroppedConnections, this));

以后每轮:

text 复制代码
poll_signal_()

都会有机会进入:

cpp 复制代码
ConnectionManager::removeDroppedConnections()

把已经被标记为 dropped 的 Connection 从连接集合中移除。

7.2 创建 TCPROS server transport

cpp 复制代码
tcpserver_transport_->listen(...)

会建立 TCP 监听 socket,并把它纳入:

cpp 复制代码
PollManager::getPollSet()

当新 TCP 连接到来时,回调:

cpp 复制代码
ConnectionManager::tcprosAcceptConnection(...)

再创建:

cpp 复制代码
Connection

并等待 connection header,最后依据 header 中有没有:

text 复制代码
topic
service

分别创建:

text 复制代码
TransportSubscriberLink
ServiceClientLink

这些连接细节在 08/11 再深入。

这一章先记住:

ConnectionManager::start() 让当前 Node 拥有了一个真正可被其它 Node 连接的 TCPROS/Service TCP 监听入口。

7.3 UDP 监听也在这里准备

cpp 复制代码
udpserver_transport_->createIncoming(0, true);

为 UDPROS 准备入口。

当前 /chatter 默认走 TCPROS,因此后面主要追 TCP 路径。


8. PollManager::start():100 ms 到底是什么意思

现在终于执行:

cpp 复制代码
PollManager::instance()->start();

源码:

cpp 复制代码
void PollManager::start()
{
    shutting_down_ = false;
    thread_ = boost::thread(&PollManager::threadFunc, this);
}

真正线程函数:

cpp 复制代码
void PollManager::threadFunc()
{
    disableAllSignalsInThisThread();

    while (!shutting_down_)
    {
        {
            boost::recursive_mutex::scoped_lock lock(signal_mutex_);
            poll_signal_();
        }

        if (shutting_down_)
        {
            return;
        }

        poll_set_.update(100);
    }
}

这里要修正一个非常容易形成的错误印象:

text 复制代码
PollManager 并不是"每 100 ms 执行一次 listener"。

真实顺序是:

text 复制代码
while
  ↓
poll_signal_()
  ↓
同步调用当前所有 Poll listener
  ↓
poll_set_.update(100)
  ↓
最多阻塞 100 ms 等待 fd 事件
  ↓
下一轮

100 是:

text 复制代码
PollSet I/O 轮询的最大阻塞超时,单位 ms

如果:

text 复制代码
socket 事件提前到达

或者其它线程调用:

cpp 复制代码
poll_set_.signal();

Poll 会提前被唤醒,并不一定真的睡满 100 ms。

8.1 当前 poll_signal_ 上已经注册了哪些回调

到目前为止至少有:

text 复制代码
checkForShutdown
TopicManager::processPublishQueues
ConnectionManager::removeDroppedConnections

所以 Poll 线程不只是:

text 复制代码
"看看要不要 shutdown"

它同时承担:

text 复制代码
推进 Publisher 发送队列
清理掉线连接
驱动 socket fd 事件

8.2 PollSet::update(100) 又做了什么

PollSet::update() 最终对已经注册的 socket 执行底层 poll/epoll 等等待;如果某个 fd 出现:

text 复制代码
POLLIN
POLLOUT
POLLERR
POLLHUP
...

就取出这个 fd 对应的 SocketUpdateFunc 并执行。

PollSet::signal() 使用内部 signal pipe 唤醒阻塞中的 poll,所以后面:

cpp 复制代码
TopicManager::publish()

在有序列化消息需要发送时会调用:

cpp 复制代码
poll_manager_->getPollSet().signal();

让 Poll 线程尽快来处理新的 publish queue,而不是傻等 100 ms 超时。


9. XMLRPCManager::start():当前 Node 的 XML-RPC server 真正开始监听

接下来:

cpp 复制代码
XMLRPCManager::instance()->start();

源码:

cpp 复制代码
void XMLRPCManager::start()
{
    shutting_down_ = false;
    port_ = 0;

    bind("getPid", getPid);

    bool bound = server_.bindAndListen(0);
    ROS_ASSERT(bound);

    port_ = server_.get_port();
    ROS_ASSERT(port_ != 0);

    std::stringstream ss;
    ss << "http://"
       << network::getHost()
       << ":"
       << port_
       << "/";

    uri_ = ss.str();

    server_thread_ = boost::thread(
        boost::bind(&XMLRPCManager::serverThreadFunc, this));
}

这里要分四步看。

9.1 getPid 也是 Node API

先:

cpp 复制代码
bind("getPid", getPid);

所以后面:

bash 复制代码
rosnode ping /hello_node

这类工具能够拿到 Node URI 后再访问 Node 的 XML-RPC API。

9.2 bindAndListen(0) 为什么传 0

进入 xmlrpcpp:

cpp 复制代码
bool XmlRpcServer::bindAndListen(int port, int backlog)
{
    int fd = XmlRpcSocket::socket();

    setfd(fd);
    XmlRpcSocket::setNonBlocking(fd);
    XmlRpcSocket::setReuseAddr(fd);
    XmlRpcSocket::bind(fd, port);
    XmlRpcSocket::listen(fd, backlog);

    _port = XmlRpcSocket::get_port(fd);

    _disp.addSource(this, XmlRpcDispatch::ReadableEvent);
    return true;
}

port = 0 是让 OS 分配一个当前可用端口。

因此每个 roscpp Node 的 XML-RPC Node API 地址通常是:

text 复制代码
http://<node-host>:<dynamic-port>/

它和固定的:

text 复制代码
ROS Master: 11311

不是一回事。

9.3 serverThreadFunc() 才真正持续处理 Node API 请求

线程入口:

cpp 复制代码
void XMLRPCManager::serverThreadFunc()
{
    disableAllSignalsInThisThread();

    while (!shutting_down_)
    {
        // 加入新的异步 XML-RPC connection
        ...

        {
            boost::mutex::scoped_lock lock(functions_mutex_);
            server_.work(0.1);
        }

        ...

        // 检查异步 XML-RPC connection 是否完成
        ...

        // 清理需要移除的异步 connection
        ...
    }
}

源码注释已经写得很直接:

text 复制代码
server_.work(0.1)
最多在 select() 中阻塞 100 ms

当远端发送:

text 复制代码
publisherUpdate
requestTopic
shutdown
getPid
getBusInfo
...

xmlrpcpp 会找到之前 bind() 注册进 method table 的 XMLRPCCallWrapper,然后:

text 复制代码
XmlRpcServerMethod::execute()
    ↓
XMLRPCCallWrapper::execute(params, result)
    ↓
func_(params, result)
    ↓
TopicManager::pubUpdateCallback(...)
或其它具体 callback

9.4 为什么里面还有 ASyncXMLRPCConnection

后面 Subscriber 要调用 Publisher:

text 复制代码
requestTopic

roscpp 没有把这个协商写成一个会长期阻塞用户线程的同步过程,而是使用:

text 复制代码
executeNonBlock("requestTopic", ...)
    ↓
PendingConnection
    ↓
XMLRPCManager::addASyncConnection(...)

serverThreadFunc() 每轮会同时检查这些异步 XML-RPC connection 是否完成。

所以 XMLRPCManager 同时承担两类职责:

text 复制代码
Server:外部调用当前 Node 暴露的 Node API
Client 管理:当前 Node 调用 Master 或其它 Node 的 XML-RPC API

10. ROSOutAppenderROS_INFO 为什么最终能变成 /rosout Topic

ros::start() 在 Manager 都运行以后继续:

cpp 复制代码
if (!(no_rosout ||
      (g_init_options & init_options::NoRosout)))
{
    g_rosout_appender = new ROSOutAppender;
    ros::console::register_appender(g_rosout_appender);
}

这里不是启动 /rosout Node;而是在当前 roscpp Node 内部注册一个 rosconsole appender。

源码:

cpp 复制代码
ROSOutAppender::ROSOutAppender()
: shutting_down_(false)
, disable_topics_(false)
, publish_thread_(boost::bind(&ROSOutAppender::logThread, this))
{
    AdvertiseOptions ops;
    ops.init<rosgraph_msgs::Log>(
        names::resolve("/rosout"), 0);

    ops.latch = true;

    SubscriberCallbacksPtr cbs(
        boost::make_shared<SubscriberCallbacks>());

    TopicManager::instance()->advertise(ops, cbs);
}

这里同时发生两件事:

text 复制代码
publish_thread_
    -> 建立 ROSOutAppender 自己的日志发布线程

TopicManager::advertise(/rosout)
    -> 当前 Node 本身成为 /rosout 的 Publisher

因此系统中每个正常启用 rosout 的 roscpp Node 都会发布:

text 复制代码
/rosout

而名为:

text 复制代码
/rosout

的那个独立 Node 再负责汇聚这些日志。

10.2 ROSOutAppender::log() 先把日志放入自己的 queue

当 rosconsole 调 appender:

cpp 复制代码
void ROSOutAppender::log(...)

它会构造:

cpp 复制代码
rosgraph_msgs::Log

填入:

text 复制代码
level
name
msg
file
function
line
topics

然后:

cpp 复制代码
log_queue_.push_back(msg);
queue_condition_.notify_all();

这时只是放入日志队列。

10.3 logThread() 再真正调用 TopicManager 发布

cpp 复制代码
void ROSOutAppender::logThread()
{
    while (!shutting_down_)
    {
        V_Log local_queue;

        {
            boost::mutex::scoped_lock lock(queue_mutex_);

            if (log_queue_.empty())
            {
                queue_condition_.wait(lock);
            }

            local_queue.swap(log_queue_);
        }

        for (...)
        {
            TopicManager::instance()->publish(
                names::resolve("/rosout"), *(*it));
        }
    }
}

因此:

text 复制代码
ROS_INFO(...)
    ↓
rosconsole
    ↓
ROSOutAppender::log()
    ↓
log_queue_
    ↓
ROSOutAppender::logThread()
    ↓
TopicManager::publish("/rosout", ...)

这里已经能看到同一个 Node 内至少存在:

text 复制代码
Poll 线程
XML-RPC server 线程
ROSOutAppender 日志线程
后面还有 internal callback queue 线程

这也是为什么 roscpp Node 不能简单理解成"只有 main + spin 那一个线程"。


11. ~get_loggers~set_logger_level 为什么在 start() 里自动出现

接下来这几段:

cpp 复制代码
{
    ros::AdvertiseServiceOptions ops;
    ops.init<roscpp::GetLoggers>(
        names::resolve("~get_loggers"), getLoggers);

    ops.callback_queue = getInternalCallbackQueue().get();
    ServiceManager::instance()->advertiseService(ops);
}

{
    ros::AdvertiseServiceOptions ops;
    ops.init<roscpp::SetLoggerLevel>(
        names::resolve("~set_logger_level"), setLoggerLevel);

    ops.callback_queue = getInternalCallbackQueue().get();
    ServiceManager::instance()->advertiseService(ops);
}

如果:

text 复制代码
Node = /hello_node

那么:

text 复制代码
~get_loggers
    -> /hello_node/get_loggers

~set_logger_level
    -> /hello_node/set_logger_level

这两个 Service 不是业务代码手工 advertise 的,而是 roscpp runtime 自动提供的内部 Service。

当:

text 复制代码
ROSCPP_ENABLE_DEBUG=true

还会额外注册:

text 复制代码
~debug/close_all_connections

11.1 ServiceManager::advertiseService() 逐层看

核心实现:

cpp 复制代码
bool ServiceManager::advertiseService(
    const AdvertiseServiceOptions& ops)
{
    boost::recursive_mutex::scoped_lock shutdown_lock(
        shutting_down_mutex_);

    if (shutting_down_)
    {
        return false;
    }

    {
        boost::mutex::scoped_lock lock(
            service_publications_mutex_);

        if (isServiceAdvertised(ops.service))
        {
            return false;
        }

        ServicePublicationPtr pub(
            boost::make_shared<ServicePublication>(
                ops.service,
                ops.md5sum,
                ops.datatype,
                ops.req_datatype,
                ops.res_datatype,
                ops.helper,
                ops.callback_queue,
                ops.tracked_object));

        service_publications_.push_back(pub);
    }

    XmlRpcValue args, result, payload;
    args[0] = this_node::getName();
    args[1] = ops.service;

    char uri_buf[1024];
    std::snprintf(uri_buf, sizeof(uri_buf),
                  "rosrpc://%s:%d",
                  network::getHost().c_str(),
                  connection_manager_->getTCPPort());

    args[2] = string(uri_buf);
    args[3] = xmlrpc_manager_->getServerURI();

    master::execute(
        "registerService", args, result, payload, true);

    return true;
}

把四个参数拆开:

text 复制代码
args[0]
    当前 Node 名,例如 /hello_node

args[1]
    Service 名,例如 /hello_node/get_loggers

args[2]
    rosrpc://host:tcp-port
    真正 Service 客户端后续连接的 ROSRPC 数据入口

args[3]
    http://host:xmlrpc-port/
    当前 Node 的 XML-RPC Node API URI

然后:

cpp 复制代码
master::execute("registerService", ...)

向 Master 注册。

这里已经能看出 Service 和 Topic 的共同设计:

text 复制代码
Master 保存"服务名与提供方"
真正请求/响应并不经 Master 转发

Service 详细建链留到第 11 章。


12. g_internal_callback_queue 到底干嘛:不是"发布 start 成功通知"

源码中可以看到:

cpp 复制代码
static CallbackQueuePtr g_internal_callback_queue;
static boost::thread g_internal_queue_thread;

又会看到:

cpp 复制代码
g_internal_queue_thread =
    boost::thread(internalCallbackQueueThreadFunc);

如果只搜索:

text 复制代码
g_internal_callback_queue->addCallback(...)

源码阅读时容易产生"没有看到 callback 入队位置"的疑问。

原因是很多地方不是直接访问变量,而是把:

cpp 复制代码
getInternalCallbackQueue().get()

作为 callback queue 指针交给其它对象。

12.1 内部 queue 是懒创建的

cpp 复制代码
CallbackQueuePtr getInternalCallbackQueue()
{
    if (!g_internal_callback_queue)
    {
        g_internal_callback_queue.reset(new CallbackQueue);
    }

    return g_internal_callback_queue;
}

第一次有人请求它时才创建。

12.2 start() 里已经有明确使用者

前面两个内部 Service:

cpp 复制代码
ops.callback_queue = getInternalCallbackQueue().get();

所以它们的 Service callback 最后不是进入业务代码使用的:

text 复制代码
g_global_queue

而是进入:

text 复制代码
g_internal_callback_queue

如果启用模拟时间:

cpp 复制代码
ops.init<rosgraph_msgs::Clock>(
    names::resolve("/clock"),
    1,
    clockCallback);

ops.callback_queue = getInternalCallbackQueue().get();
TopicManager::instance()->subscribe(ops);

/clock 的 callback 也进入内部 queue。

除此之外,roscpp 内部的重连 timer 等机制也会使用这条 queue。例如 TCPROS publisher link 掉线重试时,内部 timer callback 会绑定到:

cpp 复制代码
getInternalCallbackQueue().get()

12.3 独立线程怎样处理它

cpp 复制代码
void internalCallbackQueueThreadFunc()
{
    disableAllSignalsInThisThread();

    CallbackQueuePtr queue = getInternalCallbackQueue();

    while (!g_shutting_down)
    {
        queue->callAvailable(WallDuration(0.1));
    }
}

所以这条内部线程的核心目的可以概括为:

保证 roscpp 自己必须工作的 callback,不依赖用户是否调用 ros::spin()

例如即使用户业务代码一时没有处理 global callback queue,roscpp 仍然需要能够处理:

text 复制代码
日志级别 Service
/clock
某些内部 timer/retry

因此它和:

text 复制代码
"对外发布 start 成功通知"

没有关系。

ros::start() 最后那条:

cpp 复制代码
ROSCPP_LOG_DEBUG("Started node ...")

只是日志,不是 g_internal_callback_queue 的用途。


13. 回到业务代码:nh.advertise() 从模板 API 一直走到 registerPublisher

前面只是把 roscpp runtime 启动好。现在回到当前业务代码:

cpp 复制代码
ros::Publisher publisher =
    nh.advertise<std_msgs::String>("chatter", 10);

这时才开始建立 /chatter Publisher。

13.1 AdvertiseOptions::init<M>() 先把消息类型信息补全

NodeHandle::advertise<M>() 最终会构造 AdvertiseOptions,其中一个关键模板辅助函数是:

cpp 复制代码
template <class M>
void init(const std::string& _topic,
          uint32_t _queue_size,
          const SubscriberStatusCallback& _connect_cb =
              SubscriberStatusCallback(),
          const SubscriberStatusCallback& _disconnect_cb =
              SubscriberStatusCallback())
{
    topic = _topic;
    queue_size = _queue_size;
    connect_cb = _connect_cb;
    disconnect_cb = _disconnect_cb;

    md5sum = message_traits::md5sum<M>();
    datatype = message_traits::datatype<M>();
    message_definition = message_traits::definition<M>();
    has_header = message_traits::hasHeader<M>();
}

对于:

cpp 复制代码
M = std_msgs::String

模板在编译期就知道消息类型,因此可以从 message_traits 中取得:

text 复制代码
md5sum
    ROS1 消息兼容性标识

datatype
    例如 std_msgs/String

message_definition
    完整 .msg 定义文本

has_header
    这个消息是否包含 std_msgs/Header

因此:

cpp 复制代码
nh.advertise<std_msgs::String>("chatter", 10)

不只是保存:

text 复制代码
topic = chatter
queue_size = 10

它还把后续注册、连接 header 校验、序列化需要的消息元信息一起装进 AdvertiseOptions

13.2 NodeHandle::advertise(AdvertiseOptions&)

真正进入非模板实现:

cpp 复制代码
Publisher NodeHandle::advertise(AdvertiseOptions& ops)
{
    ops.topic = resolveName(ops.topic);

    if (ops.callback_queue == 0)
    {
        if (callback_queue_)
        {
            ops.callback_queue = callback_queue_;
        }
        else
        {
            ops.callback_queue = getGlobalCallbackQueue();
        }
    }

    SubscriberCallbacksPtr callbacks(
        boost::make_shared<SubscriberCallbacks>(
            ops.connect_cb,
            ops.disconnect_cb,
            ops.tracked_object,
            ops.callback_queue));

    Publisher pub(
        ops.topic,
        ops.md5sum,
        ops.datatype,
        ops.latch,
        *this,
        callbacks);

    if (ops.latch)
    {
        callbacks->push_latched_message_ =
            pub.getLastMessageCallback();
    }

    if (TopicManager::instance()->advertise(ops, callbacks))
    {
        ...
        return pub;
    }

    return Publisher();
}

按顺序看。

第一步:Topic 名先解析
cpp 复制代码
ops.topic = resolveName(ops.topic);

当前没有 namespace/remap 时:

text 复制代码
chatter
   ↓
/chatter

也就是说,后面 TopicManager 和 Master 看到的是解析后的名字。

第二步:确定 Publisher status callback 用哪条 queue

如果用户没有指定 callback queue:

cpp 复制代码
ops.callback_queue = getGlobalCallbackQueue();

这里的 callback 不是消息接收 callback,而是:

text 复制代码
Subscriber 连接时的 connect callback
Subscriber 断开时的 disconnect callback

当前 hello_node 没设置它们,因此现在知道这个默认行为即可。

第三步:构造 Publisher
cpp 复制代码
Publisher pub(...);

Publisher 本身更像一个轻量 handle;真正 Topic 级共享状态后面在 TopicManager::advertise() 里形成 Publication

13.3 getLastMessageCallback():latch 是怎样接进来的

需要重点关注:

cpp 复制代码
boost::function<void(const SubscriberLinkPtr&)>
getLastMessageCallback()
{
    return boost::bind(
        &Impl::pushLastMessage,
        impl_.get(),
        boost::placeholders::_1);
}

它可以近似理解为:

cpp 复制代码
[sub_impl = impl_.get()](const SubscriberLinkPtr& link)
{
    sub_impl->pushLastMessage(link);
}

如果:

cpp 复制代码
ops.latch = true;

NodeHandle::advertise() 会把这个回调保存到:

cpp 复制代码
callbacks->push_latched_message_

后面 Publication::peerConnect() 发现新 Subscriber 时:

cpp 复制代码
if (cbs->push_latched_message_)
{
    cbs->push_latched_message_(sub_link);
}

最终进入:

cpp 复制代码
Publisher::Impl::pushLastMessage(...)

把之前保存的最后一条消息重新 enqueue 给新 Subscriber。

当前 /chatter

text 复制代码
latch = false

所以这条路径不会实际触发,但它解释了为什么 Publisher 要把 getLastMessageCallback() 暴露给 NodeHandle

13.4 TopicManager::advertise():先建立 Publication,再向 Master 注册

接下来:

cpp 复制代码
TopicManager::instance()->advertise(ops, callbacks)

这个函数比较长,但当前只需要按真实执行顺序抓主干。

首先检查:

text 复制代码
datatype
md5sum
message_definition

是否合法。

然后查当前 Node 内是否已经有同名 Publication

没有的话创建:

cpp 复制代码
PublicationPtr pub(
    boost::make_shared<Publication>(
        ops.topic,
        ops.datatype,
        ops.md5sum,
        ops.message_definition,
        ops.queue_size,
        false,
        ops.has_header));

再:

cpp 复制代码
pub->addCallbacks(callbacks);
advertised_topics_.push_back(pub);

因此这里的:

text 复制代码
Publication

才是 TopicManager 维护的"这个 Topic 在当前进程中的发布实体"。

Publisher handle 可以有多份,而同一个 Topic 的 Publication 是 TopicManager 用来集中维护 subscriber links、发送队列、序列号等状态的对象。

13.5 为什么 advertise() 里还检查"自己是否也订阅了同一个 Topic"

源码会扫描:

cpp 复制代码
subscriptions_

如果当前同一个进程已经订阅同名且 md5sum 匹配的 Topic:

cpp 复制代码
sub->addLocalConnection(pub);

这样可以建立:

text 复制代码
intra-process connection

而不是等 Master 回调自己的 XML-RPC URI 再兜一圈网络协议。

当前:

text 复制代码
hello_node
hello_listener

是两个独立进程,因此当前示例不走这条 local connection;源码仍然包含该分支,需要明确它不属于 TCPROS 路径。

13.6 最后才是 registerPublisher

函数末尾:

cpp 复制代码
XmlRpcValue args, result, payload;

args[0] = this_node::getName();
args[1] = ops.topic;
args[2] = ops.datatype;
args[3] = xmlrpc_manager_->getServerURI();

master::execute(
    "registerPublisher",
    args,
    result,
    payload,
    true);

当前大致是:

text 复制代码
caller_id
    /hello_node

topic
    /chatter

topic_type
    std_msgs/String

caller_api
    http://<host>:<hello_node XMLRPC port>/

注意第四个参数仍然是:

text 复制代码
Node XML-RPC API URI

不是 TCPROS port。

master::execute() 再创建/复用指向 ROS_MASTER_URI 的 XML-RPC client,调用 Master API:

text 复制代码
registerPublisher

13.7 master::execute() 里面又做了什么

master.cpp 的主干是:

text 复制代码
master::execute(method, request, response, payload, wait_for_master)
    ↓
master::getHost() / getPort()
    ↓
XMLRPCManager::getXMLRPCClient(master_host, master_port, "/")
    ↓
XmlRpcClient::execute(method, request, response)
    ↓
XMLRPCManager::validateXmlrpcResponse(...)
    ↓
取出 response[2] 到 payload
    ↓
XMLRPCManager::releaseXMLRPCClient(...)

这里有两个细节。

第一,roscpp 会缓存并复用 XML-RPC client。getXMLRPCClient() 会先在 clients_ 中找:

text 复制代码
host + port + uri 都相同
并且当前不在使用

的 client;没有才新建。

第二,最后一个参数:

cpp 复制代码
wait_for_master = true

表示 Master 暂时不可达时,master::execute() 会按 roscpp 的重试规则等待/重试,而不是第一次连接失败就立刻返回。registerPublisherregisterSubscriber 这类启动关键注册通常使用 true

XML-RPC 返回值还要通过:

cpp 复制代码
validateXmlrpcResponse()

验证 ROS1 约定的:

text 复制代码
[status_code, status_message, payload]

只有:

text 复制代码
status_code == 1

才视为成功。

所以 Publisher 注册主链现在已经完整:

text 复制代码
nh.advertise<std_msgs::String>()
    ↓
AdvertiseOptions::init<std_msgs::String>()
    ↓
NodeHandle::advertise()
    ↓
resolveName()
    ↓
构造 Publisher handle
    ↓
TopicManager::advertise()
    ↓
创建/复用 Publication
    ↓
master::execute("registerPublisher")
    ↓
ROS Master

14. publisher.publish(msg):从用户 API 到 Publication,再交给 Poll 线程

当前业务代码:

cpp 复制代码
publisher.publish(msg);

要分清模板入口和非模板实现。

14.1 当前 hello_node 实际走的是 publish(const M&)

因为:

cpp 复制代码
std_msgs::String msg;
publisher.publish(msg);

传的是普通对象,而不是 boost::shared_ptr<M>

模板大致是:

cpp 复制代码
template <typename M>
void publish(const M& message) const
{
    SerializedMessage m;

    publish(
        boost::bind(
            serializeMessage<M>,
            boost::ref(message)),
        m);
}

这里很重要:

text 复制代码
serializeMessage<M>

被包装成 serfunc,并没有在模板入口立即执行。

这是一个"延迟序列化"的设计。

如果当前 Topic:

text 复制代码
没有 Subscriber
并且不是 latched Publisher

后面 TopicManager::publish() 可以直接不序列化,从而省掉无意义工作。

而另一个 overload:

cpp 复制代码
publish(const boost::shared_ptr<M>& message)

会额外把:

cpp 复制代码
m.type_info = &typeid(M);
m.message = message;

保存下来,从而给同进程 nocopy 路径机会。

当前 hello_node 的普通对象版本没有这些字段,因此跨进程路径一定需要序列化。

14.2 非模板 Publisher::publish() 很薄

cpp 复制代码
void Publisher::publish(
    const boost::function<SerializedMessage(void)>& serfunc,
    SerializedMessage& m) const
{
    ...

    TopicManager::instance()->publish(
        impl_->topic_,
        serfunc,
        m);

    if (isLatched())
    {
        boost::mutex::scoped_lock lock(
            impl_->last_message_mutex_);

        impl_->last_message_ = m;
    }
}

它主要做:

text 复制代码
检查 Publisher handle 是否有效
    ↓
交给 TopicManager::publish()
    ↓
如果 latch,保存最后一条消息

14.3 TopicManager::publish() 决定要不要序列化、要不要走 nocopy

关键逻辑:

cpp 复制代码
PublicationPtr p = lookupPublicationWithoutLock(topic);

if (p->hasSubscribers() || p->isLatching())
{
    bool nocopy = false;
    bool serialize = false;

    if (m.type_info && m.message)
    {
        p->getPublishTypes(serialize, nocopy, *m.type_info);
    }
    else
    {
        serialize = true;
    }

    if (!nocopy)
    {
        m.message.reset();
        m.type_info = 0;
    }

    if (serialize || p->isLatching())
    {
        SerializedMessage m2 = serfunc();
        m.buf = m2.buf;
        m.num_bytes = m2.num_bytes;
        m.message_start = m2.message_start;
    }

    p->publish(m);

    if (serialize)
    {
        poll_manager_->getPollSet().signal();
    }
}
else
{
    p->incrementSequence();
}

对于当前两个独立进程:

text 复制代码
hello_node
    -> hello_listener

是 transport subscriber,因此:

text 复制代码
serialize = true

于是调用:

cpp 复制代码
serfunc()

得到序列化后的 SerializedMessage

14.4 Publication::publish() 为什么没有立即遍历所有 TCP socket 写出去

实现:

cpp 复制代码
void Publication::publish(SerializedMessage& m)
{
    if (m.message)
    {
        ...
        // intra-process nocopy path
    }

    if (m.buf)
    {
        boost::mutex::scoped_lock lock(
            publish_queue_mutex_);

        publish_queue_.push_back(m);
    }
}

跨进程时有:

cpp 复制代码
m.buf

因此只是先进入:

text 复制代码
Publication::publish_queue_

然后前面 TopicManager::publish() 调:

cpp 复制代码
poll_manager_->getPollSet().signal();

唤醒 Poll 线程。

Poll 线程下一轮:

text 复制代码
poll_signal_()
    ↓
TopicManager::processPublishQueues()
    ↓
Publication::processPublishQueue()

processPublishQueue() 把内部 queue 换到局部变量,逐条执行:

cpp 复制代码
enqueueMessage(*it);

Publication::enqueueMessage() 再遍历:

text 复制代码
subscriber_links_

对每个 link:

cpp 复制代码
sub_link->enqueueMessage(m, true, false);

后面 transport link 怎样把消息进一步写进 Connection/TransportTCP,留到 08。

所以当前跨进程 Publisher 发送链应该理解成:

text 复制代码
用户 main 线程
Publisher::publish()
    ↓
TopicManager::publish()
    ↓
序列化
    ↓
Publication::publish()
    ↓
publish_queue_
    ↓
PollSet::signal()

Poll 线程被唤醒
    ↓
TopicManager::processPublishQueues()
    ↓
Publication::processPublishQueue()
    ↓
Publication::enqueueMessage()
    ↓
SubscriberLink::enqueueMessage()
    ↓
Connection / TransportTCP

这里已经能看出:

用户调用 Publisher::publish() 的线程与实际持续驱动网络发送的 Poll 线程不是同一个概念。

如果 Publisher 和 Subscriber 在同一个进程Publication::publish() 还可以处理:

text 复制代码
m.message != nullptr

的 nocopy 路径。

IntraProcessSubscriberLink::enqueueMessage()

cpp 复制代码
void IntraProcessSubscriberLink::enqueueMessage(
    const SerializedMessage& m,
    bool ser,
    bool nocopy)
{
    ...
    subscriber_->handleMessage(m, ser, nocopy);
}

这里的 subscriber_ 实际指向配对的:

text 复制代码
IntraProcessPublisherLink

然后:

cpp 复制代码
void IntraProcessPublisherLink::handleMessage(
    const SerializedMessage& m,
    bool ser,
    bool nocopy)
{
    ...

    SubscriptionPtr parent = parent_.lock();

    if (parent)
    {
        stats_.drops_ += parent->handleMessage(
            m,
            ser,
            nocopy,
            header_.getValues(),
            shared_from_this());
    }
}

最后仍然汇入:

cpp 复制代码
Subscription::handleMessage(...)

这就是为什么 Subscription::handleMessage() 同时能接:

text 复制代码
TCPROS 收到的序列化消息
同进程 nocopy 消息

它后面再根据:

text 复制代码
ser
nocopy
m.type_info

决定是否需要反序列化/复用对象。

14.6 Subscription::handleMessage() 最终不是直接调用业务 callback

关键部分:

cpp 复制代码
info->subscription_queue_->push(
    info->helper_,
    deserializer,
    ...,
    &was_full);

if (was_full)
{
    ++drops;
}
else
{
    info->callback_queue_->addCallback(
        info->subscription_queue_,
        (uint64_t)info.get());
}

也就是说消息到达后先进入:

text 复制代码
SubscriptionQueue

然后这个 SubscriptionQueue 自己作为一个 CallbackInterface 被加入:

text 复制代码
CallbackQueue

只有 Spinner 后面真正从 CallbackQueue 取出它并调用:

cpp 复制代码
SubscriptionQueue::call()

才会:

text 复制代码
取队头消息
    ↓
deserialize()
    ↓
SubscriptionCallbackHelper::call(...)
    ↓
chatterCallback(...)

这条链正好把:

text 复制代码
网络 I/O
消息接收队列
CallbackQueue
业务 callback

分成了不同层。


15. Subscriber:从 nh.subscribe() 一直走到 requestTopic

当前代码:

cpp 复制代码
ros::Subscriber subscriber =
    nh.subscribe("chatter", 10, chatterCallback);

bare function 版本的模板入口如下:

cpp 复制代码
template<class M>
Subscriber subscribe(
    const std::string& topic,
    uint32_t queue_size,
    void(*fp)(const boost::shared_ptr<M const>&),
    const TransportHints& transport_hints = TransportHints())
{
    SubscribeOptions ops;
    ops.template init<M>(topic, queue_size, fp);
    ops.transport_hints = transport_hints;
    return subscribe(ops);
}

15.1 SubscribeOptions::init<M>() 做什么

它会填:

text 复制代码
topic
queue_size
md5sum
datatype
helper

这里的:

cpp 复制代码
helper

SubscriptionCallbackHelper,它把各种可能的业务 callback 形式统一包装起来。

后续 SubscriptionQueue::call() 不需要关心原始 callback 的具体声明形式,例如:

text 复制代码
普通函数
成员函数
boost::function
MessageEvent

只需要统一调用 helper。

15.2 NodeHandle::subscribe(SubscribeOptions&)

非模板实现:

cpp 复制代码
Subscriber NodeHandle::subscribe(SubscribeOptions& ops)
{
    ops.topic = resolveName(ops.topic);

    if (ops.callback_queue == 0)
    {
        if (callback_queue_)
        {
            ops.callback_queue = callback_queue_;
        }
        else
        {
            ops.callback_queue = getGlobalCallbackQueue();
        }
    }

    if (TopicManager::instance()->subscribe(ops))
    {
        Subscriber sub(ops.topic, *this, ops.helper);
        ...
        return sub;
    }

    return Subscriber();
}

和 Publisher 一样:

text 复制代码
先 resolveName
再确定 callback queue
再交给 TopicManager

当前 listener 没使用自定义 queue,因此:

text 复制代码
chatterCallback
最终进入 global callback queue

15.3 TopicManager::subscribe():先建立本地 Subscription

主干:

cpp 复制代码
bool TopicManager::subscribe(const SubscribeOptions& ops)
{
    ...

    SubscriptionPtr s(
        boost::make_shared<Subscription>(
            ops.topic,
            md5sum,
            datatype,
            ops.transport_hints));

    s->addCallback(
        ops.helper,
        ops.md5sum,
        ops.callback_queue,
        ops.queue_size,
        ops.tracked_object,
        ops.allow_concurrent_callbacks);

    if (!registerSubscriber(s, ops.datatype))
    {
        ...
        return false;
    }

    subscriptions_.push_back(s);
    return true;
}

Subscription::addCallback() 内会建立:

cpp 复制代码
SubscriptionQueue(
    name_,
    queue_size,
    allow_concurrent_callbacks)

这里的:

text 复制代码
queue_size = 10

约束的是等待业务 callback 处理的订阅消息队列容量

它不是:

text 复制代码
TCP socket recv buffer = 10

也不是:

text 复制代码
Master 只记 10 条消息

15.4 registerSubscriber():向 Master 注册,同时拿回已有 Publisher

cpp 复制代码
args[0] = this_node::getName();
args[1] = s->getName();
args[2] = datatype;
args[3] = xmlrpc_manager_->getServerURI();

master::execute(
    "registerSubscriber",
    args,
    result,
    payload,
    true);

当前大致是:

text 复制代码
registerSubscriber(
    /hello_listener,
    /chatter,
    std_msgs/String,
    http://<host>:<listener-xmlrpc-port>/
)

Master 的响应 payload 不是单纯:

text 复制代码
OK

而是:

text 复制代码
当前已经存在的 Publisher Node API URI 列表

roscpp 取出后:

cpp 复制代码
s->pubUpdate(pub_uris);

因此:

text 复制代码
Publisher 先启动

时,Subscriber 第一次发现 Publisher 并不依赖未来的 publisherUpdate 推送;registerSubscriber 的返回值已经足够。

15.5 Subscription::pubUpdate():比较"新列表"和"当前连接"

它会比较:

text 复制代码
Master 提供的 new_pubs

和当前:

text 复制代码
publisher_links_
pending_connections_

得到:

text 复制代码
需要删除的连接
需要新增的 Publisher URI

对新增项:

cpp 复制代码
negotiateConnection(xmlrpc_uri);

15.6 negotiateConnection():从这里开始直接找 Publisher,不再找 Master

默认可靠传输会准备:

text 复制代码
[["TCPROS"]]

再把 Publisher Node API URI:

text 复制代码
http://host:xmlrpc-port/

拆成 host/port,创建:

cpp 复制代码
XmlRpc::XmlRpcClient

并异步执行:

cpp 复制代码
executeNonBlock("requestTopic", params)

请求参数大意:

text 复制代码
caller_id
    /hello_listener

topic
    /chatter

protocols
    [["TCPROS"]]

这里最重要的一点是:

requestTopic 是 Subscriber 调 Publisher Node API,不是调用 Master。

请求被加入 XMLRPCManager 的异步连接集合,后面由 XML-RPC server thread 持续检查。

15.7 Publisher 收到 requestTopic

Publisher 的 TopicManager::start() 早已绑定:

text 复制代码
requestTopic

所以进入:

text 复制代码
TopicManager::requestTopicCallback()
    ↓
TopicManager::requestTopic()

TCPROS 分支返回:

cpp 复制代码
["TCPROS",
 network::getHost(),
 connection_manager_->getTCPPort()]

也就是:

text 复制代码
真正 TCPROS 数据入口的 host/port

注意此时出现了三个不同地址:

text 复制代码
Master XML-RPC
    http://...:11311/

Publisher Node API XML-RPC
    http://...:<dynamic-xmlrpc-port>/

Publisher TCPROS
    tcp://...:<tcpros-port>

15.8 Subscriber 的 pendingConnectionDone() 再建立 TCPROS

异步 requestTopic 完成后:

text 复制代码
Subscription::pendingConnectionDone()

验证 XML-RPC 响应,发现:

text 复制代码
proto_name == TCPROS

于是:

cpp 复制代码
TransportTCPPtr transport(...);
transport->connect(pub_host, pub_port);

再建立:

text 复制代码
Connection
TransportPublisherLink

加入:

text 复制代码
ConnectionManager
Subscription::publisher_links_

到这里双方已经互相发现,并开始进入真正 TCPROS 建链阶段。

后面的 connection header 和消息二进制格式就是第 08 章主线。


16. spinOnce()spin():为什么消息到了,业务 callback 还需要 Spinner

前面已经看到:

text 复制代码
Subscription::handleMessage()
    ↓
SubscriptionQueue
    ↓
CallbackQueue

这只说明 callback 已经排队

还需要有人执行 CallbackQueue。

16.1 spinOnce()

实现极短:

cpp 复制代码
void spinOnce()
{
    g_global_queue->callAvailable(
        ros::WallDuration());
}

默认构造的:

cpp 复制代码
ros::WallDuration()

是零超时。

所以可以先理解为:

text 复制代码
当前调用线程
    ↓
把 global callback queue 当前可执行的 callback 处理一轮
    ↓
立即返回

CallbackQueue::callAvailable() 会把共享 callback 列表移动到当前线程的 TLS callback 列表,然后持续:

text 复制代码
callOneCB()

直到这批 callback 处理完。

所以 spinOnce() 不是"只执行一个 callback"。

它更接近:

text 复制代码
处理当前这轮所有可用 callback,然后返回

16.2 spin()

cpp 复制代码
void spin()
{
    SingleThreadedSpinner s;
    spin(s);
}

void spin(Spinner& s)
{
    s.spin();
}

进入:

cpp 复制代码
void SingleThreadedSpinner::spin(CallbackQueue* queue)
{
    if (!queue)
    {
        queue = getGlobalCallbackQueue();
    }

    ros::WallDuration timeout(0.1f);
    ros::NodeHandle n;

    while (n.ok())
    {
        queue->callAvailable(timeout);
    }
}

因此:

text 复制代码
ros::spin()
    ↓
SingleThreadedSpinner
    ↓
global CallbackQueue
    ↓
callAvailable(0.1)
    ↓
等待/执行 callback
    ↓
ros::ok() 仍为 true 就继续

16.3 hello_listener 的完整 callback 路径

完成相关源码阅读后,listener 的 callback 路径可以串联为:

这里也解释了一个很关键的边界:

text 复制代码
Poll 线程/网络线程负责把消息送到 callback queue
Spinner 所在线程负责真正执行业务 callback

第 09 章会专门深入各种 Spinner 与多线程模型。


17. ROS Master 是否属于普通 roscpp Node:启动进程与线程模型

"Master Node"这一历史叫法容易与普通 roscpp Node 混淆。普通 roscpp Node 通常通过:

cpp 复制代码
ros::init()
ros::NodeHandle

建立运行时,而 ROS Master 并不走这条路径。

结论先说:

ROS Master 是 ROS1 核心基础设施中的 XML-RPC 注册/发现服务,不是通过 roscpp ros::init() + NodeHandle 创建的普通 ROS graph Node。

它有独立进程、固定 Master API、Parameter Server 状态和自己的 XML-RPC server。

17.1 roscore 本身先进入 Python roslaunch

roscore 脚本最后执行:

python 复制代码
import roslaunch
roslaunch.main(['roscore', '--core'] + sys.argv[1:])

入口关系较短,可压缩为:
#mermaid-svg-DtmZKfUej4Hufsae{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DtmZKfUej4Hufsae .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DtmZKfUej4Hufsae .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DtmZKfUej4Hufsae .error-icon{fill:#552222;}#mermaid-svg-DtmZKfUej4Hufsae .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DtmZKfUej4Hufsae .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DtmZKfUej4Hufsae .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DtmZKfUej4Hufsae .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DtmZKfUej4Hufsae .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DtmZKfUej4Hufsae .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DtmZKfUej4Hufsae .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DtmZKfUej4Hufsae .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DtmZKfUej4Hufsae .marker.cross{stroke:#333333;}#mermaid-svg-DtmZKfUej4Hufsae svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DtmZKfUej4Hufsae p{margin:0;}#mermaid-svg-DtmZKfUej4Hufsae .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DtmZKfUej4Hufsae .cluster-label text{fill:#333;}#mermaid-svg-DtmZKfUej4Hufsae .cluster-label span{color:#333;}#mermaid-svg-DtmZKfUej4Hufsae .cluster-label span p{background-color:transparent;}#mermaid-svg-DtmZKfUej4Hufsae .label text,#mermaid-svg-DtmZKfUej4Hufsae span{fill:#333;color:#333;}#mermaid-svg-DtmZKfUej4Hufsae .node rect,#mermaid-svg-DtmZKfUej4Hufsae .node circle,#mermaid-svg-DtmZKfUej4Hufsae .node ellipse,#mermaid-svg-DtmZKfUej4Hufsae .node polygon,#mermaid-svg-DtmZKfUej4Hufsae .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DtmZKfUej4Hufsae .rough-node .label text,#mermaid-svg-DtmZKfUej4Hufsae .node .label text,#mermaid-svg-DtmZKfUej4Hufsae .image-shape .label,#mermaid-svg-DtmZKfUej4Hufsae .icon-shape .label{text-anchor:middle;}#mermaid-svg-DtmZKfUej4Hufsae .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DtmZKfUej4Hufsae .rough-node .label,#mermaid-svg-DtmZKfUej4Hufsae .node .label,#mermaid-svg-DtmZKfUej4Hufsae .image-shape .label,#mermaid-svg-DtmZKfUej4Hufsae .icon-shape .label{text-align:center;}#mermaid-svg-DtmZKfUej4Hufsae .node.clickable{cursor:pointer;}#mermaid-svg-DtmZKfUej4Hufsae .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DtmZKfUej4Hufsae .arrowheadPath{fill:#333333;}#mermaid-svg-DtmZKfUej4Hufsae .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DtmZKfUej4Hufsae .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DtmZKfUej4Hufsae .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DtmZKfUej4Hufsae .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DtmZKfUej4Hufsae .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DtmZKfUej4Hufsae .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DtmZKfUej4Hufsae .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DtmZKfUej4Hufsae .cluster text{fill:#333;}#mermaid-svg-DtmZKfUej4Hufsae .cluster span{color:#333;}#mermaid-svg-DtmZKfUej4Hufsae div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DtmZKfUej4Hufsae .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DtmZKfUej4Hufsae rect.text{fill:none;stroke-width:0;}#mermaid-svg-DtmZKfUej4Hufsae .icon-shape,#mermaid-svg-DtmZKfUej4Hufsae .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DtmZKfUej4Hufsae .icon-shape p,#mermaid-svg-DtmZKfUej4Hufsae .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DtmZKfUej4Hufsae .icon-shape .label rect,#mermaid-svg-DtmZKfUej4Hufsae .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DtmZKfUej4Hufsae .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DtmZKfUej4Hufsae .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DtmZKfUej4Hufsae :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} roscore
roslaunch --core
rosmaster --core
ROSMasterHandler
rosgraph.xmlrpc.XmlRpcNode

因此:

text 复制代码
roscore
    ↓
roslaunch --core

17.2 roslaunch 再启动独立 rosmaster --core 进程

ROSLaunchRunner::_launch_master() 中:

text 复制代码
检查 Master 是否已经运行
    ↓
create_master_process(...)
    ↓
p.start()

create_master_process() 构造的命令是:

text 复制代码
rosmaster --core -p <port> -w <workers>

默认 port:

text 复制代码
11311

因此:

text 复制代码
hello_node 的第一个 NodeHandle

不会"顺手创建 Master"。

必须已经存在:

text 复制代码
roscore/rosmaster

或者由:

text 复制代码
roslaunch

在需要时启动 core。

17.3 rosmaster 进程内部怎样启动 XML-RPC server

rosmaster_main()

python 复制代码
master = rosmaster.master.Master(port, options.num_workers)
master.start()

while master.ok():
    time.sleep(.1)

Master.start()

python 复制代码
handler = rosmaster.master_api.ROSMasterHandler(
    self.num_workers)

master_node = rosgraph.xmlrpc.XmlRpcNode(
    self.port,
    handler)

master_node.start()

XmlRpcNode.start() 又会:

python 复制代码
_thread.start_new_thread(self.run, ())

所以 Master 的 XML-RPC server 运行在 Python 创建的独立线程中。

ThreadingXMLRPCServer 本身还能为并发 XML-RPC 请求建立处理线程。

同时 ROSMasterHandler 还有:

python 复制代码
MarkedThreadPool(num_workers)

用于执行类似:

text 复制代码
publisherUpdate
paramUpdate

这类向其它 Node 发出的通知任务。

因此不要把 Master 想成:

text 复制代码
单线程 while 循环,所有 Node 都排队等它转发消息

它本身就是一套独立的 Python XML-RPC 服务实现。

17.4 Parameter Server 就在同一个 rosmaster 进程里

ROSMasterHandler 构造中同时建立:

python 复制代码
self.reg_manager = RegistrationManager(...)

self.publishers
self.subscribers
self.services

self.param_server =
    rosmaster.paramserver.ParamDictionary(...)

所以 Noetic 中:

text 复制代码
Master API
Parameter Server API

虽然职责不同,但实际都由同一个 rosmaster XML-RPC server 对外提供。

17.5 /rosout 才是一个普通 core Node

roslaunch 在启动 Master 后还会:

text 复制代码
_launch_core_nodes()

其中会启动 /rosout 等 core Node。

因此:

text 复制代码
Master
    -> 基础设施 XML-RPC server

/rosout
    -> 正常注册到 Master 的 ROS Node

这也是为什么常见:

bash 复制代码
rosnode list

会看到:

text 复制代码
/rosout

而不是把 Master 当成普通 /master Node 列出来。


18. 名称解析、namespace、remap 与 private namespace:注册前已经完成

前面 NodeHandle::advertise()subscribe() 都先执行:

cpp 复制代码
resolveName(...)

所以名称解析发生在:

text 复制代码
向 TopicManager / Master 注册之前

Master 不负责替每个 Node 解析相对名和 remap。

18.1 全局名

text 复制代码
/chatter

/ 开头,已经是完整 graph resource name。

18.2 相对名

text 复制代码
chatter

需要结合当前 NodeHandle namespace。

例如 Node namespace:

text 复制代码
/lab

则普通 NodeHandle nh;

text 复制代码
chatter
    -> /lab/chatter

18.3 private namespace

cpp 复制代码
ros::NodeHandle pnh("~");

~ 基于最终 Node 全名。

例如:

text 复制代码
Node = /lab/talker

那么:

text 复制代码
~publish_rate
    -> /lab/talker/publish_rate

这就是当前:

cpp 复制代码
pnh.param("publish_rate", ...)

为什么最终是 Node 私有参数。

18.4 remap 在注册前就完成

例如启动:

bash 复制代码
rosrun ros1_hello hello_node \
    __ns:=/lab \
    __name:=talker \
    chatter:=telemetry \
    _publish_rate:=2.0

最终:

text 复制代码
Node
    /lab/talker

Topic
    /lab/telemetry

private parameter
    /lab/talker/publish_rate

其中:

text 复制代码
chatter:=telemetry

会在 names::init() / NodeHandle::resolveName() 的名称环境中生效。

等执行:

cpp 复制代码
TopicManager::advertise()

时,ops.topic 已经是:

text 复制代码
/lab/telemetry

Master 从来不会先保存:

text 复制代码
/lab/chatter

再执行名称修改。


19. Parameter 在本章掌握到 XML-RPC 机制边界

第 03 章已经学过:

text 复制代码
rosparam
pnh.param()
private parameter

到第 07 章,需要补的是:

text 复制代码
Parameter 和 XML-RPC / Master / Node API 到底是什么关系

19.1 普通 pnh.param() 的读取路径

当前代码:

cpp 复制代码
pnh.param("publish_rate",
          publish_rate,
          1.0);

参数名先解析成:

text 复制代码
/hello_node/publish_rate

该调用链节点少、方向单一,使用 Mermaid 表示更合适:
#mermaid-svg-8sQxT3ggVosC8LoI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8sQxT3ggVosC8LoI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8sQxT3ggVosC8LoI .error-icon{fill:#552222;}#mermaid-svg-8sQxT3ggVosC8LoI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8sQxT3ggVosC8LoI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8sQxT3ggVosC8LoI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8sQxT3ggVosC8LoI .marker.cross{stroke:#333333;}#mermaid-svg-8sQxT3ggVosC8LoI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8sQxT3ggVosC8LoI p{margin:0;}#mermaid-svg-8sQxT3ggVosC8LoI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8sQxT3ggVosC8LoI .cluster-label text{fill:#333;}#mermaid-svg-8sQxT3ggVosC8LoI .cluster-label span{color:#333;}#mermaid-svg-8sQxT3ggVosC8LoI .cluster-label span p{background-color:transparent;}#mermaid-svg-8sQxT3ggVosC8LoI .label text,#mermaid-svg-8sQxT3ggVosC8LoI span{fill:#333;color:#333;}#mermaid-svg-8sQxT3ggVosC8LoI .node rect,#mermaid-svg-8sQxT3ggVosC8LoI .node circle,#mermaid-svg-8sQxT3ggVosC8LoI .node ellipse,#mermaid-svg-8sQxT3ggVosC8LoI .node polygon,#mermaid-svg-8sQxT3ggVosC8LoI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8sQxT3ggVosC8LoI .rough-node .label text,#mermaid-svg-8sQxT3ggVosC8LoI .node .label text,#mermaid-svg-8sQxT3ggVosC8LoI .image-shape .label,#mermaid-svg-8sQxT3ggVosC8LoI .icon-shape .label{text-anchor:middle;}#mermaid-svg-8sQxT3ggVosC8LoI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8sQxT3ggVosC8LoI .rough-node .label,#mermaid-svg-8sQxT3ggVosC8LoI .node .label,#mermaid-svg-8sQxT3ggVosC8LoI .image-shape .label,#mermaid-svg-8sQxT3ggVosC8LoI .icon-shape .label{text-align:center;}#mermaid-svg-8sQxT3ggVosC8LoI .node.clickable{cursor:pointer;}#mermaid-svg-8sQxT3ggVosC8LoI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8sQxT3ggVosC8LoI .arrowheadPath{fill:#333333;}#mermaid-svg-8sQxT3ggVosC8LoI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8sQxT3ggVosC8LoI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8sQxT3ggVosC8LoI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8sQxT3ggVosC8LoI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8sQxT3ggVosC8LoI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8sQxT3ggVosC8LoI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8sQxT3ggVosC8LoI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8sQxT3ggVosC8LoI .cluster text{fill:#333;}#mermaid-svg-8sQxT3ggVosC8LoI .cluster span{color:#333;}#mermaid-svg-8sQxT3ggVosC8LoI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8sQxT3ggVosC8LoI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8sQxT3ggVosC8LoI rect.text{fill:none;stroke-width:0;}#mermaid-svg-8sQxT3ggVosC8LoI .icon-shape,#mermaid-svg-8sQxT3ggVosC8LoI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8sQxT3ggVosC8LoI .icon-shape p,#mermaid-svg-8sQxT3ggVosC8LoI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8sQxT3ggVosC8LoI .icon-shape .label rect,#mermaid-svg-8sQxT3ggVosC8LoI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8sQxT3ggVosC8LoI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8sQxT3ggVosC8LoI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8sQxT3ggVosC8LoI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NodeHandle::param
NodeHandle::getParam
ros::param::get
param::getImpl

use_cache=false
master::execute

getParam
rosmaster Parameter Server

所以参数值不是存在 NodeHandle 对象里。

普通 get/set 是 Node 通过 XML-RPC 访问集中式 Parameter Server。

19.2 为什么 rosparam set 不会自动改已经构造好的 ros::Rate

当前代码:

cpp 复制代码
double publish_rate = 1.0;
pnh.param("publish_rate", publish_rate, 1.0);
ros::Rate rate(publish_rate);

这里:

text 复制代码
pnh.param()
    -> 读取一次参数
    -> 把结果复制进 C++ 变量 publish_rate

ros::Rate
    -> 再用这个数值构造对象

所以运行期间:

bash 复制代码
rosparam set /hello_node/publish_rate 5.0

只改变 Parameter Server 中的值,不会反向修改:

text 复制代码
已经存在的 double publish_rate
已经存在的 ros::Rate

19.3 getParamCached() 为什么又会出现 paramUpdate

getParamCached() 第一次缓存某个参数时,会调用 Master/Parameter Server:

text 复制代码
subscribeParam

并附带当前 Node 的 XML-RPC URI。

以后参数变化:

text 复制代码
Parameter Server
    -> Node API.paramUpdate(...)

param::init() 早在 ros::init() 阶段就已经:

cpp 复制代码
XMLRPCManager::instance()->bind(
    "paramUpdate",
    paramUpdateCallback);

这里和 Topic 发现非常像:

text 复制代码
Topic:
Subscriber -> Master.registerSubscriber
Master -> Subscriber.publisherUpdate

Parameter:
Node -> Master.subscribeParam
Master -> Node.paramUpdate

这一对照有助于理解 ROS1 的 XML-RPC 设计模式。

19.4 这一章 Parameter 掌握到这里就够

完成本节后应能回答:

text 复制代码
Parameter Server 在哪里?
普通 get/set 怎样走 XML-RPC?
private parameter 名字怎样形成?
为什么 set 参数不会自动改 C++ 变量?
getParamCached 为什么需要 paramUpdate Node API?

本章不继续深入:

text 复制代码
dynamic_reconfigure 内部实现
复杂 YAML 参数树
参数热更新架构设计
参数性能优化

这些内容与本章的 Master/Topic 注册发现主线没有直接关系。


20. 完整机制回看第一步:先分清 Master URI、Node XML-RPC URI 与 TCPROS 端口

到这里已经读过具体源码,再看注册发现就不再是几个抽象箭头。

ROS1 Topic 建链中至少要区分三个网络入口。

20.1 Master XML-RPC URI

通常:

text 复制代码
http://<master-host>:11311/

由:

text 复制代码
ROS_MASTER_URI

指定。

Node 调:

text 复制代码
registerPublisher
registerSubscriber
getSystemState
lookupNode
getParam
setParam
...

都先找到这个入口。

20.2 每个 Node 自己的 XML-RPC URI

来自:

cpp 复制代码
XMLRPCManager::start()
    -> bindAndListen(0)

例如:

text 复制代码
http://192.168.1.10:43861/

这里提供:

text 复制代码
publisherUpdate
requestTopic
getPid
getBusInfo
shutdown
paramUpdate
...

20.3 TCPROS 监听端口

来自:

cpp 复制代码
ConnectionManager::start()

最终可由:

cpp 复制代码
ConnectionManager::getTCPPort()

取得。

requestTopic 的 TCPROS 响应返回的就是这个入口。

因此:

text 复制代码
Master XML-RPC port
Node XML-RPC port
TCPROS port

是三种不同职责,不能混成一个"ROS 端口"。


21. 完整机制回看:两种启动顺序为什么会走不同的首次发现路径

完成 Publisher、Subscriber 与 Master 相关源码阅读后,再使用双路径图回看首次发现过程:

两种情况最终都会进入:

text 复制代码
Subscription::pubUpdate()
    ↓
Subscription::negotiateConnection()
    ↓
requestTopic
    ↓
TCPROS host/port
    ↓
TCP connect

两种路径真正不同的是:

text 复制代码
Publisher URI 第一次从哪里得到?

21.1 情况 A:Publisher 先启动

先:

text 复制代码
/hello_node
    -> registerPublisher(/chatter)
    -> Master

Master 已经知道:

text 复制代码
/chatter
    Publisher = /hello_node
    caller_api = http://...:<publisher-xmlrpc-port>/

之后 listener 启动:

text 复制代码
/hello_listener
    -> registerSubscriber(/chatter)

Master 的 registerSubscriber() 会直接把当前 Publisher URI 列表放进响应:

text 复制代码
registerSubscriber response
    -> [publisher API URI ...]

于是 roscpp:

text 复制代码
TopicManager::registerSubscriber()
    ↓
Subscription::pubUpdate(pub_uris)

因此这种顺序下,Subscriber 的第一次发现来源是:

text 复制代码
registerSubscriber 的返回值

21.2 情况 B:Subscriber 先启动

先:

text 复制代码
/hello_listener
    -> registerSubscriber(/chatter)

此时 Master 没有 Publisher,所以返回:

text 复制代码
[]

listener 不会退出,它只是暂时没有 Publisher link。

之后:

text 复制代码
/hello_node
    -> registerPublisher(/chatter)

Master 的 registerPublisher() 发现:

text 复制代码
/chatter 已经存在 Subscriber

于是:

text 复制代码
_notify_topic_subscribers()
    ↓
publisher_update_task()
    ↓
Subscriber Node API.publisherUpdate(...)

而 listener 端:

text 复制代码
TopicManager::pubUpdateCallback()
    ↓
TopicManager::pubUpdate()
    ↓
Subscription::pubUpdate()

因此这种顺序下,新 Publisher 的第一次发现来源是:

text 复制代码
Master 主动调用 Subscriber 的 publisherUpdate Node API

21.3 Master 端 registerSubscriber() / registerPublisher() 实际做了什么

Noetic 的实现位于:

text 复制代码
tools/rosmaster/src/rosmaster/master_api.py

registerSubscriber() 的主干可以压缩为:

text 复制代码
reg_manager.register_subscriber(topic, caller_id, caller_api)
    ↓
必要时记录 topic type
    ↓
publishers.get_apis(topic)
    ↓
把当前 Publisher Node API URI 列表直接作为返回值

这就是 Publisher 先启动时:

text 复制代码
registerSubscriber response

为什么已经足够触发第一次发现。

registerPublisher() 的主干则是:

text 复制代码
reg_manager.register_publisher(topic, caller_id, caller_api)
    ↓
记录/更新 topic type
    ↓
取当前 Publisher URI 列表
    ↓
取当前 Subscriber URI 列表
    ↓
_notify_topic_subscribers(topic, pub_uris, sub_uris)
    ↓
返回当前 Subscriber URI 列表

_notify_topic_subscribers() 最终把通知任务放进 rosmaster 的 thread pool:

text 复制代码
publisher_update_task(subscriber_api, topic, pub_uris)
    ↓
xmlrpcapi(subscriber_api).publisherUpdate(...)

因此:

text 复制代码
Publisher 注册 Master

与:

text 复制代码
Master 回调 Subscriber.publisherUpdate

是两个不同的 XML-RPC 调用。

21.4 publisherUpdaterequestTopic 的边界

这两个名字容易混:

text 复制代码
publisherUpdate
    调用方:Master
    被调用方:Subscriber Node
    作用:告诉 Subscriber 当前 Publisher URI 列表

requestTopic
    调用方:Subscriber Node
    被调用方:Publisher Node
    作用:协商数据传输协议,并获取 TCPROS/UDPROS 入口

因此:

text 复制代码
publisherUpdate

不是 Publisher 主动直接调用 Subscriber。

而:

text 复制代码
requestTopic

更不是 Subscriber 去 Master 查询 TCPROS port。

Master 只帮 Subscriber 找到:

text 复制代码
Publisher 的 Node API URI

之后 Subscriber 与 Publisher 直接协商。

21.5 Master 为什么不是 Topic 数据中转站

requestTopic 完成后:

text 复制代码
Subscriber
    -> Publisher TCPROS port

建立直接连接。

之后正常消息路径:

text 复制代码
Publisher
    ==================>
Subscriber

Master 不在 payload 路径中。

这也是为什么 Master 挂掉之后,已经建立好的部分 Topic TCP 连接并不等价于立刻断开;但新的注册发现、重连和 graph 更新会受影响。


22. 完整机制回看:Master API、Node API、Parameter Server API 与数据面的边界

完整源码路径建立后,可以把控制面与数据面的边界收敛成一张表。

层次 典型入口 调用方向 职责
Master API registerPublisherregisterSubscriberlookupNodegetSystemState Node/工具 -> Master 注册、发现、ROS graph 查询
Parameter Server API getParamsetParamhasParamsubscribeParam Node/工具 -> rosmaster 集中式参数存取与订阅
Node API publisherUpdaterequestTopicgetPidgetBusInfoshutdownparamUpdate Master/工具/其它 Node -> 某 Node 通知、查询、控制、传输协商
TCPROS/UDPROS connection header + payload Node <-> Node Topic 实际数据传输
ROSRPC Service TCP 请求/响应 Service Client <-> Server Service 实际数据传输

这张表最重要的不是背所有方法,而是知道:

text 复制代码
registerPublisher/registerSubscriber
    -> Master API

publisherUpdate/requestTopic
    -> Node API

Topic payload
    -> TCPROS/UDPROS

23. 源码主线与完整机制已经闭合:第 08 章从 TCPROS 入口继续

本章已经建立下面这条真实源码链:

text 复制代码
第一个 NodeHandle
    -> ros::start()
        -> PollManager
        -> TopicManager
        -> ServiceManager
        -> ConnectionManager
        -> XMLRPCManager
        -> ROSOutAppender
        -> internal callback queue

Publisher
    -> NodeHandle::advertise()
    -> TopicManager::advertise()
    -> registerPublisher

Subscriber
    -> NodeHandle::subscribe()
    -> TopicManager::subscribe()
    -> registerSubscriber
    -> Subscription::pubUpdate()
    -> requestTopic
    -> TransportTCP::connect()

第 08 章就从最后这里继续:

text 复制代码
TransportTCP::connect()
    ↓
Connection
    ↓
TCPROS connection header
    ↓
callerid / topic / type / md5sum
    ↓
header negotiation
    ↓
serialized message
    ↓
socket

也就是从本章的"双方怎样找到彼此并拿到 TCPROS 入口 ",进入下一章的"一条消息的字节到底怎样从 Publisher 走到 Subscriber"。


参考源码与资料

本章实现以 ROS1 Noetic noetic-devel 源码为准:

  • roscpp init.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/init.cpp
  • roscpp poll_manager.hhttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/include/ros/poll_manager.h
  • roscpp poll_manager.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/poll_manager.cpp
  • roscpp poll_set.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/poll_set.cpp
  • roscpp topic_manager.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/topic_manager.cpp
  • roscpp service_manager.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/service_manager.cpp
  • roscpp connection_manager.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/connection_manager.cpp
  • roscpp xmlrpc_manager.h/.cpphttps://github.com/ros/ros_comm/tree/noetic-devel/clients/roscpp
  • xmlrpcpp XmlRpcServer.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/utilities/xmlrpcpp/src/XmlRpcServer.cpp
  • xmlrpcpp XmlRpcServerMethod.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/utilities/xmlrpcpp/src/XmlRpcServerMethod.cpp
  • roscpp rosout_appender.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/rosout_appender.cpp
  • roscpp advertise_options.hhttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/include/ros/advertise_options.h
  • roscpp publisher.h/.cpphttps://github.com/ros/ros_comm/tree/noetic-devel/clients/roscpp
  • roscpp publication.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/publication.cpp
  • roscpp subscribe_options.hhttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/include/ros/subscribe_options.h
  • roscpp subscription.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/subscription.cpp
  • roscpp subscription_queue.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/subscription_queue.cpp
  • roscpp spinner.cpphttps://github.com/ros/ros_comm/blob/noetic-devel/clients/roscpp/src/libros/spinner.cpp
  • rosmaster master.pyhttps://github.com/ros/ros_comm/blob/noetic-devel/tools/rosmaster/src/rosmaster/master.py
  • rosmaster master_api.pyhttps://github.com/ros/ros_comm/blob/noetic-devel/tools/rosmaster/src/rosmaster/master_api.py
  • roslaunch launch.pyhttps://github.com/ros/ros_comm/blob/noetic-devel/tools/roslaunch/src/roslaunch/launch.py
  • roslaunch nodeprocess.pyhttps://github.com/ros/ros_comm/blob/noetic-devel/tools/roslaunch/src/roslaunch/nodeprocess.py
  • rosgraph xmlrpc.pyhttps://github.com/ros/ros_comm/blob/noetic-devel/tools/rosgraph/src/rosgraph/xmlrpc.py
相关推荐
吴声子夜歌1 小时前
Shell编程实例——与解析相关的任务(二)
linux·运维·shell
mengge.cloud2 小时前
0917Docker 小白实验教程(理论+实操)
linux·服务器·网络·docker
Escalating_xu2 小时前
【内存管理发展史】从 8086 实模式到分页:CPU 如何把虚拟地址变成物理地址
linux·缓存
倔强的石头1062 小时前
【Linux指南】动静态库系列(八):ELF 加载与进程地址空间:程序还没运行,为什么已经有地址
linux·运维
两点王爷3 小时前
常用的 Docker 镜像拉取地址仓库及常用命令详解
运维·服务器·容器
wdfk_prog3 小时前
用 Git Submodule + Sparse Checkout 管理 RT-Thread:内核、BSP、第三方库与业务代码分层实践
运维·缓存·docker·容器·ros
努力努力再努力wz3 小时前
【Docker入门系列】镜像为什么能复用?一文吃透 Docker Image、Registry、运行时架构与常用命令
缓存·docker·容器
神.秘.人3 小时前
【BIOS/UEFI 与 MBR/GPT 的区别详解】
运维·服务器
懂软件的胡子个哥3 小时前
微信二次开发如何接入 AI,真正可用的不是简单自动回复
运维·微信·wechatapi·个人微信号二次开发·微信群管理