1,Protobuf简介
-
protobuf是google提供的一个开源序列化框架,类似于XML,JSON这样的数据表示语言,其最大的特点是基于二进制,因此比传统的XML表示高效短小得多。虽然是二进制数据格式,但并没有因此变得复杂,开发人员通过按照一定的语法定义结构化的消息格式,然后送给命令行工具,工具将自动生成相关的类,可以支持php、java、c++、python等语言环境。通过将这些类包含在项目中,可以很轻松的调用相关方法来完成业务消息的序列化与反序列化工作。
-
protobuf在google中是一个比较核心的基础库,作为分布式运算涉及到大量的不同业务消息的传递,如何高效简洁的表示、操作这些业务消息在google这样的大规模应用中是至关重要的。而protobuf这样的库正好是在效率、数据大小、易用性之间取得了很好的平衡。
2,protobuf如何使用
我们先来说如何使用protobuf的,在来谈原理。
- 首先来装一个protobuf协议标准工具,作用:将.proto文件生成两个文件(.cc与.h),就是个类文件,提供了方法。
开源地址: https://github.com/protocolbuffers/protobuf
安装
git clone https://github.com/protocolbuffers/protobuf //下载protobuf
tar zxvf protobuf-cpp-3.8.0.tar.gz //解压
cd protobuf-3.8.0/
./configure
make
sudo make install
完成后,使用命令: protoc --version 查看版本,能够显示版本表示安装成功。
- 然后我们就编写 .proto 文件,这里写了个非常简单的 .proto 文件定义了个人信息:
syntax = "proto3"; // set proto syntax
//message --->表示一个信息类型
message Person {
string name=1; // =1后面的不是值,是tag,后面原理会讲到
int32 id=2;
string email=3;
enum PhoneType {
MOBILE=0;
HOME=1;
WORK=2;
}
message PhoneNumber {
string number=1;
PhoneType type=2 ;
}
repeated PhoneNumber phone=4;
}
- 1, 有如你所见,消息格式很简单,每个消息类型拥有一个或多个特定的数字字段,每个字段拥有一个名字和一个值类型; 注意:不需要对变量进行赋值。
- 2, 值类型可以是数字(整数或浮点)、布尔型、字符串、原始字节或者其他ProtocolBuffer类型,还允许数据结构的分级。你可以指定重复字段。
一个.proto文件可以定义多个消息类型,.proto文件的类型与生成的c++访问类的类型对照如下:
另:你可以在http://code.google.com/apis/protocolbuffers/docs/proto.html 找到更多关于如何编写 .proto 文件的信息。 - 一旦定义好了自己的报文格式(message),你就可以运行ProtocolBuffer编译器(见后面开源地址),将你的 .proto 文件编译成特定语言的类,命令如下
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR path/to/file.proto //替换成--java_out 将生成java使用的类
这里,我是用了一个脚本,格式跟上面一致,如下:
#!/bin/sh
# proto文件在哪里
SRC_DIR=./
# .h .cc输出到哪里
DST_DIR=../
#C++
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.proto
生成了两个文件.cc与.h文件(就是生成了类class)。这些类提供了简单的方法访问每个字段(像是 query() 和 set_query() ),像是访问类的方法一样将结构串行化或反串行化。例如你可以选择C++语言,运行编译如上的协议文件生成类叫做 Person 。随后你就可以在应用中使用这个类来串行化的读取报文信息。
假设服务端生成了序列化存储在output中,发送到客户端,客户端解析报文中的数据,如下
fstream input("myfile",ios::in | ios:binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
在此,就完成了简单的protobuf的实例。
我这里有另外一个实例,可以进行测试。github地址:https://github.com/Fang-create/protobuf-test.git
并且你可以在不影响向后兼容的情况下随意给数据结构增加字段,旧有的数据会忽略新的字段。所以如果使用ProtocolBuffer作为通信协议,你可以无须担心破坏现有代码的情况下扩展协议。
3,protobuf原理
首先,先看一下protobuf的数据定义结构,下面是一个例子。
syntax = "proto3"; // set proto syntax
message DemoRequest{
int32 valueInt32 = 1; //请求参数1
int64 valueInt64 = 2; //请求参数2
string valueString = 3; //请求参数3
}
3.1,protobuf的数据结构(序列化后的格式):
3.2,解析tag
- 第一位标记是否拓展下一字节,1代表拓展,0代表不拓展。
tag:默认情况下,tag占据4位,最多标识15以下,如果超过15,则拓展到下一个字节。
type:共有6种类型,用3位来标识足够了。
public static final int WIRETYPE_VARINT = 0;//数字类型
public static final int WIRETYPE_FIXED64 = 1;
public static final int WIRETYPE_LENGTH_DELIMITED = 2;//变长类型,可以理解为大多数场景下都是string类型
public static final int WIRETYPE_START_GROUP = 3;
public static final int WIRETYPE_END_GROUP = 4;
public static final int WIRETYPE_FIXED32 = 5;
- 举个例子,上文中的optional int32 valueInt32 = 1; //tag是1,类型是0(WIRETYPE_VARINT)。
- 首先长度足够没有拓展,那么第一位就是0
- 其次tag的值是1,那么2到5位就是00001
- 最后type的类型是0,那么最后6到8位就是000
- 最后合起来就是0 0001 000 = 8(十进制)
3.3,解析value,int类型
- value的字节当中,第一位标识是否需要拓展到下一个字节,1代表需要,0代表不需要。
- 如果标识的数据值小于2的7次方(小于128)时,则可以用一个字节标识,否则需要多个字节。
- 同样举个例子,上面demo中的
- optional int32 valueInt32 = 1; //tag = 1 0000 0000 后三位表示类型,宏为0,则为000,3~7为0001,为8
- 如果valueInt32=5(值)时,则value中8位的标识值为 0 0000101
- 则加上tag,完整的输出字节就是:00001000 00000101 (8,5)
- 如果valueInt32=1000时(大于128),则value包含两个字节,第一个字节标识int中的1到7位,则是:1 1101000 (-24)
- 第二个字节标识int中的8到14位,则是:0 0000111(7)
- 则加上tag完整的输出字节就是:00001000 11101000 00000111(8,-24,7)
- 说到这里,拓展一下,如果用json标识的话:
“valueInt32”:1000
长度为17,则需要17个字节来标识。
比例就是3:17。protobuf的优势就体现出来了。
3.4,解析value,string类型
- value当中标识string类型,参照int,则第一个字节标识的是字符串长度,同样的也是用7位来标识,不够的话拓展到下一个字节。 后面的字节标识就是具体的string的值了。
举个栗子,
optional string valueString = 3; //请求参数3
valueString ="suc哈"
长度则为3+3=6(protobuf中使用的UTF-8的编码格式,一个中文占3个字节)。对应的value就是
6,115,117,99,-27,-109,-120
6代表长度,115代表s,117代表u,99代表c,-27,-109,-120代表“哈”
tag=3,则3<<<3=24,加上type=2,则为26。
所以加上tag,就是:26,6,115,117,99,-27,-109,-120
3.5,解析value,boolean类型
- boolean类型在protobuf属于WIRETYPE_VARINT=0
value当中标识boolean类型时,使用0和1代替false和true。写法的方式和int类型一样。
所以如果一个字段,比如tag=1,定义为int类型,赋值为1的时候,序列化后为:8,1;
同样还是一个字段,比如tag=1,定义为boolean类型,赋值为true的时候,序列化后为:8,1。
遇到这种情况,即使序列化和反序列化定义不一致,也不会报错的。
3.6,解析value,model类型
- model类型数据在protobuf属于WIRETYPE_LENGTH_DELIMITED=2。
这种类型结构和上面的string类型一样,属于tag + length + body的。
所以只需要把body中的字节,写入成model序列化之后的byte数组就可以了。
3.7,解析value,Array类型
- 针对array类型,我们还是举一个例子,上面的
repeated int32 valueList = 5; //请求参数List
我们可以看一下生成的protobuf源码:
for (int i = 0; i < valueList_.size(); i++) {
output.writeInt32(5, valueList_.get(i));
}
其实就是写入多个相同tag的数据,每个tag都是5 —> 00101000 = 40
比如给valueList添加值10,11,12,13。则加上tag=5,序列化之后的数据为:
40,10,40,11,40,12,40,13
四、感想总结
-
总结一下,protobuf之所以相对于json能否节省传输数据,原因有如下几点:
1、针对int类型,采取了变长的传输方式。json传输int都是
2、不传输属性字段名(属性名长度多少就是几个字节),取而代替的是int类型的tag值(基本都是1个字节)。
3、不传输多余的字符,比如json中分割数据的{,"等等。
4、针对protobuf协议升级问题,增加字段后新旧协议如何处理问题总结,下次更新。------重要 -
值得学习的几点:
1、针对大概率的场景去做优化,而不用太在乎极限的场景。比如protobuf中极限情况下int会占据5个字节,要比json还要多。但是大概率的情况下,都是要比json节省的。
2、打破int类型就是4个字节的常规,使用变长的策略。
附上一个关于protobuf的实时通讯的栗子,可以参照学些。https://github.com/Fang-create/protobuf.git