以下内容大部分转载自https://www.cnblogs.com/autyinjing/p/6495103.html Protobuf学习 - 入门

介绍

Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

优势

  - 平台无关,语言无关,可扩展;(尤为重要)
  - 提供了友好的动态库,使用简单;
  - 解析速度快,比对应的XML快约20-100倍;
  - 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

为什么相比简单的struct 结构protobuf能够实现高效存储

采用对数据进行二进制压缩,因为数据在机器语言层面上的表现都是二进制,而简单的 struct 结构体中包含的基本类型如int ,在32位机器中占32位,然而当我们实际传输的时候并不是这32位都用到,protobuf就是将这一情况进行压缩达到的高效序列化数据。

安装

源码下载地址: https://github.com/google/protobuf 
需要安装依赖的库: autoconf automake libtool curl make g++ unzip  
./autogen.sh
./configure
make
make check
sudo make install

使用

和早期QT需要手动编写ui文件后用程序生成.h、.cpp一样,我们同样需要编写proto文件,在里面定义结构化的数据

proto文件内容如下:

// Filename: test.proto  注释同样用方式同C++

syntax="proto2";  //声明使用的编译器版本为v2。
package addressbook;  //声明包名,类似于namespace
//import "src/demo.proto"; //类似于C++中的include
message Person {     //类似于C++中的class
    required string name = 1;
/* 
1 是字段的标识号,在消息定义中,每个字段都有唯一的一个数字标识号,这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
标识号的范围在:1 ~ 229 - 1,其中[19000-19999]为Protobuf预留,不能使用。
*/
    required int32 id = 2;
    optional string email = 3;
/*三种字段修饰符 
required : 必须要设置的
optional : 可以有0或1个值
repeated : 可以重复任意多次(包括0)

*/
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }  //类内声明,同C++

    message PhoneNumber {
        required string number = 1;
        optional PhoneType type = 2 [default = HOME];  //设置默认值为HOME
    }

    repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person_info = 1;
}

将proto转换成C++类的声明就很容易上手理解了。

接下来需要将proto编译成.h 和.cc 文件供用户程序调用

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

protoc  --cpp_out=. test.proto
-I=<pkg locate>   import 中引用的proto文件如果不在同一目录需要用这个来引用
--cpp_out=<output locate> 编译成C++ 代码文件输出

上面的命令会生成xxx.pb.h 和 xxx.pb.cc两个C++文件。

测试程序

现在编写一个C++ 文件

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

int main(int argc, const char* argv[])
{
    addressbook::AddressBook person;
    addressbook::Person* pi = person.add_person_info();

    pi->set_name("aut");
    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("autyinjing@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;
}

测试输出结果:

g++ main.cc xxx.pb.cc  -lprotobuf -pthread 

Screenshot_2020年03月08日21时23分14秒.png

C++ 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; //获取二进制字节序的大小

官方API文档地址: https://developers.google.com/protocol-buffers/docs/reference/overview

标量类型

标量类型.png

还有什么?

  1. 编码风格
      - 花括号的使用(参考上面的proto文件)
      - 数据类型使用驼峰命名法:AddressBook, PhoneType
      - 字段名小写并使用下划线连接:person_info, email_addr
      - 枚举量使用大写并用下划线连接:FIRST_VALUE, SECOND_VALUE

  2. 适用场景

  “Protocol Buffers are not designed to handle large messages.”。protobuf对于1M以下的message有很高的效率,但是当message是大于1M的大块数据时,protobuf的表现不是很好,请合理使用。

总结:本文介绍了protobuf的基本使用方法和编码规则,还有很多内容尚未涉及,比如:反射机制、扩展、Oneof、RPC等等,更多内容需参考官方文档。