1、什么是protobuf
protobuf全称Protocol Buffers,谷歌数据交换的一种格式,以二进制格式进行存储,独立于语言,平台无关,可扩展的机制,可序列化。
2、为什么要使用protobuffer
protobuffer占用内存少,传输速率比传统的xml和json数据交换格式快,那么快在哪里呢?下面就protobuffer和json数据格式进行比较:
2.1 运行环境:pc机 window7 & 64位操作系统 8g内存
2.2数据格式准备
① protobuf定义的数据格式(student.proto)
option java_package = "com.bestjike.protobuf";
option java_outer_classname = "BestJkProtoBuf";
message Teacher {
repeated Student student = 1;
}
message Student {
optional string name = 1;
optional int32 age = 2;
optional string addr = 3;
optional string qq = 4;
optional string phone = 5;
optional string nation = 6;
optional string idcard = 7;
}
②json要定义的数据格式
{"teacher":[{"name":"hello0","age":23,"addr":"上海","qq":"126479327","phone":"15900007030","nation":"汉族","idcard":"621323199310021234"}]}
2.3.测试对比
分别是用100,1000,10000,100000数据进行测试,测试的效果是从空间上和时间去做对比。
2.3.1. protobuf测试过程:
①cmd窗口输入以下命令生成protobuf代码
D:\item\protobuf\proto>protoc.exe --java_out=./ student.proto
②测试代码如下
package com.bestjike.protobuf;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class ProtoBufTest {
public static void main(String [] args) throws FileNotFoundException, IOException{
long start = System.currentTimeMillis();
int size = 100000;
List<BestJkProtoBuf.Student> list = new ArrayList<BestJkProtoBuf.Student>();
for (int i = 0; i < size; i++) {
BestJkProtoBuf.Student.Builder builder = BestJkProtoBuf.Student.newBuilder();
builder.setName("hello"+i);
builder.setAge(23);
builder.setAddr("上海");
builder.setQq("126479327");
builder.setPhone("15900007030");
builder.setNation("汉族");
builder.setIdcard("621323199310021234");
BestJkProtoBuf.Student student = builder.build();
list.add(student);
}
File filePath = new File("D:\\proto"+size);
if(!filePath.exists()){
filePath.createNewFile();
}
OutputStream stream = new FileOutputStream(filePath);
BestJkProtoBuf.Teacher.Builder teaBuilder = BestJkProtoBuf.Teacher.newBuilder();
teaBuilder.addAllStudent(list);
BestJkProtoBuf.Teacher teacher = teaBuilder.build();
teacher.writeTo(stream);
System.out.println("写入文件的时间:"+(System.currentTimeMillis() - start));
//开始对数据进行解析
start = System.currentTimeMillis();
byte [] buffer = teacher.toByteArray();
teacher = BestJkProtoBuf.Teacher.parseFrom(buffer);
for (int i = 0; i < teacher.getStudentCount(); i++) {
System.out.println(teacher.getStudent(i).getName());
}
System.out.println("解析时间:"+(System.currentTimeMillis() - start));
}
}
2.3.2 json测试过程
package com.bestjike.protobuf;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
public class JsonTest {
public static void main(String [] args) throws IOException {
long start = System.currentTimeMillis();
int size = 100000;
List<Student> list = new ArrayList<Student>();
for (int i = 0; i < size; i++) {
Student student = new Student();
student.setName("hello"+i);
student.setAge(23);
student.setAddr("上海");
student.setQq("126479327");
student.setPhone("15900007030");
student.setNation("汉族");
student.setIdcard("621323199310021234");
list.add(student);
}
File filePath = new File("D:\\json"+size);
if(!filePath.exists()){
filePath.createNewFile();
}
PrintWriter stream = new PrintWriter(filePath);
Teacher teacher = new Teacher();
teacher.setStudentList(list);
String result = Utils.toJson(teacher);
stream.write(result);
System.out.println(result);
stream.close();
System.out.println("时间:"+(System.currentTimeMillis() - start));
start = System.currentTimeMillis();
//解析json
JSONObject studentList = JSONObject.fromObject(result);
Object objectTea = studentList.get("teacher");
JSONArray student = JSONArray.fromObject(objectTea);
int length = student.size();
for (int i = 0; i < length; i++) {
JSONObject object = JSONObject.fromObject(student.get(i));
Student stu = (Student) JSONObject.toBean(object, Student.class);
System.out.println(stu.getName());
}
System.out.println("解析时间:"+(System.currentTimeMillis() - start));
}
}
2.3.3测试结果
数据占用空间:
单位KB | 100 | 1000 | 10000 | 100000 |
protobuf | 8KB | 73KB | 732KB | 7412KB |
json | 13KB | 128KB | 1288KB | 12978KB |
解析数据时间:
单位毫秒MS | 100 | 1000 | 10000 | 100000 |
protobuf | 4MS | 28MS | 151MS | 1111MS |
json | 115MS | 298MS | 2762MS | 240332MS |
2.3.4 结果说明
测试次数超过10遍以上,以上的数值属于平均值,只是做了下大略的比较,没有考虑到最优算法,只是最平常的思维,从数据上看,无论是从空间上还是时间上protobuf是优于json的,从程序的编写程度,protobuf并没有很复杂,但是仅仅依靠上面的程序不能说明json怎么样,json解析工具也有多款:JackJson,Gson等,json通用性比较好,这一点上protobuf不能比拟的,只能说protobuf更适合适用存储,适合上面举的例子,但以上例子实际情况下是很少见的。
3. protobuf的深入分析
3.1 protobuf消息的定义message
protobuf有自己的消息定义,为.proto文件中的多个message,message中的字段filed都有修饰符,数据类型,字段名,tag以及选项。
修饰符 数据类型 字段名 tag 选项
optional int32 age= 1 [default=10];
3.1.1 修饰符
required:必须给定值,同时接受方能解析该字段值。(proto3已抛弃)
optional:可选值,没有该值不会出现在序列化后的数据中的。
repeated:和optional类似,只是它类似于Java中的list包含多个元素的值。
3.1.2 数据类型
protobuf 数据类型 | 描述 | 字节 (N不固定变长) | Java语言映射 |
bool | 布尔类型 | 1字节 | boolean |
double | 64位浮点数 | N | double |
float | 32为浮点数 | N | float |
int32 | 32位整数、 | N | int |
uint32 | 无符号32位整数 | N | int |
int64 | 64位整数 | N | long |
uint64 | 64为无符号整 | N | long |
sint32 | 32位整数,处理负数效率更高 | N | int |
sint64 | 64位整数 处理负数效率更高 | N | int |
fixed32 | 32位无符号整数 | 4 | int |
fixed64 | 64位无符号整数 | 8 | long |
sfixed32 | 32位整数、能以更高的效率处理负数 | 4 | int |
sfixed64 | 64为整数 | 8 | long |
string | 只能处理 ASCII字符 | N | String |
bytes | 用于处理多字节的语言字符、如中文 | N | ByteString |
enum | 可以包含一个用户自定义的枚举类型uint32 | N | enum |
message | 可以包含一个用户自定义的消息类型 | N | class |
比如:对枚举类型的字段可以这样
enum Content {
head = 0;
body = 1;
tail = 2;
}
3.1.3 字段名
protobuf建议带下划线的命名方式:mobile_phone
3.1.4 tag值
序列化字段值时将会按tag值从小到大进行存储。
3.1.5选项
比如设置默认值:
optional int32 age = 1 [default=20];
常用选项:java_package,java_outer_classname,optimize_for,packed,deprecated
3.2 字段的存储
当字段值被序列化后,数据以二进制的形式进行存储,序列化的代码如下:
BestJkProtoBuf.Teacher.Builder teaBuilder = BestJkProtoBuf.Teacher.newBuilder();
BestJkProtoBuf.Teacher teacher = teaBuilder.build();
byte [] buffer = teacher.toByteArray();
序列化的字段值是以<key1,value1>,<key2,value2>的形式存在
3.3 字段值的编码
序列化后的值以key+value的形式,这里主要谈到key的编码,序列化的后的数据也有它的数据类型wire type,wire type有六种:
Wire Type | Message的对应的数据类型 |
---|---|
0 | Varint:int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit :fixed64, sfixed64, double |
2 | Length-delimited :string, bytes, messages, packed repeated |
3 | Start group groups (废弃) |
4 | End group groups (废弃) |
5 | 32-bit: fixed32, sfixed32, float |
key = tag << 3 | wire_type 低3位用来表示wire type,剩下的5位是tag的值。
(1)varint
varint为可变的整数类型,而int类型一般4个字节,下面说下varint的存储数据的原理,举个例子
optional int age = 1 [default=10];
第一行为key,第二行value值
0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
每一行左边的第一位最高位为标志位,为1时,表示value不止一个字节,为0时,表示value只需要一个字节,长度是由每个字节左边第一位最高位来决定的,不难看出,当tag>16时,key需要2个字节,每一个字节只用到7个位来存储值,相对于int类型的,当值大于2的28方减1为268435455时,需要5个字节来存储,因此当数字比较的时候,varint便不适合了。
同时会发现varint表示负数会很不方便,protobuf提供了sint32和sint64,varint对这两个进行编码,ZigZag先将对应的负数转换成对应的正数,而ZigZag编码规则如下:
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2 | 4 |
...... | ..... |
比如-3的存储
0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
(2)64-bit 和 32-bit
由于varint对较大的数字存储并不合适,因此64-bit和32-bit编码弥补了varint的缺陷,它们是定长的。比如存储2的28方的值268435456
0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
(3)Length-delimited
length-delimited编码将value的值的长度也存储着,下面string为例,存储“best”字串时
0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 |
0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 |
0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 |
第一行:key
第二行:value的长度5
第三行:b
第四行:e
第五行:s
第六行:t
4.收尾
当以repeated修饰字段时,会涉及多个字段的值,因此设置选项[packed = true],只会存储一个key,否则的话每个值都会有一个key,proto3以上的版本已经优化,不需要考虑。这里不讲解proto编译器根据.proto文件如何生成对应的protobuf代码的,有兴趣的自己可以研究下。当然,这里主要说明protobuf用于java中开发,protobuf产生当然是为了解决某一类问题而产生的,protobuf是由Google开发的,最初protobuf被用来处理索引服务器的请求/响应协议,当时手动编组和解组比较繁琐,涉及到不同的协议。现在protobuf大部分应用在他们的RPC系统和数据存储系统