好记性不如铅笔头

编程

Protobuf学习笔记

以下参考

CONTENTS

Protobuf是什么?

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

为什么要用?

– 平台无关,语言无关,可扩展;
– 提供了友好的动态库,使用简单;
– 解析速度快,比对应的XML快约20-100倍;
– 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
怎么安装?
源码下载地址: https://github.com/google/protobuf
安装依赖的库: autoconf automake libtool curl make g++ unzip

安装:
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ sudo make install

编写proto文件

首先需要一个proto文件,其中定义了我们程序中需要处理的结构化数据:

// Filename: addressbook.proto
syntax="proto2";
package addressbook;

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

message Person {
    required string name = 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];
    }

    repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person_info = 1;
}

代码解释

// Filename: addressbook.proto 这一行是注释,语法类似于C++
syntax=”proto2″; 表明使用protobuf的编译器版本为v2,目前最新的版本为v3
package addressbook; 声明了一个包名,用来防止不同的消息类型命名冲突,类似于 namespace
import “src/help.proto”; 导入了一个外部proto文件中的定义,类似于C++中的 include 。
message 是Protobuf中的结构化数据,类似于C++中的类,可以在其中定义需要处理的数据
required string name = 1; 声明了一个名为name,数据类型为string的required字段,字段的标识号为1
protobuf一共有三个字段修饰符:
– required:该值是必须要设置的;
– optional :该字段可以有0个或1个值(不超过1个);
– repeated:该字段可以重复任意多次(包括0次),类似于C++中的list;
使用建议:除非确定某个字段一定会被设值,否则使用optional代替required。
string 是一种标量类型,protobuf的所有标量类型请参考文末的标量类型列表。
name 是字段名,1 是字段的标识号,在消息定义中,每个字段都有唯一的一个数字标识号,这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
标识号的范围在:1 ~ 229 – 1,其中[19000-19999]为Protobuf预留,不能使用。
Person 内部声明了一个enum和一个message,这类似于C++中的类内声明,Person外部的结构可以用 Person.PhoneType 的方式来使用PhoneType。当使用外部package中的结构时,要使用 pkgName.msgName.typeName 的格式,每两层之间使用’.’来连接,类似C++中的”::”。
optional PhoneType type = 2 [default = HOME]; 为type字段指定了一个默认值,当没有为type设值时,其值为HOME。
另外,一个proto文件中可以声明多个message,在编译的时候他们会被编译成为不同的类。

// Filename: addressbook.proto 这一行是注释,语法类似于C++
syntax=”proto2″; 表明使用protobuf的编译器版本为v2,目前最新的版本为v3
package addressbook; 声明了一个包名,用来防止不同的消息类型命名冲突,类似于 namespace
import “src/help.proto”; 导入了一个外部proto文件中的定义,类似于C++中的 include 。
message 是Protobuf中的结构化数据,类似于C++中的类,可以在其中定义需要处理的数据
required string name = 1; 声明了一个名为name,数据类型为string的required字段,字段的标识号为1
protobuf一共有三个字段修饰符:
– required:该值是必须要设置的;
– optional :该字段可以有0个或1个值(不超过1个);
– repeated:该字段可以重复任意多次(包括0次),类似于C++中的list;

生成C++文件

protoc是proto文件的编译器,目前可以将proto文件编译成C++、Java、Python三种代码文件,编译格式如下:
protoc -I=$SRC_DIR –cpp_out=$DST_DIR /path/to/file.proto
上面的命令会生成xxx.pb.h 和 xxx.pb.cc两个C++文件。
使用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("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;
}

常用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);  //从字节流解析
其他常用的函数:
1 bool IsInitialized();    //检查是否所有required字段都被设值
2 size_t ByteSize() const; //获取二进制字节序的大小

官方API文档地址:

编译生成可执行代码

编译格式和普通的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: aut
email: autyinjing@126.com
phone_type: 1
phone_number: 021-8888-8888
phone_type: 0
phone_number: 138-8888-8888
*****************************

怎么编码的?
protobuf之所以小且快,就是因为使用变长的编码规则,只保存有用的信息,节省了大量空间。
Base-128变长编码
– 每个字节使用低7位表示数字,除了最后一个字节,其他字节的最高位都设置为1;
– 采用Little-Endian字节序。
-数字1:
0000 0001
-数字300:
1010 1100 0000 0010
000 0010 010 1100
-> 000 0010 010 1100
-> 100101100
-> 256 + 32 + 8 + 4 = 300
ZigZag编码
Base-128变长编码会去掉整数前面那些没用的0,只保留低位的有效位,然而负数的补码表示有很多的1,所以protobuf先用ZigZag编码将所有的数值映射为无符号数,然后使用Base-128编码,ZigZag的编码规则如下:
(n << 1) ^ (n >> 31) or (n << 1) ^ (n >> 63)
负数右移后高位全变成1,再与左移一位后的值进行异或,就把高位那些无用的1全部变成0了,巧妙!
消息格式
每一个Protocol Buffers的Message包含一系列的字段(key/value),每个字段由字段头(key)和字段体(value)组成,字段头由一个变长32位整数表示,字段体由具体的数据结构和数据类型决定。
字段头格式:
(field_number << 3) | wire_type
-field_number:字段序号
-wire_type:字段编码类型
字段编码类型
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages(嵌套message), packed repeated fields
3 Start group groups (废弃)
4 End group groups (废弃)
5 32-bit fixed32, sfixed32, float
编码示例(下面的编码以16进制表示)
示例1(整数)
message Test1 {
required int32 a = 1;
}
a = 150 时编码如下
08 96 01
08: 1 << 3 | 0
96 01:
1001 0110 0000 0001
-> 001 0110 000 0001
-> 1001 0110
-> 150
示例2(字符串)
message Test2 {
required string b = 2;
}
b = “testing” 时编码如下
12 07 74 65 73 74 69 6e 67
12: 2 << 3 | 2
07: 字符串长度
74 65 73 74 69 6e 67
-> t e s t i n g
示例3(嵌套)
message Test3 {
required Test1 c = 3;
}
c.a = 150 时编码如下
1a 03 08 96 01
1a: 3 << 3 | 2
03: 嵌套结构长度
08 96 01
->Test1 { a = 150 }
示例4(可选字段)
message Test4 {
required int32 a = 1;
optional string b = 2;
}
a = 150, b不设值时编码如下
08 96 01
-> { a = 150 }
a = 150, b = “aut” 时编码如下
08 96 01 12 03 61 75 74
08 96 01 -> { a = 150 }
12: 2 << 3 | 2
03: 字符串长度
61 75 74
-> a u t
示例5(重复字段)
message Test5 {
required int32 a = 1;
repeated string b = 2;
}
a = 150, b = {“aut”, “honey”} 时编码如下
08 96 01 12 03 61 75 74 12 05 68 6f 6e 65 79
08 96 01 -> { a = 150 }
12: 2 << 3 | 2
03: strlen(“aut”)
61 75 74 -> a u t
12: 2 << 3 | 2
05: strlen(“honey”)
68 6f 6e 65 79 -> h o n e y
a = 150, b = “aut” 时编码如下
08 96 01 12 03 61 75 74
08 96 01 -> { a = 150 }
12: 2 << 3 | 2
03: strlen(“aut”)
61 75 74 -> a u t
示例6(字段顺序)
message Test6 {
required int32 a = 1;
required string b = 2;
}
a = 150, b = “aut” 时,无论a和b谁的声明在前面,编码都如下
08 96 01 12 03 61 75 74
08 96 01 -> { a = 150 }
12 03 61 75 74 -> { b = “aut” }制代码
还有什么?

编码风格

– 花括号的使用(参考上面的proto文件)
– 数据类型使用驼峰命名法:AddressBook, PhoneType
– 字段名小写并使用下划线连接:person_info, email_addr
– 枚举量使用大写并用下划线连接:FIRST_VALUE, SECOND_VALU
适用场景
“Protocol Buffers are not designed to handle large messages.”。protobuf对于1M以下的message有很高的效率,但是当message是大于1M的大块数据时,protobuf的表现不是很好,请合理使用。
总结:本文介绍了protobuf的基本使用方法和编码规则,还有很多内容尚未涉及,比如:反射机制、扩展、Oneof、RPC等等,更多内容需参考官方文档。

标量类型列表

proto类型 C++类型 备注
double double
float float
int32 int32 使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint32
int64 int64 使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint64
uint32 uint32 使用可变长编码
uint64 uint64 使用可变长编码
sint32 int32 使用可变长编码,有符号的整型值,编码时比通常的int32高效
sint64 int64 使用可变长编码,有符号的整型值,编码时比通常的int64高效
fixed32 uint32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效
fixed64 uint64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效
sfixed32 int32 总是4个字节
sfixed64 int64 总是8个字节
bool bool
string string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本
bytes string 可能包含任意顺序的字节数据

protobuf 文件级别优化

package IM.BaseDefine;
option java_package = “com.mogujie.tt.protobuf”;
option optimize_for = LITE_RUNTIME;
。。。。
option optimize_for = LITE_RUNTIME;
optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。
SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。
LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。
SPEED和LITE_RUNTIME相比,在于调试级别上,例如 msg.SerializeToString(&str) 在SPEED模式下会利用反射机制打印出详细字段和字段值,但是LITE_RUNTIME则仅仅打印字段值组成的字符串;
因此:可以在程序调试阶段使用 SPEED模式,而上线以后使用提升性能使用 LITE_RUNTIME 模式优化。
在C环境下使用

发表评论

3 × 1 =

此站点使用 Akismet 来减少垃圾评论。了解我们如何处理您的评论数据