google protocol buffer全解析(二)

1,182 阅读6分钟

第一部分,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文件,最终文件结构如下图

img

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

/**
 * 测试不同模型间的转换
 * @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());
}

输出结果如下

tera cn

可以看到,虽然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小