TinyDDS编程实践

背景

传统计算机网络的运行依赖于集中式运营商及服务提供商(Server),它在拓扑结构上的典型特点就是存在一个或数个“中心节点”。整个网络的数据传输和处理都集中在少数节点上。“中心节点”是维持整个系统运行的关键,也是控制整个系统运行的中枢。这种拓扑结构方便设计,但不够灵活,此外还存在健壮性和隐私性方面的缺陷。

分布式网络作为近几年发展较快的技术,受到越来越多的关注。它采用分布式、自组织组网思想。网络中没有绝对的控制中心,所有节点的地位平等,网络中的节点通过分布式算法来协调彼此的行为,无需人工干预和任何其他预置的网络设施,可以在任何时刻任何地方快速展开并自动组网。由于网络的分布式特征、节点的冗余性和不存在单点故障点,分布式网络的健壮性和抗毁性很好。此外,网络拓扑随时可以发生变化,特别适用于无法或不便预先铺设网络设施的场合和需要快速自动组网的场合等。

无线传感器网络是贯彻分布式网络思想的典型例子。近年来,随着无线通信、集成电路、传感器以及微机电系统(MEMS)等技术的飞速发展,低成本、低功耗、多功能的微型无线传感器的大量生产成为可能,这些微型无线传感器具有无线通信、数据采集和处理、协同合作等功能,无线传感器网络(简称传感器网络)就是由许多这种微型无线传感器节点协同组织起来的。传感器网络的节点可以随机或者特定地部署在目标环境中,它们之间通过特定的协议自组织起来,能够获取周围环境的信息并且相互协同工作完成特定任务。图示为无线传感器网络的结构。

TinyDDS编程实践_第1张图片

 智能网联汽车是贯彻分布式网络思想的另一个例子。随着智能网联汽车的发展,用户的需求也越来越高,“软件定义汽车”已为产业共识。从技术角度来看,汽车软件架构正由“面向信号”迈向“面向服务(SOA)”。DDS(Data Distribution Service)数据分发服务,是新一代分布式实时通信中间件协议,高实时性能、高可靠性能、开放式体系结构和发布/订阅端的非耦合性能,大大加速和简化了分布式系统的开发,使其非常适用于汽车领域。

TinyDDS编程实践_第2张图片

 智能机器人软件是贯彻分布式网络思想的重要例子。机器人操作系统(Robot Operating System)是一套开源的软件框架和工具集,用来帮助开发人员建立机器人应用程序,它提供了硬件抽象、设备驱动、函数库、可视化工具、消息传递和软件包管理等诸多功能。ROS可用于在不同进程间匿名的发布、订阅、传递信息。 ROS系统的核心部分是ROS图(ROS Graph)。ROS图是指在ROS系统中不同的节点间相互通信的连接关系。ROS2是ROS的新版本,有较大的变化,ROS2采用数据分发服务DDS作为其底层的通讯框架。

TinyDDS编程实践_第3张图片

DDS介绍 

DDS(Data Distribution Service,数据分发服务) 是新一代分布式实时通信中间件协议,是一种以数据为中心的通信协议,具有高实时性、高可靠性、开放式体系结构和发布/订阅端非耦合的特点。

DDS标准由OMG(对象管理组织,Object Management Group)定义,是基于DCPS(Data-Centric Publish-Subscribe, 以数据为中心的发布订阅)模型的一种中间件协议和API标准,它将系统的组件集成在一起,提供业务和任务关键型物联网 (IoT) 应用程序所需的低延迟数据连接、极高的可靠性和可扩展架构。

在分布式系统中,中间件是位于操作系统和应用程序之间的软件层。它使系统的各个组件能够更轻松地通信和共享数据。它通过让软件开发人员专注于其应用程序的特定目的而不是在应用程序和系统之间传递信息的机制来简化分布式系统的开发。

DDS中间件是一个软件层,它从操作系统、网络传输和低级数据格式的细节中抽象出应用程序。相同的概念和API以不同的编程语言提供,允许应用程序跨操作系统、语言和处理器架构交换信息。数据线格式、发现、连接、可靠性、协议、传输选择、QoS、安全等低级细节由中间件管理。

TinyDDS编程实践_第4张图片

DDS以数据为中心的发布--订阅模型为所有分布式节点之间建立了一个虚拟共享的全局数据空间。在该模型下分布式节点在网络上以发布或订阅的方式传输数据,节点可以是发布者或订阅者,或者既是发布者也是订阅者。网络中的数据对象用topic(主题)做标识,分布式节点在GDS中发布或者订阅感兴趣的主题信息。各节点在逻辑上无主从关系,点与点之间都是对等关系。通信方式可以是点对点,点对多,多对多等,在服务质量策略(QoS)的控制下建立连接,自动发现和配置网络参数 .

教学设计

对于大部分的计算机学院或者软件学院的学生来说,DDS是一个陌生的概念。因为当前绝大多数的网络应用都是中心化的,而TCP/IP的默认链接模式也是由Server和Client组成,自然地形成了中心化的拓扑。

对于以智能机器人软件为代表的分布式系统来说,“去中心化”和“以数据为中心”都是重要的设计思想。而如何实现“分布式网络”的无中心特性,如何在网络中没有绝对的控制中心,所有节点的地位平等的前提下,使网络中的节点通过分布式算法协调彼此的行为,实现自组织组网和动态拓扑,也是对学生的算法设计能力的考验和锻炼。

为了深入理解和掌握“分布式网络”的概念,提高编程实践能力,本课程设计了TinyDDS编程作业,将“开发一个最简单的符合DDS协议的通信中间件”作为作业目标。

编程作业要求:

  1. 具备基本的数据分发和话题订阅功能,即:对于同一个Topic,可以有一个或多个Publisher(发布者),可以有一个或多个Subscriber(订阅者)。通过同一个Topic对应的所有Subscriber可以接收到所有Publisher发布的数据。Topic同一性的认定以名称(字符串)为准,相同名称的Topic即认为是同一个Topic;
  2. 分发的数据目前仅支持字符串;
  3. 每个Participant(通信节点)可以是同一台计算机的不同进程,也可以是同一局域网内的多个计算机上的进程。DDS以完全分布式的方式组织通信,节点之间的地位是平等的,不应存在某个中心节点对通信进行整体控制。任意时刻,任意的节点加入或断开,不应该影响其他节点的数据分发;
  4. 底层通讯采用UDP。UDP是不可靠的连接,可能出现丢包的情况,我们这里为了降低实现的难度,不需要考虑丢包的情况;
  5. TinyDDS仅用于教学,不追求实用价值。不需要实现QoS和Domain特性;不必深究细节问题(如资源释放、异常处理等);  

TinyDDS编程实践_第5张图片

 DDS协议有详细的定义文档,https://www.omg.org/spec/DDS/1.4/PDF,总计140页,内容太多了,抓不住重点。许多概念和功能(如Domain、QoS)并非本项目所需要知晓的,不建议大家通读该文档。目前比较著名的DDS实现是OpenDDS和FastDDS。如果在实现过程中遇到不明白的细节问题,我们推荐参考fastDDS对于DDS Layer介绍及一个简单的C++话题订阅例程必须实现的类层次关系如下:

TinyDDS编程实践_第6张图片

  1. Entity,基类
  2. DomainEntity类,涉及到Domain的内容,无需实现,继承空壳
  3. DomainParticipant类,作为所有通信节点的容器。每个节点只允许创建单个DomainParticipant对象,生命期贯穿程序始终。需要实现的成员方法:
    create_publisher()  创建一个Publisher对象
    create_subscriber()   创建一个Subscriber对象
    create_topic()  创建一个Topic对象
  4. DomainParticipantFactory类,工厂类,唯一的功能是创建DomainParticipant对象,需要实现的成员方法:
    create_participant()
  5. Topic类,用来抽象“话题”
  6. Publisher类,用来抽象“发布者”。需要实现的成员方法:
    create_datawriter ()  创建一个DataWriter对象
  7. Subscriber类,用来抽象“订阅者”。需要实现的成员方法:
    create_datareader()  创建一个DataReader对象
  8. DataWriter类,用来抽象数据写入操作。需要实现的成员方法:
    write(),将数据通过相应Publisher发送至相应Topic
  9. DataReader类,用来抽象数据读取操作
    read(),将到达相应Subscriber的数据读出,一般不使用该方法
  10. DataWriterListener类,抽象类,回调监听器
  11. DataReaderListener类,抽象类,回调监听器,其中的on_data_available()方法由用户重载;当来自匹配Topic的新数据到来时,该方法被调用,可将数据读出。

测试例:

  1. 调用TinyDDS,测试2个Publisher和2个Subscriber(即4个进程)通过同一个Topic分发数据的代码。
  2. 调用TinyDDS,测试由3个进程环形发送数据的代码。即1号节点与2号节点通过TopicA传数据,2号节点与3号节点通过TopicB传数据,3号节点与1号节点通过TopicC传数据。

实现方法指导

UDP传输

对于不清楚UDP传输概念的同学,请阅读这个博客:
网络知识-05 传输层-UDP_惊天动地猪儿虫的博客-CSDN博客_udp传输过程

以及

一文搞懂TCP与UDP的区别 - 知乎;


关于如何编程实现UDP传输,与所用编程语言和操作系统有关:

对于Python语言,可以参考这篇博客

UDP 服务器与客户端

这里需要注意,recvfrom函数是“阻塞”的,即:未收到数据时,它会一直等待而不返回。如果需要(很可能需要)超时返回,以便无数据到达的时候能够继续执行后续代码,应先调用

s.settimeout(0.5)    #设置为0.5s超时

  • 对于C++语言,如果调用WindowsAPI,可以参考这篇文章

UDP协议实现简单的通信 C++_凶萌的小老虎的博客-CSDN博客_c++ udp通信

同样,为了实现超时返回,可以使用以下代码   

int iTimeout = 500;
setsockopt(sockRec, SOL_SOCKET, SO_RCVTIMEO, (const char *)&iTimeout, sizeof(iTimeout));

  • 对于C++语言,如果调用Linux API,可以参考这篇文章

Linux编程之UDP SOCKET全攻略 - Madcola - 博客园

同样,对于超时返回问题,参考这个问答c - Making recvfrom() function non-blocking - Stack Overflow


使用C++语言时,由于标准库未对操作系统提供的网络socket API进行必要的封装,导致编程和调试都比较麻烦,这里建议使用封装好的网络socket库,比如

ENet: ENet

Practical C++ Sockets

GitHub - DFHack/clsocket: SimpleSockets is a lightweight set of classes that allow developers to implement IP based network programs.

https://github.com/fpagliughi/sockpp

不建议使用Asio或者Qt之类的库,过于重量级了,功能强大,但是想用起来要先学习太多的概念。

节点动态发现

注意到,无论使用上面的哪一种UDP传输代码,发送方都需要填写对方的IP地址和端口号才能发送数据。接收方也要填一个端口号,然后等待数据到达。

对于端口号,我们已经给过指导了,端口号计算公式假定为7400 + 250 * domainID + 10 + 2 * participantID(默认domainID 为0,participantID由用户手工指定)。这样,可以保证不管节点在多个计算机上,还是在同一台计算机上,都可以获得不冲突的端口号。

对于IP地址的获取,涉及到分布式网络要解决的核心问题之一:节点发现。即:所有节点之间,应该通过某种策略相互发现,获取对方的IP地址,以便发送数据。且这种策略能够允许节点退出或者新节点随时加入。稍加思考,我们能够想到两类策略:

一、广播

UDP支持广播机制,即:向局域网对应子网段内的所有IP地址发送消息。

对于Python实现,只要发送方将对方IP地址换成字符串即可实现广播;

对于C++实现,可以参考这篇博客UDP之广播_致守的博客-CSDN博客_udp广播。

在基于广播的策略下,所有消息都是广播的。发送方只要在消息的开头标记自己的IP地址、监听端口号(或者participantID)、发送的Topic名称即可。所有在同一个子网段内的节点能够收到来自其他节点的所有消息,然后过滤出自己感兴趣(订阅)的Topic消息即可。例如,一种可行的消息协议格式为:’

[IP_Addr] [participantID] [Topic] [Message] [Checksum]

其中checksum字段是校验数字,用于判断本消息是否完整。如果不完整则丢弃。

显然,采用这种策略可以实现数据分发功能和节点动态发现。广播的主要缺点是严重浪费带宽资源,将本应该是n个点对点的通信退化为n个广播。此外,数据毫无隐私性。

采用广播策略实现本作业的,可以得分。

二、动态IP表

一个实用的分布式网络策略必须有效率的利用带宽资源,应尽可能地减少广播的使用。广播只用在初始的节点发现过程。一旦明确对方节点的IP地址,即改用正常的UDP通讯单播。

FastDDS正是采用这一策略。按照RTPS协议中的描述,动态发现协议包含PDP(参与者发现协议)和EDP(端点发现协议)两种协议。

PDP协议的目标是通过广播明确每个节点的IP地址和participantID,并将这个对应关系记录为动态IP表的形式,以便节点退出和新节点加入时能够及时更新表格。

EDP协议的目标是进一步与节点沟通,确定其订阅的Topic,并记录在动态Topic表里。这个表格建立了节点关心的Topic对应的发布者与订阅者的participantID

每当新消息产生时,发布者首先查询动态Topic表,确定需要将这条消息发送到哪些订阅者,得到相应订阅者participantID。然后查询动态IP表,将相应IP地址和端口号填入UDP传输指令,发送实际的消息。

由于节点在动态变化,所以动态IP表和动态Topic表都是随时更新的,这就需要每隔一段时间执行一次PDP过程,如果发现节点变动,再执行相应的EDP过程。


FastDDS对PDP和EDP的实现是比较复杂的,感兴趣的同学可以浏览https://blog.csdn.net/HBS2011/article/details/102520704及Fast RTPS原理与代码分析(3):动态发现协议之端点发现协议EDP_HBS2011的博客-CSDN博客

相关源代码在 https://github.com/eProsima/Fast-DDS/tree/master/src/cpp/rtps/builtin/discovery

对于本作业来说,PDP和EDP协议可以很简单。比如以下就是一种可行的设计:

【PDP】

[PDP_HEAD] [IP_Addr] [participantID [Checksum]

[PDP_HEAD]是包头,用以标注这是一个PDP包,通常用固定的字符(比如字母“P”)表示

checksum字段是校验数字,用于判断本消息是否完整。如果不完整则丢弃。

为了应对退出和掉线的节点,PDP还应引入超时机制,这里不再展开。

【EDP】

[EDP_HEAD] [participantID] [Topic] [Pub | Sub] [Checksum]

其中[EDP_HEAD]是包头,用以标注这是一个EDP包,通常用固定的字符(比如字母“E”)表示

[Topic]是话题名称

[Pub | Sub]标注当前EDP包的发送方是订阅该话题,还是发布该话题

checksum字段是校验数字,用于判断本消息是否完整。如果不完整则丢弃。

话题的订阅者每发现一个新节点并完成PDP过程后,都应发送EDP消息(标注为订阅者[Sub]),每个感兴趣的话题发一条。如对方恰好是发布者,则回复一条EDP消息(标注为发布者[Pub])

附注

其他基本概念:

什么是“工厂类”?:

什么是“回调监听器(Listener)”?

后记:

本题目首次用在软件学院三年级(上)的一门选修课上作为编程大作业,学生普遍反映难度太高了,做不出。现已计划次年更换为较简单的题目。

如有教师认可本题目的价值,希望将本题用到未来的教学中,可与我联系,可提供进一步的教学资料。

你可能感兴趣的:(ROS与机器人,网络,DDS)