Tensorflow的Protocol Buffers3编程(一)

作者:langb2014

Proto3.0.0 Release Note,前面入门caffe之初,有详细介绍过2.x的版本,不熟悉的可以找找,这里不罗列了。

Proto3

  • 引入 Protocol Buffers 语言版本 3 (称为 proto3)

    当 protocol buffers 最初开源时,它实现了 Protocol Buffers 语言版本 2 (称为 proto2), 这也是为什么版本数从 v2.0.0 开始。从 v3.0.0 开始, 引入新的语言版本(proto3),而旧的版本(proto2)继续被支持。

    引入proto3的主要意图是在将这个语言推向 google 新的API平台之前清理 protobuf。在 proto3 中,语言被简化,即便于使用又可以用于更大范围的编程语言。同时添加了一些新的特性来更好的支持 API 中的通用习语。

    下面是语言版本3中主要的新特性:

    1. 移除用于原生值字段的字段表述逻辑,移除必填(required)字段,并移除默认值。这显著的简化了 proto3 在实现开放结构标示(open struct representations)中实现,例如在如Android Java, Objective C, 或者 Go 语言中。
    2. 移除未知字段
    3. 移除扩展(extensions),替代为新的称为 Any 的标准类型
    4. 修复未知枚举值的语义
    5. 此外还有 map (向后移植到proto2)
    6. 此外还有少量用于表述时间,动态日志等的标准类型 (向后移植到proto2)
    7. 定义良好的JSON编码,作为二进制编码之外的备选

      引入了一个新的概念 "syntax" 来指定一个.proto文件使用的是 proto2 还是 proto3:

      // foo.proto
      syntax = "proto3";
      message Bar {...}
      

      如果没有设置,protocol buffer编译器将生成警告而"proto2"会被默认使用。在未来版本中这个警告将可能变成错误。

      我们推荐新的Protocol Buffers 用户使用proto3.当然,我们不普遍推荐现有用户从proto2迁移到proto3,因为API不兼容,而我们将继续长期支持proto2.

      proto3中的其他重要改动:

  • 在proto3语法中显式的 "optional" 关键字被禁止,因为字段默认就是可选的;必填字段不再被支持

  • 删除非零默认值,而用于非message字段的字段表述逻辑如 has_xxx() 方法被删除;设置为默认值(对于数字字段是0,字符串/字节字段是空)的原生字段在序列化时被跳过
  • 在proto3语法中group字段不再支持
  • 在proto3中默认修改重复原生字段来支持打包序列化(当前版本中在C++, Java, Python中实现)。用户依然可以通过设置packed为false来禁用打包序列化
  • 添加众所周知的类型 protos (any.proto, empty.proto, timestamp.proto, duration.proto 等).用户可以导入并使用这些protos,就像正规的proto文件一样。额外的运行时支持在每个语言中都可用。
  • Proto3 JSON在一些语言中 (在C++, Java, Python 和 C# 中被完全支持, 在 Ruby 中被部分支持)被支持。 JSON 规范在proto3语言指南中被定义:

    https://developers.google.com/protocol-buffers/docs/proto3#json

    我们将发布更详细的规范来定义 proto3-顺应 的JSON序列器和解析器的确切行为。在此之前,请不要依赖实现的特定行为,如果他们没有在上面的规范中注明。

  • Proto3强制严格 UTF-8 检查。如果字符串字段包含非UTF-8数据则解析将失败。

普遍

  • 因为新的语言实现 (C#, JavaScript, Ruby, Objective-C)到proto3
  • 添加对 map 字段的支持(同时在proto2和proto3中实现)。

    map字段可以使用下面的语法声明:

      message Foo {
        map<string, string> values = 1;
      }
    

    map字段的数据在内存中被存储为未排序的map并可以通过生成的访问器来访问。

  • 在 proto2 和 proto3 语法中同时添加 "reserved" (保留) 关键字。用户可以用这个关键字来声明保留字段数字和名称来防止他们被在同一个消息中的其他字段重用。

    为了保留字段数字,在消息中添加保留声明:

      message TestMessage {
          reserved 2, 15, 9 to 11, 3;
      }
    

    这将保留字段数字 2, 3, 9, 10, 11 and 15。如果用户使用他们中的任何一个作为字段数字,protocol buffer 编译器将报告错误。

    字段名字也可以被保留:

      message TestMessage {
          reserved "foo", "bar";
      }
    
  • 添加一个确定性的序列化API(当前在c++中可用)。这个确定性序列化保证:给定的一个二进制,等同的消息将被序列化为同样的字节。这容许应用如MapReduce基于被序列化后的字节来分组相等的消息。这个确定性序列化  跨语言。在使用schema跨越不同build时也是不稳定的。需要权威序列化的用户,如以权威形式永久保存,指纹等,应该定义自己的权威性规范并使用反射API来实现序列器,而不是依赖这个API。

  • 添加新的字段选项"json_name"。默认在proto3的JSON格式中 proto 字段名将被转换为"lowerCamelCase"(小写开头的驮峰法)。这个选项被用于覆盖这个行为并为这个字段指定不同的JSON名字。
  • 添加一致性测试来确保实现遵循 proto3 JSON 规范

    章节列表:

    • 定义消息类型
    • Scalar值类型
    • 默认值
    • 枚举
    • 使用其他消息类型
    • 内嵌类型
    • 更新消息类型
    • Any
    • Oneof
    • Maps
    • 定义服务
    • JSON 映射
    • 选项
    • 生成类

定义消息类型

注:内容翻译自官网文档 Language Guide (proto3) 中的 Defining A Message Type 一节

首先我们来看一个非常简单的例子. 让我们假设你想定义一个搜索请求消息格式, 每个搜索请求有一个查询字符串, 搜索结果的第几页/每页结果数量. 这里是你用来定义消息类型的.proto文件:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  1. 第一行指明当前使用的是proto3语法, 如果不指定则默认为proto2. 必须是.proto文件的除空行和注释内容之外的第一行
  2. SearchRequest 消息定义指明了3个字段. 每个字段有名字和类型.

定义字段类型

上面例子中, 所有字段都是Scalar Types/简单类型(下面会详细讲述), 包括两个int和一个string. 此外还可以支持复合类型包括枚举类型.

指定标签

可以看到消息定义的每个字段都有一个唯一的数字型标签. 这个标签用于在消息的二进制格式中标识字段, 一旦消息类型被使用后不可以再修改.

注意标签的值在1和15之间时编码只需一个字节, 包括标识值和字段类型(可以在Protocol Buffer Encoding中找到更多信息). 标签在16到2047之间将占用两个字节. 因此应该将从1到15的标签分派给最频繁出现的消息元素. 记得保留一些空间给未来可能添加的频繁出现的元素.

可以指定的最小的标签值是1, 最大的是2的29次方-1, 即536870911. 另外19000到19999(FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber)不能使用, 因为Protocol Buffers的实现自己保留这个标签段.

指定字段规则

消息字段可以是下列之一:

  1. 单数: 一个定义良好的消息可以有0个或1个此字段(但是不能超过1个).
  2. 重复: 这个字段可以在定义良好的消息中重复任意次(包括0次).重复值的顺序将被维持原状.

由于历史原因, 简单数字类型的重复字段并没有编码为最有效率的方式. 新的代码应该使用特别选项[packed=true]来得到更有效率的编码. 例如:

 repeated int32 samples = 4 [packed=true];

可以在Protocol Buffer Encoding中找到更多的关于packed encoding的信息.

添加更多消息类型

可以在单个 .proto 文件中定义多个消息类型. 适用于定义多个相关消息, 例如, 如果你想定义应答消息格式来相应你的SearchResponse消息类型,你应该将它添加到同一个.proto文件.

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

添加注释

可以使用C/C++风格的"//"语法在.proto文件中添加注释.

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}

保留字段

当你更新消息类型,需要彻底删除一个字段时, 或者注释掉它, 未来的用户在实现他们对这个类型的更新时可以重用这个标签数字. 这将导致言中的问题, 如果他们后来装载同一个.proto文件的老版本, 败落数据冲突, 隐私缺陷以及其他. 为了确保不发生这样的事情, 有一个方法时指明这个要删除的字段为保留字段. 如果未来任何用户试图私用这个字段标识符protocol buffer的编译器就是告警.

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

注意: 不能在同一个保留字段声明中混合使用名字和标签数字

Scalar值类型

简单消息字段可以有下列类型其中之一 - 下面的表格显示在.proto文件中指明的类型和在自动生成类中的相应类型.

注: 原表格太复杂, 翻译没有必要, 请见原页面.

下面是简化版本, 只列出了我关心的java类型映射:

.proto Type Java Type
double double
float float
int32 int
int32 int
int64 long
bool boolean
string String
bytes ByteString

默认值

当消息被解析时, 如果被编码的消息没有包含特定的简单元素, 被解析的对象对应的字段被设置为默认值. 默认值是和类型有关的:

  • 对于strings, 默认值是空字符串(注, 是"", 而不是null)
  • 对于bytes, 默认值是空字节(注, 应该是byte[0], 注意这里也不是null)
  • 对于boolean, 默认值是false.
  • 对于数字类型, 默认值是0.
  • 对于枚举, 默认值是第一个定义的枚举值, 而这个值必须是0.
  • 对于消息字段, 默认值是null.

对于重复字段, 默认值是空(通常都是空列表)

注意: 对于简单字段, 当消息被解析后, 是没有办法知道这个字段到底是有设置值然后恰巧和默认值相同(例如一个boolean设置为false)还是这个字段没有没有设置值而取了默认值. 例如, 不要用一个boolean值然后当设置为false时来切换某些行为, 而你又不希望这个行为默认会发生. 同样请注意: 如果一个简单消息字段被设置为它的默认值, 这个值不会被序列化.

枚举

当你定义消息类型时, 你可能希望某个字段只能有预先定义的多个值中的一个. 例如, 假设你想为每个SearchRequest添加一个corpus字段, 而corpus可以是UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS 或 VIDEO. 你可以简单的添加一个枚举到消息定义, 为每个可能的值定义常量.

在下面的例子中, 我们添加一个名为Corpus的枚举类型, 定义好所有可能的值, 然后添加一个类型为Corpus的字段:

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;
}

如你所见, Corpus 枚举的第一个常量设置到0: 每个枚举定义必须包含一个映射到0的常量作为它的第一个元素. 这是因为:

  • 必须有一个0值, 这样我们才能用0来作为数值默认值.
  • 0值必须是第一个元素, 兼容proto2语法,在proto2中默认值总是第一个枚举值

可以通过将相同值赋值给不同的枚举常量来定义别名. 为此需要设置allow_alias选项为true, 否则当发现别名时protocol编译器会生成错误消息.

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}

枚举常量必须在32位整形的范围内. 由于枚举值使用varint encoding, 负值是效率低下的因此不推荐使用.你可以在消息定义中定义枚举, 如前面例子那样, 或者在外部 - 这些枚举可以在.proto文件的任意消息定义中重用. 你也可以用在一个消息中声明的枚举类型作为别的消息的字段类型, 需要使用语法MessageType.EnumType.

当你运行protocol buffer 编译器处理使用枚举的.proto文件时, 生成的代码将会有java或c++的对应枚举, 对于Python, 在生成的运行时类中会有一个特别的EnumDescriptor用于创建带有整型值的symbolic常量集合.

在反序列过程中, 未被识别的枚举值将被保留在消息中, 但是当消息被反序列号时将会如何表现是和语言有关的. 在支持开放枚举类型可以用定义范围之外的值的语言中, 例如C++和Go, 未知的枚举值被简单保存为它底层整型描述. 在封闭枚举类型的语言例如Java中, 一个枚举的特例用于表示这个未识别的值, 底层整型可以被特殊的访问器访问. 在其他案例中, 如果消息被系列化, 这个未识别的值将和消息一起被序列化.

使用其他消息类型

可以使用其他消息类型作为字段类型. 例如, 假设你想在每个SearchResponse消息中包含Result消息 - 为了做到这点, 你可以在相同的.proto文件中定义Result消息类型然后具体指定SearchResponse中的一个字段为Result类型:

message SearchResponse {
  repeated Result result = 1;
}

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

导入定义

在上面的例子中, Result消息类型是定义在和SearchResponse同一个文件中 - 如果你想要作为字段类型使用的消息类型已经在其他的.proto文件中定义了呢?

你可以通过导入来使用来自其他.proto文件的定义. 为了导入其他.proto的定义, 需要在文件的顶端增加导入声明:

import "myproject/other_protos.proto";

默认只能从直接导入的.proto文件中使用其中的定义. 但是, 某些情况下可能需要移动.proto文件到新的位置. 相比直接移动.proto文件并更新所有的调用点, 现在可以有其他的方法: 在原有位置放置一个伪装的.proto文件, 通过使用import public方式转发所有的import到新的位置. 其他任何导入这个包含import public语句的proto文件都可以透明的得到通过import public方法导入的依赖. 例如:

// new.proto
// 所有的依赖被移动到这里
// old.proto
// 这里是所有client需要导入的proto
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 这里可以使用old.proto 和 new.proto 中的定义, 但是other.proto中的不能使用.

protocol编译器在通过命令行-I/--proto_path参数指定的目录集合中搜索导入的文件. 如果没有指定, 则在编译器被调用的目录下查找. 通常应该设置--proto_path参数到项目所在的根目录然后为所有的导入使用完整限定名.

使用proto2的消息类型

导入proto2的消息类型并在proto3消息中使用是可以的, 反之也如此. 但是, proto2的枚举不能在proto3语法中使用





发表评论

0个评论

我要留言×

技术领域:

我要留言×

留言成功,我们将在审核后加至投票列表中!

提示x

人工智能开发框架知识库已成功保存至我的图谱现在你可以用它来管理自己的知识内容了

删除图谱提示×

你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?

删除节点提示×

无法删除该知识节点,因该节点下仍保存有相关知识内容!

删除节点提示×

你确定要删除该知识节点吗?