Protobuf 的介绍与使用

Protocol Buffers(简称 protobuf)是 Google 开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法。它常用于:

  • 跨语⾔服务:由于 Protobuf ⽀持多种编程语⾔,它⾮常适合构建跨语⾔的 RPC(远程过程调⽤)服务。

  • 数据序列化:用于网络通信、数据存储等场景,将结构化数据序列化为字节流。

Protobuf 定义了⼀种接⼝描述语⾔(IDL),⽤于描述数据结构,然后可以⾃动⽣成各种编程语⾔的代码来操作这些数据结构。

使用流程

  1. .proto 文件中定义数据结果(消息类型)。

  2. 使用 protoc 编译器生成目标语言的代码。

  3. 在你的程序中引用生成的代码,进行序列化与反序列化。

一、Protobuf 安装

sudo apt install protobuf-compiler libprotobuf-dev
#如果需要使⽤grpc相关编译
sudo apt install protobuf-compiler-grpc

# 验证是否安装成功
protoc --version

二、Protobuf 基本语法

2.1 基本结构

  • 消息:Protobuf 中的基本数据结构单元,类似于 C++ 中的结构体或 Java 中的类。

  • 字段:消息中的数据项,每个字段都有⼀个 唯⼀的字段编号数据类型

    • 编号 1 ~ 15 占用 1 个字节,16 ~ 2047 的占用 2 个字节,常用字段应使用较小的编号
    • 19000 ~ 19999 不可用,在 Protobuf 协议实现中,对这些数进行了预留
  • 枚举:用于定义一组命名的常数。

  • 服务:在 RPC 场景中定义服务接口。

2.2 数据类型

.proto Type Notes C++ Type
double double
float float
int32 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint32 代替。 int32
int64 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint64 代替。 int64
uint32 使用变长编码[1]。 uint32
uint64 使用变长编码[1]。 uint64
sint32 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型。 int32
sint64 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int64 类型。 int64
fixed32 定长 4 字节。若值常大于 (2^{28}) 则会比 uint32 更高效。 uint32
fixed64 定长 8 字节。若值常大于 (2^{56}) 则会比 uint64 更高效。 uint64
sfixed32 定长 4 字节。 int32
sfixed64 定长 8 字节。 int64
bool bool
string 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 (2^{32})。 string
bytes 可包含任意的字节序列但长度不能超过 (2^{32})。 string

[1] 变长编码是指:经过 protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数。

2.3 定义 .proto 文件

syntax = "proto3";  // 使用 proto3 语法
package example;    // 生成 C++ 代码后,example 变成命名空间,防止命名冲突

// 默认情况protoc命令并不会针对service服务⽣成对应rpc代码
// 需要开启选项才会进⾏⽣成
option cc_generic_services = true;

// 枚举类型
enum PhoneType {
    MOBILE = 0; // 枚举类型的编号必须从0开始
    HOME = 1;
    WORK = 2;
}
  
// 定义消息类型
message Student {
    string name = 1;      // 字段编号 1
    int32 id = 2;         // 字段编号 2
    

    // 嵌套消息
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    
    repeated PhoneNumber phones = 3;  // repeated 表示数组
    map<string , float> scores = 4;   // hash类型(键值对)
}

// RPC 远程调用的相关接口
message PersonRequest {
    int32 id = 1;
}

message PersonResponse {
    Student stu = 1;
}

// 定义一个 RPC 服务
service PersonService {
    rpc getPerson(PersonRequest) returns (PersonResponse);
}

2.4 编译命令

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
  • protoc 编译工具

  • --proto_path 指定被编译的 .proto 文件所在的目录,可多次指定,可简写为 -I

  • IMPORT_PATH 如果不指定该参数,则在当前目录搜索,当某个 .proto 文件 import 其他 .proto 文件或编译的 .proto 文件不在当前目录下,这时就需要用 -I 指定搜索目录

  • --cpp_out 指编译后的⽂件为 C++ ⽂件

  • DST_DIR 编译后⽣成⽂件的⽬标路径

  • path/to/file.proto 要编译的.proto⽂件

2.5 编译后生成的结构

person.pb.h

namespace person {
    enum PhoneType : int {
        MOBILE = 0,
        HOME = 1,
        WORK = 2,
        // 用于枚举类型的边界检查
        PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ =
            ::std::numeric_limits<::int32_t>::min(),
        PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ =
            ::std::numeric_limits<::int32_t>::max(),
    };
    // string name = 1;
    void clear_name() ; // 清除 name 字段的值
    const ::std::string& name() const; // 获取 name 字段的值(只读)
    template <typename Arg_ = const ::std::string&, typename... Args_> 
    void set_name(Arg_&& arg, Args_... args); // 设置 name 字段的值(支持多种参数类型)
    ::std::string* PROTOBUF_NONNULL mutable_name(); // 获取 name 字段的可修改指针

    // int32 id = 2;
    void clear_id() ;
    ::int32_t id() const;
    void set_id(::int32_t value);

    // map<string, float> scores = 4;
    int scores_size() const; // 获取 scores map 的元素个数
    void clear_scores() ; // 清空 scores map 中的所有元素
    // 获取 scores map 的只读引用
    // 返回: 常量 map 引用,可用于遍历和查找
    // 示例: for (const auto& pair : scores()) { ... }
    // scores()["math"] = 99.8
    const ::google::protobuf::Map<::std::string, float>& scores() const;
    // 获取 scores map 的可修改指针
    // 返回: 可修改的 map 指针,用于添加、删除或修改元素
    // 示例: (*mutable_scores())["math"] = 95.5;
    ::google::protobuf::Map<::std::string, float>* PROTOBUF_NONNULL mutable_scores();

    // repeated .person.Student.PhoneNumber phones = 3;
    int phones_size() const; // 获取 phones 列表的元素个数
    void clear_phones() ;
    // 获取指定索引位置的 PhoneNumber 对象的可修改指针
    // 参数: index - 索引位置(0 到 phones_size()-1)
    // 返回: 可修改的 PhoneNumber 指针
    // 示例: mutable_phones(0)->set_number("123456");
    ::person::Student_PhoneNumber* PROTOBUF_NONNULL mutable_phones(int index);
    ::google::protobuf::RepeatedPtrField<::person::Student_PhoneNumber>* PROTOBUF_NONNULL mutable_phones();
    // 取指定索引位置的 PhoneNumber 对象的只读引用
    const ::person::Student_PhoneNumber& phones(int index) const;
    // 在 phones 列表末尾添加一个新的 PhoneNumber 对象
    // 返回: 新添加的 PhoneNumber 对象的可修改指针
    // 示例: add_phones()->set_number("987654");
    ::person::Student_PhoneNumber* PROTOBUF_NONNULL add_phones();
    // 获取整个 phones 列表的只读引用
    const ::google::protobuf::RepeatedPtrField<::person::Student_PhoneNumber>& phones() const;


    class PersonService : public ::google::protobuf::Service {
        public:
            virtual void getPerson(::google::protobuf::RpcController* PROTOBUF_NULLABLE controller,
                        const ::person::PersonRequest* PROTOBUF_NONNULL request,
                        ::person::PersonResponse* PROTOBUF_NONNULL response,
                        ::google::protobuf::Closure* PROTOBUF_NULLABLE done);
    }

    class PersonService_Stub final : public PersonService {
        public:
            void getPerson(::google::protobuf::RpcController* PROTOBUF_NULLABLE controller,
                        const ::person::PersonRequest* PROTOBUF_NONNULL request,
                        ::person::PersonResponse* PROTOBUF_NONNULL response,
                        ::google::protobuf::Closure* PROTOBUF_NULLABLE done) override;
    }
}
  • repeated 作用于不同字段的类型时:

    • 基础类型(int32, string, float, bool 等):可以直接使用 add_字段名(value) 添加元素

    • 复合类型(message,enum):需要使用 add_字段名() 返回指针,然后设置

2.6 序列化和反序列化

每个 protobuf message 编译后生成的类都继承自 google::protobuf::Message(或 MessageLite),提供了丰富的通用接口。

namespace google {
    namespace protobuf { 
        class PROTOBUF_EXPORT Message : public MessageLite {}
        class PROTOBUF_EXPORT MessageLite {
            public:
                // 序列化方法
                bool SerializeToString(std::string* output) const; // 序列化到 string 中
                bool SerializeToArray(void* data, int size) const; // 序列化到 C 风格数组中
                bool SerializeToOstream(std::ostream* output) const; // 序列化到C++流中
                std::string SerializeAsString() const; // 返回空 string 表⽰出错
                // 反序列化方法
                bool ParseFromString(const std::string& data); // 从 string 中反序列化
                bool ParseFromArray(const void* data, int size); // 从 C 风格数组中反序列化
                bool ParseFromIstream(std::istream* input); // 从 C++ 流中反序列化
        }
    }
}

2.7 基本使用

目录结构

|-test.cc
|-person.pb.cc
|-person.pb.h
|-person.proto
|-makefile

person.proto

syntax = "proto3";  
package person;    

option cc_generic_services = true;

enum PhoneType {
    MOBILE = 0; 
    HOME = 1;
    WORK = 2;
}
  
message Student {
    string name = 1;     
    int32 id = 2;       
    
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    
    repeated PhoneNumber phones = 3;  
    map<string , float> scores = 4;  
}

message PersonRequest {
    int32 id = 1;
}

message PersonResponse {
    Student stu = 1;
}

service PersonService {
    rpc getPerson(PersonRequest) returns (PersonResponse);
}

test.cc

#include "person.pb.h"
#include <iostream>

std::string serialize_test() {
    person::Student stu;
    stu.set_id(1001);
    stu.set_name("zhangsan");
    auto phone1 = stu.add_phones();
    phone1->set_number("123456789");
    phone1->set_type(person::PhoneType::HOME);
    
    auto scores = stu.mutable_scores();
    (*scores)["math"] = 95;
    (*scores)["english"] = 85.5;
    scores->insert({"chinese", 100});

    std::string str = stu.SerializeAsString();
    std::cout << str << std::endl;
    return str;
}

void unserialize_test(const std::string& str) {
    person::Student stu;
    bool ret = stu.ParseFromString(str);
    if (ret == false) {
        std::cout << "unserialize failed" << std::endl;
        return;
    }
    std::cout << stu.id() << std::endl;
    std::cout << stu.name() << std::endl;
    for (int i = 0; i < stu.phones().size(); ++i) {
        std::cout << stu.phones(i).number() << " " << stu.phones(i).type() << std::endl;
    }
    auto it = stu.scores().find("math");
    if (it != stu.scores().end()) {
        std::cout << it->second << std::endl; // 数学成绩
    }
    for (auto it: stu.scores()) {
        std::cout << it.first << " " << it.second << std::endl;
    }
}

int main() {

    std::cout << "序列化结果:" << std::endl;
    std::string res = serialize_test();
    std::cout << res << std::endl;
    std::cout << "反序列化结果:" << std::endl;
    unserialize_test(res);

    return 0;
}

Makefile

main: test.cc person.pb.cc
	g++ -std=c++17 -o $@ $^ -lprotobuf -lpthread
.PHONY:clean
clean:
	rm -f test.cc

三、补充操作

3.1 map

protobuf 中 message 消息结构中⽀持 map 字段的定义,其字段类型对应的是 protobuf 中的Map类(注意并⾮C++STL中 的 map,虽然他们的基本操作都差不多,但是类型是不同的)。

namespace google {
    namespace protobuf {
        template <typename Key, typename T>
        struct MapPair {
            MapPair(const Key& other_first, const T& other_second)
        };
        template <typename Key, typename T>
        class Map {
            public:
                using iterator = ....;
                using const_iterator = ....;
                iterator begin();
                iterator end();
                void clear();
                size_type size()
                bool empty()
                iterator find(const TrivialKey& k)
                std::pair<iterator, bool> insert(const KeyValuePair& kv)
                iterator operator[](const TrivialKey& k)
                void erase(iterator it)
        };
    }
}

3.2 Json 与 Protobuf 的相互转换

#include <google/protobuf/util/json_util.h>

// 核心转换函数
namespace google::protobuf::util {
    class PROTOBUF_EXPORT Status {
        public:
            bool ok();
            StringPiece message();
            string ToString();
    };

    struct JsonParseOptions {
        //解析过程中是否忽略未知的JSON字段
        bool ignore_unknown_fields;
        //是否在解析枚举类型时不区分⼤⼩写
        bool case_insensitive_enum_parsing;
    };

    // JSON 字符串 -> Protobuf Message
    Status JsonStringToMessage(
        const std::string& json,
        Message* message,
        const JsonParseOptions& options = JsonParseOptions()
    );

    struct JsonPrintOptions {
        //是否添加空⽩字符让json更易读
        bool add_whitespace;
        //是否总是输出基本类型的字段。默认值的基本类型字段在JSON输出中会被省略
        bool always_print_primitive_fields;
        //是否总是将枚举类型作为整数打印。默认情况下,枚举值会被渲染为字符串
        bool always_print_enums_as_ints;
        //是否保留 pb 字段名称,true则使⽤原始名⽽不是根据 JSON 命名约定进⾏转换
        bool preserve_proto_field_names;
        //默认均为false
    };
    
    // Protobuf Message -> JSON 字符串
    Status MessageToJsonString(
        const Message& message,
        std::string* json,
        const JsonPrintOptions& options = JsonPrintOptions()
    );
}

案例

#include "person.pb.h"
#include <iostream>
#include <google/protobuf/util/json_util.h>

void protobufToJsonTest() {
    person::Student stu;
    stu.set_id(100);
    stu.set_name("zhangsan");
    auto phone1 = stu.add_phones();
    phone1->set_number("123456789");
    phone1->set_type(person::PhoneType::HOME);
    
    auto scores = stu.mutable_scores();
    (*scores)["math"] = 95;
    (*scores)["english"] = 85.5;
    scores->insert({"chinese", 100});

    std::string json;
    google::protobuf::util::JsonPrintOptions options;
    options.add_whitespace = true;
    google::protobuf::util::Status res = google::protobuf::util::MessageToJsonString(stu , &json , options); 
    if(!res.ok()) {
        std::cout << "message to json failed: " << res.ToString() << std::endl;
        return;
    }
    std::cout << json << std::endl;
}

void jsonToProtobufTest() {
    std::string json = R"(
        {
            "name": "ccc",
            "id": 10000,
            "phones": [
                {
                    "number": "13620775555",
                    "type": 1
                }
            ],
            "scores": {
                "math": 98,
                "chinese": 100
            }
        }
    )";
    person::Student stu;
    google::protobuf::util::Status res = google::protobuf::util::JsonStringToMessage(json , &stu);
    if(!res.ok()) {
        std::cout << "json to protubuf failed: " << res.ToString() << std::endl;
        return;
    }
    std::cout << stu.id() << std::endl;
    std::cout << stu.name() << std::endl;
    for (int i = 0; i < stu.phones().size(); ++i) {
        std::cout << stu.phones(i).number() << " " << stu.phones(i).type() << std::endl;
    }
    for (auto it: stu.scores()) {
        std::cout << it.first << " " << it.second << std::endl;
    }
}

int main() {

    std::cout << "protobufToJsonTest: " << std::endl;
    protobufToJsonTest();
    std::cout << "jsonToProtobufTest: " << std::endl;
    jsonToProtobufTest();

    return 0;
}

输出结果:

protobufToJsonTest: 
{
 "name": "zhangsan",
 "id": 100,
 "phones": [
  {
   "number": "123456789",
   "type": "HOME"
  }
 ],
 "scores": {
  "chinese": 100,
  "math": 95,
  "english": 85.5
 }
}

jsonToProtobufTest: 
10000
ccc
13620775555 1
math 98
chinese 100
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐