在嵌入式设备上使用 Protocol Buffers (protobuf) 进行数据序列化和解析
Protocol Buffers(简称 protobuf)是 Google 开发的一种语言无关、平台无关的数据序列化协议,广泛应用于网络通信和数据存储领域。在嵌入式系统中使用 protobuf 可以实现高效的数据交换,特别是在物联网设备、传感器网络和跨平台通信场景中。
在嵌入式设备上使用 Protocol Buffers (protobuf) 进行数据序列化和解析
Protocol Buffers(简称 protobuf)是 Google 开发的一种语言无关、平台无关的数据序列化协议,广泛应用于网络通信和数据存储领域。在嵌入式系统中使用 protobuf 可以实现高效的数据交换,特别是在物联网设备、传感器网络和跨平台通信场景中。
本文将详细介绍如何在嵌入式设备上使用 protobuf,包括从 proto 文件生成 C++ 代码,以及进行交叉编译以移植到嵌入式设备的完整过程。
为什么在嵌入式系统中使用 protobuf?
与传统的 JSON 或 XML 格式相比,protobuf 具有以下优势:
- 体积小:二进制格式,比 JSON/XML 更节省空间
- 速度快:序列化和反序列化效率更高
- 类型安全:强类型定义,减少运行时错误
- 向前/向后兼容:支持版本演进
准备工作
安装 Protobuf 编译器 (protoc)
首先在开发主机上安装 protobuf 编译器:
# Ubuntu/Debian
sudo apt-get install protobuf-compiler libprotobuf-dev
# CentOS/RHEL
sudo yum install protobuf-devel protobuf-compiler
或者从源码编译安装:
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -Dprotobuf_BUILD_TESTS=OFF
make -j$(nproc)
sudo make install
sudo ldconfig
创建 Proto 文件
创建一个简单的 proto 文件,例如 person.proto,定义数据结构:
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
在这个例子中,我们定义了 Person 消息类型,包含姓名、ID、邮箱和电话号码等字段。proto 文件语法简洁明了,易于理解。
生成 C++ 代码
使用 protoc 编译器生成 C++ 代码:
protoc --cpp_out=. person.proto
这个命令会生成两个文件:
person.pb.h- 包含类定义的头文件person.pb.cc- 包含实现的源文件
生成的代码包含了序列化和反序列化的所有方法,开发者可以直接使用。
交叉编译环境准备
安装交叉编译工具链
根据目标嵌入式设备的架构安装相应的工具链:
# ARM Cortex-A 系列
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
# ARM64 架构
sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
创建交叉编译配置脚本
创建一个配置脚本 cross_compile_setup.sh:
#!/bin/bash
# 交叉编译环境配置脚本
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export CC=${CROSS_COMPILE}gcc
export CXX=${CROSS_COMPILE}g++
export STRIP=${CROSS_COMPILE}strip
export OBJCOPY=${CROSS_COMPILE}objcopy
export OBJDUMP=${CROSS_COMPILE}objdump
export AR=${CROSS_COMPILE}ar
export AS=${CROSS_COMPILE}as
export NM=${CROSS_COMPILE}nm
export RANLIB=${CROSS_COMPILE}ranlib
echo "Cross compilation environment ready!"
编译 Protobuf 库
为嵌入式设备编译 Protobuf
下载并交叉编译 Protobuf 库:
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
# 配置交叉编译
cd cmake/build
cmake .. \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_VERSION=1 \
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ \
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER \
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY \
-DCMAKE_INSTALL_PREFIX=/opt/protobuf-arm \
-Dprotobuf_BUILD_TESTS=OFF \
-Dprotobuf_WITH_ZLIB=OFF \
-DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
sudo make install
注意,为了减小库的体积,我们禁用了测试构建和 ZLIB 压缩功能。
编写使用 Protobuf 的应用程序
创建一个简单的 C++ 测试程序 test_protobuf.cpp:
#include <iostream>
#include <fstream>
#include "person.pb.h"
using namespace std;
using namespace tutorial;
void PromptForAddress(Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
Person::PhoneNumber* phone_number = person->add_phones();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(Person::HOME);
} else if (type == "work") {
phone_number->set_type(Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
int main(int argc, char* argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
AddressBook address_book;
// 读取现有的地址簿
fstream input(argv[1], ios::in | ios::binary);
if (!input.good()) {
cout << "Creating file " << argv[1] << "..." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
// 添加一个地址
PromptForAddress(address_book.add_people());
// 将新的地址簿写回磁盘
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
// 打印地址簿内容
for (int i = 0; i < address_book.people_size(); i++) {
const Person& person = address_book.people(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.has_email()) {
cout << " E-mail: " << person.email() << endl;
}
for (int j = 0; j < person.phones_size(); j++) {
const Person::PhoneNumber& phone = person.phones(j);
switch (phone.type()) {
case Person::MOBILE:
cout << " Mobile phone #: ";
break;
case Person::HOME:
cout << " Home phone #: ";
break;
case Person::WORK:
cout << " Work phone #: ";
break;
}
cout << phone.number() << endl;
}
}
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
交叉编译应用程序
创建 Makefile
创建一个交叉编译的 Makefile:
# 交叉编译配置
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
AR = $(CROSS_COMPILE)ar
STRIP = $(CROSS_COMPILE)strip
# 目录配置
PROTOBUF_ROOT = /opt/protobuf-arm
INCLUDES = -I. -I$(PROTOBUF_ROOT)/include
LIBDIRS = -L$(PROTOBUF_ROOT)/lib
LIBS = -lprotobuf -lpthread
# 编译选项
CFLAGS = -Wall -O2 -std=c++11
CFLAGS += $(INCLUDES)
# 目标文件
TARGET = test_protobuf
SOURCES = test_protobuf.cpp person.pb.cc
OBJECTS = $(SOURCES:.cpp=.o)
.PHONY: all clean
all: $(TARGET)
%.o: %.cpp
$(CXX) $(CFLAGS) -c $< -o $@
$(TARGET): $(OBJECTS)
$(CXX) $(OBJECTS) $(LIBDIRS) $(LIBS) -o $@
$(STRIP) $@
clean:
rm -f $(OBJECTS) $(TARGET)
install: $(TARGET)
$(STRIP) $(TARGET)
编译
运行 make 命令进行编译:
make
嵌入式优化技巧
代码大小优化
对于资源受限的嵌入式设备,考虑以下优化措施:
// 使用 Arena 分配器减少内存分配开销
#include <google/protobuf/arena.h>
// 在栈上分配小的 Arena
google::protobuf::ArenaOptions arena_options;
arena_options.start_block_size = 256;
arena_options.max_block_size = 1024;
google::protobuf::Arena arena(arena_options);
Person* person = google::protobuf::Arena::CreateMessage<Person>(&arena);
禁用不必要的功能
在编译 Protobuf 时可以禁用不必要的功能:
./configure \
--disable-shared \
--enable-static \
--without-zlib \
--disable-perftime \
--disable-debug-strings
使用轻量级替代方案
对于极小的嵌入式系统,考虑使用 nanopb - 一个为嵌入式系统设计的轻量级 Protobuf 实现:
git clone https://github.com/nanopb/nanopb.git
cd nanopb/generator
python3 setup.py install
部署到嵌入式设备
复制文件
将编译好的可执行文件复制到嵌入式设备:
# 使用 scp
scp test_protobuf root@your_embedded_device:/tmp/
# 或使用 rsync
rsync -avz test_protobuf root@your_embedded_device:/usr/local/bin/
检查依赖关系
在嵌入式设备上检查动态库依赖:
ldd test_protobuf
运行测试
在嵌入式设备上运行应用程序:
./test_protobuf address_book.data
性能优化最佳实践
-
重用对象:避免频繁创建和销毁 Protobuf 对象,可以复用同一个对象进行多次序列化/反序列化操作。
-
使用 Arena 分配器:对于临时对象,使用 Arena 可以显著减少内存分配开销。
-
批量处理:将多个小消息合并成一个大消息进行传输,可以提高传输效率。
-
合理选择字段类型:在满足需求的前提下,优先使用占用空间小的字段类型。
故障排除
常见问题
-
链接错误:确保交叉编译的 Protobuf 库路径正确,并且使用了正确的交叉编译器。
-
运行时错误:检查目标设备的架构是否与编译时指定的架构一致。
-
版本兼容性:确保生成代码的 protoc 版本与运行时库版本兼容。
调试技巧
使用 protobuf 的调试功能:
// 打印消息内容(仅用于调试)
cout << person.DebugString() << endl;
总结
在嵌入式设备上使用 protobuf 可以有效解决数据序列化和跨平台通信的问题。通过本文介绍的步骤,您可以:
- 正确设置开发环境
- 创建和编译 proto 文件
- 交叉编译应用程序
- 优化代码以适应嵌入式环境
- 部署和测试应用程序
protobuf 在嵌入式系统中的应用非常广泛,特别是在物联网、传感器网络和边缘计算等领域。掌握这一技术将有助于您开发更加高效和可靠的嵌入式系统。
记住,在资源受限的环境中,始终要考虑内存使用、CPU 占用和代码大小等因素,选择最适合您项目的优化策略。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)