Protobuf学习 - 入门

  古之立大事者,不唯有超世之才,亦必有坚忍不拔之志html

                         -- 苏轼·《晁错论》linux

 

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

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

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

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

 

2、怎么用?
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. 代码解释 curl

 // 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,在编译的时候他们会被编译成为不一样的类。ide

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 *****************************

 

3、怎么编码的?

  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 122 << 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 122 << 3 | 2
62 03: strlen("aut") 
63 61 75 74 -> a u t
64 122 << 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 122 << 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" }

 

4、还有什么?

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使用手册

(本文完)

相关文章
相关标签/搜索