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();
获取最新文章: 扫一扫右上角的二维码加入“创客智造”公众号