在嵌入式设备上使用 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

性能优化最佳实践

  1. 重用对象:避免频繁创建和销毁 Protobuf 对象,可以复用同一个对象进行多次序列化/反序列化操作。

  2. 使用 Arena 分配器:对于临时对象,使用 Arena 可以显著减少内存分配开销。

  3. 批量处理:将多个小消息合并成一个大消息进行传输,可以提高传输效率。

  4. 合理选择字段类型:在满足需求的前提下,优先使用占用空间小的字段类型。

故障排除

常见问题

  1. 链接错误:确保交叉编译的 Protobuf 库路径正确,并且使用了正确的交叉编译器。

  2. 运行时错误:检查目标设备的架构是否与编译时指定的架构一致。

  3. 版本兼容性:确保生成代码的 protoc 版本与运行时库版本兼容。

调试技巧

使用 protobuf 的调试功能:

// 打印消息内容(仅用于调试)
cout << person.DebugString() << endl;

总结

在嵌入式设备上使用 protobuf 可以有效解决数据序列化和跨平台通信的问题。通过本文介绍的步骤,您可以:

  1. 正确设置开发环境
  2. 创建和编译 proto 文件
  3. 交叉编译应用程序
  4. 优化代码以适应嵌入式环境
  5. 部署和测试应用程序

protobuf 在嵌入式系统中的应用非常广泛,特别是在物联网、传感器网络和边缘计算等领域。掌握这一技术将有助于您开发更加高效和可靠的嵌入式系统。

记住,在资源受限的环境中,始终要考虑内存使用、CPU 占用和代码大小等因素,选择最适合您项目的优化策略。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐