protocol buffer的高效编码方式

简介

protocol buffer这种优秀的编码方式,究竟底层是怎么工作的呢?为什么它可以实现高效快速的数据传输呢?这一切都要从它的编码方式说起。

定义一个简单的message

我们知道protocol buffer的主体就是message,接下来我们从一个简单的message出发,详细讲解protobuf中的编码方式。

比如下面的一个非常简单的消息对象:

message Student {
  optional int32 age = 1;
}

在上面的例子中,我们定义了一个Student消息对象,并给他定义了一个名叫age的字段,并给它设置一个值叫做22。然后使用protobuf将其进行序列化,这么大的一个对象,对其序列化之后的字节如下所示:

08 96 00

很简单,使用三个字节就可以表示一个messag对象,数据量非常小。

那么这三个字节到底表示什么意思呢?一起来看看吧 。

Base 128 Varints

在解释上面的三个字节的含义之前,我们需要了解一个varints的概念。

什么叫Varints呢?就是序列化整数的时候,占用的空间大小是不一样的,小的整数占用的空间小,大的整数占用的空间大,这样不用固定一个具体的长度,可以减少数据的长度,但是会带来解析的复杂度。

那么怎么知道这个数据到底需要几个byte呢?在protobuf中,每个byte的最高位是一个判断位,如果这个位被置位1,则表示后面一个byte和该byte是一起的,表示同一个数,如果这个位被置位0,则表示后面一个byte和该byte没有关系,数据到这个byte就结束了。

举个例子,一个byte是8位,如果表示的是整数1,那么可以用下面的byte来表示:

0000 0001

如果一个byte装不下的整数,那么就需要使用多个byte来进行连接操作,比如下面的数据表示的是300:

1010 1100 0000 0010

为什么是300呢?首先看第一个byte,它的首位是1,表示后面还有一个byte。再看第二个byte,它的首位是0,表示到此就结束了。我们把判断位去掉,变成下面的数字:

010 1100 000 0010

这时候还不能计算数据的值,因为在protobuf中,byte的位数是反过来的,所以我们需要把上面的两个byte交换一下位置:

000 0010 010 1100 

也就是:

10 010 1100 

=256 + 32 + 8 + 4 = 300

消息体的结构

从message的定义可以知道,protobuf中的消息体的结构是key=value的形式,其中的key就是message中定义的字段的整数值1,2,3,4等。而value就是真正对其设置的值。

当一个消息被编码之后,这些key和value会被连接在一起,组成一个byte stream。当要对其进行解析的时候,需要定位到key和value的具体长度,所以在key中需要包含两部分,第一个部分就是字段在proto文件中的值,第二个部分就是value部分占用的长度大小。

只有通过这两个部分的值结合起来,解析器才能够正确的对字段进行解析。

key的这种格式,被称为 wire types,有哪些 wire types呢?我们看一下:

类型 含义 使用场景
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

可以看到除了3,4两种类型之外,其他的类型可以分为三类,一类是固定长度的类型,如1,5,他们分别是64位和32位的数字。

第二类是0,表示Varint,这是一种可变类型,用来表示通用的数字类型,bool类型和枚举类型。第三类2,表示长度区分的类型,这种类型通常用来表示字符串,字节数字等。

所有的key都是一个varint类型,它的值是:(field_number << 3) | wire_type,也就是说key的最后三个位,用来存储wire类型。

上面我们例子中的key的值是08,用二进制表示:

000 1000

最后三位是0,表示是一个Varint类型,将08右移三位,得到1,表示key表示的字段是1这个字段,也就是age。

然后我们看下剩下的部分96 00,换成二进制是:

96 00 = 1001 0110  0000 0000

根据Varint的定义,第一位表示的是连接位,表示第二个字节的内容和第一个字节的内容是一起的。对于Varint来说,需要将低位的字节和高位的字节进行交换,如下:

1001 0110  0000 0000 去掉最高位的1 :
001 0110  0000 0000  交换低位字节和高位字节:
0000 0000  001 0110 

上面的值是16 + 4 + 2 = 22

这样我们就得到了值为1的key,对应的value是22。

符号整数

我们知道有两种表示符号整数的方式,一种是标准的int类型:int32 和 int64,一种是带符号的int类型:sint32 和 sint64。

这两种类型的区别在于对应负整数的表示上。对于int32和int64来说,所有的负整数都是以十个字节来表示的,所以占用的空间会比较大,不适合用来表示负整数。

如果使用sint32 和 sint64,那么使用的编码方式是ZigZag,对于负整数来说更加有效。

ZigZag将带符号的整数和无符号的整数进行映射,对于每个n来说,将会使用下面的公式来编码:

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

对于sint64来说就是:

(n << 1) ^ (n >> 64)

举个例子:

符号整数 编码结果
0 0
-1 1
1 2
-2 3
2147483647 4294967294
-2147483648 4294967295

字符串

字符串的wire类型是2,说明它的值是一个varint编码的长度。举个例子:

 message Student {
  optional string name = 2;
}

上我们给Student定义了第二个属性name,假如给name赋值 "testing" ,那么得到的编码是:

12 07 [74 65 73 74 69 6e 67]

中括号的编码就是"testing"的UTF8表示。

0x12 可以这样解析:

 0x12
→ 0001 0010  (binary representation)
→ 00010 010  (regroup bits)
→ field_number = 2, wire_type = 2

0x12表示字段2的类型是2,后面跟着的07就表示后续byte字节的长度了。

嵌套的消息

消息中可以嵌套消息,我们看一个例子:

message Teacher {
  optional Student s = 3;
}

假如我们把s的age字段设置为22,就和第一个例子一样,那么上面的编码就是:

 1a 03 08 96 00

可以看到后面的三个字节和第一个例子是一样的。前面两个字节的判断方式和字符串是一值的,这样就不再多讲。

总结

好了,protobuf的基本编码规则和实现已经讲完了。听起来是不是很奇妙?

本文已收录于 http://www.flydean.com/03-protobuf-encoding/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容