最近在折腾ROS两个节点之间的通信实现,每个的功能都很简单,需要解决的地方在于两个之间的通信量很大,延迟要很低才行。确定使用protobuf共享内存后,网上查阅了好多资料才最终实现。在这实现一个最小的demo作为记录,希望能对后来者有帮助! github链接

1. protobuf 与共享内存

关于二者的概念,网上资料很多,在这最基本的说明下。ROS中传统的通信方式是socket实现,通过订阅和发布信息实现通信,另外服务和参数服务器因为暂未涉及在这不进行比较。这种方式虽然延时也很低但当数据量比较大,对传输稳定性要求高或者更高传输速率时,便显得不那么合适了。

共享内存指的是通过对一块固定内存进行映射,使得不同程序或线程可以异步访问。通过这种数据拷贝的方式实现通信,速率和稳定性上有极大的提升。另外介绍下protobuf,可以简单理解为一种数据封装通信协议,可以实现不同语言之间的数据通信,而且可以离线更改和扩展,具体请查阅资料。

  • protohuf安装
  • proto文件编写
    关于安装的具体步骤以及具体语法在这不一一细谈了,可以查看参考链接自行安装。在过程中,个人感觉有几点需要注意的在这说明下: 1. Ubuntu自身带有protobuf,最好提前卸载,替换成官方最新的,网上有资料自行查阅 2. 关于protobuf的安装路径说法不一,个人倾向于默认安装也就是官方安装,如自己指定安装目录后面程序运行还需要自己配置环境变量,有点麻烦

2. 实现细节

下面通过client和server两个节点之间最简单通信,来比较详细的说明protobuf与共享内存结合实现通信的过程。

2.1 创建工程

创建ros工作空间,并创建server和client两个节点

1
2
3
4
5
6
7
8
mkdir work_ws
cd work_ws & mkdir src
catkin_init_workspace
cd ..
catkin_make
cd src
catkin_create_pkg server roscpp
catkin create_pkg client roscpp

下面分别在两个节点下创建文件,最后创建完结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
src
├── client
│ ├── CMakeLists.txt
│ ├── include
│ │ └── client
│ │ ├── client.cc
│ │ └── client.h
│ ├── package.xml
│ ├── proto
│ │ └── agreement.proto
│ └── src
│ └── client_node.cc
├── CMakeLists.txt
└── server
├── CMakeLists.txt
├── include
│ └── server
│ ├── server.cc
│ └── server.h
├── package.xml
└── src
└── server_node.cc

2.2 proto文件编写

在client节点中单独创建一个proto文件夹,用于存放proto文件。关于其具体语法请自行查阅,在这使用protobuf3版本,与版本2相比有些许不同。程序为一个最简单的message信息

1
2
3
4
5
6
7
8
9
10
syntax = "proto3";

message agreement {
double id = 1;
double x = 2;
double y = 3;
double z = 4;

bool flag = 5;
}

下面是最复杂的地方,也就是CMakeLists.txt文件的编写,网上资料很少,在这给出一个参考链接。可以结合我工程中的代码仔细理解下(我也不是很理解。。。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
cmake_minimum_required(VERSION 2.8.3)

project(client)

find_package(catkin REQUIRED COMPONENTS
roscpp
)

## System dependencies are found with CMake's conventions
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
include(FindProtobuf)
find_package(Protobuf REQUIRED)

set(proto_dir ${PROJECT_SOURCE_DIR}/proto)
file(GLOB proto_files "${proto_dir}/*.proto")

# Set up destination directories
catkin_destinations()
set(proto_gen_dir ${CATKIN_DEVEL_PREFIX}/${CATKIN_GLOBAL_INCLUDE_DESTINATION}/proto)
set(proto_gen_cpp_dir ${proto_gen_dir})
file(MAKE_DIRECTORY ${proto_gen_dir})
file(MAKE_DIRECTORY ${proto_gen_cpp_dir})
set(protogen_include_dirs ${proto_gen_cpp_dir}/../ ${proto_gen_python_dir})

# Create lists of files to be generated
set(proto_gen_cpp_files "")
foreach(proto_file ${proto_files})
get_filename_component(proto_name ${proto_file} NAME_WE)
list(APPEND proto_gen_cpp_files
${proto_gen_cpp_dir}/${proto_name}.pb.h
${proto_gen_cpp_dir}/${proto_name}.pb.cc
)
endforeach(proto_file ${proto_files})

# Run protoc and generate language-specific headers.
add_custom_command(
OUTPUT ${proto_gen_cpp_files}
COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} --proto_path=${proto_dir} --cpp_out=${proto_gen_cpp_dir} ${proto_files}
DEPENDS ${PROTOBUF_PROTOC_EXECUTABLE} ${proto_files}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)

set_source_files_properties(${proto_gen_cpp_files} PROPERTIES GENERATED TRUE)

add_custom_target(${PROJECT_NAME}_generate_headers
DEPENDS ${proto_gen_cpp_files}
)

# Create proto library for lining.
include_directories(${PROTOBUF_INCLUDE_DIR} ${PROTOBUF_INCLUDE_DIR}/../../)
add_library(${PROJECT_NAME}_proto ${proto_gen_cpp_files})
target_link_libraries(${PROJECT_NAME}_proto ${PROTOBUF_LIBRARY})
add_dependencies(${PROJECT_NAME}_proto ${PROJECT_NAME}_generate_headers)

catkin_package(
INCLUDE_DIRS ${protogen_include_dirs}
LIBRARIES ${PROJECT_NAME}_proto
)

install(TARGETS ${PROJECT_NAME}_proto
ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

install(DIRECTORY ${proto_gen_cpp_dir}/
DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
FILES_MATCHING PATTERN "*.h"
)

catkin_package(
INCLUDE_DIRS include
)

include_directories(
${catkin_INCLUDE_DIRS}
${PROTOBUF_INCLUDE_DIR}
${CATKIN_DEVEL_PREFIX}
include
)

add_library(${PROJECT_NAME}
include/${PROJECT_NAME}/client.cc
)

add_executable(client_node src/client_node.cc include/${PROJECT_NAME}/client.cc)

target_link_libraries(${PROJECT_NAME}_node
${catkin_LIBRARIES}
${PROTOBUF_LIBRARIES}
${Boost_FILESYSTEM_LIBRARY}
${Boost_SYSTEM_LIBRARY}
)

最后实现效果是在work_ws工作空间的devel文件夹下创建include文件夹放置生成的proto头文件。因为关于ROS中使用protobuf没有足够的参考的资料,所以就随性发挥了。也想过参考Apollo放置在节点的proto文件夹下,但是节点之间message的引用也很麻烦,细想放在devel文件夹下引用可能会更方便,所以就没有再花费时间更改。生成文件效果如下:

1
2
3
4
include/
└── proto
├── agreement.pb.cc
└── agreement.pb.h

大家可以对照CMakeLists.txt文件理解其中的实现过程。需要注意的是,如果其他节点想要调用生成的proto头文件,需要在其本身的CMakeLists.txt中的include_directories中包含devel文件夹的位置变量,否则引用会报错,如下

1
2
3
4
5
6
include_directories(
${catkin_INCLUDE_DIRS}
${PROTOBUF_INCLUDE_DIR}
${CATKIN_DEVEL_PREFIX} # this should be added
include
)

2.3 主程序编写

下面以client节点为例说明下主程序的实现,并介绍共享内存的调用实现。整个工程代码已上传github,请自行查阅 client.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#ifndef CLIENT_H_
#define CLIENT_H_

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <ros/ros.h>
#include <iostream>

#include "include/proto/agreement.pb.h"

namespace Client {

class client {
public:
client();
~client();

void sendMessage();
private:
int shmID; // 共享内存标识符
void *shm = nullptr; // 与进程内存连接地址
agreement *mem = nullptr;
};
} // namespace Client

#endif

关于共享内存只需要记住三个头文件,四个函数即可,理解起来比多进程容易多了。详细查阅参考资料。下面是client.cc文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

#include "client/client.h"

using namespace Client;

client::client() {
shmID = shmget((key_t)1933, sizeof(agreement), 0666 | IPC_CREAT);
if (shmID != -1) {
shm = shmat(shmID, NULL, 0);
} else {
ROS_ERROR("Cann't get shared memory,error num is %d, for %s\n", errno,
strerror(errno));
}
mem = (agreement *)shm
}

void client::sendMessage() {
std::cout << "Ready to send message to client " << std::endl;
double id = 1, x = 2, y = 3, z = 4;

while (true) {
mem->set_flag(true);
mem->set_id(id++);
mem->set_x(x++);
mem->set_y(y++);
mem->set_z(z++);
sleep(1);
}
}

client::~client() {
if (shmdt(shm) == -1) {
ROS_ERROR("Error is %d, cann't not release shared memory. for %s\n", errno,
strerror(errno));
}
}

对照参考资料,代码很容易理解,需要注意的是,在这直接指定了共享内存标识符,目的是方便后面的节点直接访问。


2.4 程序运行

最后程序运行效果如下:

client节点计数,server节点读取打印。


3. 总结

综上,在ros环境下实现了protobuf共享内存的节点通信,复杂之处主要在于包含了protobuf文件的CMakeLists.txt文件的编写,后面就简单多了。