protocol buffers 序列化数据

时间:2022-07-25
本文章向大家介绍protocol buffers 序列化数据,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一. protocol buffers 是什么?

Protocol buffers 是一种语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。

Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。

Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式

二. 为什么要发明 protocol buffers ?

大家可能会觉得 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 目前具有了更多的特性:

  • 自动生成的序列化和反序列化代码避免了手动解析的需要。(官方提供自动生成代码工具,各个语言平台的基本都有)
  • 除了用于 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;  // ID 
   required string    str = 2;  // str 
   optional 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. 分配字段编号

每个消息定义中的每个字段都有唯一的编号。这些字段编号用于标识消息二进制格式中的字段,并且在使用消息类型后不应更改。请注意,范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型。范围 16 至 2047 中的字段编号需要两个字节。所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。请记住为将来可能添加的频繁出现的元素留出一些空间。

可以指定的最小字段编号为1,最大字段编号为229-1 或 536,870,911。也不能使用数字 19000 到 19999(FieldDescriptor :: kFirstReservedNumber 到 FieldDescriptor :: kLastReservedNumber),因为它们是为 Protocol Buffers实现保留的。

如果在 .proto 中使用这些保留数字中的一个,Protocol Buffers 编译的时候会报错。

同样,您不能使用任何以前 Protocol Buffers 保留的一些字段号码。

2. 保留字段

如果您通过完全删除某个字段或将其注释掉来更新消息类型,那么未来的用户可以在对该类型进行自己的更新时重新使用该字段号。如果稍后加载到了的旧版本 .proto 文件,则会导致服务器出现严重问题,例如数据混乱,隐私错误等等。确保这种情况不会发生的一种方法是指定删除字段的字段编号(或名称,这也可能会导致 JSON 序列化问题)为 reserved。如果将来的任何用户试图使用这些字段标识符,Protocol Buffers 编译器将会报错。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注意,不能在同一个 reserved 语句中混合字段名称和字段编号。如有需要需要像上面这个例子这样写。

3. 默认字段规则

  • 字段名不能重复,必须唯一。
  • repeated 字段:可以在一个 message 中重复任何数字多次(包括 0 ),不过这些重复值的顺序被保留。 在 proto3 中,纯数字类型的 repeated 字段编码时候默认采用 packed 编码

4. 各个语言标量类型对应关系

5. 枚举

在 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 之类的封闭枚举类型的语言中,枚举值会被用来标识未识别的值,并且特殊的访问器可以访问到底层整数。

在其他情况下,如果消息被序列化,则无法识别的值仍将与消息一起序列化。

6. 枚举中的保留值

如果您通过完全删除枚举条目或将其注释掉来更新枚举类型,未来的用户可以在对该类型进行自己的更新时重新使用数值。如果稍后加载到了的旧版本 .proto 文件,则会导致服务器出现严重问题,例如数据混乱,隐私错误等等。确保这种情况不会发生的一种方法是指定已删除条目的数字值(或名称,这也可能会导致JSON序列化问题)为 reserved。如果将来的任何用户试图使用这些字段标识符,Protocol Buffers 编译器将会报错。您可以使用 max 关键字指定您的保留数值范围上升到最大可能值。

7. 允许嵌套

Protocol Buffers 定义 message 允许嵌套组合成更加复杂的消息。

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

上面的例子中,SearchResponse 中嵌套使用了 Result 。

8. 更新 message

如果后面发现之前定义 message 需要增加字段了,这个时候就体现出 Protocol Buffer 的优势了,不需要改动之前的代码

  • 1.不要改动原有字段的数据结构。
  • 2.如果您添加新字段,则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。(具体原因见 未知字段 这一章节)
  • 3.只要字段号在更新的消息类型中不再使用,字段可以被删除。您可能需要重命名该字段,可能会添加前缀“OBSOLETE_”,或者标记成保留字段号 reserved,以便将来的 .proto 用户不会意外重复使用该号码。
  • 4.int32,uint32,int64,uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之一更改为另一个字段而不破坏向前或向后兼容性。如果一个数字从不适合相应类型的线路中解析出来,则会得到与在 C++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。
  • 5.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)