【脚手架】Protobuf基础使用

目录

[一、什么是 Protocol Buffers?](#一、什么是 Protocol Buffers?)

二、环境安装与代码生成

[2.1 安装 Protobuf 编译器](#2.1 安装 Protobuf 编译器)

[2.2 定义 .proto 文件](#2.2 定义 .proto 文件)

[2.3 编译生成 C++ 代码](#2.3 编译生成 C++ 代码)

[三、Message 接口详解](#三、Message 接口详解)

[3.1 基础字段操作接口](#3.1 基础字段操作接口)

[3.2 Repeated 字段(数组)操作](#3.2 Repeated 字段(数组)操作)

[3.3 Map 字段(字典)操作](#3.3 Map 字段(字典)操作)

四、序列化与反序列化接口

[4.1 序列化接口](#4.1 序列化接口)

[4.2 反序列化接口](#4.2 反序列化接口)

[4.3 完整示例](#4.3 完整示例)

[4.4 其他实用接口](#4.4 其他实用接口)

[五、Protobuf 与 JSON 互转](#五、Protobuf 与 JSON 互转)

[5.1 转换选项配置](#5.1 转换选项配置)

[5.2 Proto 转 JSON](#5.2 Proto 转 JSON)

[5.3 JSON 转 Proto](#5.3 JSON 转 Proto)

[5.4 完整示例](#5.4 完整示例)

[六、RPC 服务接口(Service)](#六、RPC 服务接口(Service))

[6.1 生成的接口类](#6.1 生成的接口类)

[6.2 使用示例](#6.2 使用示例)

七、注意事项

[7.1 字段编号管理](#7.1 字段编号管理)

[7.2 使用技巧](#7.2 使用技巧)

[7.3 版本兼容性](#7.3 版本兼容性)

[7.4 程序启动与退出](#7.4 程序启动与退出)


一、什么是 Protocol Buffers?

Protocol Buffers(简称 Protobuf)是 Google 开发的一种轻便、高效的结构化数据存储/交换格式,类似于 XML 或 JSON,但比它们更小、更快、更简单。

Protobuf 使用接口描述语言(IDL)定义数据结构,然后自动生成各种编程语言的代码来操作这些数据结构。适合:

  • 数据序列化:网络通信、数据存储等场景

  • 跨语言服务:构建跨语言的 RPC(远程过程调用)服务

  • 数据兼容性:通过版本控制机制实现向后兼容


二、环境安装与代码生成

2.1 安装 Protobuf 编译器

bash 复制代码
# Ubuntu/Debian 系统
sudo apt install protobuf-compiler libprotobuf-dev

# 如果需要使用 gRPC 相关编译
sudo apt install protobuf-compiler-grpc

2.2 定义 .proto 文件

创建一个 example.proto 文件:

cpp 复制代码
syntax = "proto3";  // 指定使用 proto3 语法
package example;    // 定义命名空间

// 默认情况下 protoc 不会针对 service 生成 RPC 代码
// 需要开启此选项才会生成
option cc_generic_services = true;

// 定义消息结构
message Person 
{
    string name = 1;           // 普通字段
    int32 age = 2;             // 整型字段
    string email = 3;          // 字符串字段
    repeated string skills = 4; // 重复字段(数组)
    map<string, float> scores = 5; // 映射字段(字典)
}

// 定义 RPC 服务
service Greeter 
{
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest 
{
    string name = 1;
}

message HelloReply 
{
    string message = 1;
}

2.3 编译生成 C++ 代码

bash 复制代码
# 基本编译命令
protoc --cpp_out=./ example.proto

# 指定搜索路径
protoc -I=protobuf --cpp_out=./ protobuf/example.proto

# proto3 中使用 optional 需要添加实验性选项
protoc --cpp_out=./ --experimental_allow_proto3_optional example.proto

编译后会生成两个文件:

  • example.pb.h:头文件,包含类定义

  • example.pb.cc:实现文件


三、Message 接口详解

每个在 proto 中定义的 message,编译后都会生成一个继承自 google::protobuf::Message 的派生类。这个类提供了丰富的接口来操作消息字段。

3.1 基础字段操作接口

对于每个字段,Protobuf 都会生成一套标准的访问接口:

操作类型 接口命名规则 示例(name 字段)
获取值 字段名() name()
设置值 set_字段名() set_name("Jack")
获取可变引用 mutable_字段名() mutable_name()
清空字段 clear_字段名() clear_name()
检查是否存在 has_字段名() has_name()(仅 optional)

代码示例

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

int main() 
{
    example::Person person;
    
    // 设置普通字段
    person.set_name("Jack");
    person.set_age(28);
    person.set_email("jack@example.com");
    
    // 获取字段值
    std::cout << "Name: " << person.name() << std::endl;
    std::cout << "Age: " << person.age() << std::endl;
    
    // 修改字符串(获取可变指针)
    std::string* name_ptr = person.mutable_name();
    *name_ptr = "Jackson";
    
    // 清空字段
    person.clear_email();
    
    return 0;
}

3.2 Repeated 字段(数组)操作

对于标记为 repeated 的字段,生成的接口类似于 C++ 的 std::vector

cpp 复制代码
// 获取元素数量
int skills_size() const;

// 清空所有元素
void clear_skills();

// 通过索引获取元素(const 版本)
const std::string& skills(int index) const;

// 通过索引获取可变元素
std::string* mutable_skills(int index);

// 设置指定索引的元素
void set_skills(int index, const std::string& value);

// 添加新元素,返回指针用于修改
std::string* add_skills();

// 添加新元素(拷贝)
void add_skills(const std::string& value);

使用示例

cpp 复制代码
// 添加技能
person.add_skills("C++");
person.add_skills("Java");
person.add_skills("Python");

// 遍历 repeated 字段
int sz = person.skills_size();
for (int i = 0; i < sz; i++) 
{
    std::cout << "Skill " << i << ": " << person.skills(i) << std::endl;
}

// 修改指定元素
*person.mutable_skills(0) = "C++17";

3.3 Map 字段(字典)操作

Map 字段对应 google::protobuf::Map 类,接口与 C++ STL 的 std::map 类似:

cpp 复制代码
// 获取元素数量
int scores_size() const;

// 清空 map
void clear_scores();

// 获取 const map 引用
const ::google::protobuf::Map<std::string, float>& scores() const;

// 获取可变 map 指针
::google::protobuf::Map<std::string, float>* mutable_scores();

使用示例

cpp 复制代码
// 获取 map 指针并插入数据
auto score_map = person.mutable_scores();
score_map->insert({"Chinese", 88.5});
score_map->insert({"Math", 77.5});
score_map->insert({"English", 99.0});

// 使用 MapPair 插入(另一种方式)
score_map->insert(google::protobuf::MapPair<std::string, float>("Physics", 85.0));

// 遍历 map(const 版本)
for (const auto& pair : person.scores()) 
{
    std::cout << pair.first << ": " << pair.second << std::endl;
}

// 查找元素
auto it = person.scores().find("Chinese");
if (it != person.scores().end()) 
{
    std::cout << "Chinese score: " << it->second << std::endl;
}

四、序列化与反序列化接口

这是 Protobuf 最核心的功能,所有消息类都继承自 MessageLite,提供了多种序列化/反序列化方式

4.1 序列化接口

cpp 复制代码
// 序列化为 std::string
bool SerializeToString(std::string* output) const;
std::string SerializeAsString() const;  // 直接返回 string,失败返回空

// 序列化到字符数组
bool SerializeToArray(void* data, int size) const;

// 序列化到输出流
bool SerializeToOstream(std::ostream* output) const;

4.2 反序列化接口

cpp 复制代码
// 从 std::string 解析
bool ParseFromString(const std::string& data);

// 从字符数组解析
bool ParseFromArray(const void* data, int size);

// 从输入流解析
bool ParseFromIstream(std::istream* input);

4.3 完整示例

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

int main() 
{
    // ========== 序列化 ==========
    example::Person person;
    person.set_name("Jack");
    person.set_age(28);
    person.set_email("jack@example.com");
    person.add_skills("C++");
    person.add_skills("Python");
    
    auto scores = person.mutable_scores();
    scores->insert({"Algorithm", 95.0});
    scores->insert({"DataStructure", 88.0});
    
    // 序列化为字符串
    std::string serialized = person.SerializeAsString();
    if (serialized.empty()) 
    {
        std::cerr << "Serialization failed!" << std::endl;
        return -1;
    }
    
    std::cout << "Serialized size: " << serialized.size() << " bytes" << std::endl;
    
    // ========== 反序列化 ==========
    example::Person restored;
    bool success = restored.ParseFromString(serialized);
    if (!success) 
    {
        std::cerr << "Deserialization failed!" << std::endl;
        return -1;
    }
    
    // 验证数据
    std::cout << "\nRestored data:" << std::endl;
    std::cout << "Name: " << restored.name() << std::endl;
    std::cout << "Age: " << restored.age() << std::endl;
    
    return 0;
}

4.4 其他接口

cpp 复制代码
// 清空所有字段
void Clear();

// 获取类型名称
std::string GetTypeName() const;

// 获取字段描述符(用于反射)
const Descriptor* GetDescriptor();

// 获取反射接口
const Reflection* GetReflection();

// 交换两个消息的内容
void swap(Message& other);
void CopyFrom(const Message& from);  // 深拷贝

五、Protobuf 与 JSON 互转

Protobuf 提供了与 JSON 格式互转的实用工具,位于 google::protobuf::util 命名空间:

5.1 转换选项配置

cpp 复制代码
// JSON 解析选项
struct JsonParseOptions 
{
    bool ignore_unknown_fields;        // 是否忽略未知的 JSON 字段
    bool case_insensitive_enum_parsing; // 解析枚举时是否不区分大小写
};

// JSON 输出选项
struct JsonPrintOptions 
{
    bool add_whitespace;                    // 是否添加空白字符让 JSON 更易读
    bool always_print_primitive_fields;     // 是否总是输出基本类型字段
    bool always_print_enums_as_ints;        // 是否将枚举作为整数输出
    bool preserve_proto_field_names;        // 是否保留原始字段名(不使用驼峰命名)
};

5.2 Proto 转 JSON

cpp 复制代码
#include <google/protobuf/util/json_util.h>

std::string ProtoToJson(const google::protobuf::Message& msg) 
{
    google::protobuf::util::JsonOptions options;
    options.add_whitespace = true;  // 格式化输出
    options.always_print_primitive_fields = true;  // 输出所有字段
    
    std::string json_output;
    auto status = google::protobuf::util::MessageToJsonString(msg, &json_output, options);
    
    if (!status.ok()) 
    {
        std::cerr << "Conversion failed: " << status.ToString() << std::endl;
        return "";
    }
    
    return json_output;
}

5.3 JSON 转 Proto

cpp 复制代码
bool JsonToProto(const std::string& json_str, google::protobuf::Message* msg) 
{
    google::protobuf::util::JsonParseOptions options;
    options.ignore_unknown_fields = true;  // 忽略未知字段
    
    auto status = google::protobuf::util::JsonStringToMessage(json_str, msg, options);
    
    if (!status.ok()) 
    {
        std::cerr << "Parse failed: " << status.ToString() << std::endl;
        return false;
    }
    
    return true;
}

5.4 完整示例

cpp 复制代码
#include <iostream>
#include <google/protobuf/util/json_util.h>
#include "example.pb.h"

int main() 
{
    // 创建并填充消息
    example::Person person;
    person.set_name("Jack");
    person.set_age(0);  // 测试默认值输出
    person.set_email("jack@example.com");
    person.add_skills("C++");
    person.add_skills("Java");
    
    auto scores = person.mutable_scores();
    scores->insert({"Math", 88.0});
    
    // Proto -> JSON
    std::cout << "=== Proto to JSON ===" << std::endl;
    std::string json_str = ProtoToJson(person);
    std::cout << json_str << std::endl;
    
    // JSON -> Proto
    std::cout << "\n=== JSON to Proto ===" << std::endl;
    example::Person restored;
    if (JsonToProto(json_str, &restored)) 
    {
        // 使用 has_ 接口检查 optional 字段(proto3 需要开启 optional 选项)
        if (restored.has_name()) 
        {
            std::cout << "name: " << restored.name() << std::endl;
        }
        if (restored.has_age()) 
        {
            std::cout << "age: " << restored.age() << std::endl;
        }
    }
    
    return 0;
}

输出示例

cpp 复制代码
{
  "name": "Jack",
  "age": 0,
  "email": "jack@example.com",
  "skills": ["C++", "Java"],
  "scores": {
    "Math": 88
  }
}

六、RPC 服务接口(Service)

当 proto 文件中定义了 service 时,Protobuf 会生成对应的 RPC 接口类:

6.1 生成的接口类

cpp 复制代码
// 服务端接口(纯虚类,需要继承实现)
class Greeter : public ::google::protobuf::Service 
{
public:
    virtual void SayHello(
        ::google::protobuf::RpcController* controller,
        const ::example::HelloRequest* request,
        ::example::HelloReply* response,
        ::google::protobuf::Closure* done) = 0;
};

// 客户端 Stub(用于发起 RPC 调用)
class Greeter_Stub : public Greeter 
{
public:
    Greeter_Stub(::google::protobuf::RpcChannel* channel);
    
    void SayHello(::google::protobuf::RpcController* controller,
                  const ::example::HelloRequest* request,
                  ::example::HelloReply* response,
                  ::google::protobuf::Closure* done) override;
};

6.2 使用示例

cpp 复制代码
// 服务端实现
class GreeterServiceImpl : public example::Greeter 
{
public:
    void SayHello(::google::protobuf::RpcController* controller,
                  const ::example::HelloRequest* request,
                  ::example::HelloReply* response,
                  ::google::protobuf::Closure* done) override 
    {
        std::string greeting = "Hello, " + request->name();
        response->set_message(greeting);
        done->Run();  // 通知 RPC 框架调用完成
    }
};

// 客户端调用
void CallService(::google::protobuf::RpcChannel* channel) 
{
    example::Greeter_Stub stub(channel);
    example::HelloRequest request;
    request.set_name("World");
    
    example::HelloReply response;
    ::google::protobuf::RpcController* controller = ...;
    ::google::protobuf::Closure* done = ...;
    
    stub.SayHello(controller, &request, &response, done);
}

七、注意事项

7.1 字段编号管理

  • 不要重用字段编号 :删除字段后,应将其编号标记为 reserved

  • 不要随意更改字段类型:可能影响兼容性

  • 预留字段编号:为未来扩展预留空间

cpp 复制代码
message User 
{
    string name = 1;
    int32 age = 2;
    reserved 3 to 10;  // 预留编号
    reserved "foo", "bar";  // 预留字段名
    string email = 11;
}

7.2 使用技巧

  1. 重用消息对象:消息对象被清除后会保留内存空间供重用,减少内存分配压力

  2. 避免频繁拷贝 :使用 Swap()mutable_ 接口避免不必要的拷贝

7.3 版本兼容性

  • 不要更改已有字段的编号

  • 不要删除 required 字段(proto3 已移除 required)

  • 可以添加新的 optionalrepeated 字段,但必须使用新的编号

  • 使用 optional 关键字明确标识可选字段,便于通过 has_ 接口检查

7.4 程序启动与退出

cpp 复制代码
#include <google/protobuf/stubs/common.h>

int main() 
{
    // 验证链接的库版本与编译的头文件版本兼容
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    
    // ... 你的代码 ...
    
    // 可选:清理所有全局对象(仅在内存泄漏检测或动态库场景需要)
    google::protobuf::ShutdownProtobufLibrary();
    
    return 0;
}

参考资源

Protocol Buffers 官方文档https://protobuf.dev/


感谢阅读,本文如有错漏之处,烦请斧正。

相关推荐
南境十里·墨染春水2 小时前
C++笔记 构造函数 析构函数 及二者关系(面向对象)
开发语言·c++·笔记·ecmascript
OxyTheCrack3 小时前
【C++】一篇文章详解C++17新特性
c++
mmz12073 小时前
贪心算法3(c++)
c++·算法·贪心算法
j_xxx404_3 小时前
蓝桥杯基础--排序模板合集II(快速,归并,桶排序)
数据结构·c++·算法·蓝桥杯·排序算法
Jordannnnnnnn3 小时前
追赶29,28
c++
:mnong3 小时前
油藏数值模型ReservoirSim 系统设计分析
c++
计算机安禾3 小时前
【数据结构与算法】第13篇:栈(三):中缀表达式转后缀表达式及计算
c语言·开发语言·数据结构·c++·算法·链表
简单~3 小时前
C++ 函数模板完全指南
c++·函数模板
·心猿意码·3 小时前
C++ 线程安全单例模式的底层源码级解析
c++·单例模式