发布于: 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;
}
总结
通过本指南,你已经学会了:
- 如何在 C++ 中安装和配置 Protobuf 环境
- 如何定义 .proto 文件并生成 C++ 代码
- 如何创建、序列化和反序列化 Protobuf 消息
- 如何使用高级特性如反射、Arena 内存管理
- 性能优化和最佳实践
- 完整的地址簿管理器实现
C++ 中的 Protobuf 提供了高性能、类型安全的数据序列化方案,是构建高性能分布式系统的理想选择。