DDS分布式系统快速开发

2022-12-30 07:51唐江文
计算机工程与设计 2022年12期
关键词:开发者代码消息

唐江文

(中国电子科技集团公司 电子科学研究院,北京 100041)

0 引 言

数据分发服务(data-distribution service,DDS)是一种发布订阅式的通信中间件,它由OMG组织于2004年发布为国际开放标准,并不断扩充发展至今。DDS引入了虚拟全局数据空间的概念,在全局数据空间中,应用可以通过读写带有主题的数据对象的方式进行通信。同时,DDS提供了QoS参数配置功能,可以实现对可靠性、带宽、通信截止时间以及资源上限的灵活控制。DDS可以实现分布式通信实体间的解耦,发布者和订阅者可以动态加入和退出,使系统具有高扩展性;另外,DDS采用组播自动发现的机制,DDS应用可以灵活部署在网络中的任一计算机设备上。

目前,DDS广泛应用于各种需要实时数据交换的场景和领域。文献[1]在EXata仿真器构建的子系统间,使用DDS实现数据交互和时间同步,提高了大规模电力通信网的仿真效率;文献[2]基于DDS实现了组织灵活、支持实时状态监控与用户管理的语音通信系统;文献[3]基于DDS实现了一种分布式的热处理集散控制监控系统,用于航天火箭发动机制造领域;文献[4]中的ROS2,是基于DDS开发的多Agent机器人系统开发框架,广泛应用于机器人研究和仿真;文献[5]基于DDS实现了航天器软件通信框架,可以实现灵活便捷的通信过程。

通过前面各领域的工程实践,已经充分证明了DDS作为分布式系统的通信中间件,具有的高扩展性和高灵活性。但在开发基于DDS的大规模分布式系统时暴露了一项不足之处:缺乏分布式系统建模的能力。这是因为用于DDS开发的IDL语言,只描述接口的数据类型,而没有描述主题、消息域,更没有描述应用。这些信息的缺失造成DDS无法像常见的建模语言(例如UML和SysML)那样方便地进行代码自动生成[6,7],进而限制了DDS应用的开发效率,不得不使用文档以文字的形式来描述系统模型,并且编写大量重复冗余的代码,易错难查。

本文扩展了IDL语法,添加了topic和domain关键字,从而可以从IDL文件中同时解析出来接口数据类型信息和主题、消息域信息;另外又为应用设计了应用描述XML文件,用于获取应用对主题的发布订阅相关信息。实现了编译器dseappgen,直接为应用生成面向主题发布订阅的C++代码文件,并自动编译成动态库,去除了重复冗余代码,大大简化了开发者的开发工作,并降低了出错风险。另外,本文还在自动生成的代码中植入了一些增强功能,比如植入了Lua语言解释器[8,9],让开发者可以进行快速仿真;又如Json字符串与DDS消息之间的相互转换,增强了DDS原有的序列化能力;并且植入了轻量级的Http Server,支持用户为应用进行B/S架构开发。

1 DDS通信原理简介

DDS是通过自动发现协议实现与网络IP的解耦[10]。只要应用位于同一个支持组播的二层网络中,就可以自动匹配具有相同主题的发布者和订阅者,所以应用可以部署运行在网络中的任何一台计算机设备上,这让开发者无需关注网络具体的组网结构。

DDS的发布者和订阅者的自动匹配能力,源自于DDS的自动发现协议,协议涉及的主要概念和基本过程如图1所示。DDS中的通信实体称作“参与者(Participant)”,参与者记录了当前计算机设备所有的IP地址和端口,以及共享内存的“端口”。当应用中实例化一个参与者时,它会执行参与者发现协议(PDP),用PDP写者(PDPWriter)通过组播的方式将自己的IP地址以及EDP的发布(Pub)/订阅(Sub)的读者(Reader)/写者(Writer)等信息发送给其它参与者,其它参与者会记录下这位新参与者的信息,并把自己的相应信息发送回来,从而保证每个参与者都能感知到网络中其它参与者的存在,并且知晓与其它参与者的通信方法。

图1 DDS通信原理:自动发现协议支撑发布订阅通信方式

当用户使用DDS进行业务数据通信时,首先会定义某个主题的发布者(Publisher)/订阅者(Subscriber),然后关联到用户写者(UserWriter)/用户读者(UserReader)。此时,参与者会执行终端发现协议(EDP),将用户写者/用户读者的相关信息,比如主题、消息类型等,发送给网络中需要这些信息的其它参与者,进行用户读/写者的匹配,这样发布者就知道了其发布的消息应该发送给哪些订阅者,从而实现了发布订阅的通信方式。

图2简单展示了DDS底层数据缓存及收发模型,DDS在发送消息时,从更改池(CacheChange Pool)中取出一个更改(CacheChange,“更改”可以简单理解为一个消息),从负载池(Payload Pool)中取出一个负载(Payload),将需要发送的数据序列化后填入负载,然后挂载到更改中。随后,更改会被添加到写者历史(Writer History,可以理解为一个消息队列)里,发送线程会将写者历史中的更改通过指定的传输方式(UDP、TCP、共享内存)发送到相应接收方。发送结束后,该更改会从写者历史中移除,其负载会在释放数据后重新放到负载池中,而该更改也会被回收到更改池中。接收消息的一方,其内存也会进行类似的链式循环操作。可见,DDS充分利用了内存池技术,最大程度地提高了内存效率。

图2 DDS底层数据缓存及收发模型

2 DDS分布式系统开发流程的改进

传统DDS分布式系统的开发流程如图3所示,设计人员一侧,首先撰写系统软件设计文档,然后手动编写IDL文件,由ddsgen生成各接口正反序列化代码。开发人员一侧,手动编写主题、消息域、主题与接口相关、主题与发布订阅相关以及大量业务代码,最后和ddsgen自动生成的代码完成编译链接生成DDS应用。

图3 传统DDS分布式系统的开发流程

其中存在的问题是,设计文档是一种供人阅读的、格式相对自由的文本,无法为开发提供强制约束。设计人员根据设计文档编写IDL文件,容易出现誊抄错误,后期一旦文档发生更改需要手动进行同步,费时费力。而且,主题、消息域、主题与接口的关联、主题与发布订阅的关联是设计内容,却侵入到了开发人员的代码中,设计和开发出现交叉,导致设计和开发边界不清,既加大了开发者工作量,也让代码容易出错。事实上,设计一旦固定下来,业务之外的其它代码基本是固定不变的,完全可以自动生成,避免不必要的出错风险。

改进后的DDS应用开发流程如图4所示,由格式化、结构化较强的类代码文件(IDL文件、应用描述XML文件)替代系统软件设计文档作为原始设计输入,后期系统软件设计文档可以由这些文件自动生成,这就解决了文档和工程实现之间难以同步的问题,而且这些类代码文件还可以通过一些版本管理工具(如Git)进行历史版本管理。之前业务代码之外需要手动编写的代码也可以通过语法解析和模板生成的方式,进行自动生成。这样一来,设计和开发实现完全解耦,业务开发人员只需要关心业务内容,编写业务代码即可,大大减少了开发人员的工作量,同时也避免了出错风险。最后将这些代码自动编译链接,生成DDS应用。

图4 DDS分布式系统的快速开发流程

自动生成代码的另一个优势是,还可以根据解析出来的接口、主题、消息域等元信息,自动生成各种增强性功能代码,这里主要添加了Lua解释器、Json与接口消息的转换、Http Server、消息记录、消息统计等功能,进一步增强DDS应用的基础能力。

3 改进的IDL编译器dseappgen关键技术

前面介绍了DDS分布式系统开发流程的改进,而支撑改进的核心是dseappgen这个改进的IDL编译器的实现。本部分主要介绍dseappgen的关键技术途径,分为5个方面:①新的设计输入;②语法解析;③代码生成;④Lua解释器;⑤其它增强功能。

3.1 新的设计输入

前面提到,在新的开发流程中使用格式化、结构化较强的类代码文件(IDL文件、应用描述XML文件)替代系统软件设计文档作为原始设计输入。

在包含传统的IDL文件内容,诸如模块(module)、结构体(struct)、枚举类型(enum)等等之外,dseappgen又添加了两个新的语法关键字——主题(topic)和消息域(domain),具体语法可以见表1。这两个关键字描述了系统中有哪些主题和消息域,以及主题与哪个结构体进行关联。从而把主题和消息域的定义,从代码转移到了IDL文件。IDL文件还加入了@description的注解语法,可以为各种语法元素添加注解,这些注解经过dseappgen处理后,可以生成为代码注释,以及软件设计文档,方便阅读。

表1 topic和domain关键字的基本语法

应用描述XML文件,该文件指明了引用哪个IDL文件,并且描述了该应用具体用到了哪些主题,这些主题在哪些消息域中进行发布或是订阅。dseappgen在获取这些信息后,一方面可以从IDL中裁减出该应用引用到的主题、消息域和接口,屏蔽掉未引用到的,从而加快代码生成和编译链接效率;另一方面,可以避免未引用主题和接口的干扰,防止应用开发者错误使用非设计的主题和接口。应用描述XML文件的基本格式见表2。其中,本文将主题的发布订阅方式具体划分为4种,即INPUT(只订阅该主题),OUTPUT(只发布该主题),LOOPBACK(在同一消息域中既发布也订阅该主题),CONVERT(在一个消息域中订阅该主题,在另一消息域中发布该主题),方便设计者灵活运用。

表2 应用描述XML文件的基本格式

这两类设计输入文件,IDL文件是面向系统全局,其内容由系统内所有应用共享,而应用描述XML文件是面向具体应用,每个应用有一份自己的应用描述XML文件,这两种文件实现了对DDS分布式系统的建模。

3.2 语法解析

语法解析是实现代码自动生成的前提,是实现用IDL和应用描述XML文件替代设计文档的关键技术。需要用语法解析技术解析出的元信息包括模块(module)、结构体(struct)、结构体元素(struct element)、枚举(enum)、枚举元素(enum element)、内置类型(如short、long等)、主题(topic)、消息域(domain)、应用名等,并且要解析出这些元信息之间的关联关系。

其中,应用、应用和主题之间的关联关系可以使用XML解析工具进行解析。而描述其它元信息及关联关系的IDL文件,使用的是经过关键字扩展后的IDL语法,这里采用ANTLR4进行语法分析,该工具在语法分析领域应用广泛[11,12]。核心的语法结构使用广泛应用的巴科斯范式(Backus-Naur form[13],BNF)表示见表3。

表3 语法结构核心部分的巴科斯范式

ANTLR4可以对语法元素进行分析和提取,但语法树的数据结构需要自行设计。dseappgen的语法树主要包括语法元素哈希表,以及表征语法元素关联关系的树形链表。语法树构建成功之后,就可以支撑后续步骤——代码生成——进行语法元素查找和遍历。

3.3 代码生成

在获取了各种必要的语法元信息之后,就需要对具体的DDS应用进行代码生成。代码生成从技术上来说,是通过一种模板引擎,将代码模板中待替换的内容,替换为相应的信息,在本文所研究的场景中,即替换为各种由语法解析分析出来的语法元信息。这里选用的模板引擎是StringTemplate,该引擎灵活高效,简单易用,而且能够满足代码递归生成的需求,该引擎经常和ANTLR4配合使用。

在选定代码生成引擎之后,需要对代码模板的结构进行设计。需要考虑的问题有目标生成的代码结构,以及暴露给用户的C++API。图5展示了代码模板的基本结构。在代码模板中,自动生成了与应用描述XML文件内容相对应的参与者、主题、发布者、订阅者等各种DDS的通信概念实体,从而免去了开发者手动编写的工作量,而且让开发者完全不需要了解这些具体概念。并且这些API将主题名具化到函数名里(图5中“xxx”表示的是某个主题的名字),从而可以让C++编译器协助检查,尤其是在使用IDE开发过程中,会有函数名自动提示,大大降低了开发者出错风险。与该应用无关的主题不会出现在生成的API中,也防止了开发者错误发布或订阅与其无关的主题。

图5 代码模板的基本结构

对于消息结构体的正反序列化代码,依然调用DDS自带的生成工具(ddsgen)进行生成。不过由于每个应用只引用了IDL文件中的部分结构体,所以为了避免生成冗余的正反序列化代码,这里通过对语法树中语法元素的依赖关系进行分析,只提取引用到的必要的语法信息,生成新的精简过的IDL文件,然后再交给ddsgen处理。其中,语法元素的依赖关系分析用到了DAG(有向无环图)的拓扑排序算法[14]。

代码生成还包括各种增强功能,以及CMake编译文件和配置文件的生成等等。

3.4 Lua解释器

Lua是一种可以动态解释执行的脚本语言,它具有轻量、方便嵌入集成、执行速度快等优点,在许多大型软件中作为嵌入式脚本语言存在。这里,引入Lua脚本语言,进一步提高DDS分布式系统的开发效率,并赋予DDS应用随时编写随时运行的动态执行能力,方便分布式系统的功能调试和仿真,尤其适用于功能原型的快速搭建,以及消息激励器、算法仿真器等模拟器的开发。

Lua脚本由Lua解释器加载执行,本文将Lua解释器内嵌到DDS应用中,并将DDS消息发布订阅相关API以Lua语言的形式暴露给用户。这里需要解决的主要问题是Lua对DDS C++接口的封装,Lua数据结构与DDS消息结构体的转换,暴露给用户的Lua API的设计。图5也展示了DDS应用中Lua相关部分的代码模板结构。代码模板将发布者和订阅者封装成Lua对象,并通过Lua Table和C++结构的转换实现消息在Lua API和DDS C++API之间的传递,用户只需要获取Lua封装的相应主题的发布者和订阅者对象,即可实现消息的发布订阅操作。

3.5 其它增强功能

考虑到DDS应用开发者在日常开发过程中的一些常用需求,又在生成的代码中加入了一些增强功能。

3.5.1 Json字符串与DDS消息的转换

开发者经常需要通过打印消息内容进行功能调试,然而实际系统中用到的DDS消息往往字段较多,逐字段打印会加大开发者工作量。前面通过语法解析,已经获取了DDS消息每个字段的名称和类型,因此可以通过代码生成的方法自动生成DDS消息与Json字符串之间的互相转换代码(这里用到了C++Json库nlohmann)。这样,需要打印消息内容时就可以将DDS消息以格式化良好的Json字符串形式打印,大大减轻了开发者的工作负担。另外,开发者也可以在代码中使用Json字符串作为DDS消息的字面值,然后转换为DDS消息结构体,进而进行消息发布,这样具有更好的可读性。

3.5.2 Http Server的嵌入

DDS应用多是运行在服务器的后台程序,若想与DDS应用在运行时进行交互,会比较困难,因此在DDS应用中嵌入了轻量级的Http Server——Boost Beast[15],它允许用户通过浏览器加载Web页面,执行Javascript代码,可以进行HTTP请求应答通信,支持WebSocket,当用户有运行时交互或者图形化交互需求时,可以开启该轻量级Http Server。

3.5.3 消息记录和统计

DDS分布式系统中往往记录DDS消息,以方便对系统进行数据分析或故障排查。由于在代码生成中已经掌控了DDS消息的发布和订阅,因此在发布和订阅过程中加入对消息的记录和统计。消息记录的内容包括消息Json字符串、应用名称、发布订阅时间等,使用Elasticsearch作为记录存储数据库,以支持快速高效的记录查询。消息统计主要统计的是消息的发布订阅次数、频率,通过Http Server开放了相应Restful Api,支持当前流行的数据采集平台Prometheus进行访问采集。

图6给出了dseappgen编译生成的可执行文件内部各功能的组成、协作以及与外部的交互。

图6 编译生成的可执行文件内部功能结构

4 简单实例

这里以一个简单的发布订阅实例说明使用dseappgen编译器后给DDS分布式系统开发带来的便利。

为了实现如图7所示的发布订阅通信功能,表4是该DDS分布式系统的IDL文件,表5是应用pubtest的应用描述XML文件,表6是应用subtest的应用描述XML文件,这3个文件由设计者编写。

图7 一个简单的发布订阅通信实例

表4 示例IDL文件test.idl

表5 示例应用pubtest的应用描述XML文件pubtest.xml

表6 示例应用subtest的应用描述XML文件subtest.xml

然后设计者使用表7中命令调用dseappgen生成两个可执行程序pubtest和subtest(dseappgen也可以生成供C++开发者使用的动态库,这里以生成Lua解释器为例)。

表7 使用dseappgen编译生成可执行程序

接下来,开发者可以为pubtest和subtest撰写Lua脚本实现消息的发布订阅。发布Lua脚本见表8,订阅Lua脚本见表9。

表8 示例应用pubtest的脚本pubtest.lua

表9 示例应用subtest的脚本subtest.lua

分别启动pubtest和subtest,将会看到subtest打印出pubtest发布的消息。从该示例可以直观看出,设计者和开发者的工作是解耦的。设计者使用IDL和应用描述XML文件就完成了整个分布式系统的应用组成和消息互联的设计,也就是完成了系统建模。通过dseappgen自动生成可执行文件后,即可将工作转移到开发者进行业务开发。dseappgen的代码生成隐藏了DDS诸多概念,使得发布订阅API变得非常简单,即使并不了解DDS的各种概念,也可以简单几行代码轻松实现发布订阅通信。

5 优势分析

本文在实现改进的IDL编译器dseappgen的基础上,提出了新的DDS分布式系统快速开发流程,主要的优势有以下几点:

(1)赋予IDL语言进行分布式系统建模的能力,贯彻了基于模型的系统工程化(MBSE)思想,IDL文件和应用描述XML文件完全建模了整个分布式系统的框架和通信结构,方便项目管理者从全局掌握项目拓扑情况。

(2)传统DDS分布式系统开发方式,主题和消息域的定义是侵入到代码中的,无法实现设计和开发的解耦,而新的开发流程中,主题和消息域定义在IDL文件中,应用的主题和消息域使用情况定义在应用描述XML文件中,从而使得设计者和开发者完全解耦。

(3)dseappgen生成的动态库或Lua解释器,只包含应用描述XML文件中涉及到的主题的API,避免了开发者发布订阅无关主题,方便项目管理者对主题访问权限进行管理;而且API是具化到函数名中的,C++编译器和Lua解释器会协助检查,防止开发者错误使用。

(4)自动生成的动态库和Lua解释器,隐藏了DDS内部复杂概念,使得发布订阅API更加简化,一行代码实现通信,大大减少了开发者的代码量,尤其是在动辄几百个主题的大型项目中,大大提高了开发效率,降低了项目风险。

(5)Lua语言的嵌入,以及各种增强功能的加入,让DDS应用更适合快速开发、快速测试、快速仿真。

(6)与当前热门的机器人操作系统ROS2[16]相比,ROS2同样使用DDS进行各应用节点之间的通信,并且定义了参数、服务等概念,并提供了基于Python的脚本开发方式,加上开源社区为ROS2提供了丰富的自动化控制领域的第三方库,是目前自动化领域设计仿真的首选框架。而本文更关注的是,为大型DDS分布式系统开发,提供一种开发模式,让设计者和开发者为完成项目更好更高效地进行合作,而ROS2并没有在这方面提供相应的解决方案,而且本文也提供了基于Lua的脚本开发方式。

6 结束语

本文结合当前DDS分布式系统工程开发现状,对工程开发实践中遇到的问题进行了分析,设计实现了改进的IDL编译器dseappgen,并基于此工具提出DDS分布式系统开发流程,其中涉及到的技术手段包括语法解析、代码生成、Lua解释器等等。新方法贯彻了MBSE的思想,将工程实践中设计和开发进行解耦合,一方面让设计独立于开发,不需要将设计侵入代码,另一方面也节省了开发者的很多工作量,尤其是Lua解释器的引入,让DDS应用拥有了随时修改随时运行的能力,非常适用于算法验证、消息激励等模拟器的开发。

目前dseappgen及其带来的新开发模式在很多项目中开始了实践和应用,很大程度加快了项目的设计、开发、测试进度。

为了继续推进DDS分布式系统开发的工程化,未来还将基于本文提出的开发流程,构建具有图形界面的“低代码[17]”开发平台,可以用于工程的系统原型快速搭建,提升工程开发的自动化,降低工程开发的复杂性。

猜你喜欢
开发者代码消息
一张图看5G消息
创世代码
创世代码
创世代码
创世代码
晚步见道旁花开
“85后”高学历男性成为APP开发新生主力军
16%游戏开发者看好VR