0%

boost serialize(序列化)和boost deserialize(反序列化)类对象型数据并通过socket网络传输

在自动驾驶领域,传感器与主机通过网线连接,实现二者的实时通信。同时,在接收到雷达数据之后,后台处理系统到前端用户显示界面,也需要通过上述方法进行通信,因为后台处理系统一般都不自带显示器,例如PX-2,TX2等平台就提供网线接口。
本文以16线激光雷达生成数据,处理后台系统处理后点云为例,进行分割分类数据传输到前端用户界面进行显示。
主要用到了以下两个方面:

  • boost::serialize 和 boost::deserialize 函数
  • socket sendto() 和 recvfrom() 函数

本文首先介绍不使用boost序列化方法进行传输方法,之后再介绍boost序列化方法进行数据传输的方法。主要梳理boost::serialize(序列化)和boost::deserialize(反序列化)相关内容,socket部分稍作介绍。关于二者的相关知识链接,请参阅底部参考文献。

1. 非序列化数据传输方法

1.1 数据类格式

非序列化数据传输方法中数据类表示如下:

1
2
3
4
5
6
struct CommunicationMsg
{
Header header;
PoseMsg pose;
char perceptions[20000];
}

headerpose为另外的数据类,perceptionschar型数据容器。在非序列化方法下,将CommunicationMsg通过socket进行传输时,必须保证接收端的CommunicationMsg也必须为同样的数据顺序格式,即headerposeperceptions三者顺序以及结构必须保持一致。

1.2 发送端代码

非序列化发送端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool sendMsg(const std::vector<PerceptOutput> &datas, const Header &header, const PoseMsg &pose)
{
CommunicationMsg send_msg;
send_msg.header = header;
send_msg.pose = pose;
std::vector<PerceptResultMsg> msgs = convertToMsg(datas);

/// origin send
std::string tmp_string = toString(msgs);
send_msg.header.length = static_cast<int>(tmp_string.size());
for(int i = 0; i < static_cast<int>(tmp_string.size()); ++i)
{
send_msg.perceptions[i] = tmp_string[i];
}
char pot[20480];
memset(pot, 0, sizeof(pot));
memcpy(pot, &send_msg, sizeof(send_msg));
sendto(sender_sockfd_, pot, sizeof(pot), 0, (struct sockaddr *) &sender_dest_addr_, sizeof(sender_dest_addr_));

return true;
}

可以看到上述代码中的pot变量为固定长度2048020480这个数字是根据自己的数据长度来定,唯一的原则就是要保证所要发送的数据长度必须小于该数大小。这里就体现出非序列化方法中固定数组长度弊端

  1. 数据小于该数组长度时,造成网络带宽的浪费;
  2. 数据大于该数组长度时,造成数据溢出。

1.3 接收端代码

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
bool receMsg(std::vector<PerceptResultMsg> &msgs, Header &header, PoseMsg &pose)
{
char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
socklen_t cliaddr_len = sizeof(receiver_dest_addr_);

char recv_buf[50000] = {0};
int recv_len = recvfrom(receiver_sockfd_, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &receiver_dest_addr_, &cliaddr_len);
inet_ntop(AF_INET, &receiver_dest_addr_.sin_addr, cli_ip, INET_ADDRSTRLEN);

/// origin receive
CommunicationMsg *tmp_msg = new CommunicationMsg;
tmp_msg = (CommunicationMsg *) recv_buf;
int str_len = tmp_msg->header.length;
std::string tmp_str;
tmp_str.resize(str_len);
for(int i = 0; i < str_len; ++i)
{
tmp_str[i] = tmp_msg->perceptions[i];
}
msgs = toData(tmp_str);
header = tmp_msg->header;
pose = tmp_msg->pose;

return true;
}

接收端代码中recv_buf固定大小为50000,由于数据在完全接收之前不知道数据长度为多少,所以必须使用更大的数组容器来盛放接收数据。此处,能想到的可以讲数据长度变量和数据本身分开成两次先后接收,可以先接收到数据长度,然后使用该长度去接收数据,不过这样使得操作更加麻烦,可能用更加智能的方法,本文未涉及。
接收端将接收到的数据通过此行代码 tmp_msg = (CommunicationMsg *) recv_buf;进行强制类型转换,该方法监督粗暴,但后患无穷,具体会导致哪些不容易发现的bug,本文未进行深究,但此方法在使用中也是可行方案之一。这个方法依据数据在数组中的放置方式,完全按照该类结构来储存,所以可以采用此方法。此方法属于C/C++语言的自带特性中的强制类型转换应用。

2. boost serialize (序列化) 和 boost deserialize (反序列化)

对于发送和接收消息,我觉得可以通过举一个例子来形象的理解:谍战中的密码报文发送与接收。当友军持有相同的密码解读字典时,发送密码一方通过将需要发送的信息,通过查询密码字典翻译为密码,然后通过发报机发送出去,接收一方首先记录下接收到的信息,然后在通过查询密码字典将信息翻译出来,从而得到真实的信息。只要理解了上面的原理,将该模型抽象出来,就会在生活中发现许多相似的场景:理解相同语言人之间的对话,收音机调到某一频率收听节目,与黄金等值的货币之间的兑换等等。这就是 boost 序列化的最形象的理解。

2.1 类对象数据序列化

数据格式的序列化可分为:XML格式,文本格式(text)和二进制格式(binary)。二进制格式虽然在传输过程中速度会更快,但是会破坏类对象型数据数据结构,所以不采用此方法。文本格式是最容易理解和输出可是化的格式,所见即所得,所以本文采用此格式进行数据传输。
使用文本格式序列化须包含以下头文件:#include <boost/archive/text_iarchive.hpp>#include <boost/archive/text_oarchive.hpp>。另外,boost自带对STL库的数据支持,例如本文使用到的对std::vector支持,需要包含头文件#include <boost/serialization/vector.hpp>
类对象数据是否侵入式序列化有两种方式:侵入式序列化(intrusive)和非侵入式序列化(non-intrusive)。

关于二者的介绍,[参考文献1]中进行了详细描述。本文将以第1章中数据为例,使用侵入式序列化进行简单的介绍。

侵入式序列化

侵入式序列化操作相关代码:

1
2
3
4
5
6
7
8
std::vector<PerceptResultMsg> pcep_msg;
template<typename Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & a;
ar & b;
...
}

以本文数据类型为例:

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
//boost
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/vector.hpp>


// header
struct Header{
//public:
// Header() {}
// Header( unsigned short int& in_head, unsigned short int& in_object_num, unsigned int& in_length,
// unsigned long int& in_frame_id, unsigned long int& in_timestamp):
// head(in_head), object_num(in_object_num), length(in_length), frame_id(in_frame_id), timestamp(in_timestamp){}

friend class boost::serialization::access;
template<class Archive>
void serialize(Archive& ar, const unsigned int version)
{
ar & head;
ar & object_num;
ar & length;
ar & frame_id;
ar & timestamp;
}

unsigned short int head;
unsigned short int object_num;
unsigned int length;
unsigned long int frame_id;
unsigned long int timestamp;
};

struct CommunicationMsg{
friend class boost::serialization::access;
Header header;
PoseMsg pose;
// char perceptions[20000];

// intrusive
std::vector<PerceptResultMsg> pcep_msg;
template<typename Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & header;
ar & pose;
ar & pcep_msg;
}

既然是“侵入式”,那么就要对原有数据类进行更改,上述代码中的更改将std::vector<PerceptResultMsg> pcep_msg替代原有char perceptions[20000]为容器。对于侵入式序列化的类数据每一个数据成员,同样要进行侵入式序列化处理,因为boost serialize只对其支持的数据结构(基本数据类型和STL库里的数据类型)进行序列化操作,对于用户自定义的类型数据,无法自动进行序列化处理。代码中以struct header子数据成员为例进行了侵入式序列化操作。

2.2 序列化操作代码

1
2
3
4
5
6
7
std::string Serialize(const CommunicationMsg &msg)
{
std::ostringstream archiveStream;
boost::archive::text_oarchive archive(archiveStream);
archive << msg;
return archiveStream.str();
}

CommunicationMsg类型对象传入该函数可得到string类型数据传输流,用以 socket 通信。

2.3 接收端反序列化

为了反序列化出原有CommunicationMsg类型数据对象,那么定义声明CommunicationMsg类型文件必须在接收端同样有一份,这样反序列化操作才能根据该文件得出经 socket 传输过来的数据属于哪个变量。

1
2
3
4
5
6
7
8
CommunicationMsg DeSerialize(const std::string &message)
{
CommunicationMsg msg;
std::istringstream archiveStream(message);
boost::archive::text_iarchive archive(archiveStream);
archive >> msg;
return msg;
}

3. socket 通信

基于 socket 的通信,本文使用了以下两个函数:

1
2
3
sendto(sender_sockfd_, pot_1, sizeof(pot_1), 0, (struct sockaddr *) &sender_dest_addr_, sizeof(sender_dest_addr_));

recvfrom(receiver_sockfd_, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &receiver_dest_addr_, &cliaddr_len);

3.1 序列化数据发送端代码

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
bool sendMsg(const std::vector<PerceptOutput> &datas, const Header &header, const PoseMsg &pose)
{
CommunicationMsg send_msg;
send_msg.header = header;
send_msg.pose = pose;
std::vector<PerceptResultMsg> msgs = convertToMsg(datas);

/// serialize send
try
{
send_msg.pcep_msg.resize( msgs.size() );
for (int i = 0; i < msgs.size(); ++i)
{
send_msg.pcep_msg[i] = msgs[i];
}
std::string send_str = this->Serialize( send_msg );
char pot_1[send_str.length()];
for (int i = 0; i < send_str.length(); ++i)
{
pot_1[i] = send_str.c_str()[i];
}
sendto(sender_sockfd_, pot_1, sizeof(pot_1), 0, (struct sockaddr *) &sender_dest_addr_, sizeof(sender_dest_addr_));
}
catch ( std::exception& e )
{
std::cerr << e.what() << std::endl;
}
return true;
}

至于为什么使用异常处理模块,考虑到偶尔有无法序列化的对象,防止程序崩溃继续运行的手段,对于某一帧点云数据无法传输序列化,通过异常处理手段来跳过是可以忍受的。反序列化端接收代码同样如此。

3.2 序列化数据接收端代码

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
bool receMsg(std::vector<PerceptResultMsg> &msgs, Header &header, PoseMsg &pose)
{
char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
socklen_t cliaddr_len = sizeof(receiver_dest_addr_);

char recv_buf[50000] = {0};
int recv_len = recvfrom(receiver_sockfd_, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *) &receiver_dest_addr_, &cliaddr_len);
inet_ntop(AF_INET, &receiver_dest_addr_.sin_addr, cli_ip, INET_ADDRSTRLEN);
/// ---------- boost deserialize ---------------
std::string rec_str;
for (int i = 0; i < recv_len; ++i)
{
rec_str += recv_buf[i];
}

try {
CommunicationMsg boost_msg;
boost_msg = this->Deserialize( rec_str );
std::cout << "boost_msg.pcep_msg.size(): " << boost_msg.pcep_msg.size() << std::endl;
header = boost_msg.header;
pose = boost_msg.pose;
msgs = boost_msg.pcep_msg;
}
catch ( std::exception& e )
{
std::cerr << e.what() << std::endl;
}
return true;
}

后记

此次小的更新虽然只是一个很小部分代码更新,对于整体性能影响也不大,顶多对网络带宽占用率有一点降低,提升一点传输速度,但是在此处花费的时间将近一周,实在不应该。个人觉得应该学到的更多的是找到问题,解决问题的能力。对于调试大型工程代码有了一点自己的认识,从未接触过的领域,可以触类旁通的去理解,但实际问题还是需要实际的知识和工具来解决,不要局限在仅有的知识储备上来看问题,容易闭门造车。

以上。


参考文献:

  1. Boost - 序列化 (Serialization)
  2. boost::serialize 官方教程
  3. 使用boost库序列化传输对象