Protobuf,从入门到穿拖鞋
来源:互联网 发布:怎么下载word2013软件 编辑:程序博客网 时间:2024/04/28 23:05
从公司的项目源码中看到了这个东西,觉得挺好用的,特为大家总结。下面的操作以C++为编程语言,protoc的版本为libprotoc 3.2.0。
一、Protobuf 概述
1. Protocol是什么?
Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。
2. 为什么要用Protocol?
- 平台无关,语言无关,可扩展;
- 提供了友好的动态库,使用简单;
- 解析速度快,比对应的XML快约20-100倍;
- 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
3. 怎么安装Protocol?
源码下载地址:
https://github.com/google/protobuf
安装依赖的库:
autoconf automake libtool curl make g++ unzip
安装:
$ ./autogen.sh$ ./configure$ make$ make check$ sudo make install
二、Protobuf 怎么用?
1. 编写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;}
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三种代码文件,编译格式如下:
protoc -I=$SRC_DIR --cpp_out=$DST_DIR /path/to/file.proto
上面的命令会生成xxx.pb.h 和 xxx.pb.cc两个C++文件。
4. 使用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;}
5. 常用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; //获取二进制字节序的大小
6. 编译生成可执行代码
编译格式和普通的C++代码一样,但要加上 -lprotobuf -pthread
g++ main.cc xxx.pb.cc -I $INCLUDE_PATH -L $LIB_PATH -lprotobuf -pthread
7. 输出结果
before clear(), id = 1219after clear(), id = 0*****************************id: 1087name: autemail: autyinjing@126.comphone_type: 1phone_number: 021-8888-8888phone_type: 0phone_number: 138-8888-8888*****************************
三、怎么编码的?
protobuf之所以小且快,就是因为使用变长的编码规则,只保存有用的信息,节省了大量空间。
1. Base-128变长编码
- 每个字节使用低7位表示数字,除了最后一个字节,其他字节的最高位都设置为1;
- 采用Little-Endian字节序。
示例:
-数字1:
0000 0001
-数字300:
1010 1100 0000 001
0000 0010 010 1100
-> 000 0010 010 1100
-> 100101100
-> 256 + 32 + 8 + 4 = 300
2. ZigZag编码
Base-128变长编码会去掉整数前面那些没用的0,只保留低位的有效位,然而负数的补码表示有很多的1,所以protobuf先用ZigZag编码将所有的数值映射为无符号数,然后使用Base-128编码,ZigZag的编码规则如下:
(n<<1)^(n>>31) or (n<<1)^(n>>63)
负数右移后高位全变成1,再与左移一位后的值进行异或,就把高位那些无用的1全部变成0了,巧妙!
3. 消息格式
每一个Protocol Buffers的Message包含一系列的字段(key/value),每个字段由字段头(key)和字段体(value)组成,字段头由一个变长32位整数表示,字段体由具体的数据结构和数据类型决定。
字段头格式:
(field_number << 3) | wire_type-field_number:字段序号-wire_type:字段编码类型
4. 字段编码类型
TypeMeaningUsed For0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum164-bitfixed64, sfixed64, double2Length-delimitedstring, bytes, embedded messages(嵌套message), packed repeated fields3Start groupgroups (废弃) 4End groupgroups (废弃)532-bitfixed32, sfixed32, float
5. 编码示例(下面的编码以16进制表示)
示例1(整数)message Test1 { required int32 a = 1;}a = 150 时编码如下08 96 0108: 1 << 3 | 096 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 6712: 2 << 3 | 207: 字符串长度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 011a: 3 << 3 | 203: 嵌套结构长度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 7408 96 01 -> { a = 150 }12: 2 << 3 | 203: 字符串长度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 7908 96 01 -> { a = 150 }12: 2 << 3 | 203: strlen("aut") 61 75 74 -> a u t12: 2 << 3 | 205: strlen("honey")68 6f 6e 65 79 -> h o n e ya = 150, b = "aut" 时编码如下08 96 01 12 03 61 75 7408 96 01 -> { a = 150 }12: 2 << 3 | 203: 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 7408 96 01 -> { a = 150 }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++类型备注doubledoublefloatfloat
int32int32使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint32int64int64使用可变长编码,编码负数时不够高效——如果字段可能含有负数,请使用sint64uint32uint32使用可变长编码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,从入门到穿拖鞋
- 夏天需注意 切不可穿拖鞋驾驶以防危险
- 夏天需注意 切不可穿拖鞋驾驶以防危险
- SpringMVC中利用json从后台穿数据到前台
- 今天上班穿了一只拖鞋和一只凉鞋
- Protobuf入门
- protobuf入门
- Protobuf 入门
- protobuf入门
- protobuf入门
- ProtoBuf入门
- protobuf入门
- protobuf入门
- 从入门到精通
- MVP从入门到...
- python从入门到..........
- scala 从入门到入门+
- Maven从入门到入门
- Qt GUI程序中主线程与非主线程之间的通信
- jsonview浏览器插件 查看格式化json数据
- redis位图法统计活跃用户+统计3天的连续登录用户案例
- C++学习之路C++Primer学习笔记(二)如何选择类型
- JRebel for Android 破解
- Protobuf,从入门到穿拖鞋
- kamailio 启动错误
- 逃避苹果审核,进行强制更新
- 双向可控硅的工作原理
- 我们对待人生的态度才能够100%的影响我们的生活
- 转载:scikit-learn学习之SVM算法
- 基于thinkphp5+Avant模板的后台管理系统
- python中map()函数的用法讲解
- Android Studio3.0,卡编译的解决办法...