作者: 华丞臧
专栏:【C++】
各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注
)。如果有错误的地方,欢迎在评论区指出。推荐一款刷题网站 LeetCode
本文章主要介绍三个任务,一是在服务器上安装一个自己的ubuntu虚拟机;二是在ubuntu虚拟机上编译开源项目FastDDS;三是编译FastDDS的ShapesDemo。本文安装虚拟机、编译FastDDS以及ShapesDemo使用的都是ubuntu虚拟机,镜像为ubuntu-20.04.6-live-server-amd64.iso。
通过查阅资料,我了解到服务器上安装虚拟机可以使用kvm软件,使用下面的指令可以安装kvm软件及其依赖项:
yum -y install qemu-kvm libvirt libvirt-python libguestfs-tools virt-install virt-clone virt-v2v virt-manager virt-viewer
创建虚拟机,首先需要一个磁盘镜像文件,这里使用qemu-img来创建,指令如下:
qemu-img create -f qcow2 vm01.qcow2 20G # 新建磁盘镜像文件
有了磁盘镜像文件就可以开始创建虚拟机了,我采用命令行式创建虚拟机,使用ubuntu-20.04.6-live-server-amd64.iso镜像,指令如下:
virt-install --virt-type=kvm --name=ub01 --vcpus=16 --memory=16384
--location /home/iso/ubuntu-20.04.6-live-server-amd64.iso,
kernel=casper/vmlinuz,initrd=casper/initrd
--disk path=/home/imags/zch/vm01.qcow2,size=80,format=qcow2
--network bridge=br0 --graphics none --extra-args='console=ttyS0' --force
虚拟机创建好之后会进入安装界面,设置好用户和密码就可以开始使用ubuntu虚拟机了。
创建好并进入虚拟机后,网络是不可用的,我们可以通过配置/etc/netplan/00-installer-config.yaml
文件使网络可用,配置文件内容如下:
# This is the network config written by 'subiquity'
network:
ethernets:
ens2:
addresses: [192.168.2.225/24] # 与宿主机在同一网段即可
gateway4: 192.168.2.1
dhcp4: false
optional: true
nameservers:
addresses: [192.168.2.1, 8.8.8.8]
version: 2
renderer: networkd
文件配置好后,输入sudo netplan apply
指令使配置生效,即可使用网络。
virsh list --all # 显示所有已经定义的虚拟机的状态信息
virsh start ub01 # 启动名为ub01虚拟机
virsh console ub01 # 连接名为ub01虚拟机
virsh shutdown ub01 # 关闭名为ub01虚拟机
virsh destroy ub01 # 摧毁名为ub01虚拟机
virsh undefine ub01 # 删除名为ub01虚拟机
数据分发服务 (DDS) 是一种以数据为中心的通信协议,用于分布式软件应用程序通信。它描述了支持数据提供程序和数据使用者之间通信的通信应用程序编程接口 (API) 和通信语义。
由于它是以数据为中心的发布订阅 (DCPS) 模型,因此在其实现中定义了三个关键应用程序实体:发布实体,用于定义信息生成对象及其属性;订阅实体,定义信息消耗对象及其属性;以及定义作为主题传输的信息类型的配置实体,并使用其服务质量 (QoS) 属性创建发布者和订阅者,以确保上述实体的正确性能。
简单来说DDS就是一种以数据为中心的通信协议,用于分布式软件应用程序通信。而FastDDS一个基于DDS标准实现的开源库,是FastRTPS库的升级版本,为DDS标准提供高性能和可扩展性的实现。
在DCPS模型中主要有四个基本元素:
DDS 域由域 ID 标识。域参与者定义域 ID 以指定其所属的 DDS 域。具有不同 ID 的两个域参与者不知道彼此在网络中的存在。因此,可以创建多个通信渠道。这适用于涉及多个 DDS 应用程序,其各自的域参与者相互通信的场景,但这些应用程序不得干扰。DomainParticipant充当其他 DCPS 实体的容器,充当Publisher、Subscriber和Topic实体的工厂,并在域中提供管理服务。
实时发布订阅(RTPS)协议是为支持DDS而开发的,是一种发布订阅同行中间件,通信功能强大,支持UDP/IP、TCP以及共享内存(SHM)。
RTPS中定义了域的概念,它定义了一个单独的通信平面。多个域可以同时并存,域中可以包含任意数量的RTPSParticipants,即能够发送接收数据的元素。RTPSParticipants有两种端点:
RTPSParticipant中可以有任意数量的发送和接收的端点。他们通过围绕Topic展开,Topic定义和标记正在交换的数据,参与者通过RTPSWriter将数据写入Topic,也可以通过RTPSReader来接收与订阅相关的数据。
FastDDS官方给出了三种方式安装,我使用从源代码安装FastDDS库到Linux的方式。从源代码在 Linux 环境中安装 eProsima Fast DDS,将安装以下软件包:
foonathan_memory_vendor
,一个与 STL 兼容的C++内存分配器库。fastcdr
,一个根据标准 CDR 序列化机制进行序列化的C++库。fastrtps
, eProsima Fast DDS库的核心库。安装过程中所需要的工具如下:
使用下面的指令安装所需工具:
sudo apt install cmake g++ python3-pip wget git
eProsima Fast DDS 在从 Linux 环境中的源代码安装时必须具有以下依赖项:
Asio 和 TinyXML2 库
sudo apt install libasio-dev libtinyxml2-dev
OpenSSL
sudo apt install libssl-dev
Libp11 和 SoftHSM 库
sudo apt install libp11-dev libengine-pkcs11-openssl
sudo apt install softhsm2
# 请注意,softhsm2 软件包会创建一个名为 softhsm 的新组。要授予对 HSM 模块的访问权限,用户必须属于此组
sudo usermod -a -G softhsm 用户名
具体安装方法参考eProsima Fast DDS 。
创建一个目录用来下载和构建eProsima Fast DDS及其依赖项:
mkdir Fast-DDS
克隆以下依赖项并用cmake编译:
安装fastrtps
# 安装colcon和vcstool
pip3 install -U colcon-common-extensions vcstool
# 下载安装fastrtps
cd ~/Fast-DDS
wget https://raw.githubusercontent.com/eProsima/Fast-DDS/master/fastrtps.repos
mkdir src
vcs import src < fastrtps.repos
# 如果vcs找不到可以运行下面的指令
~/.local/bin/vcs import src < fastrtps.repos
# 构建包
colcon build
# 如果colcon找不到
~/.local/bin/colcon build
# 本地准备环境
source ~/Fast-DDS/install/setup.bash
# 或者
echo 'source ~/Fast-DDS/install/setup.bash' >> ~/.bashrc
安装foonathan_memory_vendor
cd ~/Fast-DDS
git clone https://github.com/eProsima/foonathan_memory_vendor.git
mkdir foonathan_memory_vendor/build
cd foonathan_memory_vendor/build
cmake .. -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install -DBUILD_SHARED_LIBS=ON
cmake --build . --target install
安装Fast-CDR
cd ~/Fast-DDS
git clone https://github.com/eProsima/Fast-CDR.git
mkdir Fast-CDR/build
cd Fast-CDR/build
cmake .. -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install
cmake --build . --target install
安装好上述依赖项就可以安装eProsima Fast-DDS:
cd ~/Fast-DDS
git clone https://github.com/eProsima/Fast-DDS.git
mkdir Fast-DDS/build
cd Fast-DDS/build
cmake .. -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install
cmake --build . --target install
在~/FastDDS中创建一个文件夹demo用来跑测试样例:
cd ~/Fast-DDS
mkdir demo && cd demo
# 也可以不用克隆,在安装fastdds中可以找到DynamicHelloWorldExample,路径为Fast-DDS/examples/cpp/dds
git clone https://github.com/eProsima/FastDDS/tree/master/examples/cpp/dds/DynamicHelloWorldExample
mkdir build && cd build
cmake ..
# 说明cmake最低版本要求
cmake_minimum_required(VERSION 3.16.3)
# 定义项目名称为DDSHelloWorldExample,指定版本号和编程语言C++
project(DDSHelloWorldExample VERSION 1 LANGUAGES CXX)
# 查找依赖项
if(NOT fastcdr_FOUND)
find_package(fastcdr REQUIRED) # 如果没找到使用find_package命令来查找
endif()
if(NOT fastrtps_FOUND)
find_package(fastrtps REQUIRED)
endif()
# 检查编译器是否支持C++11标准
include(CheckCXXCompilerFlag)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
check_cxx_compiler_flag(-std=c++11 SUPPORTS_CXX11)
if(NOT SUPPORTS_CXX11)
message(FATAL_ERROR "Compiler doesn't support C++11")
endif()
endif()
message(STATUS "Configuring HelloWorld example...") # 输出信息
file(GLOB DDS_HELLOWORLD_EXAMPLE_SOURCES_CXX "*.cxx") # 定义一个变量表示"*.cxx"
file(GLOB DDS_HELLOWORLD_EXAMPLE_SOURCES_CPP "*.cpp") # 定义一个变量表示"*.cpp"
# 使用${}依赖项生成DDSHelloWorldExample可执行文件
add_executable(DDSHelloWorldExample ${DDS_HELLOWORLD_EXAMPLE_SOURCES_CXX} ${DDS_HELLOWORLD_EXAMPLE_SOURCES_CPP})
# 为可执行文件添加编译定义选项
target_compile_definitions(DDSHelloWorldExample PRIVATE
$<$<AND:$<NOT:$<BOOL:${WIN32}>>,$<STREQUAL:"${CMAKE_BUILD_TYPE}","Debug">>:__DEBUG>
$<$<BOOL:${INTERNAL_DEBUG}>:__INTERNALDEBUG> # Internal debug activated.
)
# 可执行文件链接库
target_link_libraries(DDSHelloWorldExample fastrtps fastcdr fastdds::optionparser)
# 将DDSHelloWorldExample安装到指定路径
install(TARGETS DDSHelloWorldExample
RUNTIME DESTINATION examples/cpp/dds/HelloWorldExample/${BIN_INSTALL_DIR})
编译完成HelloWorldExample后,在build文件夹中找到DDSHelloWordExample可执行程序,在虚拟机上运行两个程序,如下:
./DDSHelloWordExample publisher
./DDSHelloWordExample subscriber
我们可以在不同主机(服务器)上安装同样的样例程序来测试,也可以在同一台服务器上不同的虚拟机上测试。下图是在同一服务器的不同虚拟机上进行测试的结果:
ShapesDemo是一个常见的示例程序,用于演示分布式系统中使用DDS(Data Distribution Service)实现数据发布和订阅的过程。
该示例程序通常以图形形状(如圆、矩形、三角形等)为数据对象,展示了如何在发布者和订阅者之间进行实时数据的传输和交互。
在ShapesDemo示例中,通常会有两个主要组件:
通过运行ShapesDemo示例程序,我们能够观察到发布者创建图形形状对象并发送数据,而订阅者接收并处理这些数据的过程。这有助于理解DDS在实时数据传输中的工作原理以及如何使用DDS实现分布式系统中的数据发布和订阅功能。
ShapesDemo即形状演示,可以理解为用图形演示。Fast-DDS的ShapesDemo就是用图形来演示Fast-DDS,让我们能够以更加形象直观的理解FastDDS。
安装ShapesDemo前,用户必须确保ubuntu上Qt5,可以在终端上运行一下指令来安装:
sudo apt install -y qtbase5-dev
可以使用colcon安装ShapesDemo,首先安装colcon和vcs,安装指令如下:
pip install -U colcon-common-extensions vcstool
创建一个ShapesDemo文件夹用来下载安装eProsima Shapes Demo 及其依赖项的存储库文件,步骤如下:
mkdir -p ShapesDemo/src && cd ShapesDemo
wget https://raw.githubusercontent.com/eProsima/ShapesDemo/master/shapes-demo.repos
vcs import src < shapes-demo.repos
# 找不到vcs,尝试使用下面的指令
~/.local/bin/vcs import src < shapes-demo.repos
# 构建包
colcon build
# 找不到colcon,尝试使用下面的指令
~/.local/bin/colcon build
链接应用程序可执行文件以使其可从当前目录访问:
source install/setup.bash
运行ShapesDemo:
ShapesDemo
使用服务器上的虚拟机,用xshell连接的服务器,而xshell并不支持弹窗,ShapesDemo运行会弹出一个窗口用来做形状演示。我这里使用的是Xmanager软件来支持ShapesDemo,使用Xmanager配置步骤如下:
创建一个XDMCP会话,配置如下,其中主机为虚拟机ip地址。
配置DISPLAY环境变量
export DISPLAY=192.168.3.240:0.0 #此ip为使用的电脑ip
# 注意如果使用同一台主机上的两个虚拟机来演示,其中一个DISPLAY配置如下
export DISPLAY=192.168.3.240:1.0
Fast-DDS-Gen是使用Gradle构建的,Gradle是一个开源的构建自动化工具,需要执行Java版本。FasT-DDS-Gen是一个代码生成工具,用于根据DDS标准规范生成相关的代码。它使用IDL描述数据类型和通信接口,并根据IDL文件生成相应的数据类型定义、消息传输代码和序列化/反序列化代码等。
安装Java JDK
sudo apt install openjdk-11-jdk
cd ~
git clone --recursive https://github.com/eProsima/Fast-DDS-Gen.git
cd Fast-DDS-Gen
./gradlew assemble
首先我们要编写一个IDL文件,内容如下:
struct Test
{
string message;
unsigned long index;
};
编写并保存好IDL文件,接下来就可以使用Fast-DDS-Gen工具来生成代码了,运行下面的指令:
/scripts/fastddsgen -example CMake Test.idl
代码生成完我们可以来测试这些代码,运行下面的指令:
mkdir build && cd build
cmake ..
make
使用fastddsgen和idl文件可以自动帮我们生成主体代码。但当需要使用一个新的类型时,我们每次都需要新建一个idl文件并使用fastddsgen工具来生成代码。使用XML配置文件,我们就可以直接在XML文件中添加我们所需要的类型即可进行使用,不用fastddsgen工具来生成代码。
XML配置文件是一种常见的用于配置应用程序或系统的文件格式。它使用XML(可扩展标记语言)的语法,以层次结构的方式组织和描述配置信息。XML文件中类型配置文件的定义是标签式的,每个元素可以包含一个或多个类型定义。在一个元素中定义多个类型或为每个元素定义单个类型具有相同的结果。
XML可用基本类型的标识符
boolean |
char8 |
char16 |
---|---|---|
byte |
octet |
uint8 |
int8 |
int16 |
int32 |
uint16 |
uint32 |
int64 |
uint64 |
float32 |
float64 |
float128 |
string |
wstring |
XML配置文件实例:
<?xml version="1.0" encoding="UTF-8" ?> # XML声明,指定XML版本和字符编码
<types xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles" > #使用网址标识的命名空间
<type> # 类型元素
<struct name="HelloWorld"> # 创捷结构体,名为HelloWorld
<member name="message" type="string"/> # 结构体成员变量
<member name="index" type="uint32"/>
<member name="array" type="uint32" arrayDimensions="5,2"/> # 5*2的数组
</struct> # 结构体结束标志
</type> # 类型结束标志
<type>
<struct name="Test">
<member name="message" type="string"/>
<member name="index" type="uint32"/>
</struct>
</type>
</types>
使用fastddsgen和IDL与使用XML配置文件相比,前者会用IDL结构体生成一个类,同时声明定义其数据类型封装结构体;后者不需要封装目标结构体并声明定义,eProsima Fast DDS中提供了一些接口用来获取目标结构以及其成员变量、定义对象等,下面代码是一段实例:
// 加载目标xml文件,并判断是否加载成功
if (eprosima::fastrtps::xmlparser::XMLP_ret::XML_OK != eprosima::fastrtps::xmlparser::XMLProfileManager::loadXMLFile("helloworld_example_type_profile.xml"))
{ // 加载失败
std::cout <<
"Cannot open XML file \"helloworld_example_type_profile.xml\". Please, run the publisher from the folder "
<< "that contatins this XML file." << std::endl;
return false;
}
// 获取动态数据类型Test,并用build构建该类型对象
eprosima::fastrtps::types::DynamicType_ptr dyn_type =
eprosima::fastrtps::xmlparser::XMLProfileManager::getDynamicTypeByName("Test")->build();
// 使用动态类型创建TypeSupport
TypeSupport m_type(new eprosima::fastrtps::types::DynamicPubSubType(dyn_type));
// 创建动态数据对象
m_Hello = eprosima::fastrtps::types::DynamicDataFactory::get_instance()->create_data(dyn_type);
//m_Hello->set_string_value("hello", 0); // 两个参数:value id(id与XML文件中定义成员的顺序相关)
m_Hello->set_uint32_value(0, 1);
使用XML配置文件,我们就只需要完成发布者、订阅者、主函数代码的编写即可。具体实例代码请看
XMLFastDDSExample
eProsima Fast DDS还提供了一种动态定义的方式,这种方式让我们可以不用依赖代码文件以外的文件来定义所需要的结构。eProsima Fast 提供了一些接口在代码中来定义结构体数据类型,我们可以用这些接口定义自己所需要的数据类型。
下面是一段代码实例:
void DynamicDataPublisher::publishData()
{
static long long i = 0;
std::cout << "第" << std::to_string(i) << "次更新数据" << std::endl;
// 根据动态类型构建对象
auto struct_data =
DynamicDataFactory::get_instance()->create_data(dynamic_struct_ptr_);
assert(struct_data != nullptr);
// 设置结构体的值
assert(struct_data->set_byte_value(i % 127, 0) == ReturnCode_t::RETCODE_OK);
assert(struct_data->set_int32_value(i % (0xffffffff / 2 - 1), 1) ==
ReturnCode_t::RETCODE_OK);
assert(
struct_data->set_string_value(std::string("seq:") + std::to_string(i),
2) == ReturnCode_t::RETCODE_OK);
std::cout << "数据为:" << i % 127 << " " << i % (0xffffffff / 2 - 1)
<< " " << std::string("seq:") + std::to_string(i) << " "
<< std::endl;
data_writer_->write(struct_data);
++i;
}
void DynamicDataPublisher::create_dynamic_struct_type()
{
// 创建byte类型构建器
DynamicTypeBuilder_ptr base_type_builder =
DynamicTypeBuilderFactory::get_instance()->create_byte_builder();
assert(base_type_builder != nullptr);
// 创建byte(位集)动态对象
auto base_type = base_type_builder->build();
// 创建int32构建器
DynamicTypeBuilder_ptr base_type_builder2 =
DynamicTypeBuilderFactory::get_instance()->create_int32_builder();
assert(base_type_builder2 != nullptr);
auto base_type2 = base_type_builder2->build(); // 创建int32动态对象
// 创建string构建器
DynamicTypeBuilder_ptr base_type_builder3 =
DynamicTypeBuilderFactory::get_instance()->create_string_builder();
assert(base_type_builder3 != nullptr);
auto base_type3 = base_type_builder3->build();
// 创建结构体构建器
DynamicTypeBuilder_ptr struct_type_builder =
DynamicTypeBuilderFactory::get_instance()->create_struct_builder();
assert(struct_type_builder != nullptr);
// 构建名为MyStruct的结构体
struct_type_builder->set_name("MyStruct");
// 增加结构体成员
assert(struct_type_builder->add_member(0, "byte_value", base_type) ==
ReturnCode_t::RETCODE_OK);
assert(struct_type_builder->add_member(1, "int32_value", base_type2) ==
ReturnCode_t::RETCODE_OK);
assert(struct_type_builder->add_member(2, "string_value", base_type3) ==
ReturnCode_t::RETCODE_OK);
// 创建MyStruct动态类型对象
auto struct_type = struct_type_builder->build();
dynamic_struct_ptr_ = struct_type;
// 创建类型支持对象
eprosima::fastdds::dds::TypeSupport typeSupport(
new eprosima::fastrtps::types::DynamicPubSubType(struct_type));
typeSupport.get()->auto_fill_type_information(false);//指定是否自动填充类型信息
typeSupport.get()->auto_fill_type_object(true);// 指定是否自动填充类型对象
typeSupport.register_type(this->participant_);//注册类型
}
动态类型具体使用方法可以参考eprosima Fast DDS。