Protocol Buffer编码方式

[toc]

本文翻译自: https://developers.google.com/protocol-buffers/docs/encoding

注:1、本文并非逐字逐句翻译,仅仅按照原文结构,以及知识点进行翻译,文章某些顺序以及描述方式将会被本人修改。

2、阅读本文之前需要对protocol buffer有一定认识,参见protocol buffer语法

主要介绍protocol buffer的二进制编码格式。在使用protocol buffer的时候这些可以不用理会,但是这些知识会让我们了解protocol buffer的编码格式是如何影响到我们编码之后的文件大小。

一个简单的Message

现在用一个最简单的Message举例:

message Test1{
    required int32 a = 1;
}

现在我们在一个应用程序中将a设置为150。然后利用输出流序列化这些信息。当我们打开这个序列化文件的时候可以看到一共利用3个bytes表示的这个Message:

08 96 01

这时如何编码的呢?我们将稍后展示。

基于128 Varints(整型变量)

想要了解protocol buffer的编码方式,我们首先要了解什么是varints,varints是一种将整数用1个或者多个bytes表示的一种序列化方法。越小的数使用越少的bytes。

除了最后一个byte之外,每一个varint中的byte的MSB(most significant bit)都是置位(set)的,这表示还有后续byte。低7位以二进制补码(the two’s complement)的形式表示这个数。

如:数字1,仅用1个byte表示即可。所以MSB没有置位。

0000 0001

如果1个byte不够,仅仅将MSB置位是不够的,还需要知道这几个bytes之间的关系,比如7bits仅仅可以表示0~127(这里考虑非负数)。则我们需要2个bytes来表示128。那么这2个byte到底哪一个表示高位哪一个表示低位呢?

protocol buffer将这个问题描述为一个LSB群(least significant group)的问题。protocol的做法是,least significant group first。这个如何理解呢?我看看一个例子。

现在我们表示数字300,这将比1更复杂:

1010 1100 0000 0010

我们主要关注两点:

  • 首先,由于有了多个bytes因此,除了最后一个以外其余的MSB均要置位。
  • 其次,least significant group first
    010 100 000 0010//我们去掉MSB得到
    000 0010 010 100//将左右调换
    10010100//拼接(为了方便观察,删除了前面的0),这个就是300
    

    least significant group first机制相比都能领悟到:其实就是将数字的二进制补码的每7位分为一组, 低7位先输出,编码在前面,在输出下一组,依次类推。

Message结构

本小节将描述protocol如何将Message类型等信息进行编码的。

protocol的Message实际上就是一系列的key-value pair。Message的二进制形式其实是使用域的数字作为key,这个域的名字以及类型需要在译码端通过引用该Message的定义来确定。

当一个Message进行编码时,key(注意,这里是tag number)和对应的值级联输出。在译码端译码时候,解析器需要跳过那些不能识别的域(嘿嘿,靠什么识别?)。这样,我们在添加新域的时候也不会影响旧代码的使用。事实上,key保存着两个值。其一,是.proto文件中定义的tag。其二,就是线型(wire type),它是用来提供下一个值长度的信息。

可用的线型:

类型 意义 用途
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 group开始 group(已经弃用)
4 group结束 group(已经弃用)
5 32-bit fixed32, sfixed32, float

我们可以看出其实每一个key都编码成另一种整型变量,要将最后的3位用来保存线型,因此要做如下操作:

(field_number << 3) | wire_type。

现在我们再来看一个简单的例子,我们现在都知道第一个数字是Key的一个整型变量,我们先表示一个数08(去掉MSB)

000 1000

从这个数字的低三位我们取出它的线型(0),然后向右边移动三位可以得到它的域tag(1)。所以现在我们知道tag为1,接下来的数是一个varint。接下来我们就可以利用上一节中介绍的varint译码方法进行译码。拿出我们最开始的例子:

96 01 = 1001 0110  0000 0001
   → 000 0001  ++  001 0110 (丢弃MSB,按照7bits进行反转)
   → 10010110
   → 2 + 4 + 16 + 128 = 150

可以看出存储的数据是150。

可见 08 96 01表示的是保存了一个1=150的键值对。既没有保存类型,也没有保存名字。

其他类型

有符号整数(signed integers)

从前几节我们了解到,线型为0的所有protocol buffer类型都会按照varints进行编码。然而,对于有符号类型(sint32和sint64)同“标准的”整型(int32 和int64)在编码负数时有着很大差异。如果用int32或者int64编码负数,那么就会按照长度为10bytes的varint方式进行编码,因为对于编码而言,把负数按照一个大的整数来处理更有效。如果我们使用有符号整数来对负数进行编码,那么它就会按照ZigZag方式进行,这会更加有效。

ZigZag将一个有符号数映射成为一个无符号数,这个无符号数是该有符号数的绝对值形式,因此,ZigZag的varint编码长度会更加短。这种编码方式是按照“锯齿状”往返于正数和负数之间,即

 0->0
-1->1
 1->2
-2->3
 2->4

很容易看出规律,编码之前的序列是在整数和负数之间对称跳转,编码之后的序列是在正半轴递增。

我们也就可以利用一个很简单的编码方式来进行编码:

对于sint32:

(n << 1) ^ (n >> 31)

对于sint64:

(n << 1) ^ (n >> 61)

如何理解这个编码代码呢?我们通过’^’将其分为左边部分和右边部分。从映射关系可以看出,我们将(0,-1)映射到[0,1],(1,-2)映射到[2,3],相当于把正半轴的数每一个变为原来两倍,这样整个空间看起来“扩大”了一倍,因此有足够的空间存放负数。将对于绝对值n,2n+1表示-n,而2n表示n。

左边部分就是绝对值扩大一倍,而右边部分是数学右移(最高位补原来的值),判断n的最高位是不是1。

若n是负数,由于异或的关系,左边部分和右边部分的前导1会全部清零。变为非负数,最低位根据n的最高位是否为1(即n是否是负数来确定),中间部分则是将补码变为原码(思考一下这时为什么)。

在解析sint32或者sint64时,就会按照上述过程的逆过程变为原来有符号数(通过译码端引用.proto文件知道是否使用ZigZag方式)。

非整型变量

非整型变量十分简单,double和fixed64的线型是1,在解析是这个会告诉解析器需要提取一块64bits的数据。类似的float和fixed32的线型是5,这告诉解析器需要提取一块32bits数据。这些数据都是按照小端方式存放的。

string

线型2(长度确定),这个表示会将string编码成为长度+特定字符串形式。先按照varint方式编码字符串长度(n bytes),后面紧接着n个bytes的数据。

message Test2{
    required string b = 2;
}

我们现在将b的值设定为”testing”

那么就会得到如下序列:

12 07 74 65 73 74 69 6e 67

红色的序列就是UTF8编码的”testing”,这里的Key是 0x12→tag=2, type=2。后面紧跟的数表示序列长度,为7。

嵌套的Message

这里我们利用本文开始处定义的test1来定义test3:

message Test3{
    required Test1 c =3;
}

下面是编码之后的序列,同样的,我们也将Test1的域a设置为150。那么我们就得到这个序列:

1a 03 08 96 01

可以看出此时编码序列的最后三个bytes和我们第一个例编码出来的相同(08 96 01)。通过对1a和03可知,嵌套的Message编码方式是和string一致的(如何区分嵌套Message和string?,也要靠.proto)。

optional和repeated元素

proto2编码一个repeated域(没有[packed=true]选项)。编码之后的序列有0个或者多个key-value对,并且使用的是同一个tag。这些repeated域中的值不用连续出现,它们可以交错出现在其它域之间。解析时,这些元素相互之间的顺序是会保存下来,但是相对于其他域的顺序将会丢失(丢就丢咯~)。而proto3会利用打包编码(packed encoding)的方式对repeated域进行编码,之后会介绍。

在proto3的非repeated域以及proto2的optional域可能会出现对于一个tag没有key-value对的情况。

通常对于一个非repeated域不会出现多于一个实例。但是,解析器是被设计为可以处理多于一个实例的情况。

  • 对于数字以及string类型:如果一个域的实例出现多次,那么解析结果是解析器看到的最后一个值。
  • 对于嵌套Message域:如果一个域的实例出现多次,那么解析器将会“合并”这些域的实例。就像在执行Message::MergeFrom方法一样。所有的标准域都会被后来的覆盖,对于一个嵌套Message来说也会进行“合并”,对于repeated域来说,多出的实例会级联起来。所以解析两个级联的Message和独立解析两个Message然后将其“融合”得到的结果是一致的。如下:
Message message;
message.ParseFromString(str1 + str2);

上面的代码和下面是等价的:

MyMessage message,message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

这一个性质有的时候挺有用的,我们可以I在不知道他们具体类型的情况下合并两个Message

packed repeated域

protocol buffer在2.1.0版本引入了packed repeated域,这个域在proto2中的声明仅仅需要在repeated域后面添加[packed=true]。在proto3里面repeated域会默认声明为packed。经过这个声明之后的repeated域编码方式与proto2中的不同(更有效了)。如果packed repeated域中没有元素,那么它将不会被编码。否则,其中所有的元素将会统一打包编码为一个线型为2(长度确定)的key-value对。其中的每一个元素都将按照正常方式在这个包中编码(但是编码中不会出现tag)。

例如:

message Test4{
    repeated int32 d=4 [packed=true];
}

假设我们已经构造了一个Test4并且,设置repeated域d的值为3,270和86942。然后proto将会编码成为如下格式:

22          //tag(域d的tag序列号为4,线型为2),注意这个是16进制数
06          //表示接下来的长度(bytes)
03          //第一个数(varint 3)
8E 02       //第二个数(varint 270)
9E A7 05    //第三个数(varin 86942)

当且仅当repeated域中的元素是数字变量(varint,32-bit或者64-bit)的时候才可以声明为packed。

虽然packed repeated域通常不会编码出超过一个key-value对。但是编码器也要准备好接收多个key-value对。这种情况下有效的域应该被级联。每一个对都必须包含所有的元素。

当一个repeated域没有被声明为packed,但是是按照packed方式编译的,那么protocol解析器也要求可以解析这个域,反之亦然。这就保证了[packed=true]选项的前后向兼容性。

域的顺序

在protocol提供的C++,JAVA,PYTHON序列化代码中,当一个Message进行序列化时,可知的域是按照tag顺序序列化写入的,这些tag序号是我们在.proto文件中定义的。这允许解析器基于tag序列进行一些优化。但是protocol解析器是要满足,可以按照任意顺序解析的要求。因为不是所有的Message都是序列化一个对象,如,有时我们会通过一个简单的级联方式“合并”两个Message。

如果一个Message有一个“不可知”域,那么java和C++应用将会在顺序的域后边,按照不确定的顺序写入。

时间: 2024-10-27 19:07:38

Protocol Buffer编码方式的相关文章

protocol buffer 编码

protocol buffer能够跨平台提供轻量的序列化和反序列化,得益于其平台无关的编码格式,本文就介绍下其中的编码格式. Varints 在protocol buffer中大量使用到了Varints的编码格式,这是一个可变长度的编码格式用于编码整形数字.Varint的最小单位是byte,即8位,每byte第一位(msb)是标志位用于标记是否还有后续byte. ===1=== 0000 0001 ===300=== 1010 1100 0000 0010 上面300的例子首先读入第一个字节发现

快来看看Google出品的Protocol Buffer,别仅仅会用Json和XML了

前言 习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 事实上 是 Google出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json.XML 真的强!太! 多! 由于 Google出品,我相信Protocol Buffer已经具备足够的吸引力 今天,我将献上一份 Protocol Buffer的介绍 & 使用攻略,希望你们会喜欢. 文件夹 1. 定义 一种 结构化数据 的数据存储格式(相似于 `XML.J

快来看看Google出品的Protocol Buffer,别只会用Json和XML了

前言 习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json.XML 真的强!太!多! 由于 Google出品,我相信Protocol Buffer已经具备足够的吸引力 今天,我将献上一份 Protocol Buffer的介绍 & 使用攻略,希望你们会喜欢. 目录 1. 定义 一种 结构化数据 的数据存储格式(类似于 `XML.Json

Google protocol buffer 使用和原理浅析 - 附带进阶使用方式

Protocol Buffer ??Google Protocol Buffer又简称Protobuf,它是一种很高效的结构化数据存储格式,一般用于结构化数据的串行化,简单说就是我们常说的数据序列化.这种序列化的协议非常轻便高效,而且是跨平台的,目前已支持多种主流语言(3.0版本支持C++, JAVA, C#, OC, GO, PYTHON等). ??通过这种方式序列化得到的二进制流数据比传统的XML, JSON等方式的结果都占用更小的空间,并且其解析效率也更高,用于通讯协议或数据存储领域是非常

Protocol Buffer技术详解(语言规范)

Protocol Buffer技术详解(语言规范) 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的主要在于不仅可以保持Google文档的良好风格和系统性,同时再结合一些比较实用和通用的用例,这样就更加便于公司内部的培训,以及和广大网友的技术交流.需要说明的是,Blog的内容并非line by line的翻译,其中包含一些经验性总结,与此同时,对于一些不是非常常用的功能并未予以说明,有兴趣的开发者

序列化笔记之一:Google的Protocol Buffer格式分析

从公开介绍来看,ProtocolBuffer(PB)是google 的一种数据交换的格式,它独立于语言,独立于平台.作为一个学了多年通信的人,ProtocolBuffer在我看来是一种信源编码.所谓信源编码,就是将待传输的信源符号经过某种变换,转换成码流进行传输的这个变换过程.信源编码可分为两类:有损编码与无损编码,PB自然是属于无损编码,在无损编码中,又分为定长编码和变长编码,定长编码就是一个符号变换后的码字的比特长度是固定的,比如ASCII.Unicode都是定长编码,码字是8比特,16比特

【Google Protocol Buffer】Google Protocol Buffer

http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/ Google Protocol Buffer 的使用和原理 Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式.它可用于通讯协议.数据存储等领域的语言无关.平台无关.可扩展的序列化结构数据格式.目前提供了 C++.Java.Python 三种语言的 API. 17 评论: 刘 明, 软件工程师, 上海交大电

Protocol Buffer基本介绍

转自:http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.html 该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo.这样做的目的主要在于不仅可以保持Google文档的良好风格和系统性,同时再结合一些比较实用和通用的用例,这样就更加便于公司内部的培训,以及和广大网友的技术交流.需要说明的是,Blog的内容并非line by line的翻

Protocol Buffer详解

1.Protocol Buffer 概念 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件.他们用于 RPC 系统和持续数据存储系统. Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化.它很适合做数据存储或 RPC 数据交换格式.可用于通讯协议.数据存储等领域的语言无关