深入Protobuf源码-编码实现 - 上善若水 - BlogJava
liebian365 2024-10-24 14:38 13 浏览 0 评论
深入Protobuf源码-编码实现
基本类型编码
在前文有提到消息是一系列的基本类型以及其他消息类型的组合,因而基本类型是probobuf编码实现的基础,这些基本类型有:
.proto Type | Java Type | C++ Type | Wire Type |
double | double | double | WIRETYPE_FIXED64(1) |
float | float | float | WIRETYPE_FIXED32(5) |
int64 | long | int64 | WIRETYPE_VARINT(0) |
int32 | int | int32 | WIRETYPE_VARINT(0) |
uint64 | long | unit64 | WIRETYPE_VARINT(0) |
uint32 | int | unit32 | WIRETYPE_VARINT(0) |
sint64 | long | int64 | WIRETYPE_VARINT(0) |
sint32 | int | int32 | WIRETYPE_VARINT(0) |
fixed64 | long | unit64 | WIRETYPE_FIXED64(1) |
fixed32 | int | unit32 | WIRETYPE_FIXED32(5) |
sfixed64 | long | int64 | WIRETYPE_FIXED64(1) |
sfixed32 | int | int32 | WIRETYPE_FIXED32(5) |
bool | boolean | bool | WIRETYPE_VARINT(0) |
string | String | string | WIRETYPE_LENGTH_DELIMITED(2) |
bytes | ByteString | string | WIRETYPE_LENGTH_DELIMITED(2) |
在Java种对不同类型的选择,其他的类型区别很明显,主要在与int32、uint32、sint32、fixed32中以及对应的64位版本的选择,因为在Java中这些类型都用int(long)来表达,但是protobuf内部使用ZigZag编码方式来处理多余的符号问题,但是在编译生成的代码中并没有验证逻辑,比如uint的字段不能传入负数之类的。而从编码效率上,对fixed32类型,如果字段值大于2^28,它的编码效率比int32更加有效;而在负数编码上sint32的效率比int32要高;uint32则用于字段值永远是正整数的情况。
在实现上,protobuf使用CodedOutputStream实现序列化逻辑、CodedInputStream实现反序列化逻辑,他们都包含write/read基本类型和Message类型的方法,write方法中同时包含fieldNumber和value参数,在写入时先写入由fieldNumber和WireType组成的tag值(添加这个WireType类型信息是为了在对无法识别的字段编码时可以通过这个类型信息判断使用那种方式解析这个未知字段,所以这几种类型值即可),这个tag值是一个可变长int类型,所谓的可变长类型就是一个字节的最高位(msb,most significant bit)用1表示后一个字节属于当前字段,而最高位0表示当前字段编码结束。在写入tag值后,再写入字段值value,对不同的字段类型采用不同的编码方式:
1. 对int32/int64类型,如果值大于等于0,直接采用可变长编码,否则,采用64位的可变长编码,因而其编码结果永远是10个字节,所有说它int32/int64类型在编码负数效率很低(然而这里我一直木有想明白对int32类型为什么需要做64位的符号扩展,不扩展,5个字节就可以了啊,而且对64位的负数也不需要用符号扩展,或者无法符号扩展,google上也没有找到具体原因)。
2. 对uint32/uint64类型,也采用变长编码,不对负数做验证。
3. 对sint32/sint64类型,首先对该值做ZigZag编码,以保留,然后将编码后的值采用变长编码。所谓ZigZag编码即将负数转换成正数,而所有正数都乘2,如0编码成0,-1编码成1,1编码成2,-2编码成3,以此类推,因而它对负数的编码依然保持比较高的效率。
4. 对fixed32/sfixed32/fixed64/sfixed64类型,直接将该值以小端模式的固定长度编码。
5. 对double类型,先将double转换成long类型,然后以8个字节固定长度小端模式写入。
6. 对float类型,先将float类型转换成int类型,然后以4个字节固定长度小端模式写入。
7. 对bool类型,写0或1的一个字节。
8. 对string类型,使用UTF-8编码获取字节数组,然后先用变长编码写入字节数组长度,然后写入所有的字节数组。
Tag | msgByteSize | msgByte |
9. 对bytes类型(ByteString),先用变长编码写入长度,然后写入整个字节数组。
Tag | msgByteSize | msgByte |
10. 对枚举类型(类型值WIRETYPE_VARINT),用int32编码方式写入定义枚举项时给定的值(因而在给枚举类型项赋值时不推荐使用负数,因为int32编码方式对负数编码效率太低)。
11. 对内嵌Message类型(类型值WIRETYPE_LENGTH_DELIMITED),先写入整个Message序列化后字节长度,然后写入整个Message。
Tag | msgByteSize | msgByte |
注:ZigZag编码实现:(n << 1) ^ (n >> 31) / (n << 1) ^ (n >> 63);在CodedOutputStream中还存在一些用于计算某个字段可能占用的字节数的compute静态方法,这里不再详述。
在protobuf的序列化中,所有的类型最终都会转换成一个可变长int/long类型、固定长度的int/long类型、byte类型以及byte数组。对byte类型的写只是简单的对内部buffer的赋值:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->public void writeRawByte(final byte value) throws IOException {
if (position == limit) {
refreshBuffer();
}
buffer[position++] = value;
}
对32位可变长整形实现为:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->public void writeRawVarint32(int value) throws IOException {
while (true) {
if ((value & ~0x7F) == 0) {
writeRawByte(value);
return;
} else {
writeRawByte((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
对于定长,protobuf采用小端模式,如对32位定长整形的实现:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->public void writeRawLittleEndian32(final int value) throws IOExcep-tion {
writeRawByte((value ) & 0xFF);
writeRawByte((value >> 8) & 0xFF);
writeRawByte((value >> 16) & 0xFF);
writeRawByte((value >> 24) & 0xFF);
}
对byte数组,可以简单理解为依次调用writeRawByte()方法,只是CodedOutputStream在实现时做了部分性能优化。这里不详细介绍。
对CodedInputStream则是根据CodedOutputStream的编码方式进行解码,因而也不详述,其中关于ZigZag的解码:(n >>> 1) ^ -(n & 1)
repeated字段编码
对于repeated字段,一般有两种编码方式:
1. 每个项都先写入tag,然后写入具体数据。如对基本类型:
Tag | Data | Tag | Data | … |
而对message类型:
Tag | Length | Data | Tag | Length | Data | … |
2. 先写入tag,后count,再写入count个项,每个项包含length|data数据。即:
Tag | Count | Length | Data | Length | Data | … |
从编码效率的角度来看,个人感觉第二中情况更加有效,然而不知道处于什么原因考虑,protobuf采用了第一种方式来编码,个人能想到的一个理由是第一种情况下,每个消息项都是相对独立的,因而在传输过程中接收端每接收到一个消息项就可以进行解析,而不需要等待整个repeated字段的消息包。对于基本类型,protobuf也采用了第一种编码方式,后来发现这种编码方式效率太低,因而可以添加[packed = true]的描述将其转换成第三种编码方式(第二种方式的变种,对基本数据类型,比第二种方式更加有效):
3. 先写入tag,后写入字段的总字节数,再写入每个项数据。即:
Tag | dataByteSize | Data | Data | … |
目前protobuf只支持基本类型的packed修饰,因而如果将packed添加到非repeated字段或非基本类型的repeated字段,编译器在编译.proto文件时会报错。
未识别字段编码
在protobuf中,将所有未识别字段保存在UnknownFieldSet中,并且在每个由protobuf编译生成的Message类以及GeneratedMessage.Builder中保存了UnknownFieldSet字段unknownFields;该字段可以从CodedInputStream中初始化(调用UnknownFieldSet.Builder的mergeFieldFrom()方法)或从用户自己通过Builder设置;在序列化时,调用UnknownFieldSet的writeTo()方法将自身内容序列化到CodedOutputStream中。
UnknownFieldSet顾名思义是未知字段的集合,其内部数据结构是一个FieldNumber到Field的Map,而一个Field用于表达一个未知字段,它可以是任何值,因而它包含了所有5中类型的List字段,这里并没有对一个Field验证,因而允许多个相同FieldNumber的未知字段,并且他们可以是任意类型值。UnknownFieldSet采用MessageLite编程模式,因而它实现了MessageLite接口,并且定义了一个Builder类实现MessageLite.Builder接口用于手动或从CodedInputStream中构建UnknownFieldSet。虽然Field本身没有实现MessageLite接口,它依然实现了该接口的部分方法,如writeTo()、getSerializedSize()用于实现向CodedOutputStream中序列化自身,并且定义了Field.Builder类用于构建Field实例。
在一个Message序列化时(writeTo()方法实现),在写完所有可识别的字段以及扩展字段,这个定义在Message中的UnknownFieldSet也会被写入CodedOutputStream中;而在从CodedInputStream中解析时,对任何未知字段也都会被写入这个UnknownFieldSet中。
扩展字段编码
在写框架代码时,经常由扩展性的需求,在Java中,只需要简单的定义一个父类或接口即可解决,如果框架本身还负责构建实例本身,可以使用反射或暴露Factory类也可以顺利实现,然而对序列化来说,就很难提供这种动态plugin机制了。然而protobuf还是提出来一个相对可以接受的机制(语法有点怪异,但是至少可以用):在一个message中定义它支持的可扩展字段值的范围,然后用户可以使用extend关键字扩展该message定义(具体参考相关章节)。在实现中,所有这些支持字段扩展的message类型继承自ExtendableMessage类(它本身继承自GeneratedMessage类)并实现ExtendableMessageOrBuilder接口,而它们的Builder类则继承自ExtendableBuilder类并且同时也实现了ExtendableMessageOrBuilder接口。
ExtendableMessage和ExtendableBuilder类都包含FieldSet<FieldDescriptor>类型的字段用于保存该message所有的扩展字段值。FieldSet中保存了FieldDescriptor到其Object值的Map,然而在ExtendableMessage和ExtendableBuilder中则使用GeneratedExtension来表识一个扩展字段,这是因为GeneratedExtension除了包含对一个扩展字段的描述信息FieldDescriptor外,还存储了该扩展字段的类型、默认值等信息,在protobuf消息定义编译器中会为每个扩展字段生成相应的GeneratedExtension实例以供用户使用:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->public static final GeneratedExtension<Foo, Integer> bar = Generated-Message.newFileScopedGeneratedExtension( Integer.class, null );
bar.internalInit(descriptor.getExtensions().get(0));
Base base = Base.newBuilder().setExtension(SearchRequestProtos.bar, 11).build();用户使用该bar静态字段用于作为key与它对应的值关联,这种关联关系写入extensions字段中。从而在序列化时,对每个字段,按正常的值字段先写Tag在写实际值内容将它序列化到CodedOutputStream中(ExtensionWriter.writeUntil()方法);在反序列化中,我们需要告诉protobuf哪些字段是扩展字段,从而它在解析到无法识别的字段可以判断这个字段是否是扩展字段,因而protobuf提供了ExtensionRegistry类,它用于注册所有识别的扩展字段,并且在protobuf编译出来的代码中也存在一个静态方法将所有已定义的扩展字段注册到用户提供的ExtensionRegistry实例中:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
-->public static void registerAllExtensions(ExtensionRegistry registry) {
registry.add(SearchRequestProtos.bar);
}
posted on 2015-04-01 09:23 DLevin 阅读(11133) 评论(1) 编辑 收藏 所属分类: Protobuf
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)