第一次接触Protobuf是在2015年参与一个分布式存储项目时。当时我们团队正被JSON的性能问题困扰——一个简单的配置加载竟然需要200ms,这在实时系统中简直不可接受。我的导师扔给我一个.proto文件说:"试试这个"。从此,Protobuf就成了我工具箱中的常备武器。
Protobuf(Protocol Buffers)是Google开发的一种跨语言数据序列化工具。与JSON/XML这类文本格式不同,Protobuf采用二进制编码,体积更小、解析更快。根据我的实测数据,同样的数据结构,Protobuf的序列化大小只有JSON的1/3~1/2,解析速度却能快5-10倍。
注意:Protobuf不适合人类直接阅读,如果需要配置文件的可读性,建议结合JSON使用
在Ubuntu 20.04上安装最新版(本文以v3.19.4为例):
bash复制# 安装依赖
sudo apt-get install autoconf automake libtool curl make g++ unzip
# 下载并编译
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protobuf-cpp-3.19.4.tar.gz
tar -xzf protobuf-cpp-3.19.4.tar.gz
cd protobuf-3.19.4
./configure
make -j$(nproc)
sudo make install
sudo ldconfig
Windows用户可以使用vcpkg:
powershell复制vcpkg install protobuf:x64-windows
CMake项目配置示例(关键部分):
cmake复制find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
add_executable(proto_demo main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(proto_demo ${Protobuf_LIBRARIES})
创建addressbook.proto:
protobuf复制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;
}
使用protoc编译器:
bash复制protoc --cpp_out=. addressbook.proto
这将生成:
序列化示例(writer.cpp):
cpp复制#include "addressbook.pb.h"
#include <fstream>
void AddPerson(tutorial::Person* person) {
person->set_id(1234);
person->set_name("John Doe");
person->set_email("jdoe@example.com");
tutorial::Person::PhoneNumber* phone = person->add_phones();
phone->set_number("123456789");
phone->set_type(tutorial::Person::WORK);
}
int main() {
tutorial::AddressBook book;
AddPerson(book.add_people());
std::fstream output("addressbook.dat",
std::ios::out | std::ios::binary);
book.SerializeToOstream(&output);
return 0;
}
反序列化示例(reader.cpp):
cpp复制#include "addressbook.pb.h"
#include <iostream>
void ListPeople(const tutorial::AddressBook& book) {
for (const auto& person : book.people()) {
std::cout << "Name: " << person.name() << std::endl;
std::cout << "Email: " << person.email() << std::endl;
for (const auto& phone : person.phones()) {
std::cout << "Phone: " << phone.number();
switch (phone.type()) {
case tutorial::Person::MOBILE: std::cout << " (MOBILE)"; break;
case tutorial::Person::HOME: std::cout << " (HOME)"; break;
case tutorial::Person::WORK: std::cout << " (WORK)"; break;
}
std::cout << std::endl;
}
}
}
int main() {
tutorial::AddressBook book;
std::fstream input("addressbook.dat",
std::ios::in | std::ios::binary);
book.ParseFromIstream(&input);
ListPeople(book);
return 0;
}
Protobuf采用Tag-Length-Value编码:
字段编号选择建议:
cpp复制tutorial::Person person;
for (int i = 0; i < 100; i++) {
person.Clear(); // 复用对象减少内存分配
// 设置字段...
}
cpp复制message LargeData {
repeated int32 values = 1 [packed=true];
}
// 使用时
LargeData data;
data.mutable_values()->Reserve(10000); // 预分配
cpp复制#include <google/protobuf/arena.h>
google::protobuf::Arena arena;
auto person = google::protobuf::Arena::CreateMessage<tutorial::Person>(&arena);
Protobuf支持多种语言,但需注意:
code复制undefined reference to `google::protobuf::...'
解决:确保正确链接-lprotobuf
code复制This file was generated by a newer version of protoc...
解决:统一protoc编译器与运行时库版本
cpp复制if (!book.ParseFromIstream(&input)) {
std::cerr << "Failed to parse address book." << std::endl;
}
cpp复制if (person.has_email()) { // 检查optional字段
// 处理email
}
cpp复制std::string debug_str;
google::protobuf::TextFormat::PrintToString(book, &debug_str);
std::cout << debug_str << std::endl;
bash复制xxd addressbook.dat | less
protobuf复制message Foo {
reserved 2, 5 to 10;
reserved "old_field";
}
推荐目录结构:
code复制/proto
/base
common.proto
/module_a
a.proto
/module_b
b.proto
导入规范:
protobuf复制import "base/common.proto";
cpp复制TEST(ProtoTest, SerializationCycle) {
tutorial::AddressBook src, dst;
// 填充src...
std::string buffer;
src.SerializeToString(&buffer);
dst.ParseFromString(buffer);
ASSERT_EQ(src.DebugString(), dst.DebugString());
}
cpp复制static void BM_Serialize(benchmark::State& state) {
tutorial::AddressBook book;
// 准备数据...
for (auto _ : state) {
std::string buffer;
book.SerializeToString(&buffer);
}
}
BENCHMARK(BM_Serialize);
gRPC + Protobuf示例:
protobuf复制service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
相比JSON的优势:
LevelDB存储示例:
cpp复制leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
tutorial::Person person;
// 填充person...
std::string buffer;
person.SerializeToString(&buffer);
db->Put(leveldb::WriteOptions(), "key1", buffer);
| 特性 | Protobuf | JSON |
|---|---|---|
| 编码格式 | 二进制 | 文本 |
| 大小 | 小(30-50%) | 大 |
| 解析速度 | 快(5-10x) | 慢 |
| 可读性 | 需要工具 | 直接可读 |
| 动态性 | 需要schema | 无需schema |
FlatBuffers优势:
Protobuf优势:
在实际项目中,Protobuf已经成为我们微服务通信和数据存储的标准方案。一个特别有用的技巧是在开发阶段使用TextFormat进行调试,而在生产环境使用二进制格式以获得最佳性能。对于C++开发者来说,掌握Arena分配器的使用可以显著降低复杂消息对象的内存分配开销。