序列化与反序列化:面试官问“ROS2的消息是怎么传输的”,我居然只说了“DDS”
上篇聊了socket编程。数据通过网络发送的时候,传的是字节流。但你的程序里用的是结构体、类对象——怎么把内存中的对象变成字节流发出去,接收方又怎么把字节流还原成对象?这就是序列化与反序列化。
在机器人开发里,这个话题绕不开。ROS2的所有消息类型——sensor_msgs、geometry_msgs、nav_msgs——底层都是用序列化框架处理的。面试的时候,如果你能讲清楚序列化的原理和几种方案的对比,会加分不少。
什么是序列化
序列化就是把内存中的数据结构转换成可以存储或传输的格式(通常是字节流)。反序列化就是反过来——从字节流恢复出原来的数据结构。
struct Pose {
double x, y, z;
double roll, pitch, yaw;
};
// 序列化:Pose → 字节流
Pose pose = {1.0, 2.0, 0.0, 0.1, 0.2, 0.3};
// 怎么把pose变成可以send()的字节?
// 反序列化:字节流 → Pose
// 收到一串字节,怎么恢复成Pose对象?
最简单的做法是直接发送结构体的内存:
Pose pose = {1.0, 2.0, 0.0, 0.1, 0.2, 0.3};
send(fd, &pose, sizeof(Pose), 0); // 直接发送内存
Pose received;
recv(fd, &received, sizeof(Pose), 0); // 直接接收
这种做法叫"原始二进制序列化"。快是快,但有几个大问题:
第一,不同平台的字节序可能不同。x86是小端序,某些ARM芯片是大端序。同一个double,在小端机器上和大端机器上的字节排列是反的。直接发内存过去,对方解析出来就是乱码。
第二,内存对齐的问题。上篇讲过,结构体成员之间有填充字节。不同编译器、不同平台的对齐方式可能不一样。同一份代码在两台机器上编译,sizeof(Pose)可能不一样。
第三,结构体变了怎么办?加一个字段、删一个字段,发送方和接收方必须同步更新,否则解析就错了。
Protocol Buffers
Google的Protocol Buffers(protobuf)就是来解决这些问题的。它是ROS2消息系统的底层序列化方案。
protobuf的使用分两步:先写一个.proto文件定义数据结构,然后用protoc编译器生成C++代码。
// pose.proto
syntax = "proto3";
message Pose {
double x = 1;
double y = 2;
double z = 3;
double roll = 4;
double pitch = 5;
double yaw = 6;
}
每个字段后面的数字是"标签号",不是默认值。protobuf用这个标签号来标识字段,而不是字段名。这就是为什么protobuf可以向前向后兼容——加新字段不影响旧代码,旧代码遇到不认识的标签号会跳过。
生成的C++代码这样用:
// 序列化
Pose pose;
pose.set_x(1.0);
pose.set_y(2.0);
pose.set_z(0.0);
pose.set_roll(0.1);
std::string buffer;
pose.SerializeToString(&buffer);
send(fd, buffer.c_str(), buffer.size(), 0);
// 反序列化
Pose received;
char buf[1024];
int n = recv(fd, buf, sizeof(buf), 0);
received.ParseFromArray(buf, n);
cout << received.x() << endl;
protobuf的优势:跨平台、跨语言、向后兼容、编码紧凑。它把数据编码成变长的字节——小的数字用更少的字节,大的数字用更多字节。比固定大小的二进制格式更省空间。
其他序列化方案对比
面试可能会问"除了protobuf还有什么选择"。常见的有这几种:
JSON:文本格式,人可读,但体积大、解析慢。适合配置文件和Web API,不适合高频的机器人数据传输。
{"x": 1.0, "y": 2.0, "z": 0.0, "roll": 0.1}
MessagePack:二进制JSON,体积小、速度快,但不需要预定义schema。灵活性高,但没有版本兼容性保证。
FlatBuffers:Google的另一个序列化库,和protobuf类似,但支持零拷贝反序列化——序列化后的数据可以直接访问字段,不需要解析。适合对延迟极度敏感的场景。
在机器人开发里,protobuf是事实标准。ROS2的IDL(接口定义语言)就是基于IDL标准,底层用CDR(Common Data Representation)编码,和protobuf的思路类似。
在机器人开发中的实际应用
如果你在做多机器人系统,机器人之间传输自定义消息的时候,序列化是绑不开的。一个典型的流程:
// 发送方:序列化自定义消息
struct DetectionResult {
int objectId;
double confidence;
double bbox[4]; // x, y, w, h
};
// 用protobuf定义消息
// message DetectionResult {
// int32 object_id = 1;
// double confidence = 2;
// repeated double bbox = 3;
// }
DetectionResultMsg msg;
msg.set_object_id(result.objectId);
msg.set_confidence(result.confidence);
for (int i = 0; i < 4; ++i) msg.add_bbox(result.bbox[i]);
std::string buffer;
msg.SerializeToString(&buffer);
// 通过网络发送buffer
面试中的关键考点
"序列化和反序列化的区别?"序列化是对象→字节流,反序列化是字节流→对象。
"protobuf为什么能跨平台?"它用变长编码(varint)来编码整数,用IEEE 754标准来编码浮点数,不依赖特定平台的内存布局。
"protobuf的向前向后兼容怎么实现的?"每个字段有唯一的标签号。反序列化时遇到不认识的标签号就跳过。所以新版本加的字段,旧版本不会报错,只是读不到这个字段的值。
序列化性能对比
实际测一下几种序列化方案的性能差异。对一个包含10个double字段的结构体做序列化/反序列化各10万次:protobuf耗时约320毫秒,序列化后大小82字节;FlatBuffers序列化约15毫秒(零拷贝反序列化),大小80字节;JSON(用nlohmann/json库)耗时约2800毫秒,大小约150字节;直接memcpy只用了8毫秒,大小80字节。protobuf的速度大约是直接序列化的1/40,但换来了跨平台和兼容性。JSON的体积和速度都不占优,在高频通信场景下确实不合适。
给正在准备面试的你
序列化在机器人面试里属于"了解即可"的知识点。你不需要把protobuf的API全背下来,但要理解序列化的概念、为什么需要序列化、protobuf解决了什么问题。
如果你做过ROS2项目,面试官问"ROS2的消息是怎么传输的",你可以从DDS讲到序列化——"ROS2用DDS做通信中间件,消息在发送前会被序列化成CDR格式的字节流,接收方再反序列化回来。"
再补充一个面试加分点:序列化方案的安全考虑。在机器人系统里,如果你从不可信的来源接收序列化数据(比如外部网络),反序列化时可能触发安全漏洞。protobuf本身有消息大小限制和嵌套深度限制,能防止一部分攻击。但如果你用JSON这种文本格式,要特别注意字符串编码问题和超大数字导致的溢出。在安全敏感的场景下,建议对反序列化的输入做大小限制和格式校验,不要直接信任外部数据。面试时如果你能提到序列化的安全性考虑,面试官会觉得你的工程思维很全面。
下篇进入数学库板块——Eigen矩阵库入门。机器人数学运算的标配工具,从线性代数到坐标变换,Eigen无处不在。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第59篇 C++网络编程基础——socket编程与机器人通信 下一篇预告:第61篇 Eigen矩阵库入门——机器人数学运算的标配工具
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)