ProtoBuf超详细使用教程

一、ProtoBuf是什么

ProtoBuf是Google开发的一种轻量高效的结构化数据交换格式,和JSON、XML干的是同一件事------存数据、传数据,但它比JSON更小、更快、更简单。

它的玩法很特别:先用一种IDL语言(接口描述语言)定义数据结构,然后用编译器自动生成各种编程语言的代码,C++、Java、Go、Python通通支持。

ProtoBuf的用途

  • 数据序列化:网络通信、数据存储,把结构体转成二进制字节流传输
  • 跨语言RPC服务:gRPC就是基于ProtoBuf的,多语言服务之间通信全靠它
  • 版本兼容:字段加加减减都不影响旧代码,向后兼容性拉满

安装

Ubuntu一条命令搞定:

bash 复制代码
sudo apt install protobuf-compiler libprotobuf-dev

二、Proto文件语法

基本组成

ProtoBuf里有几个核心概念,我们逐个拆分理解一下:

  • 消息(message):基本数据结构单元,就相当于C++里的结构体
  • 字段:消息里的每个数据项,都有唯一的编号(标签号)和类型
  • 枚举(enum):定义一组命名常量,和C++枚举差不多
  • 服务(service):RPC场景下定义服务接口的

语法示例

先看一个完整的proto文件例子,名叫example.proto

protobuf 复制代码
syntax = "proto3";           // 指定语法版本,用proto3
package example;             // 命名空间,生成C++代码后对应namespace

// 要生成service的RPC代码必须开这个选项
option cc_generic_services = true;

// 定义一个消息Person,就像C++的struct
message Person {
    string name = 1;        // 字符串类型,标签号1
    int32 age = 2;          // 32位整数,标签号2
    string email = 3;       // 字符串,标签号3
    repeated string skills = 4;  // repeated相当于数组,标签号4
    map<string, float> scores = 5; // map类型,标签号5
}

// RPC请求消息
message HelloRequest {
    string name = 1;
}

// RPC响应消息
message HelloReply {
    string message = 1;
}

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

关键字拆解

  1. syntax = "proto3":指定用proto3语法,现在基本都用这个版本
  2. package example :包名,生成C++代码后就是namespace example
  3. message :定义消息结构体,每个字段后面的数字是标签号,序列化的时候靠这个编号识别字段,不是按顺序的哦
  4. repeated:可重复字段,就相当于数组/vector,可以有多个值
  5. map:键值对,就相当于C++的map
  6. service / rpc:定义RPC服务接口和方法

三、编译生成代码

写好proto文件后,用protoc编译器生成C++代码:

bash 复制代码
protoc --cpp_out=./ example.proto

参数说明:

  • --cpp_out=./:生成C++代码,输出到当前目录
  • -I--proto_path:指定proto文件所在的路径

编译完会生成两个文件:

  • example.pb.h ------ 头文件,类的声明
  • example.pb.cc ------ 源文件,类的实现

生成的代码长啥样

生成的Person类,每个字段都会对应生成get/set方法,我们来看看:

cpp 复制代码
namespace example {  // 和proto里的package对应

class Person : public ::google::protobuf::Message {
public:
    // ===== 普通字符串字段name =====
    void clear_name();
    const std::string& name() const;       // 获取值
    void set_name(const std::string& value); // 设置值
    std::string* mutable_name();           // 获取指针用来修改

    // ===== 普通整数字段age =====
    void clear_age();
    int32_t age() const;
    void set_age(int32_t value);

    // ===== repeated数组字段skills =====
    int skills_size() const;               // 获取元素个数
    void clear_skills();
    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);   // 追加元素(直接传值)

    // ===== map字段scores =====
    int scores_size() const;
    void clear_scores();
    const ::google::protobuf::Map<std::string, float>& scores() const;
    ::google::protobuf::Map<std::string, float>* mutable_scores();
};

}

是不是很有规律?每个字段都有clear_xxx()xxx()(get)、set_xxx()(set)、mutable_xxx()(拿指针改)。

repeated字段多了xxx_size()add_xxx();map字段用mutable_xxx()拿指针后操作,和STL的map用法差不多~


四、核心接口介绍

1. Message基类 ------ 序列化反序列化

所有生成的消息类都继承自MessageLite,自带序列化反序列化方法:

cpp 复制代码
// 反序列化:从字符串解析
bool ParseFromString(const std::string& data);

// 反序列化:从内存块解析
bool ParseFromArray(const void* data, int size);

// 序列化:写入字符串
bool SerializeToString(std::string* output) const;

// 序列化:直接返回字符串
std::string SerializeAsString() const;

// 序列化:写入内存块
bool SerializeToArray(void* data, int size) const;

2. Map类

ProtoBuf的map不是STL的map,但用法几乎一模一样:

cpp 复制代码
iterator begin();
iterator end();
size_type size();
bool empty();
void clear();
iterator find(const Key& k);
insert(键值对);
operator[](key);
erase(iterator);

3. JSON转换工具

ProtoBuf还自带和JSON互转的工具,调试的时候超好用!

cpp 复制代码
// Proto转JSON
util::Status MessageToJsonString(const Message& message, std::string* output);

// JSON转Proto
util::Status JsonStringToMessage(StringPiece input, Message* message);

还有配置选项可以控制格式,比如要不要加缩进、要不要输出默认值等等~


五、序列化 & 反序列化

目录结构

复制代码
|-- makefile
|-- example.proto
`-- example.cc

1. proto文件 example.proto

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

option cc_generic_services = true;

// 定义Person消息
message Person {
    string name = 1;
    int32 age = 2;
    string email = 3;
    repeated string skills = 4;   // 数组
    map<string, float> scores = 5; // 映射表
}

2. 测试代码 example.cc

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

int main(int argc, char* argv[])
{
    // ========== 第一步:构造数据 ==========
    example::Person person;
    
    // 普通字段直接set
    person.set_name("Jack");
    person.set_age(28);
    person.set_email("qq.com");
    
    // repeated数组字段用add追加
    person.add_skills("C++");
    person.add_skills("Java");
    person.add_skills("Python");
    
    // map字段要先拿mutable指针再操作
    auto score_map = person.mutable_scores();
    score_map->insert({"Ch", 88});
    score_map->insert({"Mt", 77});
    score_map->insert(google::protobuf::MapPair<std::string, float>("En", 99));

    // ========== 第二步:序列化 ==========
    std::string pstr = person.SerializeAsString();
    if (pstr.empty()) {
        std::cout << "SerializeAsString Failed!\n";
        return -1;
    }
    std::cout << "序列化成功,数据长度: " << pstr.size() << " 字节\n" << std::endl;

    // ========== 第三步:反序列化 ==========
    example::Person tmp;
    bool ret = tmp.ParseFromString(pstr);
    if (ret == false) {
        std::cout << "ParseFromString Failed!\n";
        return -1;
    }

    // ========== 第四步:读取验证 ==========
    
    // 普通字段直接get
    std::cout << "name: " << tmp.name() << std::endl;
    std::cout << "age: " << tmp.age() << std::endl;
    std::cout << "email: " << tmp.email() << std::endl;

    // repeated字段:先拿size,再按下标遍历
    int sz = tmp.skills_size();
    for (int i = 0; i < sz; i++) {
        std::cout << "skills: " << tmp.skills(i) << std::endl;
    }

    // map字段:迭代器遍历,和STL一样
    auto beg = tmp.scores().begin();
    auto end = tmp.scores().end();
    for (; beg != end; ++beg) {
        std::cout << beg->first << " = " << beg->second << std::endl;
    }

    // map查找操作
    auto it = tmp.scores().find("Ch");
    if (it != tmp.scores().end()) {
        std::cout << "Ch: " << it->second << std::endl;
    }

    return 0;
}

3. Makefile

makefile 复制代码
all: example

example: example.cc example.pb.cc
	g++ -std=c++17 $^ -o $@ -lprotobuf

# 自动编译proto文件生成pb.h和pb.cc
example.pb.cc : example.proto
	protoc --cpp_out=./ $^

运行结果

复制代码
name: Jack
age: 28
email: qq.com
skills: C++
skills: Java
skills: Python
En = 99
Ch = 88
Mt = 77
Ch: 88

六、Proto与JSON互相转换

调试的时候二进制看不清内容?ProtoBuf自带JSON转换功能,直接转成JSON字符串打印出来,一目了然!

代码示例

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

// Proto消息 转 JSON字符串
std::string p2j(google::protobuf::Message &msg) {
    google::protobuf::util::JsonOptions opts;
    opts.add_whitespace = true;  // 加空白字符,格式化输出更好看
    
    std::string json_str;
    auto ret = google::protobuf::util::MessageToJsonString(msg, &json_str);
    if (ret.ok() == false) {
        std::cout << ret.ToString() << std::endl;
        return "";
    }
    std::cout << json_str << std::endl;
    return json_str;
}

// JSON字符串 转 Proto消息
void j2p(const std::string &json_str) {
    example::Person person;
    auto ret = google::protobuf::util::JsonStringToMessage(json_str, &person);
    if (ret.ok() == false) {
        std::cout << ret.ToString() << std::endl;
        return;
    }
    
    // 读取数据
    if (person.has_name())
        std::cout << "person.name:" << person.name() << std::endl;
    if (person.has_age())
        std::cout << "person.age:" << person.age() << std::endl;
    if (person.has_email())
        std::cout << "person.email:" << person.email() << std::endl;
    
    int sz = person.skills_size();
    for (int i = 0; i < sz; i++)
        std::cout << "person.skills:" << person.skills(i) << std::endl;
    
    for (const auto &f : person.scores())
        std::cout << f.first << " = " << f.second << std::endl;
}

int main(int argc, char* argv[])
{
    example::Person person;
    person.set_name("Jack");
    person.set_age(0);
    person.set_email("qq.com");
    person.add_skills("C++");
    person.add_skills("Java");
    person.add_skills("Python");
    
    auto score_map = person.mutable_scores();
    score_map->insert({"Ch", 88});
    score_map->insert({"Mt", 77});
    score_map->insert({"En", 99});

    std::cout << "---------- Proto转JSON ----------\n";
    std::string str = p2j(person);
    
    std::cout << "\n---------- JSON转Proto ----------\n";
    j2p(str);

    return 0;
}

注意:要用has_xxx()判断字段存不存在,proto3里需要给字段加optional关键字才会生成has方法,编译的时候还要加--experimental_allow_proto3_optional参数哦~

运行结果

复制代码
---------- Proto转JSON ----------
{
 "name": "Jack",
 "age": 0,
 "email": "qq.com",
 "skills": [
  "C++",
  "Java",
  "Python"
 ],
 "scores": {
  "En": 99,
  "Ch": 88,
  "Mt": 77
 }
}

---------- JSON转Proto ----------
person.name:Jack
person.age:0
person.email:qq.com
person.skills:C++
person.skills:Java
person.skills:Python
Ch = 88
En = 99
Mt = 77

总结

ProtoBuf核心知识点梳理一下:

四步走流程:

  1. .proto文件,用IDL语法定义消息结构
  2. protoc编译器生成C++代码(.pb.h和.pb.cc)
  3. 代码里用生成的类构造数据,SerializeAsString()序列化
  4. 接收端ParseFromString()反序列化,读取数据

字段操作规律:

  • 普通字段:set_xxx() 设置,xxx() 读取
  • repeated数组:add_xxx() 追加,xxx_size() 长度,xxx(i) 按下标取
  • map映射:mutable_xxx() 拿指针,然后和STL map一样用
  • 想修改就用mutable_xxx()拿指针,想只读就直接xxx()

常用API:

  • 序列化:SerializeAsString() / SerializeToString()
  • 反序列化:ParseFromString() / ParseFromArray()
  • JSON互转:MessageToJsonString() / JsonStringToMessage()
相关推荐
神仙别闹1 小时前
基于QT(C++)实现Windows 自启动项查看和分析
c++·windows·qt
野生风长2 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
雪的季节2 小时前
Python「假多态」与 C++「真多态」的核心区别
开发语言·c++
zmzb01032 小时前
C++课后习题训练记录Day166
开发语言·c++
皓月斯语3 小时前
程序设计语言的特点
开发语言·数据结构·c++
邪修king3 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
梓䈑9 小时前
【算法题攻略】BFS 解决FloodFill 算法、最短路问题、多源BFS 和 拓扑排序
c++·算法·宽度优先
程序喵大人13 小时前
【C++进阶】STL容器与迭代器 - 01 STL 容器先解决元素放在哪里
开发语言·c++·stl
野生风长17 小时前
C++入门基础:从命名空间到引用与指针的全面解析
开发语言·c++