Google Protocol Buffer原理

来源:互联网 发布:知乎登陆过于频繁 编辑:程序博客网 时间:2024/05/16 12:07

GoogleProtocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。

Protobuf在我们大学学习的七层网络中应该是处于表示层。Protobuf 有如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构(这一块本篇文章不介绍,后续进行深入了解)。只需使用Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

首先我们得下载protocol.exe工具类(生成java类或者C++ python),建一个proto文件,如下

 message Person {   required string name = 1;   required int32 id = 2;        // Unique ID number for this person.   optional string email = 3;   enum PhoneType {     MOBILE = 0;     HOME = 1;     WORK = 2;   }   message PhoneNumber {     required string number = 1;     optional PhoneType type = 2 [default = HOME];   }   repeated PhoneNumber phone = 4;  }
2.使用命令行生成具体的实体类。命令为:protoc.exe --java_out=./ ***.proto(注意/后面是一个空格),生成的类就不列出来了,代码太多。

3.序列化

package proto;import java.io.*;/** * Created by ** on 2016/10/9. */public class ProtocTest {    public static void main(String[] args) throws IOException {        // 按照定义的数据结构,创建一个Person        PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();        personBuilder.setId(1);        personBuilder.setName("xxg");        personBuilder.setEmail("xxg@163.com");        PersonMsg.Person xxg = personBuilder.build();        // 将数据写到输出流,如网络输出流,这里就用ByteArrayOutputStream来代替        ByteArrayOutputStream output = new ByteArrayOutputStream();        xxg.writeTo(output);        byte[] byteArray = output.toByteArray();        System.out.println(byte2hex(byteArray));    }    /**     * byte to hex 16 convert.    * */    private static String byte2hex(byte [] buffer){        String h = "";        for(int i = 0; i < buffer.length; i++){            String temp = Integer.toHexString(buffer[i] & 0xFF);            if(temp.length() == 1){                temp = "0" + temp;            }            h = h + " "+ temp;        }        return h;    }}

得到的结果为:

0a 03 78 78 67 10 01 1a 0b 78 78 67 40 31 36 33 2e 63 6f 6d

我们来解释下这些序列化之后的字符含义。

因为Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。考察消息结构之前,让我首先要介绍一个叫做 Varint 的术语。Varint 是一种紧凑的表示数字的方法(类似我们数据库中的varchar的意思,变长的int类型)。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。下面就详细介绍一下 Varint。Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian (大部分机器都是小端法计算)的方式。

消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:

采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。

则最终的 Message Buffer 中有两个 Key-Value 对,一个对应消息中的 id;另一个对应 str。

Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field。

Key 的定义如下:

 (field_number << 3) | wire_type
可以看到 Key 由两部分组成。第一部分是 field_number,比如消息 lm.helloworld 中 field id 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型。.

Wire Type 可能的类型如下表所示:

那我们现在解释下我们刚才运行的结果:0a 03 78 78 67 10 01 1a 0b 78 78 67 40 31 36 33 2e 63 6f 6d

第一个字节是0a 解析成二进制的结果:00001010-----------后面三位是010十进制2表示的是值类型:string,然后前面00001表示的是id值我们知道name=1,所以这个值是指name的值,我们知道2类型是需要长度标识的,所以后面的03就是字符串长度3位78 78 67,78换算成十进制120,然后我们对应到ascii码表为x,同理后面的推算都是如此,然后我们看看10 对应的二进制为:00010000,所以是int类型并且对应的2(id=2),值就是01


0 0
原创粉丝点击