[转] Protobuf高效结构化数据存储格式

 从公司的项目源码中看到了这个东西,觉得挺好用的,写篇博客做下小总结。下面的操作以C++为编程语言,protoc的版本为libprotoc 3.2.0。

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

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

3. 怎么安装? 
  源码下载地址: https://github.com/google/protobuf 
  安装依赖的库: autoconf automake libtool curl make g++ unzip  
安装:

1 $ ./autogen.sh
2 $ ./configure
3 $ make
4 $ make check
5 $ sudo make install

 

二、怎么用? 
1. 编写proto文件 
  首先需要一个proto文件,其中定义了我们程序中需要处理的结构化数据:

 1 // Filename: addressbook.proto
 2 
 3 syntax="proto2";
 4 package addressbook;
 5 
 6 import "src/help.proto";      //举例用,编译时去掉
 7 
 8 message Person {
 9     required string name = 1;
10     required int32 id = 2;
11     optional string email = 3;
12 
13     enum PhoneType {
14         MOBILE = 0;
15         HOME = 1;
16         WORK = 2;
17     }
18 
19     message PhoneNumber {
20         required string number = 1;
21         optional PhoneType type = 2 [default = HOME];
22     }
23 
24     repeated PhoneNumber phone = 4;
25 }
26 
27 message AddressBook {
28     repeated Person person_info = 1;
29 }

2. 代码解释

 // Filename: addressbook.proto 这一行是注释,语法类似于C++ 
 syntax="proto2"; 表明使用protobuf的编译器版本为v2,目前最新的版本为v3 
 package addressbook; 声明了一个包名,用来防止不同的消息类型命名冲突,类似于 namespace 
 import "src/help.proto";  导入了一个外部proto文件中的定义,类似于C++中的 include 。不过好像只能import当前目录及当前目录的子目录中的proto文件,比如import父目录中的文件时编译会报错(Import "../xxxx.proto" was not found or had errors.),使用绝对路径也不行,尚不清楚原因,官方文档说使用 -I=PATH 或者 --proto_path=PATH 来指定import目录,但实际实验结果表明这两种方式指定的是将要编译的proto文件所在的目录,而不是import的文件所在的目录。(哪位大神若清楚还请不吝赐教!) 
 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,在编译的时候他们会被编译成为不同的类。

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

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

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

4. 使用C++文件

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

 1 #include <iostream>
 2 #include "addressbook.pb.h"
 3 
 4 int main(int argc, const char* argv[])
 5 {
 6     addressbook::AddressBook person;
 7     addressbook::Person* pi = person.add_person_info();
 8 
 9     pi->set_name("aut");
10     pi->set_id(1219);
11     std::cout << "before clear(), id = " << pi->id() << std::endl;
12     pi->clear_id();
13     std::cout << "after  clear(), id = " << pi->id() << std::endl;
14     pi->set_id(1087);
15     if (!pi->has_email())
16         pi->set_email("autyinjing@126.com");
17 
18     addressbook::Person::PhoneNumber* pn = pi->add_phone();
19     pn->set_number("021-8888-8888");
20     pn = pi->add_phone();
21     pn->set_number("138-8888-8888");
22     pn->set_type(addressbook::Person::MOBILE);
23 
24     uint32_t size = person.ByteSize();
25     unsigned char byteArray[size];
26     person.SerializeToArray(byteArray, size);
27 
28     addressbook::AddressBook help_person;
29     help_person.ParseFromArray(byteArray, size);
30     addressbook::Person help_pi = help_person.person_info(0);
31 
32     std::cout << "*****************************" << std::endl;
33     std::cout << "id:    " << help_pi.id() << std::endl;
34     std::cout << "name:  " << help_pi.name() << std::endl;
35     std::cout << "email: " << help_pi.email() << std::endl;
36 
37     for (int i = 0; i < help_pi.phone_size(); ++i)
38     {
39         auto help_pn = help_pi.mutable_phone(i);
40         std::cout << "phone_type: " << help_pn->type() << std::endl;
41         std::cout << "phone_number: " << help_pn->number() << std::endl;
42     }
43     std::cout << "*****************************" << std::endl;
44 
45     return 0;
46 } 

5. 常用API

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

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

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

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

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

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

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

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

其他常用的函数:

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

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

6. 编译生成可执行代码

  编译格式和普通的C++代码一样,但是要加上 -lprotobuf -pthread 

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

7. 输出结果

 1 before clear(), id = 1219
 2 after  clear(), id = 0
 3 *****************************
 4 id:   1087
 5 name: aut
 6 email: autyinjing@126.com
 7 phone_type: 1
 8 phone_number: 021-8888-8888
 9 phone_type: 0
10 phone_number: 138-8888-8888
11 *****************************

 

三、怎么编码的?

  protobuf之所以小且快,就是因为使用变长的编码规则,只保存有用的信息,节省了大量空间。
1. Base-128变长编码
  - 每个字节使用低7位表示数字,除了最后一个字节,其他字节的最高位都设置为1;
  - 采用Little-Endian字节序。

示例:

1 -数字1:
2 0000 0001
3 
4 -数字300:
5 1010 1100 0000 0010
6 000 0010 010 1100
7 -> 000 0010 010 1100
8 -> 100101100
9 -> 256 + 32 + 8 + 4 = 300

2. ZigZag编码

  Base-128变长编码会去掉整数前面那些没用的0,只保留低位的有效位,然而负数的补码表示有很多的1,所以protobuf先用ZigZag编码将所有的数值映射为无符号数,然后使用Base-128编码,ZigZag的编码规则如下:

1 (n << 1) ^ (n >> 31) or (n << 1) ^ (n >> 63)

负数右移后高位全变成1,再与左移一位后的值进行异或,就把高位那些无用的1全部变成0了,巧妙!

3. 消息格式

  每一个Protocol Buffers的Message包含一系列的字段(key/value),每个字段由字段头(key)和字段体(value)组成,字段头由一个变长32位整数表示,字段体由具体的数据结构和数据类型决定。 
字段头格式:

1 (field_number << 3) | wire_type
2 -field_number:字段序号
3 -wire_type:字段编码类型

4. 字段编码类型

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

 

   
 
 
 



 

 

 5. 编码示例(下面的编码以16进制表示)

 1 示例1(整数)
 2 message Test1 {
 3     required int32 a = 1;
 4 }
 5 a = 150 时编码如下
 6 08 96 01
 7 08: 1 << 3 | 0
 8 96 01:
 9 1001 0110 0000 0001
10 -> 001 0110 000 0001
11 -> 1001 0110
12 -> 150
13 
14 示例2(字符串)
15 message Test2 {
16     required string b = 2;
17 }
18 b = "testing" 时编码如下
19 12 07 74 65 73 74 69 6e 67
20 12: 2 << 3 | 2
21 07: 字符串长度
22 74 65 73 74 69 6e 67
23 -> t e s t i n g
24 
25 示例3(嵌套)
26 message Test3 {
27     required Test1 c = 3;
28 }
29 c.a = 150 时编码如下
30 1a 03 08 96 01
31 1a: 3 << 3 | 2
32 03: 嵌套结构长度
33 08 96 01
34 ->Test1 { a = 150 }
35 
36 示例4(可选字段)
37 message Test4 {
38     required int32 a = 1;
39     optional string b = 2;
40 }
41 a = 150, b不设值时编码如下
42 08 96 01
43 -> { a = 150 }
44 
45 a = 150, b = "aut" 时编码如下
46 08 96 01 12 03 61 75 74
47 08 96 01 -> { a = 150 }
48 12: 2 << 3 | 2
49 03: 字符串长度
50 61 75 74
51 -> a u t
52 
53 示例5(重复字段)
54 message Test5 {
55     required int32 a = 1;
56     repeated string b = 2;
57 }
58 a = 150, b = {"aut", "honey"} 时编码如下
59 08 96 01 12 03 61 75 74 12 05 68 6f 6e 65 79
60 08 96 01 -> { a = 150 }
61 12: 2 << 3 | 2
62 03: strlen("aut") 
63 61 75 74 -> a u t
64 12: 2 << 3 | 2
65 05: strlen("honey")
66 68 6f 6e 65 79 -> h o n e y
67 
68 a = 150, b = "aut" 时编码如下
69 08 96 01 12 03 61 75 74
70 08 96 01 -> { a = 150 }
71 12: 2 << 3 | 2
72 03: strlen("aut") 
73 61 75 74 -> a u t
74 
75 示例6(字段顺序)
76 message Test6 {
77     required int32 a = 1;
78     required string b = 2;
79 }
80 a = 150, b = "aut" 时,无论a和b谁的声明在前面,编码都如下
81 08 96 01 12 03 61 75 74
82 08 96 01 -> { a = 150 }
83 12 03 61 75 74 -> { b = "aut" }

 

四、还有什么?

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等等,更多内容需参考官方文档。

 

标量类型列表

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 可能包含任意顺序的字节数据


 

 

 

 

 

 

 

 

 

 

 

 

  

参考资料 
1. Protocol Buffers Developer Guide

2. Google Protocol Buffer 的使用和原理

3. 浅谈几种序列化协议

4. 序列化和反序列化

5. Protobuf使用手册

 

原文链接:

https://www.cnblogs.com/autyinjing/p/6495103.html

posted on 2018-11-05 15:40  Johnny9  阅读(3220)  评论(0编辑  收藏  举报

导航