< >
Home » ROS与C++入门教程 » ROS与C++入门教程-Advanced: Internals(高级:内部)

ROS与C++入门教程-Advanced: Internals(高级:内部)

ROS与C++入门教程-Advanced: Internals(高级:内部)

说明:

  • 此网页包含的roscpp的内部组织的整体的一个高层次的概述,从底部开始了。

一般的理念

  • 对于roscpp的理念是只有特定的API集是外部可见的。
  • 公共的头文件不能包含私有的,除非是私有的需要模板。
  • 这意味着类型删除经常在早期处理
  • 一般情况下,我们尽量减少包含头文件,例如没有Boost.Thread头文件增加到公共的API。
  • 将来也会删除Boost.Bind ,因为它显著增加了编译时间。

xmlrpc

  • 我们使用修改过的第三方库xmlrpcpp,不幸的是,它不允许我们为它的套接字使用自己的轮询代码,所以我们利用单独的线程来完成这个工作。
  • 所有的使用都通过独立XMLRPCManager进行,所有与主机的通讯都使用 master::execute函数。

PollSet/PollManager(轮询和管理)

  • 在底层,所有non-xmlrpc网络都通过poll(),通过PollSet 类进行管理。目前有一个PollSet被使用,它单独由PollManager管理。PollSet让你注册文件描述和增加要监听的事件(读/写)

Connections and Transports(连接和传输)

  • 在系统最低层,transport系统尝试去做传输类型:如TCP, UDP。有一个基本类Transport,被扩展到TransportTCP和TransportUDP ,它们实现 read() 和write()。
  • 在系统最高层,Connection 类提供基于回调的方法读写数据到transport。Connection 负责跟踪,如多少数据被发送和收到以及当操作完成调用回调函数等。
  • Connections通过ConnectionManager实现追踪。它主要管理连入的TCP/UDP连接。也一样保持指向Connection对象的指针,直到它们允许被删除才取消指向。
  • 当ConnectionManager收到一个新连入的TCP or UDP连接,它会创建TransportSubscriberLink 或ServiceClientLink,依赖在于是话题或服务连接。

Initialization初始化

  • roscpp初始化有两个部分 init()和start()方法。
  • init()完成的工作不多:
    • 主要是简单解析环境变量和命令行相关,如master URI, local hostname/ip address, remappings等
    • init()不允许执行任何的通讯。因此用户可以手工检查master是否启动,是否有自定义的行为等。
    • init()同样不开始任何线程。
  • start()完成大部分初始化工作:
    • start() 也可以手工调用,需要调用shutdown()关闭。
    • 或第一个NodeHandle创建时调用start() ,然后最后一个NodeHandle销毁后再调用shutdown()完成关闭
  • start()完成的工作包括:
    • 初始化各种独立线程
    • 安装SIGINT型号处理器
    • 创建rosout log4cxx 附加
    • 创建~get_loggers和~set_logger_level服务
    • 检查/use_sim_time参数,如果设置,订阅到/clock话题

Topics话题

  • 话题通过TopicManager管理,它维护一个Subscriptions 和Publications的列表。
  • 当任何的信息到达TopicManager,任何编译时间信息已被删除。
  • 这意味着,TopicManager::subscribe() 只能访问到SubscribeOptions类的内容

(1)Subscriptions订阅

  • 订阅话题通过Subscription 类管理,每个话题只允许对应一个Subscription类 。
  • 多个订阅到相同的话题,会共享单独一个连接,避免带宽浪费
  • Subscription对象管理 N个PublisherLink对象,每个连接对应不同的publisher在相应的话题上。
  • 有两种类型的PublisherLink 对象, TransportPublisherLink使用传输层连接发布器,IntraprocessPublisherLink连接到本地,进程内的发布器。允许那些完全跳过传输层(并尽可能不提供拷贝消息传递)

(2)publisherUpdate

  • 当一个新发布器注册到master,节点会收到publisherUpdate的xmlrpc调用,它在正确的话题上会转发到Subscription::pubUpdate。对非进程内连接,它会开始一个异步xmlrpc请求到新的发布器。当xmlrpc请求通过,Subscription::pendingConnectionDone() 就会调用。一旦这个请求通过,就会为这个发布器创建TransportPublisherLink。

(3)Retry重试

  • TCP TransportPublisherLink的连接丢失会尝试重连,它的第一个重连都安排在失连的100ms后。之后的每次都会时间加倍,直到建议的20秒。这是上限。

(4)订阅回调和反序列化

  • 当消息到达,它会推送到SubscriptionQueue队列,当队列填满,它会丢弃旧的消息。值得注意的是,消息还没有被反序列化。一个MessageDeserializer对象放入到队列,而不是消息本身。如果可能,这个对象会被多个回调函数共享。(如果它们的rtti 信息匹配)。
  • 这意味着:
    • 因为队列满而丢弃的消息不会被发序列化。
    • 消息不会反序列,直到第一个关联到订阅的回调函数被调用才进行。

发布器

  • 发布话题通过Publication类管理,一个发布类对应一个话题。多个发布器在相同的话题,共享连接给它们的订阅器。
  • Publication对象管理N个SubscriberLink对象,每一个连接到不同的订阅器,在其对应的话题上。
  • 有两个类型的SubscriberLink对象。 TransportSubscriberLink 连接到订阅器通过传输层(ConnectionManager创建)
  • IntraprocessSubscriberLink 连接到进程内的订阅器,忽略所有的传输层。

No-Copy Intraprocess Support非复制进程的支持

  • roscpp组合使用 boost::shared_ptr 和rtti提供基本安全的没有复制的进程内消息传递。
  • 它仅仅是基本安全的,因为它依赖于发布者从不修改刚刚发布的消息对象。
  • 注意:这只是应用,如果消息作为boost::shared_ptr传递,否则还是要求序列化和反序列化,通过它,将会忽略传输层。
  • 一个进程内消息的路径如下:
    • Publisher::publish()
    • TopicManager::publish()
      • 调用Publication::getPublishTypes()确认我们订阅器的类型。
      • 如果有"serialization required"的订阅器, 序列化这个消息。
      • 如果没有任何"no-copy"的订阅,立即清除消息指针和RTTI信息
    • Publication::publish()
      • 如果有,找到Publication中的IntraprocessSubscriberLink
      • 直接入队的消息,只允许"no-copy"的发布(这可能不是必须的)
    • IntraprocessSubscriberLink::enqueueMessage()
    • IntraprocessPublisherLink::handleMessage()
    • Subscription::handleMessage()
      • 对于每个订阅器,检查rtti消息匹配,如果是,则将消息添加到其订阅队列。
  • 有一些部分,这不是真正必要的。出于性能原因,它可能是一个好主意来摆脱intraprocesspublisherlink / intraprocesssubscriberlink直接连接的发布和订阅。

Services服务

  • 服务单独由ServiceManager管理

(1)服务端

  • 服务端有ServicePublication类管理,它会跟踪许多ServiceClientLink 对象。
  • 在任何时给定的服务只由一个服务端提供服务。在roscpp节点也一样。
  • ServiceClientLink对象(由ConnectionManager创建)处理单独服务客户端。

(2)客户端

  • 连接到服务端由ServiceServerLink管理,每个新的服务端创建一个新的ServiceServerLink

User API用户API

  • 用户API基本都包含在ros.h头文件,除了callback_queue.h,它需要移除一些额外的包含,才能使用到。

(1)多种回调形式

  • 示例:
void (const MsgConstPtr& msg);
void (const MsgPtr& msg);
void (const Msg& msg);
void (Msg msg);
void (const ros::MessageEvent<Msg const>& evt);
etc.
  • 允许这样调用的机制叫ParameterAdapter,它是指定不同参数类型的模板类。
  • 例如:标准形式(const MsgConstPtr&) :
template<typename M>
struct ParameterAdapter<const boost::shared_ptr<M const>& >
{
  typedef typename boost::remove_reference<typename boost::remove_const<M>::type>::type Message;
  typedef ros::MessageEvent<Message const> Event;
  typedef const boost::shared_ptr<Message const> Parameter;
  static const bool is_const = true;

  static Parameter getParameter(const Event& event)
  {
    return event.getMessage();
  }
};
  • 它提供一个消息的typedef,它保证是non-const和non-reference。
  • 它提供MessageEvent类型作为Event的typedef
  • 它提供Parameter的typedef,它返回传递到回调函数的实际值和is_const静态变量,告诉我们如果消息是常量不允许更改。
  • ParameterAdapter是模板的函数形式(不仅仅是message类型),并提取该消息类型和其他信息。
  • ParameterAdapter 与SubscriptionCallbackHelperT(也是模板的函数形式) 一起使用,调用回调类似如下:
callback_(ParameterAdapter<P>::getParameter(event));
  • 添加新的回调形式,只要他们仍然是单参数,创建一个新的ParameterAdapter指定的(如果必要的话,也可以在ROS外实现)。

回调队列

  • 在roscpp,CallbackQueue类是比较复杂的一个。支持很多功能。
  • 目前支持功能:
    • 确保特定的人(指定 removal id传递给 addCallback())任意回调,一旦removeByID返回,回调不会被调用。
    • 移除当前正在进行的回调
    • 递归调用(在同一个队列中调用 CallbackQueue::callAvailable() 或callOne()的回调里再调用 CallbackQueue::callAvailable() 或callOne())
    • 线程安全地调用 callAvailable() 和callOne()
  • 这些要求为CallbackQueue增加了许多复杂性,同时降低的执行效率。如果选择一个不同的接口或不同的需求集,则可以编写一个比当前的快得多且不太复杂的队列。
  • 除去(或改变语义的)callAvailable()可以让CallbackQueue 简单得多,并可能消除很多复杂的需要(如线程本地存储的需要)。

Future未来

(1)Transports传输层

  • 传输层是当我们只有一个TCP传输时,想到任何必要的变化将发生在UDP实现时。这没有发生,并且事实证明传输层被设计的方式是一个错误,并且只支持TCP情况。这是我们缺乏对UDP的大消息支持的原因。
  • 传输层现在不能正常工作的主要原因是它不知道消息。 目前,例如,接收消息是“拉”模型。 TransportPublisherLink请求其Connection4字节长度,并且当到达时,解析,分配空间,并要求消息本身。 相反,传输应该将消息推送到PublisherLink,并且Connection类可以完全取消。 基本上,我设计了一个更通用的网络传输系统,当我应该设计一个消息传递传输系统。

(2)Sub/Pub(发布/订阅)

  • 虽然roscpp支持非复制进程内消息传递,但它不是最快的实现。 这主要是因为roscpp开始不支持内部进程消息传递,并且已经成长为当前的形式。 理想情况下,这将交换,与内部进程的情况成为主要的,with the network side hooking into it。 通过对lockfree包中的lock-free结构的一些增强,它甚至可以以lock-free的方式进行,尽管你还需要一个lockfree CallbackQueue实现。这是可能的,但也许不是与当前CallbackQueue做同样的保证 - 这就是为什么CallbackQueue被抽象为CallbackQueueInterface。

(3)NodeHandle

  • IMHO一个错误是用NodeHandle做的,因为它做的太多了,你可以在roscpp中做的一切都是作为一个方法。 这意味着即使你需要的是它的命名空间/重映射功能,你拖着很多行李。 经过相当多的思考,我宁愿一个Context类只处理命名空间和重新映射(并且有公共和私有命名空间的概念)。 我也希望我减少了每个函数类型的重载,并使用命名参数idiom(如TransportHints)将它们移动到XOptions结构中。 理想情况下,正常订户也会遵循message_filters模式。
  • 订阅一个话题可能看起来像这样:
ros::Subscriber<Foo> sub(SubscribeOptions(context.name("foo"), 5).allowConcurrent().callbackQueue(my_queue).callback(myCallback));

// Alternatively, you could leave out the callback above, and then do:
// sub.registerCallback(myCallback);
  • 还要注意使用上面的context.name(),它应该返回一个Name对象而不是一个字符串。 这可能是太远,但有好处,不直接使用字符串(如知道一个名字是否已经重新映射已经或没有)。 如果直接传递字符串,将使用全局上下文。

(4)Single Master Assumption单主机假设

  • 目前有一个假设,在所有的roscpp,有一个单一的主人。 这主要是从旧ros::Node保持而来,有相同的假设。需要改变以允许多个主设备(除了在用户API侧)的主要内容是在内部存储主设备以及任何主题/服务。 Name类将在这里帮助,因为它可以包括主人。
  • 在用户API方面,我一直在想:
ros::MasterContext master("http://pri:11311");
ros::Context context(master);
...
  • 如果没有指定MasterContext,它会回退到全局(从环境/命令行创建),就像现在一样。

(5)Global Singletons独立的全局变量

  • 现在有许多独立的全局变量。 我想把它们清理成只有一个单例或全局访问器,用一个类来管理他们的生命周期,例如:
class Globals
{
  ConnectionManager* connection_manager_;
  XMLRPCManager* xmlrpc_manager_;

...

public:
  ConnectionManager* getConnectionManager();
...
};

Globals* getGlobals();

纠错,疑问,交流: 请进入讨论区点击加入Q群

获取最新文章: 扫一扫右上角的二维码加入“创客智造”公众号


标签: ROS与C++入门教程