发布于: Invalid Date
作者: Protobuf Decoder 团队

C++ 中使用 Protocol Buffers 完整指南

从零开始学习如何在 C++ 项目中使用 Protocol Buffers,包括安装、定义、编译和使用

protobuf
c++
教程
序列化

C++ 中使用 Protocol Buffers 完整指南

概述

Protocol Buffers (简称 Protobuf) 是 Google 开发的一种高效、可扩展的序列化结构化数据的方式。本指南将详细介绍如何在 C++ 中使用 Protobuf,从环境搭建到实际应用。

为什么选择 Protobuf?

  • 高性能:比 XML 和 JSON 更小更快
  • 类型安全:强类型系统,编译时检查
  • 跨语言:支持多种编程语言
  • 向后兼容:支持 schema 演进
  • 内存高效:紧凑的二进制格式

环境准备

安装 Protocol Buffers

Windows (使用 vcpkg)

vcpkg install protobuf protobuf:x64-windows

Linux/macOS

# Ubuntu/Debian
sudo apt-get install libprotobuf-dev protobuf-compiler

# macOS
brew install protobuf

从源码编译

git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
mkdir build && cd build
cmake -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
sudo make install

验证安装

protoc --version

定义 Protocol Buffers

创建 .proto 文件

创建一个名为 addressbook.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;
}

编译 .proto 文件

生成 C++ 代码

protoc --cpp_out=. addressbook.proto

这将生成两个文件:

  • addressbook.pb.h - 头文件
  • addressbook.pb.cc - 实现文件

使用 CMake 集成

创建 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(protobuf_tutorial)

set(CMAKE_CXX_STANDARD 17)

find_package(Protobuf CONFIG REQUIRED)

# 生成 protobuf 文件
set(PROTO_FILES addressbook.proto)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})

add_executable(tutorial
    main.cpp
    ${PROTO_SRCS}
    ${PROTO_HDRS}
)

target_link_libraries(tutorial protobuf::libprotobuf)

基础使用

创建和序列化消息

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"

using namespace std;

void PromptForAddress(tutorial::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;
        }

        tutorial::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(tutorial::Person::MOBILE);
        } else if (type == "home") {
            phone_number->set_type(tutorial::Person::HOME);
        } else if (type == "work") {
            phone_number->set_type(tutorial::Person::WORK);
        } else {
            cout << "Unknown phone type. Using default.\n";
        }
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2) {
        cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
        return -1;
    }

    tutorial::AddressBook address_book;

    {
        fstream input(argv[1], ios::in | ios::binary);
        if (!input) {
            cout << argv[1] << ": File not found. Creating a new file." << 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;
        }
    }

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

反序列化和读取消息

#include <iostream>
#include <fstream>
#include "addressbook.pb.h"

using namespace std;

void ListPeople(const tutorial::AddressBook& address_book) {
    for (int i = 0; i < address_book.people_size(); i++) {
        const tutorial::Person& person = address_book.people(i);

        cout << "Person ID: " << person.id() << endl;
        cout << "  Name: " << person.name() << endl;
        if (person.has_email()) {
            cout << "  E-mail address: " << person.email() << endl;
        }

        for (int j = 0; j < person.phones_size(); j++) {
            const tutorial::Person::PhoneNumber& phone_number = person.phones(j);

            switch (phone_number.type()) {
                case tutorial::Person::MOBILE:
                    cout << "  Mobile phone #: ";
                    break;
                case tutorial::Person::HOME:
                    cout << "  Home phone #: ";
                    break;
                case tutorial::Person::WORK:
                    cout << "  Work phone #: ";
                    break;
            }
            cout << phone_number.number() << endl;
        }
    }
}

int main(int argc, char* argv[]) {
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    if (argc != 2) {
        cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
        return -1;
    }

    tutorial::AddressBook address_book;

    fstream input(argv[1], ios::in | ios::binary);
    if (!address_book.ParseFromIstream(&input)) {
        cerr << "Failed to parse address book." << endl;
        return -1;
    }

    ListPeople(address_book);

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

高级用法

内存管理

#include "addressbook.pb.h"
#include <google/protobuf/arena.h>

void UseArena() {
    google::protobuf::Arena arena;
    
    // 在 Arena 上创建消息
    auto* person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);
    person->set_name("张三");
    person->set_id(1234);
    
    // 不需要手动删除,Arena 会自动回收内存
}

性能优化

#include <chrono>
#include <vector>
#include "addressbook.pb.h"

void PerformanceTest() {
    using namespace std::chrono;
    
    const int kNumMessages = 100000;
    std::vector<tutorial::Person> people;
    people.reserve(kNumMessages);
    
    // 创建测试数据
    for (int i = 0; i < kNumMessages; ++i) {
        tutorial::Person person;
        person.set_name("Person" + std::to_string(i));
        person.set_id(i);
        person.set_email("person" + std::to_string(i) + "@example.com");
        
        auto* phone = person.add_phones();
        phone->set_number("123-456-" + std::to_string(i));
        phone->set_type(tutorial::Person::MOBILE);
        
        people.push_back(std::move(person));
    }
    
    // 序列化性能测试
    auto start = high_resolution_clock::now();
    std::string serialized_data;
    for (const auto& person : people) {
        person.SerializeToString(&serialized_data);
    }
    auto end = high_resolution_clock::now();
    
    auto duration = duration_cast<microseconds>(end - start);
    std::cout << "Serialization time: " << duration.count() << " microseconds" << std::endl;
}

反射和动态消息

#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include "addressbook.pb.h"

void UseReflection() {
    tutorial::Person person;
    person.set_name("李四");
    person.set_id(5678);
    
    const google::protobuf::Descriptor* descriptor = person.GetDescriptor();
    const google::protobuf::Reflection* reflection = person.GetReflection();
    
    // 获取字段信息
    const google::protobuf::FieldDescriptor* name_field = descriptor->FindFieldByName("name");
    if (name_field) {
        std::string name_value = reflection->GetString(person, name_field);
        std::cout << "Name field value: " << name_value << std::endl;
    }
    
    // 动态设置字段值
    const google::protobuf::FieldDescriptor* email_field = descriptor->FindFieldByName("email");
    if (email_field) {
        reflection->SetString(&person, email_field, "[email protected]");
    }
}

最佳实践

错误处理

#include "addressbook.pb.h"
#include <google/protobuf/util/json_util.h>

bool ValidatePerson(const tutorial::Person& person) {
    if (person.name().empty()) {
        std::cerr << "Error: Name cannot be empty" << std::endl;
        return false;
    }
    
    if (person.id() <= 0) {
        std::cerr << "Error: ID must be positive" << std::endl;
        return false;
    }
    
    if (!person.email().empty() && person.email().find('@') == std::string::npos) {
        std::cerr << "Error: Invalid email format" << std::endl;
        return false;
    }
    
    return true;
}

void HandleErrors() {
    tutorial::Person person;
    
    // 设置无效数据
    person.set_id(-1);
    
    if (!ValidatePerson(person)) {
        std::cerr << "Person validation failed" << std::endl;
        return;
    }
}

内存池使用

#include <google/protobuf/arena.h>
#include "addressbook.pb.h"

class EfficientMessageHandler {
public:
    EfficientMessageHandler() : arena_(new google::protobuf::Arena()) {}
    
    ~EfficientMessageHandler() {
        delete arena_;
    }
    
    tutorial::Person* CreatePerson(const std::string& name, int id) {
        auto* person = google::protobuf::Arena::CreateMessage<tutorial::Person>(arena_);
        person->set_name(name);
        person->set_id(id);
        return person;
    }
    
private:
    google::protobuf::Arena* arena_;
};

线程安全

#include <mutex>
#include "addressbook.pb.h"

class ThreadSafeAddressBook {
public:
    void AddPerson(const tutorial::Person& person) {
        std::lock_guard<std::mutex> lock(mutex_);
        *address_book_.add_people() = person;
    }
    
    tutorial::AddressBook GetSnapshot() {
        std::lock_guard<std::mutex> lock(mutex_);
        return address_book_;
    }
    
private:
    std::mutex mutex_;
    tutorial::AddressBook address_book_;
};

完整示例

地址簿管理器

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include "addressbook.pb.h"

class AddressBookManager {
public:
    AddressBookManager(const std::string& filename) : filename_(filename) {
        Load();
    }
    
    ~AddressBookManager() {
        Save();
    }
    
    bool AddPerson(const tutorial::Person& person) {
        if (!ValidatePerson(person)) {
            return false;
        }
        
        *address_book_.add_people() = person;
        return true;
    }
    
    tutorial::Person* FindPersonById(int id) {
        for (int i = 0; i < address_book_.people_size(); ++i) {
            if (address_book_.people(i).id() == id) {
                return address_book_.mutable_people(i);
            }
        }
        return nullptr;
    }
    
    std::vector<tutorial::Person> GetAllPeople() {
        std::vector<tutorial::Person> result;
        for (int i = 0; i < address_book_.people_size(); ++i) {
            result.push_back(address_book_.people(i));
        }
        return result;
    }
    
    bool Save() {
        std::fstream output(filename_, std::ios::out | std::ios::trunc | std::ios::binary);
        return address_book_.SerializeToOstream(&output);
    }
    
private:
    bool Load() {
        std::fstream input(filename_, std::ios::in | std::ios::binary);
        if (!input) {
            return true; // 文件不存在,创建新的
        }
        return address_book_.ParseFromIstream(&input);
    }
    
    bool ValidatePerson(const tutorial::Person& person) {
        return !person.name().empty() && person.id() > 0;
    }
    
    std::string filename_;
    tutorial::AddressBook address_book_;
};

int main() {
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    
    AddressBookManager manager("addressbook.dat");
    
    // 添加测试数据
    tutorial::Person person;
    person.set_name("王五");
    person.set_id(1001);
    person.set_email("[email protected]");
    
    auto* phone = person.add_phones();
    phone->set_number("13800138000");
    phone->set_type(tutorial::Person::MOBILE);
    
    manager.AddPerson(person);
    
    // 列出所有人员
    auto people = manager.GetAllPeople();
    for (const auto& p : people) {
        std::cout << p.name() << " (ID: " << p.id() << ")" << std::endl;
    }
    
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

构建和运行

使用 CMake 构建

mkdir build
cd build
cmake ..
make

# 运行程序
./tutorial addressbook.dat

使用 g++ 直接编译

g++ -std=c++17 -I/usr/local/include -L/usr/local/lib \
    main.cpp addressbook.pb.cc -lprotobuf -o tutorial

常见问题解决

链接错误

# 如果找不到 protobuf 
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

# Windows 下设置环境变量
set PATH=%PATH%;C:\path\to\protobuf\lib

版本兼容性

// 检查 protobuf 版本
#if GOOGLE_PROTOBUF_VERSION >= 3006001
    // 使用新版本特性
#endif

调试技巧

#include <google/protobuf/text_format.h>

void DebugPrint(const tutorial::Person& person) {
    std::string debug_str;
    google::protobuf::TextFormat::PrintToString(person, &debug_str);
    std::cout << "Debug info:\n" << debug_str << std::endl;
}

总结

通过本指南,你已经学会了:

  1. 如何在 C++ 中安装和配置 Protobuf 环境
  2. 如何定义 .proto 文件并生成 C++ 代码
  3. 如何创建、序列化和反序列化 Protobuf 消息
  4. 如何使用高级特性如反射、Arena 内存管理
  5. 性能优化和最佳实践
  6. 完整的地址簿管理器实现

C++ 中的 Protobuf 提供了高性能、类型安全的数据序列化方案,是构建高性能分布式系统的理想选择。

相关文章

Python 中使用 Protocol Buffers 完整指南
从零开始学习如何在 Python 项目中使用 Protocol Buffers,包括安装、定义、编译和使用
如何根据 Proto 文件生成对应语言的代码
详细介绍如何使用 Protocol Buffers 编译器从 .proto 文件生成各种编程语言的代码文件,包括安装配置、命令使用和实际示例。
Protocol Buffers 基础入门指南
从零开始学习 Protocol Buffers,了解其基本概念、语法和使用方法。