这一系列文章主要是对protocol buffer这种编码格式的使用方式、特点、使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务。

在上一篇文章中,我们展示了protobuf在java中的基本使用方式。而本文将继续深入探究protobuf的编码原理。

主要分为两个部分

第一部分是结合上一篇文章留下的几个伏笔展示protobuf的使用特性

第二部分是分析protobuf的编码原理,解释特性背后的原因

第一部分,Protobuf使用特性

1.不同类型对象的转换

我们先定义如下一个.proto文件

syntax = "proto3";

option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "DifferentModels"; message Person {
string name = 1;
int32 id = 2;
string email = 3;
} message Article {
string title = 1;
int32 wordsCount = 2;
string author = 3;
}

其中我们定义了2个模型,一个Person,一个Article,虽然他们的字段名字不相同,但是类型和编号都是一致的

接着我们生成.java文件,最终文件结构如下图

此时我们尝试做如下的一个转换

/**
* 测试不同模型间的转换
* @throws Exception
*/
@Test
public void parseDifferentModelsTest() throws Exception {
//创建一个Person对象
DifferentModels.Person person = DifferentModels.Person.newBuilder()
.setName("person name")
.setId(1)
.setEmail("tera@google.com")
.build();
//对person编码
byte[] personBytes = person.toByteArray();
//将编码后的数据直接merge成Article对象
DifferentModels.Article article = DifferentModels.Article.parseFrom(personBytes);
System.out.println("article's title:" + article.getTitle());
System.out.println("article's wordsCount:" + article.getWordsCount());
System.out.println("article's author:" + article.getAuthor());
}

输出结果如下

article's title:person name
article's wordsCount:1
article's author:tera@google.com

可以看到,虽然jsonBytes是由person对象编码得到的,但是可以用于article对象的解码,不但不会报错,所有的数据内容都是完整保留的

这种兼容性的前提是模型中所定义的字段类型和序号都是一一对应相同的

在平时的编码中,我们经常会遇到从数据库中读取数据模型,然后将其转换成业务模型,而很多时候,这2种模型的内容其实是完全一致的,此时我们也许就可以使用protobuf的这种特性,就可以省去很多低效的赋值代码

2.protobuf序号的重要性

在上一篇文章中,我们看到在定义.proto文件时,字段后面会跟着一个"= X",这里并不是指这个字段的值,而是表示这个字段的“序号”,和正确地编码与解码息息相关,在我看来是protocol buffer的灵魂

我们定义如下的.proto文件,这里注意,Model1和Model2的name和id的序号有不同

syntax = "proto3";

option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "TagImportance"; message Model1 {
string name = 1;
int32 id = 2;
string email = 3;
} message Model2 {
string name = 2;
int32 id = 1;
string email = 3;
}

定义如下的测试方法

/**
* 序号的重要性测试
*
* @throws Exception
*/
@Test
public void tagImportanceTest() throws Exception {
TagImportance.Model1 model1 = TagImportance.Model1.newBuilder()
.setEmail("model1@google.com")
.setId(1)
.setName("model1")
.build();
TagImportance.Model2 model2 = TagImportance.Model2.parseFrom(model1.toByteArray());
System.out.println("model2 email:" + model2.getEmail());
System.out.println("model2 id:" + model2.getId());
System.out.println("model2 name:" + model2.getName());
System.out.println("-------model2 数据---------");
System.out.println(model2);
}

输出结果如下

model2 email:model1@google.com
model2 id:0
model2 name:
-------model2 数据---------
email: "model1@google.com"
1: "model1"
2: 1

可以看到,虽然Model1和Model2定义的字段类型和名字都是相同的,然而name和id的序号颠倒了一下,导致最终model2在解析byte数组时,无法正确将数据解析到对应的字段上,所以输出的id为0,而name字段为null

不过即使字段无法一一对应,但在输出model2.toString()时,我们依然可以看到数据是被解析到了,只不过无法对应到具体字段,只能用1,2来表示其字段名

3.protobuf序号对编码结果大小的影响

protobuf的序号不仅影响编码、解码的正确性,一定程度上还会影响编码结果的字节数

我们在上面的.proto文件中增加一个Model3,其中Model3中定义的字段没有变化,但是序号更改为16,17,18

syntax = "proto3";

option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "TagImportance"; message Model1 {
string name = 1;
int32 id = 2;
string email = 3;
} message Model2 {
string name = 2;
int32 id = 1;
string email = 3;
} message Model3 {
string name = 16;
int32 id = 17;
string email = 18;
}

测试方法

/**
* 序号对编码大小的影响
*
* @throws Exception
*/
@Test
public void tagSizeInfluenceTest() throws Exception {
TagImportance.Model1 model1 = TagImportance.Model1.newBuilder()
.setEmail("model1@google.com")
.setId(1)
.setName("model1")
.build();
System.out.println("model1 编码大小:" + model1.toByteArray().length); TagImportance.Model3 model3 = TagImportance.Model3.newBuilder()
.setEmail("model1@google.com")
.setId(1)
.setName("model1")
.build();
System.out.println("model3 编码大小:" + model3.toByteArray().length);
}

输出结果如下

model1 编码大小:29
model3 编码大小:32

可以看到,在数据量完全相同的情况下,编号偏大的对象编码的结果也会偏大

4.模型字段数据类型兼容性

在上一篇文章中我在getName()方法中提到了灵活性,接下去就展示一下该特性

我们定义如下的.proto文件

syntax = "proto3";

option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "ModelTypeCompatible"; message OldPerson {
string name = 1;
int32 id = 2;
string email = 3;
} message NewPerson {
Name name = 1;
int32 id = 2;
string email = 3;
} message Name {
string first = 1;
string last = 2;
int32 usedYears = 3;
}

其中定义了2个Person对象

在OldPerson中,name是一个纯String

在NewPerson中,name字段则被定义为了一个对象

此时我们做如下的操作

/**
* 模型字段不同类型的兼容性
*
* @throws Exception
*/
@Test
public void typeCompatibleTest() throws Exception {
ModelTypeCompatible.NewPerson newPerson = ModelTypeCompatible.NewPerson.newBuilder()
.setName(ModelTypeCompatible.Name.newBuilder()
.setFirst("tera")
.setLast("cn")
.setUsedYears(10)
).setId(5)
.setEmail("tera@google.com")
.build();
ModelTypeCompatible.OldPerson oldPerson = ModelTypeCompatible.OldPerson.parseFrom(newPerson.toByteArray());
System.out.println(oldPerson.getName());
}

输出结果如下

teracn

可以看到,虽然NewPerson的name字段是一个对象,但是却可以被成功地转换成OldPerson的String类型的name字段,虽然其中的usedYears字段被舍弃了

这种兼容性的前提是从对象类型向String类型转换,而反向是不可以的

5.protobuf与json之间的转换和对比

json是现在应用最为广泛的数据结构之一,因此当我们决定使用protobuf时,不可避免的问题就是它和json的兼容性

因此接下去我们看下protobuf和json之间是如何转换的

我们先构造一个简单的java类

public class PersonJson {
public String name;
public int id;
public String email;
}

重复利用前一篇文章中生成的protobuf模型BasicUsage.Person,以及前文就引入的json相关的maven,我们测试如下方法

/**
* json和protobuf的互相转换
*/
@Test
void jsonToProtobuf() throws Exception {
//构造简单的模型
PersonJson model = new PersonJson();
model.email = "personJson@google.com";
model.id = 1;
model.name = "personJson";
String json = JSON.toJSONString(model);
System.out.println("原始json");
System.out.println("------------------------");
System.out.println(json);
System.out.println(); //parser
JsonFormat.Parser parser = JsonFormat.parser();
//需要build才能转换
BasicUsage.Person.Builder personBuilder = BasicUsage.Person.newBuilder();
//将json字符串转换成protobuf模型,并打印
parser.merge(json, personBuilder);
BasicUsage.Person person = personBuilder.build();
//需要注意的是,protobuf的toString方法并不会自动转换成json,而是以更简单的方式呈现,所以一般没法直接用
System.out.println("protobuf内容");
System.out.println("------------------------");
System.out.println(person.toString()); //修改protobuf模型中的字段,并再转换会json字符串
person = person.toBuilder().setName("protobuf").setId(2).build();
String buftoJson = JsonFormat.printer().print(person);
System.out.println("protobuf修改过数据后的json");
System.out.println("------------------------");
System.out.println(buftoJson);
}

输出结果如下

原始json
------------------------
{"email":"personJson@google.com","id":1,"name":"personJson"} protobuf内容
------------------------
name: "personJson"
id: 1
email: "personJson@google.com" protobuf修改过数据后的json
------------------------
{
"name": "protobuf",
"id": 2,
"email": "personJson@google.com"
}

可以看到json和protobuf是可以做到完全兼容的互相转换

此时我们就可以比较一下,相容的数据内容经过json和protobuf分别编码后的数据字节大小,我们就使用上面的数据内容,做如下的测试

/**
* json和protobuf的编码数据大小
*/
@Test
void codeSizeJsonVsProtobuf() throws Exception {
//构造简单的模型
PersonJson model = new PersonJson();
model.email = "personJson@google.com";
model.id = 1;
model.name = "personJson";
String json = JSON.toJSONString(model);
System.out.println("原始json");
System.out.println("------------------------");
System.out.println(json);
System.out.println("json编码后的字节数:" + json.getBytes("utf-8").length + "\n"); //parser
JsonFormat.Parser parser = JsonFormat.parser();
//需要build才能转换
BasicUsage.Person.Builder personBuilder = BasicUsage.Person.newBuilder();
//将json字符串转换成protobuf模型,并打印
parser.merge(json, personBuilder);
BasicUsage.Person person = personBuilder.build();
//需要注意的是,protobuf的toString方法并不会自动转换成json,而是以更简单的方式呈现,所以一般没法直接用
System.out.println("protobuf内容");
System.out.println("------------------------");
System.out.println(person.toString());
System.out.println("protobuf编码后的字节数:" + person.toByteArray().length);
}

输出内容如下

原始json
------------------------
{"email":"personJson@google.com","id":1,"name":"personJson"}
json编码后的字节数:60 protobuf内容
------------------------
name: "personJson"
id: 1
email: "personJson@google.com" protobuf编码后的字节数:37

可以看到,相同的数据内容,protobuf编码的结果是json编码结果的60%左右(当然这个数值是会随着数据内容的不同浮动)

这里先总结一下之前的特性

1.protobuf的解码不需要类型相同,也不需要字段名相同

2.protobuf的解码依赖于序号的正确性

3.protobuf中的序号大小会影响最终编码大小

4.protobuf的对象类型可以向String类型兼容

5.protobuf可以和json完全兼容,且编码大小要比json小

第二部分,Protobuf编码原理

首先,我们需要了解一种最基本的编码方式varints(原文档的单词,没有找到特别准确的翻译,所以就就保留英文),这是一种用1个或多个字节对Integer进行编码的方法

当一个Integer采用这种方式编码后,除了最后一个字节,每一个字节的最高位都是1,而最后一个字节的最高位则是0,从而在解码的时候可以通过判断最高位的值来确定是否已经解码到了最后一个字节。

每一个字节除了最高位的其他7个bit则用来存放数字本身的编码

例如300,编码后得到2个字节,红色表示最高位bit,蓝色表示数字本身编码

010 1100  000 0010

其中第一个字节最高位bit为1,表示后面还有字节需要一并进行解码。第二个字节最高位bit为0,则表示已经到达最后一个字节了

解码时

1.去掉2个字节的最高位

010 1100  000 0010

2.反转2个字节的顺序

000 0010  010 1100

3.连接2个字节,构成了300的二进制形式

100101100

接着我们来看一个实际的例子,编码一个Person对象,只给里面的id字段赋值

/**
* varint数字编码
*/
@Test
void varintTest() {
BasicUsage.Person person = BasicUsage.Person.newBuilder()
.setId(91809)
.build();
Utility.printByte(person.toByteArray());
}

输出的编码结果如下

16    -95    -51    5
00010000 10100001 11001101 00000101

其中黄色部分即是91809的varints编码,我们来验证一下

红色表示最高位,蓝色表示数字本身编码,在读取该部分字节的时候是一个一个读取的

读取到第一个字节时,发现最高位是1,因此会继续读取第二个字节,第二个字节最高位也是1,因此继续读取第三个字节,而第三个字节最高位为0,从而结束读取,就处理这3个字节

   

1.去掉3个字节的最高位

0100001 1001101 0000101 

2.反转3个字节的顺序

0000101 1001101 0100001

3.连接3个字节,构成了91809的二进制形式

10110011010100001

接着我们看person编码结果的第一个字节

16    -95    -51    5
00010000 10100001 11001101 00000101

这个字节表示的是数据的序号类型,编码方式也是varient,因此我将其分为3个部分

000

红色0为最高位bit,表示是否解析到了本次varient的最后一个字节

中间蓝色的4个bit 0010表示序号,十进制2,即id的序号

最后3个黄色底的0为该字段的类型,000表示int32类型

此时一个最简单的protobuf的编码就解析完成了

到这里我们先总结一下protobuf编码的性质,将特别抽象的的内容转换成一个我们可以直觉理解的东西

先看原始数据,如果用json表示出来就是如下形式

{
"id": 91890
}

而protobuf编码后的数据格式如下

00010000 10100001 11001101 00000101

其中第一个字节表示序号和字段类型,即序号为2,类型为int的字段

后三个字节表示数据的值,值为91890

这时候就会有这样一个问题,那id这个字段名去哪儿了?

答案就是,id的字段名被protobuf舍弃了!

所以,protobuf最终的编码结果是抛弃了所有的字段名,仅仅保留了字段的序号、类型和数据的值。

因此在第一篇文章的开头,就提到protobuf并非是一种可以完全自解释的编码格式,意思就是如此。

也正因为如此,所以我也认为这个序号正是protobuf编码的灵魂所在

有了这个概念之后,我们就可以解释之前5个示例了

示例1:protobuf的解码不需要类型相同,也不需要字段名相同

因为protobuf编码后的结果根本就不包含类的信息,也不包含字段名的信息,因此解码的时候自然也就不依赖于类和字段名

示例2:protobuf的解码依赖于序号的正确性

因为编码后的结果的序号和类型是在同一个字节中,是一一对应的关系,如果编码的对应关系和解码的对应关系不同,则自然编码和解码的过程会出问题

示例3:protobuf中的序号大小会影响最终编码大小

我们前面看到序号和字段类型的字节结构如下,表示序号的部分是中间的4个bit,0010

000

而4个bit所能表示的最大数是1111,也就是15,因此当序号大于15的时候,一个字节就不够表达了,就需要额外一个字节,例如序号为17,类型为int的字段,它的序号字节就会如下

000 

其中黄色底的000表示类型Int,去除后,剩下的bit通过标准的varient解码后,得到的结果就是17

因此,如果序号超过15,那么就会多需要一个字节来表示序号。回过头看示例3,model3编码结果正好比model1编码结果多3个字节,正是3个字段的序号导致的

示例4:protobuf的对象类型可以向String类型兼容

上面提到了int的类型在字节中的bit表示是000,那么接下去我么可以看下其他类型对应的bit表示

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, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

这里可以看到,0就是表示int32,表达方式是varient

而2则可以表示string、embedded messages等,而这里的embedded messages对应的就是子对象

既然类型的表示是相同的,那么在解码的时候自然就是可以从embeedded messages向string兼容

然而由于messages的结构是要比string复杂的,因此反向是无法兼容的

其实这个更广域和普世来说,总是复杂信息可以向简单信息转换,而反向一般是不可行的

示例5:protobuf可以和json完全兼容,且编码大小要比json小

兼容性是由java类库实现的,这个不在编码原理的范畴内,这里主要看下编码大小比json小的原因

例如示例中的json

{"email":"personJson@google.com","id":1,"name":"personJson"}

json的编码后,为了保证格式的正确和自解释的功能,其中还包含了很多格式字符,包括{  "  ,  }等,还包括了email、id、name字段名本身

而protobuf编码后,则仅仅保留了序号、类型,以及字段的值,没有任何其他额外的符号,因此就比json节省了很多字节数

那么protobuf的编码原理基础就先了解到这里,下一篇文章将继续解释其他protobuf类型的编码原理

最后总结下本文内容,通过5个示例展示了protobuf在使用上的一些特性,并通过基本的编码原理解释了特性的本质原因

特性有以下5点

1.protobuf的解码不需要类型相同,也不需要字段名相同

2.protobuf的解码依赖于序号的正确性

3.protobuf中的序号大小会影响最终编码大小

4.protobuf的对象类型可以向String类型兼容

5.protobuf可以和json完全兼容,且编码大小要比json小

google protocol buffer——protobuf的使用特性及编码原理的更多相关文章

  1. google protocol buffer——protobuf的编码原理二

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们主要通 ...

  2. google protocol buffer——protobuf的基本使用和模型分析

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 1.什么是protobuf ...

  3. google protocol buffer——protobuf的问题及改进一

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们完整了 ...

  4. google protocol buffer——protobuf的问题和改进2

    这一系列文章主要是对protocol buffer这种编码格式的使用方式.特点.使用技巧进行说明,并在原生protobuf的基础上进行扩展和优化,使得它能更好地为我们服务. 在上一篇文章中,我们举例了 ...

  5. Google Protocol Buffer

    Google Protocol Buffer(protobuf)是一种高效且格式可扩展的编码结构化数据的方法.和JSON不同,protobuf支持混合二进制数据,它还有先进的和可扩展的模式支持.pro ...

  6. Google Protocol Buffer 的编码方式

    Google Protocol Buffer 使用到了两种编码方式:Varints 和 zigzag. 一 Varints 编码 每个 byte 只用 7bit 表示数字,最高位 bit作为标志位,如 ...

  7. Google Protocol Buffer 的使用和原理[转]

    本文转自: http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/ Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构 ...

  8. Google Protocol Buffer的安装与.proto文件的定义

    什么是protocol Buffer呢? Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准. 我理解的就是:它是一种轻便高效的结构 ...

  9. Google Protocol Buffer 的使用和原理

    Google Protocol Buffer 的使用和原理 Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式.它 ...

随机推荐

  1. ASP.NET Core 监听SQL Server数据库的实时信息

    1.开发环境: 开发工具:Visual Studio 2019 数据库:SQL Server2012 开发环境:.Net Core 3.1 2.使用技术: Signalr:实现消息推送 SqlDepe ...

  2. PHP 超级全局变量讲解

    PHP 超级全局变量 超级全局变量在PHP 4.1.0之后被启用, 是PHP系统中自带的变量,在一个脚本的全部作用域中都可用. PHP 超级全局变量 PHP中预定义了几个超级全局变量(superglo ...

  3. PHP sin() 函数

    实例 返回不同数的正弦: <?php高佣联盟 www.cgewang.comecho(sin(3) . "<br>");echo(sin(-3) . " ...

  4. 4.13 省选模拟赛 传销组织 bitset 强连通分量 分块

    考试的时候昏了头 没算空间 这道题我爆零了.值得注意的是 一般认为bitset的空间是 int 的1/w倍 对于那m条边 无论如何构造 这m条关系都是存在的 题目其实是想让我们用这m条关系来计算给出的 ...

  5. 为什么 2020 还要学 Node.js

    更佳阅读体验 https://www.yuque.com/sunluyong/node 前言 前些日子刷知乎看到个 2019 年初的问题 2019年nodejs凉了吗?凉到什么程度了?才看到问题的时候 ...

  6. [转]Java 逃逸分析

    作者:栈长  公众号:Java技术栈 记得几年前有一次栈长去面试,问到了这么一个问题:Java中的对象都是在堆中分配吗?说明为什么! 当时我被问得一脸蒙逼,瞬间被秒杀得体无完肤,当时我压根就不知道他在 ...

  7. 无版号的ios手游下架 TF签名才是正确选择?

    2020年8月1日开始,无版号的ios手游就要全部下架appstore了,在这样关键的时刻,TF签名成了ios手游的最后一根救命稻草.因为被下架或者根本无法通过appstore的上架审核,ios手游的 ...

  8. Druid数据源的使用

    1 Druid数据源简介 Druid是Java语言中最好的数据库连接池.Druid能够提供强大的监控和扩展功能.通过访问http://localhost:8080(自己的端口)/druid/ 可以查看 ...

  9. 从零搭建Spring Boot脚手架(3):集成mybatis

    1. 前言 今天继续搭建我们的kono Spring Boot脚手架,上一文集成了一些基础的功能,比如统一返回体.统一异常处理.快速类型转换.参数校验等常用必备功能,并编写了一些单元测试进行验证,今天 ...

  10. 学习Hibernate5 JPA这一篇就够了

    配套资料,免费下载 链接:https://pan.baidu.com/s/158JkczSacADez-fEMDXSYQ 提取码:2ce1 复制这段内容后打开百度网盘手机App,操作更方便哦 第一章 ...