十二.Protobuf3编码
本文档描述了协议缓冲消息的二进制格式。在应用程序中使用Protocol Buffer不需要理解这一点,但是了解不同的Protocol Buffer格式如何影响编码消息的大小会非常有用。
一条简单的信息
假设您有以下非常简单的消息定义:
message Test1 {
optional int32 a = 1;
}
在应用程序中,您创建一个Test1消息,并将设置为150。然后将消息序列化为输出流。如果您能够检查编码的消息,您会看到三个字节:
08 96 01
到目前为止,数字如此之小——但这意味着什么呢?继续阅读...
Base 128 Varints
要理解您的简单Protocol Buffer编码,您首先需要理解varints。Varints是一种使用一个或多个字节序列化整数的方法。较小的数字占用较小的字节数。
变量中的每个字节,除了最后一个字节,都设置了最高有效位( msb ),这表明还会有更多的字节。每个字节的低7位用于存储7位组中数字的二进制补码表示,最低有效组优先。
例如,这里是数字1——它是一个字节,所以msb没有设置:
0000 0001
这里是300,这有点复杂:
1010 1100 0000 0010
你怎么知道这是300?首先从每个字节中删除msb,因为这只是为了告诉我们是否已经到达数字的末尾(如您所见,它设置在第一个字节中,因为varint中有一个以上的字节) :
1010 1100 0000 0010
→ 010 1100 000 0010
颠倒两组7位,因为正如您所记得的,变量首先存储具有最低有效组的数字。然后将它们连接起来,得到最终值:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
消息结构
如您所知,协议缓冲消息是一系列键值对。消息的二进制版本仅使用字段的编号作为密钥—每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即.proto文件)来确定。
当消息被编码时,密钥和值被连接成字节流。当消息被解码时,解析器需要能够跳过它不能识别的字段。这样,新字段可以添加到邮件中,而不会破坏不知道它们的旧程序。为此,线格式消息中每一对的“键”实际上是两个值—来自.proto文件的字段号,加上一个线类型,该线类型提供刚好足够的信息来找到以下值的长度。在大多数语言实现中,这个键被称为标签。
可用的消息类型如下:
类型 | 意义 | 用于 |
---|---|---|
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 (已废弃) |
4 | End group | groups (已废弃) |
5 | 32-bit | fixed32, sfixed32, float |
流式消息中的每个密钥都与值(字段编号< < 3) | wire_type )不同,换句话说,数字的最后三位存储了wire类型。
现在让我们再看一遍我们的简单例子。您现在知道了,数据流中的第一个数字总是一个不同的varint 键,这里是08,或者(去掉msb ) :
000 1000
您取最后三位得到wire类型(0),然后右移三位得到字段号(1)。现在你知道字段号是1,下面的值是一个varint。使用上一节中不同的解码知识,您可以看到接下来的两个字节存储了值150。
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (丢弃msb并反转7位组)
→ 10010110
→ 128 + 16 + 4 + 2 = 150
更多值类型
有符号整数
正如您在上一节中看到的,所有与类型0相关联的Protocol Buffer类型都被编码为varints。然而,在编码负数时,有符号int类型( sint32和sint64 )和“标准”int类型( int32和int64 )之间有一个重要的区别。如果使用int32或int64作为负数的类型,得到的varints总是10字节长:实际上,它被视为一个非常大的无符号整数。如果您使用其中一种有符号类型,结果varints将使用ZigZag编码,这种编码效率更高。
ZigZag编码将有符号整数映射到无符号整数,因此绝对值小的数字(例如,-1)也有一个小的可变编码值。它这样做的方式是通过正整数和负整数来回“zig-zags”,因此-1编码为1,1编码为2,-2编码为3,依此类推,如下表所示:
原始值 | 编码为 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
换句话说,每个值n使用:
(n << 1) ^ (n >> 31)
对于 sint32,
(n << 1) ^ (n >> 63)
对于64位版本。
请注意,第二个移位(n >> 31)部分是算术移位。换句话说,移位的结果要么是一个全为零位的数字(如果n为正),要么是全一位的数字(如果n为负)。
当sint32或sint64被解析时,其值被解码回原始的签名版本。
非varint数字
非varint数字类型很简单:double和固定64位具有类型1,这告诉解析器期望固定的64位数据块;类似地,float和固定32位具有类型5,这告诉它期望32位。在这两种情况下,这些值都以小字节顺序存储。
字符串
类型为2 (长度分隔)意味着该值是一个可变的编码长度,后跟指定的数据字节数。
message Test2 {
optional string b = 2;
}
将b的值设置为“testing”:
12 07 74 65 73 74 69 6e 67
红色部分是“testing”的UTF8。这里的键是0x12 →字段号= 2,类型= 2。值的长度变化是7,我们发现后面有7个字节:我们的字符串。
嵌入消息
下面是一个消息定义,其中嵌入了我们示例类型的消息,Test1 :
message Test3 {
optional Test1 c = 3;
}
这是编码版本,Test1的字段设置为150 :
1a 03 08 96 01
如您所见,最后三个字节与我们的第一个示例( 08 96 01 )完全相同,前面是数字3:嵌入消息与字符串的处理方式完全相同(wire type = 2)。
可选和重复元素
如果proto2消息定义有重复的元素(除了[packed=true]选项),则编码的消息具有零个或多个具有相同字段编号的键值对。这些重复值不必连续出现;它们可以与其他字段交错。在解析时,元素相对于彼此的顺序会保留下来,尽管相对于其他字段的顺序会丢失。在proto3中,重复字段使用打包编码,您可以在下面阅读。
对于proto3中的任何非重复字段或proto2中的可选字段,编码消息可能具有也可能不具有与该字段编号的键值对。
通常,一个编码的消息绝不会有一个以上的不重复字段实例。然而,解析器会处理这种情况。对于数字类型和字符串,如果同一个字段出现多次,解析器将接受它看到的最后一个值。对于嵌入的消息字段,解析器合并同一个字段的多个实例,就像使用Message::MergeFrom方法一样:也就是说,后一个实例中的所有单个标量字段替换前一个实例中的字段,单个嵌入的消息被合并,重复的字段被连接。这些规则的效果是,解析两个编码消息的连接会产生完全相同的结果,就像您分别解析了这两个消息并合并了结果对象一样:
MyMessage message;
message.ParseFromString(str1 + str2);
相当于:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
这个属性有时很有用,因为它允许您合并两个消息,即使您不知道它们的类型。
打包重复字段
2.1.0版引入了打包的重复字段,在proto2中,这些字段被声明为类似于重复字段,但带有特殊的[packed=true]选项。在proto3中,默认情况下,标量数值类型的重复字段被打包。这些功能像重复的字段,但编码不同。编码消息中不会出现包含零个元素的打包重复字段。否则,字段的所有元素都被打包成一个具有类型2 (长度分隔)的键值对。每个元素都以正常方式编码,除了前面没有键。
例如,假设您有消息类型:
message Test4 {
repeated int32 d = 4 [packed=true];
}
现在假设您构建了一个Test4,为重复的字段d提供值3、270和86942。然后,编码的形式将是:
22 // key (field number 4, wire type 2)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
只有原始数字类型(使用变量、32位或64位类型的类型)的重复字段才能声明为"packed"。
请注意,虽然通常没有理由为一个打包的重复字段编码多个键值对,但编码器必须准备好接受多个键值对。在这种情况下,有效载荷应该连接在一起。每对必须包含整数个元素。
Protocol Buffer解析器必须能够解析像未打包一样打包的重复字段,反之亦然。这允许以向前和向后兼容的方式将[打包=真]添加到现有字段中。
字段排序
字段编号可以在.proto 文件中以任何顺序使用。选择的顺序对消息的序列化方式没有影响。
当消息被序列化时,对于如何写入其已知或未知字段没有保证顺序。序列化顺序是一个实现细节,任何特定实现的细节都可能在将来发生变化。因此,Protocol Buffer解析器必须能够以任何顺序解析字段。
含义
不要假设序列化消息的字节输出是稳定的。尤其是对于具有表示其他序列化Protocol Buffer消息的可传递字节字段的消息。
默认情况下,对同一Protocol Buffer消息实例重复调用序列化方法可能不会返回相同的字节输出;即默认串行化不是确定性的。
--确定性序列化只保证特定二进制文件的相同字节输出。字节输出可能在二进制文件的不同版本之间发生变化。
对于Protocol Buffer消息实例foo,以下检查可能会失败。
foo.SerializeAsString() == foo.SerializeAsString()
Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
这里有几个示例场景,其中逻辑等价的协议缓冲消息foo和bar可以序列化为不同的字节输出。
-bar由旧服务器序列化,旧服务器将某些字段视为未知字段。
-bar由用不同编程语言实现的服务器序列化,并以不同的顺序序列化字段。
-bar有一个以非确定性方式序列化的字段。
-bar有一个存储Protocol Buffer消息序列化字节输出的字段,该消息以不同方式序列化。
-bar由新的服务器序列化,由于实现的变化,该服务器以不同的顺序序列化字段。
-foo和bar都是单个消息的串联,但顺序不同。
十二.Protobuf3编码的更多相关文章
- 我是如何一步步编码完成万仓网ERP系统的(十二)库存 1.概述
https://www.cnblogs.com/smh188/p/11533668.html(我是如何一步步编码完成万仓网ERP系统的(一)系统架构) https://www.cnblogs.com/ ...
- SQL注入汇总(手注,盲注,报错注入,宽字节,二次编码,http头部){10.22、23 第二十四 二十五天}
首先什么是SQL注入: 所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令. SQL注入有什么危害? 危害:数据泄露.脱库 ...
- Web 前端开发精华文章推荐(jQuery、HTML5、CSS3)【系列十二】
2012年12月12日,[<Web 前端开发人员和设计师必读文章>系列十二]和大家见面了.梦想天空博客关注 前端开发 技术,分享各种增强网站用户体验的 jQuery 插件,展示前沿的 HT ...
- 第十二章Fundamental Data Types 基本数据类型
目录: 12.1 数值概论 12.2整数 12.3浮点数 12.4 字符和字符串 12.5布尔变量 12.6枚举类型 12.7具名常量 12.8数组 12.9创建你自己的类型 12.1 数值概论 ...
- NeHe OpenGL教程 第三十二课:拾取游戏
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
- Python 第十二篇:HTML基础
一:基础知识: HTML是英文Hyper Text Mark-up Language(超文本标记语言)的缩写,他是一种制作万维网页面标准语言(标记).相当于定义统一的一套规则,大家都来遵守他,这样就可 ...
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- 从头开始学JavaScript (十二)——Array类型
原文:从头开始学JavaScript (十二)--Array类型 一.数组的创建 注:ECMAscript数组的每一项都可以保存任何类型的数据 1.1Array构造函数 var colors = ne ...
- MINA、Netty、Twisted一起学(十二):HTTPS
由于HTTPS协议是由HTTP协议加上SSL/TLS协议组合而成,在阅读本文前可以先阅读一下HTTP服务器和SSL/TLS两篇博文,本文中的代码也是由这两篇博文中的代码组合而成. HTTPS介绍 上一 ...
随机推荐
- 根据GSVA结果绘制不同组的趋势图
首先需要将GSVA的矩阵结果转换成如下格式: 然后使用如下代码进行作图 infile <- "draw_pre_violin_heatmap.txt" data <- ...
- [转帖]直击案发现场!TCP 10倍延迟的真相是?
直击案发现场!TCP 10倍延迟的真相是? http://zhuanlan.51cto.com/art/201911/605268.htm 内核参数调优 非常重要啊. 什么是经验?就是遇到问题,解决问 ...
- 43 多线程(十三)——CAS(了解即可)
emm...记一些关键词吧 原子操作 悲观锁.乐观锁 比较交换 硬件层面 C.C#底层实现 JUC = java.util.concurrent 高级并发才使用 面试会问到 了解即可 剩下的内容交给未 ...
- 【Linux】一步一步学Linux——Linux系统常用快捷键(12) 待更新...
目录 00. 目录 01. Gnome下的快捷键 02. 其它 03. 参考 00. 目录 @ 参考博客:https://blog.csdn.net/dengjin20104042056/articl ...
- 【Linux】Linux目录结构及详细介绍
00. 目录 01. 常用目录介绍 /:根目录,位于Linux文件系统目录结构的顶层,一般根目录下只存放目录,不要存放文件,/etc./bin./dev./lib./sbin应该和根目录放置在一个分区 ...
- Android--ScrollView边界回弹效果
/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Versi ...
- javascript匿名函数自执行 (function(window,document,undefined){})(window,document);
使用匿名自执行函数的作用: (function(window,document,undefined){})(window,document); 1.首先匿名函数 (function(){}) (); ...
- 如何配置php session使用redis集群
本实例基于phpredis 2.2.8,支持集群处理,php5.3及以上. 1.页面修改: ini_set('session.save_handler', 'rediscluster'); ini_s ...
- 【.Net Core】编译时禁止自动生成netcoreapp文件夹
原文:[.Net Core]编译时禁止自动生成netcoreapp文件夹 每次在编译生成文件时,VS都会自动在<OutputPath>属性指定的路劲后再追加一个用NetCore命名的文件夹 ...
- 在 WPF 中获取一个依赖对象的所有依赖项属性
原文:在 WPF 中获取一个依赖对象的所有依赖项属性 本文介绍如何在 WPF 中获取一个依赖对象的所有依赖项属性. 本文内容 通过 WPF 标记获取 通过设计器专用方法获取 通过 WPF 标记获取 p ...