GRPC学习之路(4)——protobuf编码过程解析

上一篇文章通过一个例子大致了解了protobuf的作用,我曾经打开那个存储对象编码后的文件,里面像是有一团乱码:

这篇文章主要研究protobuf是如何编码的,同时你也能感受到protobuf为什么更快更省带宽。

Base 128 Varints

在开始研究过程之前,必须先要了解VarintsVarints提供了一种办法能让一个或者多个字节代表整型变量,通常在java中一个int需要占用4个字节,即使数字1也需要4个字节,而使用Varints能用更少的字节代表比较小的数字,这样做的目的就是为了减少编码后使用的空间,毕竟整型很常用,使用Varints带来的提高还是很客观的。了解Varints的作用后,下面介绍一下它是怎么做到的。

比如数字149通过Varints编码后变成了

10010101 00000001

这里面有2个字节,在Varints中每个字节的第一个bit都是代表后面还有没有更多的字节,从上面的例子能看出,第一个字节首bit是1,代表后面还有字节,需要继续处理,第二个的首bit是0,代表到此为止后面没有字节了。

还有一个需要注意的是,Varints采用的是 least significant group first, 网上没有找到合适的翻译,其实就是它在表示整型变量时将字节顺序反过来存储,比如上面这个例子:

10010101 00000001  -> 0010101 0000001  //去掉首bit

0010101 0000001 ->00000010010101  //反转顺序

00000010010101换算成10进制就是149

编码消息包含哪些元素

假设现在有一个消息实体定义为如下:

message Message {
   int32 a = 1;
}

上面的Message里只有一个整型变量a,  tag为1. 如果现在将一个a为149的Message写入文件,然后从文件中按照字节读出刚刚写入的Message内容:

00001000 10010101 00000001

同样的149和上面介绍的相比,多了一个字节00001000, 这个是什么作用呢?

protobuf 的消息编码都是按照多个key-value对来存储的,既然上面的149是value, 那多出来的字节肯定就是key了,而在protobuf中一个key包含2部分

  1. field number 也就是上面所说的tag,在这个例子中也就是1
  2. wire type通俗的讲就是类型名称

常用的wire type如下,这个例子中我们的wire type是0

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

而在protobuf中key的计算方式是 key = (field_number << 3) | wire_type, 也就是 1<<3 | 0 即 0001000

你可以理解为key中的后三位就是wire type

实战

准备有两个字段的Message,一个整型一个字符串类型

syntax = "proto3";
package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "TestMessage";
message Message {
   int32 a = 1;
   string query = 2;
}

通过前面文章提到的maven插件生成对象代码TestMessage.java,  新建一个对象并写入文件中:

TestMessage.Message.Builder message  =  TestMessage.Message.newBuilder();
message.setA(149);
message.setQuery("zack");
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream("testmessage.txt");
message.build().writeTo(output);
output.close();

然后从上面的文件中读出字节流:

  File file = new File(fileName);
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            int tempbyte;
            while ((tempbyte = in.read()) != -1) {
                System.out.print(Integer.toBinaryString(tempbyte)+" ");
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

得出的结果如下:

1000 10010101 1 10010 100 1111010 1100001 1100011 1101011

确实挺长,我们可以一点一点解刨,前3个字节和上面的例子一样1000 10010101 1,就是149的整型变量,那来看看后面的6个字节:

10010 100 1111010 1100001 1100011 1101011

  • 第一个字节 10010:后三位010即wire type是2, 吻合;  剩下的2个字节10即field_number, 也吻合
  • 第二个字节100: 由于这个wire type是2,因此这个字节代表的是后面的字节有几个,也就是4个字符,一个字符占用一个字节,而我们代码里存的是zack, 确实是4个字节
  • 剩下的4个字节毫无疑问就是zack四个字符的Ascii的值, 注意这里不是用Varints那种方式去解析,还记得Varints的应用范围吗?它是用来表示整型变量的,这里是字符。

 

总结

现在回想一下整个过程,你是否发现protobuf在使用尽量少的字节去表达尽量多的含义,包括减少整型变量的空间占用以及在表示变量类型和field_num时只使用少的字节数,同样的消息,编码占用的空间越少,则它传输也就越快。

至此,我们研究了protobuf是如何将一个对象的属性编码成一个字节流的过程,如果你还想知道其他类型的字段是如何编码的,可以参考protobuf的官网,这里就不细讲了,原理都是差不多的。

另附文章中提到的工程文件:

Proto3Tutorial

欢迎关注我的个人的博客www.zhijianliu.cn, 虚心求教,有错误还请指正轻拍,谢谢

版权声明:本文出自志健的原创文章,未经博主允许不得转载

 

展开阅读全文

没有更多推荐了,返回首页