从零开始学习Protobuf(C++实战版)

前言

作为一名C++开发者,你是否曾为以下问题烦恼过?

  • 不同服务之间数据传输格式混乱
  • JSON序列化/反序列化性能瓶颈
  • 协议字段频繁变更导致的兼容性问题
  • 手写解析代码繁琐且容易出错

今天我要介绍的Google Protocol Buffers(简称Protobuf)就是解决这些问题的利器!下面让我用最通俗易懂的方式,带你从零开始掌握Protobuf。

一、什么是Protobuf?

1.1 序列化的概念

序列化 :把内存中的对象转换为字节序列的过程
反序列化:把字节序列恢复为对象的过程

简单来说,就像你要把一本书寄给朋友:

  • 序列化 = 把书打包成包裹
  • 反序列化 = 朋友收到包裹后拆开阅读

1.2 为什么需要序列化?

  1. 存储数据:将对象保存到文件或数据库
  2. 网络传输:通过网络发送对象数据
  3. 分布式系统:不同服务间的数据交换

1.3 Protobuf vs JSON vs XML

特性 JSON XML Protobuf
格式 文本 文本 二进制
可读性 差(需反序列化)
大小 中等
速度 中等
通用性 通用 通用 Google生态

简单理解:Protobuf就像快递的真空压缩包装,体积小、速度快,但需要专门工具才能打开。

二、环境安装(C++版)

2.1 Windows安装

bash 复制代码
# 1. 下载 protoc-xx.x-win64.zip
# 从 https://github.com/protocolbuffers/protobuf/releases 下载

# 2. 解压到 D:\protobuf
# 目录结构:
# D:\protobuf\bin\protoc.exe
# D:\protobuf\include\google\protobuf\...

# 3. 添加环境变量
# 将 D:\protobuf\bin 添加到系统 Path

# 4. 验证安装
protoc --version

2.2 Linux安装

bash 复制代码
# 1. 安装依赖
sudo apt-get install autoconf automake libtool curl make g++ unzip -y

# 2. 下载并解压
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
unzip protobuf-all-21.11.zip
cd protobuf-21.11

# 3. 编译安装
./autogen.sh
./configure
make
sudo make install

# 4. 验证安装
protoc --version

2.3 C++项目配置

bash 复制代码
# CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.10)
project(MyProtobufDemo)

# 查找 Protobuf
find_package(Protobuf REQUIRED)

# 包含目录
include_directories(${Protobuf_INCLUDE_DIRS})

# 添加可执行文件
add_executable(demo main.cpp contacts.pb.cc)

# 链接库
target_link_libraries(demo ${Protobuf_LIBRARIES})

三、第一个Protobuf程序

3.1 创建 .proto 文件

创建 contacts.proto

protobuf 复制代码
// 指定使用proto3语法
syntax = "proto3";

// 包名,相当于C++的命名空间
package contacts;

// 定义联系人消息
message PeopleInfo {
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
}

字段格式字段类型 字段名 = 字段编号;

3.2 编译生成C++代码

bash 复制代码
# 生成C++头文件和源文件
protoc --cpp_out=. contacts.proto

# 生成的文件:
# contacts.pb.h - 类声明
# contacts.pb.cc - 类实现

3.3 编写C++测试程序

创建 main.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include "contacts.pb.h"

int main() {
    // 1. 创建联系人对象
    contacts::PeopleInfo people;
    people.set_name("张三");
    people.set_age(20);
    
    // 2. 序列化为字符串
    std::string serialized_str;
    if (!people.SerializeToString(&serialized_str)) {
        std::cerr << "序列化失败!" << std::endl;
        return -1;
    }
    
    std::cout << "序列化成功! 字节数: " << serialized_str.size() << std::endl;
    
    // 3. 反序列化
    contacts::PeopleInfo new_people;
    if (!new_people.ParseFromString(serialized_str)) {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 4. 输出结果
    std::cout << "姓名: " << new_people.name() << std::endl;
    std::cout << "年龄: " << new_people.age() << std::endl;
    
    return 0;
}

3.4 编译运行

bash 复制代码
# 编译
g++ main.cpp contacts.pb.cc -o demo -std=c++11 -lprotobuf

# 运行
./demo

输出结果

text 复制代码
序列化成功! 字节数: 7
姓名: 张三
年龄: 20

四、Proto3语法详解

4.1 字段规则

基本字段类型
.proto类型 C++类型 说明
string std::string UTF-8字符串
int32 int32_t 32位整数
int64 int64_t 64位整数
bool bool 布尔值
double double 双精度浮点数
float float 单精度浮点数
bytes std::string 字节序列
字段修饰符
protobuf 复制代码
// singular: 0或1个(proto3默认)
string name = 1;

// repeated: 多个(类似数组)
repeated string phone_numbers = 2;

// optional: 可选字段(proto3需要显式声明)
optional string email = 3;

4.2 嵌套消息

protobuf 复制代码
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    
    // 内嵌消息
    message Phone {
        string number = 1;
        string type = 2;  // "home", "work", "mobile"
    }
    
    repeated Phone phones = 3;
}

C++中使用:

cpp 复制代码
contacts::PeopleInfo person;
contacts::PeopleInfo_Phone* phone = person.add_phones();
phone->set_number("13800138000");
phone->set_type("mobile");

4.3 枚举类型

protobuf 复制代码
message Phone {
    string number = 1;
    
    enum PhoneType {
        MP = 0;    // 移动电话
        TEL = 1;   // 固定电话
    }
    
    PhoneType type = 2;
}

4.4 导入其他proto文件

protobuf 复制代码
// phone.proto
syntax = "proto3";
package phone;

message Phone {
    string number = 1;
}

// contacts.proto
syntax = "proto3";
package contacts;
import "phone.proto";  // 导入其他proto

message PeopleInfo {
    string name = 1;
    repeated phone.Phone phones = 2;  // 使用导入的类型
}

五、实战项目:通讯录系统

让我们通过一个完整的通讯录项目来深入学习。

5.1 版本1.0:基础通讯录

contacts_v1.proto:

protobuf 复制代码
syntax = "proto3";
package contacts_v1;

// 联系人
message PeopleInfo {
    string name = 1;
    int32 age = 2;
}

// 通讯录
message Contacts {
    repeated PeopleInfo contacts = 1;
}

write_contacts.cpp:

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts_v1.pb.h"

void AddPeople(contacts_v1::PeopleInfo* person) {
    std::cout << "请输入姓名: ";
    std::string name;
    std::getline(std::cin, name);
    person->set_name(name);
    
    std::cout << "请输入年龄: ";
    int age;
    std::cin >> age;
    person->set_age(age);
    std::cin.ignore(256, '\n');
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << std::endl;
        return -1;
    }
    
    contacts_v1::Contacts contacts;
    
    // 读取已有通讯录
    std::fstream input(argv[1], std::ios::in | std::ios::binary);
    if (input) {
        if (!contacts.ParseFromIstream(&input)) {
            std::cerr << "解析通讯录失败!" << std::endl;
            return -1;
        }
    }
    input.close();
    
    // 添加新联系人
    AddPeople(contacts.add_contacts());
    
    // 写入文件
    std::fstream output(argv[1], 
                       std::ios::out | std::ios::trunc | std::ios::binary);
    if (!contacts.SerializeToOstream(&output)) {
        std::cerr << "写入通讯录失败!" << std::endl;
        return -1;
    }
    
    output.close();
    std::cout << "联系人添加成功!" << std::endl;
    
    return 0;
}

5.2 版本2.0:增强功能

让我们为通讯录添加更多功能:

contacts_v2.proto:

protobuf 复制代码
syntax = "proto3";
package contacts_v2;

message PeopleInfo {
    string name = 1;
    int32 age = 2;
    
    message Phone {
        string number = 1;
        enum PhoneType {
            MP = 0;    // 移动电话
            TEL = 1;   // 固定电话
        }
        PhoneType type = 2;
    }
    
    repeated Phone phones = 3;
    
    // 使用oneof实现多选一
    oneof other_contact {
        string qq = 4;
        string wechat = 5;
    }
    
    // 使用map存储备注
    map<string, string> remarks = 6;
}

高级特性解析

  1. oneof字段:多个字段中只能设置一个
  2. map字段:键值对存储
  3. 枚举类型:类型安全的选项

5.3 版本3.0:Any类型和未知字段

contacts_v3.proto:

protobuf 复制代码
syntax = "proto3";
package contacts_v3;
import "google/protobuf/any.proto";

message Address {
    string home = 1;
    string office = 2;
}

message PeopleInfo {
    string name = 1;
    int32 age = 2;
    
    // 使用Any存储任意类型
    google.protobuf.Any extra_info = 3;
}

使用Any类型:

cpp 复制代码
// 设置Any类型
contacts_v3::PeopleInfo person;
contacts_v3::Address address;
address.set_home("北京市");
address.set_office("海淀区");

google::protobuf::Any* any_data = person.mutable_extra_info();
any_data->PackFrom(address);

// 读取Any类型
if (person.has_extra_info() && 
    person.extra_info().Is<contacts_v3::Address>()) {
    contacts_v3::Address unpacked_addr;
    person.extra_info().UnpackTo(&unpacked_addr);
    std::cout << "家庭地址: " << unpacked_addr.home() << std::endl;
}

六、性能对比测试

让我们通过实际测试看看Protobuf的优势:

compare_performance.cpp:

cpp 复制代码
#include <iostream>
#include <chrono>
#include <json/json.h>
#include "contacts.pb.h"

// 测试10000次序列化/反序列化
const int TEST_COUNT = 10000;

void TestProtobuf() {
    contacts::PeopleInfo person;
    person.set_name("张三");
    person.set_age(25);
    
    auto start = std::chrono::high_resolution_clock::now();
    
    std::string buffer;
    for (int i = 0; i < TEST_COUNT; ++i) {
        person.SerializeToString(&buffer);
        contacts::PeopleInfo new_person;
        new_person.ParseFromString(buffer);
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "Protobuf 测试 " << TEST_COUNT << " 次耗时: " 
              << duration.count() << "ms" << std::endl;
    std::cout << "序列化大小: " << buffer.size() << " 字节" << std::endl;
}

void TestJSON() {
    Json::Value person;
    person["name"] = "张三";
    person["age"] = 25;
    
    Json::StreamWriterBuilder writer;
    auto start = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < TEST_COUNT; ++i) {
        std::string json_str = Json::writeString(writer, person);
        Json::Value parsed;
        Json::Reader().parse(json_str, parsed);
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "JSON 测试 " << TEST_COUNT << " 次耗时: " 
              << duration.count() << "ms" << std::endl;
    std::cout << "序列化大小: " << Json::writeString(writer, person).size() 
              << " 字节" << std::endl;
}

int main() {
    std::cout << "=== 性能对比测试 ===" << std::endl;
    TestProtobuf();
    TestJSON();
    return 0;
}

测试结果(仅供参考):

text 复制代码
=== 性能对比测试 ===
Protobuf 测试 10000 次耗时: 45ms
序列化大小: 12 字节
JSON 测试 10000 次耗时: 120ms
序列化大小: 25 字节

可以看到Protobuf在性能和空间上都有明显优势!

七、最佳实践和注意事项

7.1 字段编号规则

  1. 编号范围: 1-536,870,911(但1-15更高效)
  2. 保留编号: 不要修改已使用的字段编号
  3. 删除字段 : 使用reserved关键字
protobuf 复制代码
message MyMessage {
    // 保留已删除的字段编号
    reserved 2, 15, 9 to 11;
    reserved "old_field";
    
    string new_field = 16;
}

7.2 版本兼容性

protobuf 复制代码
// 错误:修改字段类型
int32 age = 2;  // 原来
string age = 2; // 错误!

// 正确:添加新字段
int32 age = 2;
string birthday = 3; // 新增

7.3 编译优化选项

protobuf 复制代码
// 优化速度(默认)
option optimize_for = SPEED;

// 优化代码大小
option optimize_for = CODE_SIZE;

// 精简运行时(移动设备)
option optimize_for = LITE_RUNTIME;

八、网络应用示例

让我们看看如何在网络编程中使用Protobuf:

server.cpp(简化版):

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include "contacts.pb.h"

class ContactServer {
private:
    contacts::Contacts contacts_;
    
public:
    void AddContact(const contacts::PeopleInfo& person) {
        *contacts_.add_contacts() = person;
        SaveToFile();
    }
    
    std::string SerializeContacts() {
        std::string data;
        contacts_.SerializeToString(&data);
        return data;
    }
    
private:
    void SaveToFile() {
        std::ofstream file("contacts.dat", std::ios::binary);
        contacts_.SerializeToOstream(&file);
    }
};

// 网络处理线程
void HandleClient(int client_fd, ContactServer& server) {
    char buffer[1024];
    ssize_t n = read(client_fd, buffer, sizeof(buffer));
    
    if (n > 0) {
        contacts::PeopleInfo person;
        if (person.ParseFromArray(buffer, n)) {
            server.AddContact(person);
            std::cout << "收到新联系人: " << person.name() << std::endl;
        }
    }
    
    close(client_fd);
}

九、常见问题解答

Q1: Protobuf和JSON如何选择?

使用Protobuf

  • 高性能要求的场景
  • 内部服务通信
  • 移动端应用
  • 需要向前/向后兼容

使用JSON

  • 需要人类可读的配置文件
  • Web API接口
  • 简单的数据交换
  • 第三方接口对接

Q2: Protobuf有哪些缺点?

  1. 可读性差:二进制格式无法直接阅读
  2. 需要编译:需要proto编译器
  3. 灵活性差:相比JSON动态性较差
  4. 生态限制:主要在Google生态中流行

Q3: 如何处理字段变更?

  1. 添加字段:直接添加,使用新编号
  2. 删除字段 :标记为reserved,不要重用编号
  3. 修改字段:创建新字段,逐步迁移

十、总结

通过本文的学习,你应该已经掌握了:

基础概念 :理解序列化和Protobuf的作用

环境搭建 :在不同平台安装配置Protobuf

语法掌握 :熟悉proto3的各种语法特性

实战开发 :完成通讯录项目的各个版本

性能优化 :了解Protobuf的性能优势

最佳实践:掌握开发中的注意事项

Protobuf作为现代分布式系统的核心技术之一,掌握它对于C++开发者来说至关重要。希望这篇教程能帮助你在Protobuf的学习道路上顺利前行!

相关推荐
哎呦 你干嘛~3 小时前
MODBUS协议学习
学习
林开落L3 小时前
从入门到了解:Protobuf、JSON、XML 核心解析(C++ 示例)
xml·c++·json·protobuffer·结构化数据序列化机制
牛奔3 小时前
Go 是如何做抢占式调度的?
开发语言·后端·golang
Queenie_Charlie3 小时前
stars(树状数组)
数据结构·c++·树状数组
小陈phd3 小时前
多模态大模型学习笔记(一)——机器学习入门:监督/无监督学习核心任务全解析
笔记·学习·机器学习
符哥20083 小时前
C++ 进阶知识点整理
java·开发语言·jvm
小猪咪piggy3 小时前
【Python】(4) 列表和元组
开发语言·python
会周易的程序员3 小时前
openplc runtimev4 Docker 部署
运维·c++·物联网·docker·容器·软件工程·iot
小陈phd3 小时前
多模态大模型学习笔记(二)——机器学习十大经典算法:一张表看懂分类 / 回归 / 聚类 / 降维
学习·算法·机器学习