当前位置: 首页 > news >正文

铁道部建设管理司网站网站建设为大学生服务

铁道部建设管理司网站,网站建设为大学生服务,net网站开发参考文献,网站收录不好的原因高效的数据压缩编码方式 Protobuf github地址 目录 ProtocolBuffers 是什么为什么要发明 ProtocolBuffersproto3 定义 Message 分配字段编号保留字段默认字段规则各个语言标量类型对应关系枚举枚举中的保留值允许嵌套枚举不兼容性更新 Message未知字段Map 类型JsonMapping p…高效的数据压缩编码方式 Protobuf github地址 目录 ProtocolBuffers 是什么为什么要发明 ProtocolBuffersproto3 定义 Message 分配字段编号保留字段默认字段规则各个语言标量类型对应关系枚举枚举中的保留值允许嵌套枚举不兼容性更新 Message未知字段Map 类型JsonMapping proto3 定义 ServicesProtocolBuffer 命名规范ProtocolBuffer 编码原理 MessageStructure 编码SignedIntegers 编码Non-varint Numbers字符串嵌套式 MessageOptional 和 Repeated 的编码PackedRepeatedFieldsMessageStructure 编码FieldOrder protocolBuffers 的优缺点最后 ProtocolBuffers是什么 Protocol buffers 是一种语言中立平台无关可扩展的序列化数据的格式可用于通信协议数据存储等。 Protocol buffers 在序列化数据方面它是灵活的高效的。相比于 XML 来说Protocol buffers 更加小巧更加快速更加简单。一旦定义了要处理的数据的数据结构之后就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。 Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 为什么要发明ProtocolBuffers 大家可能会觉得 Google 发明 protocol buffers 是为了解决序列化速度的其实真实的原因并不是这样的。 protocol buffers 最先开始是 google 用来解决索引服务器 request/response 协议的。没有 protocol buffers 之前google 已经存在了一种 request/response 格式用于手动处理 request/response 的编组和反编组。它也能支持多版本协议不过代码比较丑陋 if (version 3) {...} else if (version 4) {if (version 5) {...}...}如果非常明确的格式化协议会使新协议变得非常复杂。因为开发人员必须确保请求发起者与处理请求的实际服务器之间的所有服务器都能理解新协议然后才能切换开关以开始使用新协议。 这也就是每个服务器开发人员都遇到过的低版本兼容、新旧协议兼容相关的问题。 protocol buffers 为了解决这些问题于是就诞生了。protocol buffers 被寄予一下 2 个特点 可以很容易地引入新的字段并且不需要检查数据的中间服务器可以简单地解析并传递数据而无需了解所有字段。数据格式更加具有自我描述性可以用各种语言来处理(C, Java 等各种语言) 这个版本的 protocol buffers 仍需要自己手写解析的代码。 不过随着系统慢慢发展演进protocol buffers 目前具有了更多的特性 自动生成的序列化和反序列化代码避免了手动解析的需要。官方提供自动生成代码工具各个语言平台的基本都有除了用于 RPC远程过程调用请求之外人们开始将 protocol buffers 用作持久存储数据的便捷自描述格式例如在Bigtable中。服务器的 RPC 接口可以先声明为协议的一部分然后用 protocol compiler 生成基类用户可以使用服务器接口的实际实现来覆盖它们。 protocol buffers 现在是 Google 用于数据的通用语言。在撰写本文时谷歌代码树中定义了 48162 种不同的消息类型包括 12183 个 .proto 文件。它们既用于 RPC 系统也用于在各种存储系统中持久存储数据。 小结 protocol buffers 诞生之初是为了解决服务器端新旧协议(高低版本)兼容性问题名字也很体贴“协议缓冲区”。只不过后期慢慢发展成用于传输数据。 Protocol Buffers 命名由来 Why the name “Protocol Buffers”? The name originates from the early days of the format, before we had the protocol buffer compiler to generate classes for us. At the time, there was a class called ProtocolBuffer which actually acted as a buffer for an individual method. Users would add tag/value pairs to this buffer individually by calling methods like AddValue(tag, value). The raw bytes were stored in a buffer which could then be written out once the message had been constructed. Since that time, the “buffers” part of the name has lost its meaning, but it is still the name we use. Today, people usually use the term “protocol message” to refer to a message in an abstract sense, “protocol buffer” to refer to a serialized copy of a message, and “protocol message object” to refer to an in-memory object representing the parsed message. 这个名字起源于 format 早期在我们有 protocol buffer 编译器为我们生成类之前。当时有一个名为 ProtocolBuffer 的类它实际上充当了单个方法的缓冲区。用户可以通过调用像 AddValue(tag,value) 这样的方法分别将标签/值对添加到此缓冲区。原始字节存储在一个缓冲区中一旦构建消息就可以将其写出。 从那时起名为“缓冲”的部分已经失去了意义但它仍然是我们使用的名称。今天人们通常使用术语“protocol message”来指代抽象意义上的消息“protocol buffer”指的是消息的序列化副本而“protocol message object”指的是代表内存中对象解析的消息。 回到顶部 proto3定义Message 目前 protocol buffers 最新版本是 proto3与老的版本 proto2 还是有些区别的。这两个版本的 API 不完全兼容。 proto2 和 proto3 的名字看起来有点扑朔迷离那是因为当我们最初开源的 protocol buffers 时它实际上是 Google 的第二个版本了所以被称为 proto2这也是我们的开源版本号从 v2 开始的原因。初始版名为 proto1从 2001 年初开始在谷歌开发的。 在 proto 中所有结构化的数据都被称为 message。 message helloworld {required int32 id 1; // IDrequired string str 2; // stroptional int32 opt 3; //optional field }上面这几行语句定义了一个消息 helloworld该消息有三个成员类型为 int32 的 id另一个为类型为 string 的成员 str。opt 是一个可选的成员即消息中可以不包含该成员。 接下来说明一些 proto3 中需要注意的地方。 syntax proto3;message SearchRequest {string query 1;int32 page_number 2;int32 result_per_page 3; }如果开头第一行不声明 syntax proto3;则默认使用 proto2 进行解析。 分配字段编号 每个消息定义中的每个字段都有唯一的编号。这些字段编号用于标识消息二进制格式中的字段并且在使用消息类型后不应更改。请注意范围 1 到 15 中的字段编号需要一个字节进行编码包括字段编号和字段类型具体原因见 Protocol Buffer 编码原理 这一章节。范围 16 至 2047 中的字段编号需要两个字节。所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。 可以指定的最小字段编号为1最大字段编号为 229-1 或 536, 870, 911。也不能使用数字 19000 到 19999 (FieldDescriptor :: kFirstReservedNumber 到 FieldDescriptor :: kLastReservedNumber)因为它们是为 Protocol Buffers 实现保留的。 如果在 .proto 中使用这些保留数字中的一个 Protocol Buffers 编译的时候会报错。 同样您不能使用任何以前 Protocol Buffers 保留的一些字段号码。保留字段是什么下一节详细说明。 保留字段 如果您通过完全删除某个字段或将其注释掉来更新消息类型那么未来的用户可以在对该类型进行自己的更新时重新使用该字段号。如果稍后加载到了的旧版本 .proto 文件则会导致服务器出现严重问题例如数据混乱隐私错误等等。确保这种情况不会发生的一种方法是指定删除字段的字段编号或名称这也可能会导致 JSON 序列化问题为 reserved 。如果将来的任何用户试图使用这些字段标识符 Protocol Buffers 编译器将会报错。 message Foo {reserved 2, 15, 9 to 11;reserved foo, bar; }注意不能在同一个 reserved 语句中混合字段名称和字段编号。 如有需要需要像上面这个例子这样写。 默认字段规则 字段名不能重复必须唯一。repeated 字段可以在一个 message 中重复任何数字多次(包括 0 )不过这些重复值的顺序被保留。 在 proto3 中纯数字类型的 repeated 字段编码时候默认采用 packed 编码具体原因见 Protocol Buffer 编码原理 这一章节 各个语言标量类型对应关系 枚举 在 message 中可以嵌入枚举类型。 message SearchRequest {string query 1;int32 page_number 2;int32 result_per_page 3;enum Corpus {UNIVERSAL 0;WEB 1;IMAGES 2;LOCAL 3;NEWS 4;PRODUCTS 5;VIDEO 6;}Corpus corpus 4; }枚举类型需要注意的是一定要有 0 值。 枚举为 0 的是作为零值当不赋值的时候就会是零值。为了和 proto2 兼容。在 proto2 中零值必须是第一个值。 另外在反序列化的过程中无法被识别的枚举值将会被保留在 messaage 中。因为消息反序列化时如何表示是依赖于语言的。在支持指定符号范围之外的值的开放枚举类型的语言中例如 C 和 Go 未知的枚举值只是存储为其基础整数表示。在诸如 Java 之类的封闭枚举类型的语言中枚举值会被用来标识未识别的值并且特殊的访问器可以访问到底层整数。 在其他情况下如果消息被序列化则无法识别的值仍将与消息一起序列化。 枚举中的保留值 如果您通过完全删除枚举条目或将其注释掉来更新枚举类型未来的用户可以在对该类型进行自己的更新时重新使用数值。 如果稍后加载到了的旧版本 .proto 文件则会导致服务器出现严重问题例如数据混乱隐私错误等等。确保这种情况不会发生的一种方法是指定已删除条目的数字值或名称这也可能会导致 JSON 序列化问题为 reserved 。 如果将来的任何用户试图使用这些字段标识符 Protocol Buffers 编译器将会报错。您可以使用 max 关键字指定您的保留数值范围上升到最大可能值。 enum Foo {reserved 2, 15, 9 to 11, 40 to max;reserved FOO, BAR; }注意不能在同一个 reserved 语句中混合字段名称和字段编号。 如有需要需要像上面这个例子这样写。 允许嵌套 Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息。 message SearchResponse {repeated Result results 1; }message Result {string url 1;string title 2;repeated string snippets 3; }上面的例子中SearchResponse 中嵌套使用了 Result 。 更多的例子 message SearchResponse {message Result {string url 1;string title 2;repeated string snippets 3;}repeated Result results 1; }message SomeOtherMessage {SearchResponse.Result result 1; }message Outer { // Level 0message MiddleAA { // Level 1message Inner { // Level 2int64 ival 1;bool booly 2;}}message MiddleBB { // Level 1message Inner { // Level 2int32 ival 1;bool booly 2;}} }枚举不兼容性 可以导入 proto2 消息类型并在 proto3 消息中使用它们反之亦然。然而 proto2 枚举不能直接用在 proto3 语法中但是如果导入的 proto2 消息使用它们这是可以的。 更新Message 如果后面发现之前定义 message 需要增加字段了这个时候就体现出 Protocol Buffer 的优势了不需要改动之前的代码。不过需要满足以下 10 条规则 不要改动原有字段的数据结构。 如果您添加新字段则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值以便新代码可以正确地与旧代码生成的消息进行交互。同样由新代码创建的消息可以由旧代码解析旧的二进制文件在解析时会简单地忽略新字段。具体原因见 未知字段 这一章节 只要字段号在更新的消息类型中不再使用字段可以被删除。您可能需要重命名该字段可能会添加前缀 “OBSOLETE_” 或者标记成保留字段号 reserved 以便将来的 .proto 用户不会意外重复使用该号码。 int32 uint32 int64 uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之一更改为另一个字段而不破坏向前或向后兼容性。如果一个数字从不适合相应类型的线路中解析出来则会得到与在 C 中将该数字转换为该类型相同的效果例如如果将 64 位数字读为 int32 它将被截断为 32 位。 sint32 和 sint64 相互兼容但与其他整数类型不兼容。 只要字节是有效的 UTF-8 string 和 bytes 是兼容的。 嵌套式 message 与 bytes 兼容如果 bytes 包含 message 的 encoded version。 fixed32 与 sfixed32 兼容而 fixed64 与 sfixed64 兼容。 enum 就数组而言是可以与 int32 uint32 int64 和 uint64 兼容请注意如果它们不适合值将被截断。但是请注意当消息反序列化时客户端代码可能会以不同的方式对待它们例如未识别的 proto3 枚举类型将保留在消息中但消息反序列化时如何表示是与语言相关的。这点和语言相关上面提到过了 Int 域始终只保留它们的值。 将单个值更改为新的成员是安全和二进制兼容的。如果您确定一次没有代码设置多个字段则将多个字段移至新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。注意字段和值的区别字段是 field 值是 value 未知字段 未知数字段是 protocol buffers 序列化的数据表示解析器无法识别的字段。例如当一个旧的二进制文件解析由新的二进制文件发送的新数据的数据时这些新的字段将成为旧的二进制文件中的未知字段。 Proto3 实现可以成功解析未知字段的消息但是实现可能会或可能不会支持保留这些未知字段。你不应该依赖保存或删除未知域。对于大多数 Google protocol buffers 实现未知字段在 proto3 中无法通过相应的 proto 运行时访问并且在反序列化时被丢弃和遗忘。这是与 proto2 的不同行为其中未知字段总是与消息一起保存并序列化。 Map类型 repeated 类型可以用来表示数组 Map 类型则可以用来表示字典。 mapkey_type, value_type map_field N;mapstring, Project projects 3;key_type 可以是任何 int 或者 string 类型(任何的标量类型具体可以见上面标量类型对应表格但是要除去 float、double 和 bytes ) 枚举值也不能作为 key。 key_type 可以是除去 map 以外的任何类型。 需要特别注意的是 map 是不能用 repeated 修饰的。 线性数组和 map 迭代顺序的是不确定的所以你不能依靠你的 map 是在一个特定的顺序。 为 .proto 生成文本格式时 map 按 key 排序。数字的 key 按数字排序。 从数组中解析或合并时如果有重复的 key 则使用所看到的最后一个 key覆盖原则。从文本格式解析映射时如果有重复的 key 解析可能会失败。 Protocol Buffer 虽然不支持 map 类型的数组但是可以转换一下用以下思路实现 maps 数组 message MapFieldEntry {key_type key 1;value_type value 2; }repeated MapFieldEntry map_field N;上述写法和 map 数组是完全等价的所以用 repeated 巧妙的实现了 maps 数组的需求。 JsonMapping Proto3 支持 JSON 中的规范编码使系统之间共享数据变得更加容易。编码在下表中按类型逐个描述。 如果 JSON 编码数据中缺少值或其值为空则在解析为 protocol buffer 时它将被解释为适当的默认值。如果一个字段在协议缓冲区中具有默认值默认情况下它将在 JSON 编码数据中省略以节省空间。具体 Mapping 的实现可以提供选项决定是否在 JSON 编码的输出中发送具有默认值的字段。 proto3 的 JSON 实现中提供了以下 4 中 options : 使用默认值发送字段在默认情况下默认值的字段在 proto3 JSON 输出中被忽略。一个实现可以提供一个选项来覆盖这个行为并使用它们的默认值输出字段。 忽略未知字段默认情况下 Proto3 JSON 解析器应拒绝未知字段但可能提供一个选项来忽略解析中的未知字段。 使用 proto 字段名称而不是 lowerCamelCase 名称默认情况下 proto3 JSON 的 printer 将字段名称转换为 lowerCamelCase 并将其用作 JSON 名称。实现可能会提供一个选项将原始字段名称用作 JSON 名称。 Proto3 JSON 解析器需要接受转换后的 lowerCamelCase 名称和原始字段名称。 发送枚举形式的枚举值而不是字符串在 JSON 输出中默认使用枚举值的名称。可以提供一个选项来使用枚举值的数值。 回到顶部 proto3定义Services 如果要使用 RPC 远程过程调用系统的消息类型可以在 .proto 文件中定义 RPC 服务接口 protocol buffer 编译器将使用所选语言生成服务接口代码和 stubs 。所以例如如果你定义一个 RPC 服务入参是 SearchRequest 返回值是 SearchResponse 你可以在你的 .proto 文件中定义它如下所示 service SearchService {rpc Search (SearchRequest) returns (SearchResponse); }与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC 在谷歌开发的语言和平台中立的开源 RPC 系统。 gRPC 在 protocol buffer 中工作得非常好并且允许你通过使用特殊的 protocol buffer 编译插件直接从 .proto 文件中生成 RPC 相关的代码。 如果你不想使用 gRPC 也可以在你自己的 RPC 实现中使用 protocol buffers 。您可以在 Proto2 语言指南中找到更多关于这些相关的信息。 还有一些正在进行的第三方项目为 Protocol Buffers 开发 RPC 实现。 回到顶部 ProtocolBuffer命名规范 message 采用驼峰命名法。message 首字母大写开头。字段名采用下划线分隔法命名。 message SongServerRequest {required string song_name 1; }枚举类型采用驼峰命名法。枚举类型首字母大写开头。每个枚举值全部大写并且采用下划线分隔法命名。 enum Foo {FIRST_VALUE 0;SECOND_VALUE 1; }每个枚举值用分号结束不是逗号。 服务名和方法名都采用驼峰命名法。并且首字母都大写开头。 service FooService {rpc GetSomething(FooRequest) returns (FooResponse); }回到顶部 ProtocolBuffer编码原理 在讨论 Protocol Buffer 编码原理之前必须先谈谈 Varints 编码。 Base 128 Varints 编码 Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。 Varint 中的每个字节最后一个字节除外都设置了最高有效位(msb)这一位表示还会有更多字节出现。每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示最低有效组首位。 如果用不到 1 个字节那么最高有效位设为 0 如下面这个例子1 用一个字节就可以表示所以 msb 为 0. 0000 0001如果需要多个字节表示msb 就应该设置为 1 。例如 300如果用 Varint 表示的话 1010 1100 0000 0010如果按照正常的二进制计算的话这个表示的是 88068(65536 16384 4096 2048 4)。 那 Varint 是怎么编码的呢 下面代码是 Varint int 32 的编码计算方法。 // *号后加了空格强迫症 0.0 char * EncodeVarint32(char * dst, uint32_t v) {// Operate on characters as unsignedsunsigned char* ptr reinterpret_castunsigned char*(dst);static const int B 128;if (v (17)) {* (ptr) v;} else if (v (114)) {* (ptr) v | B;* (ptr) v7;} else if (v (121)) {* (ptr) v | B;* (ptr) (v7) | B;* (ptr) v14;} else if (v (128)) {* (ptr) v | B;* (ptr) (v7) | B;* (ptr) (v14) | B;* (ptr) v21;} else {* (ptr) v | B;* (ptr) (v7) | B;* (ptr) (v14) | B;* (ptr) (v21) | B;* (ptr) v28;}return reinterpret_castchar*(ptr); }300 100101100由于 300 超过了 7 位 Varint 一个字节只有 7 位能用来表示数字最高位 msb 用来表示后面是否有更多字节所以 300 需要用 2 个字节来表示。 Varint 的编码以 300 举例 if (v (114)) {* (ptr) v | B;* (ptr) v7; }1. 100101100 | 10000000 1 1010 1100 2. 110101100 取出末尾 7 位 010 1100 3. 100101100 7 10 0000 0010 4. 1010 1100 0000 0010 (最终 Varint 结果)Varint 的解码算法应该是这样的实际就是编码的逆过程 如果是多个字节先去掉每个字节的 msb通过逻辑或运算每个字节只留下 7 位。 逆序整个结果最多是 5 个字节排序是 1-2-3-4-5逆序之后就是 5-4-3-2-1字节内部的二进制位的顺序不变变的是字节的相对位置。 解码过程调用 GetVarint32Ptr 函数如果是大于一个字节的情况会调用 GetVarint32PtrFallback 来处理。 inline const char* GetVarint32Ptr(const char* p,const char* limit,uint32_t* value) {if (p limit) {uint32_t result * (reinterpret_castconst unsigned char*(p));if ((result 128) 0) {* value result;return p 1;}}return GetVarint32PtrFallback(p, limit, value); }const char* GetVarint32PtrFallback(const char* p,const char* limit,uint32_t* value) {uint32_t result 0;for (uint32_t shift 0; shift 28 p limit; shift 7) {uint32_t byte * (reinterpret_castconst unsigned char*(p));p;if (byte 128) {// More bytes are presentresult | ((byte 127) shift);} else {result | (byte shift);* value result;return reinterpret_castconst char*(p);}}return NULL; }至此Varint 处理过程读者应该都熟悉了。上面列举出了 Varint 32 的算法64 位的同理只不过不再用 10 个分支来写代码了太丑了。32位 是 5 个 字节64位 是 10 个字节 64 位 Varint 编码实现 const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) {uint64_t result 0;for (uint32_t shift 0; shift 63 p limit; shift 7) {uint64_t byte * (reinterpret_castconst unsigned char*(p));p;if (byte 128) {// More bytes are presentresult | ((byte 127) shift);} else {result | (byte shift);* value result;return reinterpret_castconst char*(p);}}return NULL; }读到这里可能有读者会问了 Varint 不是为了紧凑 int 的么那 300 本来可以用 2 个字节表示现在还是 2 个字节了哪里紧凑了花费的空间没有变啊 Varint 确实是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字一般需要 4 个 byte 来表示。但是采用 Varint 对于很小的 int32 类型的数字则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面采用 Varint 表示法大的数字则需要 5 个 byte 来表示。从统计的角度来说一般不会所有的消息中的数字都是大数因此大多数情况下采用 Varint 后可以用更少的字节数来表示数字信息。 300 如果用 int32 表示需要 4 个字节现在用 Varint 表示只需要 2 个字节了。缩小了一半 MessageStructure编码 protocol buffer 中 message 是一系列键值对。message 的二进制版本只是使用字段号(fields number 和 wire_type )作为 key 。每个字段的名称和声明类型只能在解码端通过引用消息类型的定义即 .proto 文件来确定。这一点也是人们常常说的 protocol buffer 比 JSON XML 安全一点的原因如果没有数据结构描述 .proto 文件拿到数据以后是无法解释成正常的数据的。 由于采用了 tag-value 的形式所以 option 的 field 如果有就存在在这个 message buffer 中如果没有就不会在这里这一点也算是压缩了 message 的大小了。 当消息编码时键和值被连接成一个字节流。当消息被解码时解析器需要能够跳过它无法识别的字段。这样可以将新字段添加到消息中而不会破坏不知道它们的旧程序。这就是所谓的 “向后”兼容性。 为此线性的格式消息中每对的 key 实际上是两个值其中一个是来自 .proto 文件的字段编号加上提供正好足够的信息来查找下一个值的长度。在大多数语言实现中这个 key 被称为 tag 。 注意上图中3 和 4 已经被废弃了所以 wire_type 取值目前只有 0、1、2、5。 key 的计算方法是 (field_number 3) | wire_type 换句话说key 的最后 3 位表示的就是 wire_type 。 举例一般 message 的字段号都是 1 开始的所以对应的 tag 可能是这样的 000 1000末尾 3 位表示的是 value 的类型这里是 000即 0 代表的是 varint 值。右移 3 位即 0001这代表的就是字段号 (field number) 。tag 的例子就举这么多接下来举一个 value 的例子还是用 varint 来举例 96 01 1001 0110 0000 0001→ 000 0001 001 0110 (drop the msb and reverse the groups of 7 bits)→ 10010110→ 128 16 4 2 150可以 96 01 代表的数据就是 150 。 message Test1 {required int32 a 1; }如果存在上面这样的一个 message 的结构如果存入 150在 Protocol Buffer 中显示的二进制应该为 08 96 01 。 额外说一句type 需要注意的是 type 2 的情况tag 里面除了包含 field number 和 wire_type 还需要再包含一个 length 决定 value 从那一段取出来。具体原因见 Protocol Buffer 字符串 这一章节 SignedIntegers编码 从上面的表格里面可以看到 wire_type 0 中包含了无符号的 varints 但是如果是一个无符号数呢 一个负数一般会被表示为一个很大的整数因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数那么一定需要 10 个 byte 长度。 为何 32 位和 64 位的负数都需要 10 个 byte 长度呢 inline void CodedOutputStream::WriteVarint32SignExtended(int32 value) {WriteVarint64(static_castuint64(value)); }因为源码里面是这么规定的。32 位的有符号数都会转换成 64 位无符号来处理。至于源码为什么要这么规定呢猜想可能是怕 32 位的负数转换会有溢出的可能。(只是猜想) 为此 Google Protocol Buffer 定义了 sint32 这种类型采用 zigzag 编码。将所有整数映射成无符号整数然后再采用 varint 编码方式编码这样绝对值小的整数编码后也会有一个较小的 varint 编码值。 Zigzag 映射函数为 Zigzag(n) (n 1) ^ (n 31), n 为 sint32 时Zigzag(n) (n 1) ^ (n 63), n 为 sint64 时按照这种方法-1 将会被编码成 11 将会被编码成 2-2 会被编码成 3如下表所示 需要注意的是第二个转换 (n 31) 部分是一个算术转换。所以换句话说移位的结果要么是一个全为0如果n是正数要么是全部1如果n是负数。 当 sint32 或 sint64 被解析时它的值被解码回原始的带符号的版本。 Non-varintNumbers Non-varint 数字比较简单double 、fixed64 的 wire_type 为 1在解析时告诉解析器该类型的数据需要一个 64 位大小的数据块即可。同理float 和 fixed32 的 wire_type 为5给其 32 位数据块即可。两种情况下都是高位在后低位在前。 说 Protocol Buffer 压缩数据没有到极限原因就在这里因为并没有压缩 float、double 这些浮点类型。 字符串 wire_type 类型为 2 的数据是一种指定长度的编码方式key length content key 的编码方式是统一的 length 采用 varints 编码方式 content 就是由 length 指定长度的 Bytes 。 举例假设定义如下的 message 格式 message Test2 {optional string b 2; }设置该值为 testing 二进制格式查看 12 07 74 65 73 74 69 6e 6774 65 73 74 69 6e 67 是 testing 的 UTF8 代码。 此处 key 是16进制表示的所以展开是 12 - 0001 0010 后三位 010 为 wire type 2 0001 0010 右移三位为 0000 0010 即 tag 2 。 length 此处为 7后边跟着 7 个 bytes 即我们的字符串 testing 。 所以 wire_type 类型为 2 的数据编码的时候会默认转换为 T-L-V (Tag - Length - Value) 的形式。 嵌套式Message 假设定义如下嵌套消息 message Test3 {optional Test1 c 3; }设置字段为整数150编码后的字节为 1a 03 08 96 0108 96 01 这三个代表的是 150上面讲解过这里就不再赘述了。 1a - 0001 1010 后三位 010 为 wire type 20001 1010 右移三位为 0000 0011 即 tag 3 。 length 为 3代表后面有 3 个字节即 08 96 01 。 需要转变为 T - L - V 形式的还有 string , bytes , embedded messages , packed repeated fields 即 wire_type 为 2 的形式都会转变成 T - L - V 形式 Optional和Repeated的编码 在 proto2 中定义成 repeated 的字段没有加上 [packedtrue] option 编码后的 message 有一个或者多个包含相同 tag 数字的 key-value 对。这些重复的 value 不需要连续的出现他们可能与其他的字段间隔的出现。尽管他们是无序的但是在解析时他们是需要有序的。在 proto3 中 repeated 字段默认采用 packed 编码具体原因见 Packed Repeated Fields 这一章节 对于 proto3 中的任何非重复字段或 proto2 中的可选字段编码的 message 可能有也可能没有包含该字段号的键值对。 通常编码后的 message 其 required 字段和 optional 字段最多只有一个实例。但是解析器却需要处理多对一的情况。对于数字类型和 string 类型如果同一值出现多次解析器接受最后一个它收到的值。对于内嵌字段解析器合并 (merge) 它接收到的同一字段的多个实例。就如 MergeFrom 方法一样所有单数的字段后来的会替换先前的所有单数的内嵌 message 都会被合并 (merge) 所有的 repeated 字段都会串联起来。这样的规则的结果是解析两个串联的编码后的 message 与分别解析两个 message 然后 merge 结果是一样的。例如 MyMessage message; message.ParseFromString(str1 str2);等价于 MyMessage message, message2; message.ParseFromString(str1); message2.ParseFromString(str2); message.MergeFrom(message2);这种方法有时是非常有用的。比如即使不知道 message 的类型也能够将其合并。 PackedRepeatedFields 在 2.1.0 版本以后protocol buffers 引入了该种类型其与 repeated 字段一样只是在末尾声明了 [packedtrue] 。类似 repeated 字段却又不同。在 proto3 中 Repeated 字段默认就是以这种方式处理。对于 packed repeated 字段如果 message 中没有赋值则不会出现在编码后的数据中。否则的话该字段所有的元素会被打包到单一一个 key-value 对中且它的 wire_type2 长度确定。每个元素正常编码只不过其前没有标签 tag 。例如有如下 message 类型 message Test4 {repeated int32 d 4 [packedtrue]; }构造一个 Test4 字段并且设置 repeated 字段 d 3个值3270 和 86942编码后 22 // tag 0010 0010(field number 010 0 4, wire type 010 2)06 // payload size (设置的length 6 bytes)03 // first element (varint 3)8E 02 // second element (varint 270)9E A7 05 // third element (varint 86942)形成了 Tag - Length - Value - Value - Value …… 对。 只有原始数字类型使用 varint 32位或64位的重复字段才可以声明为 packed 。 有一点需要注意对于 packed 的 repeated 字段尽管通常没有理由将其编码为多个 key-value 对编码器必须有接收多个 key-pair 对的准备。这种情况下payload 必须是串联的每个 pair 必须包含完整的元素。 Protocol Buffer 解析器必须能够解析被重新编译为 packed 的字段就像它们未被 packed 一样反之亦然。这允许以正向和反向兼容的方式将 [packed true] 添加到现有字段。 FieldOrder 编码/解码与字段顺序无关这一点由 key-value 机制保证。 如果消息具有未知字段则当前的 Java 和 C 实现在按顺序排序的已知字段之后以任意顺序写入它们。当前的 Python 实现不会跟踪未知字段。 回到顶部 protocolBuffers的优缺点 protocol buffers 在序列化方面与 XML 相比有诸多优点 更加简单 数据体积小 3- 10 倍 更快的反序列化速度提高 20 - 100 倍 可以自动化生成更易于编码方式使用的数据访问类 举个例子 如果要编码一个用户的名字和 email 信息用 XML 的方式如下 personnameJohn Doe/nameemailjdoeexample.com/email/person相同需求如果换成 protocol buffers 来实现定义文件如下 # Textual representation of a protocol buffer. # This is *not* the binary format used on the wire. person {name: John Doeemail: jdoeexample.com }protocol buffers 通过编码以后以二进制的方式进行数据传输最多只需要 28 bytes 空间和 100-200 ns 的反序列化时间。但是 XML 则至少需要 69 bytes 空间经过压缩以后去掉所有空格和 5000-10000 的反序列化时间。 上面说的是性能方面的优势。接下来说说编码方面的优势。 protocol buffers 自带代码生成工具可以生成友好的数据访问存储接口。从而开发人员使用它来编码更加方便。例如上面的例子如果用 C 的方式去读取用户的名字和 email 直接调用对应的 get 方法即可所有属性的 get 和 set 方法的代码都自动生成好了只需要调用即可 cout Name: person.name() endl;cout E-mail: person.email() endl;而 XML 读取数据会麻烦一些 cout Name: person.getElementsByTagName(name)-item(0)-innerText() endl;cout E-mail: person.getElementsByTagName(email)-item(0)-innerText() endl;Protobuf 语义更清晰无需类似 XML 解析器的东西因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作。 使用 Protobuf 无需学习复杂的文档对象模型Protobuf 的编程模式比较友好简单易学同时它拥有良好的文档和示例对于喜欢简单事物的人们而言 Protobuf 比其他的技术更加有吸引力。 protocol buffers 最后一个非常棒的特性是即“向后”兼容性好人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变(因为存储方式本来就是无序的k-v 形式)。 当然 protocol buffers 也并不是完美的在使用上存在一些局限性。 由于文本并不适合用来描述数据结构所以 Protobuf 也不适合用来对基于文本的标记文档如 HTML 建模。另外由于 XML 具有某种程度上的自解释性它可以被人直接读取编辑在这一点上 Protobuf 不行它以二进制的方式存储除非你有 .proto 定义否则你没法直接读出 Protobuf 的任何内容。 回到顶部 最后 读完本篇 Protocol Buffer 编码原理以后读者应该能明白以下几点 Protocol Buffer 利用 varint 原理压缩数据以后二进制数据非常紧凑 option 也算是压缩体积的一个举措。所以 pb 体积更小如果选用它作为网络数据传输势必相同数据消耗的网络流量更少。但是并没有压缩到极限float、double 浮点型都没有压缩。 Protocol Buffer 比 JSON 和 XML 少了 {、}、 : 这些符号体积也减少一些。再加上 varint 压缩gzip 压缩以后体积更小 Protocol Buffer 是 Tag - Value (Tag - Length - Value) 的编码方式的实现减少了分隔符的使用数据存储更加紧凑。 Protocol Buffer 另外一个核心价值在于提供了一套工具一个编译工具自动化生成 get/set 代码。简化了多语言交互的复杂度使得编码解码工作有了生产力。 Protocol Buffer 不是自我描述的离开了数据描述 .proto 文件就无法理解二进制数据流。这点即是优点使数据具有一定的“加密性”也是缺点数据可读性极差。所以 Protocol Buffer 非常适合内部服务之间 RPC 调用和传递数据。 Protocol Buffer 具有向后兼容的特性更新数据结构以后老版本依旧可以兼容这也是 Protocol Buffer 诞生之初被寄予解决的问题。因为编译器对不识别的新增字段会跳过不处理。 Protocol Buffer 编码原理篇到此结束下篇来讲讲 Protocol Buffer 反序列化解包性能快的原因。 回到顶部
http://www.yutouwan.com/news/53736/

相关文章:

  • wordpress插件 图片口碑好的常州网站优化
  • 请人做网站得多少钱php后台网站开发教程
  • 旅游网站系统建设一个购物交易网站怎么做
  • 建ic网站广西智能网站建设平台
  • 排版设计模板安新seo优化排名网站
  • 庆阳网站设计京东官网登录
  • 网站没有问题但是一直做不上首页开发软件开发
  • 手机微网站模板手机建站平台哪个便宜
  • 怎么做短链接网站建网站做外贸
  • 网站首页代码怎么做网站怎么发布到服务器
  • 大气物流网站源码浙江省建设培训中心网
  • 怎么看出是模板网站网站建设的请示
  • 做外贸的数据网站有哪些选择做印象绍兴网站的原因
  • 娄底网站建设设计大连企业网站建设
  • 做服装外贸哪个网站好wordpress微信收款
  • 爱站长尾关键词挖掘工具江门恒达互联网网站建设
  • 公司做网站需要准备哪些资料wordpress图片无法显示
  • 在谷歌上做英文网站网站的步骤
  • 北京手机网站设计南宁有做网站的公司吗
  • 做视频网站注意什么问题全国企业信息系统查询系统
  • 杭州高端定制网站注册公司名字核名查询系统
  • 黑白网站模板o2o平台有哪些行业
  • 电脑软件下载平台哪个好seo长沙
  • 招聘网有哪些网站比较好网站生成手机站
  • 郑州专业网站设计公司地址会计培训班初级费用
  • 开发区网站建设公司福州制作网站设计找哪家公司
  • 赤坎手机网站建设wordpress常规选项
  • 做流量的网站多语言免费网站建设
  • 苏州建网站的公司济南公司制作网站
  • 政协网站建设申请函广东网站建设微信商城开发