ProtoBuf入门

Posted 他叫小黑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ProtoBuf入门相关的知识,希望对你有一定的参考价值。

最近在入android O的坑。看到ProtoBuf了解一下。
Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。你可以用定义自己ProtoBuf的数据结构,用ProtoBuf编译器生成特定语言的源代码,(如C++,Java,Python等,目前ProtoBuf对主流的编程语言都提供了支持)方便的进行序列化和反序列化。
Protobuf解析速度快,比对应的XML快约20-100倍;序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
详细内容看官网


1.编写proto文件

syntax="proto2"; //表明使用protobuf的编译器版本为v2,目前最新的版本为v3
package addressbook; //声明了一个包名,用来防止不同的消息类型命名冲突

import "src/help.proto"; //举例用,编译时去掉

message Person 
    required string name = 1; //声明了一个名为name,数据类型为string的required字段,字段的标识号为1
    required int32 id = 2;
    optional string email = 3;

    enum PhoneType 
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    

    message PhoneNumber 
        required string number = 1;
        optional PhoneType type = 2 [default = HOME];//为type字段指定了一个默认值,当没有为type设值时,其值为HOME。 
    

    repeated PhoneNumber phone = 4;


message AddressBook 
    repeated Person person_info = 1;

message 是Protobuf中的结构化数据,类似于C++中的类,可以在其中定义需要处理的数据。一个proto文件中可以声明多个message,在编译的时候他们会被编译成为不同的类。
protobuf一共有三个字段修饰符:

  • required:该值是必须要设置的;
  • optional :该字段可以有0个或1个值(不超过1个);
  • repeated:该字段可以重复任意多次(包括0次),类似于C++中的list;

required string name = 1;string 是一种标量类型,protobuf的所有标量类型请参考文末的标量类型列表。
name是字段名,1是字段的标识号,在消息定义中,每个字段都有唯一的一个数字标识号,这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
标识号的范围在:1 ~ 2^29 -1,其中[19000-19999]为Protobuf预留,不能使用。

Person内部声明了一个enum和一个message,这类似于C++中的类内声明,Person外部的结构可以用 Person.PhoneType 的方式来使用PhoneType。当使用外部package中的结构时,要使用 pkgName.msgName.typeName 的格式,每两层之间使用”.”来连接,类似C++中的”::”。

2.编译

protoc是proto文件的编译器,目前可以将proto文件编译成C++、Java、Python三种代码文件,编译格式如下:

protoc -I=$SRC_DIR --cpp_out=$DST_DIR /path/to/file.proto

上面的命令会在$DST_DIR生成file.pb.h 和 file.pb.cc两个C++文件。如果需要生成其他语言的代码譬如java换成- -java_out即可。在生成的文件中会生成一个你定义的消息类型的的类以及读取和设置该消息类型各个属性的set和Get方法等,比如上例中会生成一个Person类,和name(),set_name()这样的成员方法,以及序列化和反序列的方法,你可以用这些方法方便的读取和设置相应的属性,以及序列化和反序列化方法。

3.使用C++文件

现在编写一个main.cc文件:

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

int main(int argc, const char* argv[])

    addressbook::AddressBook person;
    addressbook::Person* pi = person.add_person_info();

    pi->set_name("xiaohei");
    pi->set_id(1219);
    std::cout << "before clear(), id = " << pi->id() << std::endl;
    pi->clear_id();
    std::cout << "after  clear(), id = " << pi->id() << std::endl;
    pi->set_id(1087);
    if (!pi->has_email())
        pi->set_email("xiaohei@126.com");

    addressbook::Person::PhoneNumber* pn = pi->add_phone();
    pn->set_number("021-8888-8888");
    pn = pi->add_phone();
    pn->set_number("138-8888-8888");
    pn->set_type(addressbook::Person::MOBILE);

    uint32_t size = person.ByteSize();
    unsigned char byteArray[size];
    person.SerializeToArray(byteArray, size);

    addressbook::AddressBook help_person;
    help_person.ParseFromArray(byteArray, size);
    addressbook::Person help_pi = help_person.person_info(0);

    std::cout << "*****************************" << std::endl;
    std::cout << "id:    " << help_pi.id() << std::endl;
    std::cout << "name:  " << help_pi.name() << std::endl;
    std::cout << "email: " << help_pi.email() << std::endl;

    for (int i = 0; i < help_pi.phone_size(); ++i)
    
        auto help_pn = help_pi.mutable_phone(i);
        std::cout << "phone_type: " << help_pn->type() << std::endl;
        std::cout << "phone_number: " << help_pn->number() << std::endl;
    
    std::cout << "*****************************" << std::endl;

    return 0;

编译生成可执行代码 。编译格式和普通的C++代码一样,但是要加上 -lprotobuf -pthread

g++ main.cc xxx.pb.cc -I $INCLUDE_PATH -L $LIB_PATH -lprotobuf -pthread 

输出结果如下:

before clear(), id = 1219
after  clear(), id = 0
*****************************
id:   1087
name: xiaohei
email: xiaohei@126.com
phone_type: 1
phone_number: 021-8888-8888
phone_type: 0
phone_number: 138-8888-8888
*****************************

4.常用API

protoc为message的每个required字段和optional字段都定义了以下几个函数(不限于这几个):

TypeName xxx() const;          //获取字段的值
bool has_xxx();              //判断是否设值
void set_xxx(const TypeName&);   //设值
void clear_xxx();          //使其变为默认值

为每个repeated字段定义了以下几个:

TypeName* add_xxx();        //增加结点
TypeName xxx(int) const;    //获取指定序号的结点,类似于C++的"[]"运算符
TypeName* mutable_xxx(int); //类似于上一个,但是获取的是指针
int xxx_size();            //获取结点的数量

下面几个是常用的序列化函数:

bool SerializeToOstream(std::ostream * output) const; //输出到输出流中
bool SerializeToString(string * output) const;        //输出到string
bool SerializeToArray(void * data, int size) const;   //输出到字节流

与之对应的反序列化函数:

bool ParseFromIstream(std::istream * input);     //从输入流解析
bool ParseFromString(const string & data);       //从string解析
bool ParseFromArray(const void * data, int size); //从字节流解析

其他常用的函数:

bool IsInitialized();    //检查是否所有required字段都被设值
size_t ByteSize() const; //获取二进制字节序的大小

5.标量类型列表

proto类型C++类型备注
doubledouble
floatfloat
int32int32使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint32
int64int64使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint64
uint32uint32使用可变长编码
uint64uint64使用可变长编码
sint32int32使用可变长编码,有符号的整型值,编码时比通常的int32高效
sint64int64使用可变长编码,有符号的整型值,编码时比通常的int64高效
fixed32uint32总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效
fixed64uint64总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效
sfixed32int32总是4个字节
sfixed64int64总是8个字节
boolbool
stringstring一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本
bytesstring可能包含任意顺序的字节数据

以上是关于ProtoBuf入门的主要内容,如果未能解决你的问题,请参考以下文章

Protobuf从入门到“顺手”

protobuf转json

如何在 gRPC 中使用标量类型作为函数参数?

三.protobuf3标量值类型

ProtoBuf入门

十五.ProtoBuf3的基础总结