Proto3 语言指南

由于工程项目中拟采用一种简便高效的数据交换格式,百度了一下发现除了采用 xml、JSON 还有 ProtoBuf(Google 出品),赶紧去瞄了一下。花了一个周末的时间把它走马观花的学习了一下,顺便将官方的指南翻译了出来。

首先申明,哥们儿英语高中水平,借助了必应词典勉强将其意译了出来,如果你发现翻译中有纰漏,请一定不要告诉我~

怕有误人子弟之嫌,先贴上官方文档的地址,本译文仅供参考:
https://developers.google.com/protocol-buffers/docs/proto3

Proto3 语言指南

  • 定义消息类型
  • 标准类型
  • 默认值
  • 枚举
  • 使用其它消息类型
  • 嵌套
  • 更新消息类型
  • Any
  • Oneof
  • Map 类型
  • 定义服务
  • JSON 映射
  • 选项
  • 创建您的类

本指南描述如何使用 ProtoBuf 语言规范来组织你的.proto 文件,以及如何编译.proto 文件来生成相应的操作类。它涵盖了proto3 语法,如果你想查看老版本 proto2 的相关信息,请参考《Proto2 语言指南》

这是一个参考指南---通过一个例子一步一步地介绍本文档描述的 proto3 语言特性,请根据你选择的编程语言参考基础教程

定义消息类型

让我们先来看一个简单的例子。假设你想定义一个搜索请求的消息格式,它包含一个查询字符串、一个你感兴趣的特定页号、以及每页结果数。下面就是这个.proto 文件所定义的消息类型。

syntax = "proto3";


message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指定你正在使用 proto3 语法:如果你不这么做 protocol buffer 编译器会假设您使用的是 proto2 。 这一行不允许存在空白字符或注释。
  • 这个 SearchRequest 消息定义了三个字段(名称/值对),每一条 SearchRequest 消息类型的数据都包含这三个字段定义的数据。每个字段包含一个名称和类型。
指定字段类型

在上面的例子中,所有的字段都是 标准 类型:二个整形(page_number 和 resulet_per_page)和一个字符串类型(query)。然而,你也可以用复杂类型来定义字段,包括 枚举 和其它消息类型。

指定标签

通过上面的例子你可以看到,这里每个字段都定义了一个唯一的数值标签。 这些唯一的数值标签用来标识 二进制消息 中你所定义的字段,一旦定义了编译后就无法修改。需要特别提醒的是标签 1–15 标识的字段编码仅占用 1 个字节(包括字段类型和标识标签),更多详细的信息请参考 ProtoBuf 编码 。 数值标签 16–2047 标识的字段编码占用 2 个字节。因此,你应该将标签 1–15 留给那些在你的消息类型中使用频率高的字段。记得预留一些空间(标签 1–15)给将来可能添加的高频率字段。

最小的数值标签是 1, 最大值是 2 29 - 1, 即 536,870,911。 你不能使用的标签范围还有:19000–19999
( FieldDescriptor::kFirstReservedNumber – FieldDescriptor::kLastReservedNumber ),这些是 ProtoBuf 系统预留的,如果你在你的.proto 文件中使用了其中的数值标签,protoc 编译器会报错。同样地,你不能使用保留字段中 reserved 关键字定义的标签。

定义字段的规则

消息的字段可以是一下情况之一:

  • 单数(默认):该字段可以出现 0 或 1 次(不能大于 1 次)。
  • 可重复(repeated):该字段可以出现任意次(包含 0)。 可重复字段数值的顺序是系统预定义的。
    由于一些历史原因,默认情况下,数值类型的可重复(repeated)字段的编码性能没有想象中的好,你应该在其后用特殊选项 [packed=true] 来申
    明以获得更高效的编码。 例如:
    repeated int32 samples = 4 [packed=true];

你能够在 ProtoBuf 编码 中查阅更多的关于 packed 关键字的信息

添加更多的消息类型

同一个.proto 文件中可以定义多个消息类型。这在定义多个相关的消息时非常有用。例如,如果你想针对用于搜索查询的 SearchRequest 消息定义
一个保存查找结果的 SearchResponse 消息,你可以把它们放在同一个.proto 文件中:

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

message SearchResponse {
  ...
}

添加注释

在.proto 文件中,使用 C/C++格式的注释语法 // syntax

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 文件,可能会导致严重的问题。包括数据冲突、 隐秘的 bug 等等。为了保证这种情况不会发生,当你想删除一个字段的时候,
可以使用 reserved 关键字来申明该字段的标签(和/或名字,这在 JSON 序列化的时候也会产生问题)。 将来如果有人使用了你使用 reserved
关键字定义的标签或名字,编译器就好报错。

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

注意:你不能同时在一条 reserved 语句中申明标签和名字。

.proto 文件编译生成了什么?

当你使用 protoc 编译器 编译一个.proto 文件的时候,编译器会根据你选择的语言和你在这个.proto 文件定义的消息类型生成代码,,这些代码的
功能包括:字段值的 getter,setter,消息序列化并写入到输出流,从输入流接反序列化读取消息等。
对于 C++语言,编译器会根据定义的.proto 文件编译生成一个.h 头文件和一个.cc 源码实现文件。
4

  • 对于 Java 语言,编译器会为每一个消息类型创建一个带类的.java 文件,同时这个 java 文件中包含用来创建该消息类型的实例的特殊 Builder 构造
    类。
  • Python 语言有点不一样---编译器在你定义的.proto 文件中创建一个包含静态描述符的模块,每个消息类型对应一个静态描述符,在 Python 程序解
    释运行的时候,会根据静态描述符用一个元类去创建相应的数据访问类。
  • 对于 Go 语言,针对每一个定义的消息类型编译器会创建一个带类型的.pb.go 文件。
  • 对于 Ruby 语言,编译器会创建一个带 Ruby 模块的.rb 文件,其中包含了所有你定义的消息类型。
  • 对于 JavaNano,编译器会创建 Java 语言类似的输出文件,但是没有 Builder 构造类。
  • 对于 Ojective-C,编译器会创建一个 pbobjc.h 和一个 pbobjc.m 文件,为每一个消息类型都创建一个类来操作。
  • 对于 C#语言,编译器会为每一个.proto 文件创建一个.cs 文件,为每一个消息类型都创建一个类来操作。
    针对所选择的不同的编程语言,你能够在后续的教程中找到更多的关于操作它们的编程接口(proto3 版本的即将推出)。 更详细
    的针对特定编程语言的 API 操作细节,请参考 API 参考
标准类型

.proto文件中消息结构里用于定义字段的标准数据类型如下表所示,后面几列是.proto文件中定义的标准类型编译转换后在编程语言中的类型对照。

如果你想了解这些数据类型在序列化的时候如何编码,请参考 ProtoBuf 编码
[1] 在 Java 中,无符号的 32 位整形和 64 位整形都是用的相应的有符号整数表示,最高位储存的是符号标志。
[2] 在任何情况下,给字段赋值都会执行类型检查,以确保所赋的值是有效的。
5
[3] 默认情况下 64 位整数或 32 位无符号整数通常在编码的时候都是用 long 类型表示,但是你可以在设定字段的时候指定位 int 类型。 在任何情况
下,这个值必须匹配所设定的数据类型。参考[2]
[4] Python 中字符串通常编码为 Unicode,但是如果给定的字符串是 ASCII 类型,可以设置位 str 类型(可能会有变化)

默认值

当一个消息被解析的时候,如果在编码后的消息结构中某字段没有初始值,相应的字段在被解析的对象中会被设置默认值。这些默认值都是类型相关的。

  • 字符串默认值为空字符串。
  • 字节类型默认值是空字节。
  • 布尔类型默认值为 false。
  • 数值类型默认值位 0。
  • 枚举 类型默认值是第一个枚举元素,它必须为 0。
  • 消息类型字段默认值为 null。

可重复类型字段的默认值为(相应编程语言中的)空列表。
需要提醒的是:对于标准数据类型的字段,当消息被解析的时候你是没有办法显示地设定默认值的(例如布尔类型是否默认设置为 false),记住当你定义自己的消息类型的时候不要设置它的默认值。例如,不要在你的消息类型中定义一个表示开关变量的布尔类型字段,如果你不希望它默认初始化为 false 的话。 还要注意的是,在序列化的时候,如果标准类型的字段的值等于它的默认值,这个值是不会存储到介质上的。

枚举

当你定义一个消息的时候,你可能会希望某个字段在预定的取值列表里面取值。 例如,假设你想为 SearchRequest 消息定义一个 corpus字段,它的取值可能是 UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS 或者 VIDEO。你只需要简单的利用 enum 关键字定义一个枚举类型,它的每一个可能的取值都是常量。

在下面的例子中,我们定义了一个名为 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 的元素必须是第一个元素,为了兼容 proto2 语法(proto2 中枚举类型的第一个元素总是默认值)。

你可以通过给不同的枚举常量赋同样的值的方式来定义别名。 为了定义别名,你需要设置 allow_alias=true,否则编译器会报错。


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 位整数值的范围。 由于枚举的值采用 varient 编码 方式,负数编码效率低所以不推荐。枚举类型可以定义在消息结构体内部(上例所示),也可以定义在外部。如果定义在了外部,同一个.proto 文件中的所有消息都能重用这个枚举类型。当然,你也可以用
MessageType.EnumType 语法格式在一个消息结构内部引用其它消息结构体内部定义的枚举类型来定义字段。

当你用 protoc 编译器编译一个包含枚举类型的.proto 文件时,对于 Java 或 C++编译生成的代码中会包含相应的枚举类型,对于 Python 语言会生成
一个特殊的 EnumDescriptor 类,在 Python 运行时会生成一系列整形的符号常量供程序使用。

在消息反序列化的时候,无法识别的枚举类型会被保留在消息中,但是这种枚举类型如何复现依赖于所使用的编程语言。对于支持开放式枚举类型的编程语言,枚举类型取值超出特定的符号范围,例如 C++和 Go 语言,未知的枚举值在复现的时候简单地以基础型整数形式存储。对于支持封闭式枚举类型的编程语言,例如 Java,未知的枚举值可以通过特殊的访问函数读取。在任何情况下,只要消息被序列化,无法识别的枚举值也会跟着被序列化。

欲详细了解枚举类型如何在消息类型内工作,请根据你选择的编程语言,参考 生成代码参考

使用其它消息类型

你可以使用其它消息类型来定义字段。 假如你想在每一个 SearchResponse 消息里面定义一个 Result 消息类型的字段,你只需要同一个.proto文件中定义 Result 消息,并用它来定义 SearchResponse 中的一个字段即可。

message SearchResponse {
    repeated Result result = 1;
}

message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
}
导入定义

在上面的例子中,Result 消息类型和 SearchResponse 消息类型是定义在同一个文件中的,如果你想用另外一个.proto 文件中定义的消息类型来定义字段该怎么做呢?

你可以导入其它.proto 文件中已经定义的消息,来定义该消息类型的字段。为了导入其它.proto 文件中的定义,你需要在你的.proto 文件头部申明import 语句:

import "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的.proto 文件中的定义。然而,有时候你可能需要移动一个.proto 文件到一个新的位置。如果直接移动这个.proto文件,你需要一次更新所有引用了这个.proto 文件的所有调用站点。你可以将文件移动之后(译注:下面例子中的 new.proto),在原来的位置创建一个虚拟文件(译注:下面例子中的 old.proto),在其中用 import public 申明指向新位置的.proto 文件(译注:这样就实现了跨文件引用,而不需要更新调用站点里面的代码了)。通过 import public 申明的导入依赖关系可以被任何调用了包含该 import public 语句的调用者间接继承。(译注:这段话绕来绕去就是说,默认情况下,a 中 import b,b 中 import c,a 只能引用 b 里面的定义,而不能引用 c 里面的定义;如果你想 a 跨文件导入引用 c 里面的定义,就要在 b 中申明 import public c,这样 a 既能引用 b 里面的定义,又能引用 c 里面的定义了) 例如:

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

protoc 编译器通过命令行参数 -I 或 --proto_path 指定导入文件的搜索查找目录 。如果没有指定该参数,默认在编译器所在目录查找。 通常,你需要设定 --proto_path 参数为你的项目根目录,使用全名称目录路径指定导入文件搜索路径。

使用 proto2 消息类型

你在 proto3 中可以引用 proto2 消息类型,反之亦可。 然而,proto2 语法格式的枚举类型,不可以在 proto3 中引用。

消息嵌套

你可以在一个消息结构内部定义另外一个消息类型,如下例所示---Result 消息类型定义在 SearchResponse 消息体内部。

message SearchResponse {

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

    repeated Result result = 1;
}

如果你想在它的父消息外部复用这个内部定义的消息类型,可以采用 Parent.Type 语法格式:

message SomeOtherMessage {
    SearchResponse.Result result = 1;
}

只要你愿意,消息可以嵌套任意层。

message Outer { // Level 0

      message MiddleAA { // Level 1

            message Inner { // Level 2
                int64 ival = 1;
                bool booly = 2;
            }

      }

      message MiddleBB { // Level 1

            message Inner { // Level 2
                  int32 ival = 1;
                  bool booly = 2;
            }

        }
}
更新一个消息类型

如果现有的消息类型无法满足你的需要---例如,你想为这个消息添加一个字段,但是你又想沿用原来的代码格式,不用担心!
你可以非常简单地就能在不破坏现有代码的基础上更新这个消息类型。只需要遵循以下原则:

  • 不要修改现有字段的数值标签。
  • 如果你为一个消息添加了字段,所有已经序列化调者依然可以通过旧的消息格式解析新消息生成的代码。 你需要注意的是这些元素的 默认值 ,以便新代码可以真确地与旧代码生成的消息交互。 同理,旧代码也可以解析新代码生成的消息:原二进制程序在解析新的消息时,会忽略新添加的字段。 注意:所有未知字段在消息反序列化的时候会被自动抛弃,所以当消息传递给新代码时,新字段不可用。(这和 proto2 不一样,proto2 中位置字段也会跟消息一起序列化)
  • 字段可以被移除,只要你不再在你更新后的消息里面使用被删除字段的数值标签即可。 或许你想重命名一个字段,通过添加前缀"OBSOLETE_"或者通过 reserved 关键字申明原数值标签,以免将来别人重用这个数值。
  • int32, uint32, int64, uint64, 和 bool 类型是相互兼容的,这意味着你可以改变这类字段的数值类型,而不必担心破坏了向前或向后兼容性。 如果数值从传输介质上解析的时候,不匹配相应的数值类型,其效果相当于你在 C++里面做了类型转换(例如,一个 64 位的整数被解析为 32 位整数时,它会被转换为 32 位整数)。
  • sint32 和 sint64 互相兼容,但是它们不和其它整数类型兼容。
  • 只要 bytes 类型是有效的 UTF-8 格式,它就和 string 类型兼容。
  • 内嵌的消息类型和包含该消息类型编码后的字节内容的 bytes 类型兼容。
  • fxied32 与 sfixed32 兼容,同样的,fixed64 与 sfixed64 兼容。
Any

Any 类型允许你在没有某些消息类型的.proto 定义时,像使用内嵌的消息类型一样使用它来定义消息类型的字段。一个 Any 类型的消息是一个包含任意字节数的序列化消息,拥有一个 URL 地址作为全局唯一标识符来解决消息的类型。为了使用 Any 类型的消息,你需要import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
        string message = 1;
        repeated Any details = 2;
}

给定 Any 消息类型的默认 URL 是: type.googleapis.com/packagename.messagename。
不同的语言实现都会支持运行库帮助通过类型安全的方式来封包或解包 Any 类型的消息。在 Java 语言中,Any 类型有专门的访问函数 pack()和unpack()。在 C++中对应的是 PackFrom()和 PackTo()方法。

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;

for (const Any& detail : status.details()) {
    if (detail.IsType<NetworkErrorDetails>()) {
          NetworkErrorDetails network_error;
          detail.UnpackTo(&network_error);
          ... processing network_error ...
    }
}

当前,Any 类型的运行时库还在开发中。
如果你已经熟悉 proto2 语法 ,Any 类型就是替代了 proto2 中的 extensions

Oneof

如果你的消息中定义了很多字段,而且最多每次只能有一个字段被设置赋值,那么你可以利用 Oneof 特性来实现这种行为并能节省内存。

Oneof 字段除了拥有常规字段的特性之外,所有字段共享一片 oneof 内存,而且每次最多只能有一个字段被设置赋值。设置 oneof组中的任意一个成员的值时,其它成员的值被自动清除。 你可以用 case()或 WhickOneof()方法检查 oneof 组中哪个成员的值被设置了,具体选择哪个方法取决于你所使用的编程语言。

使用 Oneof

在.proto 文件中定义 oneof 需要用到 oneof 关键字,其后紧跟的是 oneof 的名字。下例中的 oneof 名字是 test_oneof:

message SampleMessage {
    oneof test_oneof {
          string name = 4;
          SubMessage sub_message = 9;
      }
}

然后你就可以往 oneof 定义中添加 oneof 的字段了。 你可以使用任何类型的字段,但是不要使用 repeated 可重复字段。

在编译后生成的代码中,oneof 字段和常规字段一样拥有 setter 或 getter 操作函数。根据你所选择的编程语言,你能够找到一个特殊方法函数用来检查是哪一个 oneof 被赋值了。 更多详情请参考 API 参考

Oneof 特性
  • 设置 oneof 组中某一个成员的值时,其它成员的值被自动清除。因此,当你的 oneof 组中有多个成员的时候,只有最有一个被赋
    值的字段拥有自己的值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
  • 如果解析器发现多个 oneof 组的成员被存储在介质上,只有最后一个成员被解析到消息中。
  • oneof 字段不能是 repeated 可重复的。
  • oneof 字段可以使用反射函数。
  • 如果你正在使用 C++,确认没有代码造成内存崩溃。 在下面的例子中,代码会造成内存崩溃。因为 sub_message 在调用 set_name("name")的时候已经被清除.
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
  • 在 C++中,如果你调用 swap()方法交换两个拥有 oneof 的消息,被交换的消息会拥有对方的 oneof 实例:在下面的例子中,msg1 会拥有一个sub_message,而 msg2 拥有 name。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
向后兼容的问题

在添加或删除 oneof 字段的时候,需要格外小心。 如果检查一个 oneof 值的时候返回 None 或 NOT_SET,意味着这个 oneof 没有字段被赋值过或者如果被赋值过但是使用的是其它版本的 oneof。 没有办法区分它们,因此没法区分传输介质上的一个未知字段是不是 oneof 成
员。(译注:on the wire,我翻译为传输介质不知道准确否?)

标签重用的问题
  • 将字段移进或移出 oneof 组: 在消息序列化和解析之后,可能会丢失某些信息(某些字段会被清除)。
  • 删除一个 oneof 字段之后又添加回去: 在消息序列化和解析之后,可能会清除当前为 oneof 设置的值。
  • 分割或合并 oneof: 这种情况和移动常规字段到 oneof 组的类似。
Map

如果你想定义 map 类型的数据,ProtoBuf 提供非常便捷的语法:

map<key_type, value_type> map_field = N;

其中,key_type 可以任意整数类型或字符串类型(除浮点类型或 bytes 类型意外的任意 标准类型 )。value_Type 可以是任意类型。
例如,你可以创建一个 map 类型的 projects,关联一个 string 类型和一个 Project 消息类型(译注:作为键-值对),用 map 定义如下:

map<string, Project> projects = 3;

map 类型的字段不可重复(不能用 repeated 修饰)。 需要注意的是:map 类型字段的值在传输介质上的顺序和迭代器的顺序是未定义的,你不能指望 map 类型的字段内容按指定顺序排列。
proto3 目前支持的所有语言都 map 类型的操作 API。 欲知详情,请根据你所选择的编程语言阅读 API 参考 中相关内容。

向后兼容性

在传输介质上,下面的代码等效于 map 语法的实现。因此,即使目前 ProtoBuf 不支持 map 可重复的特性依然可以用下面这种(变通的)方式来处理:

message MapFieldEntry {
    key_type key = 1;
    value_type value = 2;
}
repeated MapFieldEntry map_field = N;

你可以在.proto 文件中选择使用 package 说明符,避免 ProtoBuf 消息类型之间的名称冲突。(译注:这个和 java 里面包的概念以及 C++中命名空间的作用一样)

package foo.bar;
message Open { ...}

你可以在消息类型内部使用包描述符的引用来定义字段:

message Foo {
...
foo.bar.Open open = 1;
...
}

包描述符对编译后生成的代码的影响依赖于你所选择的编程语言:

  • 在 C++中,生成的类被包装在以包描述符命名的命名空间中。 例如,Open 类将会出现在命名空间 foo::bar 中。
  • 在 Java 里面,除非你在.proto 文件中显示声明了 option java_package,否则这个包名会被 Java 直接采用。
  • 在 Python 里面,包名被直接忽略了,因为 Python 模块的组织依据的是 Python 文件的在文件系统中的存放位置。
  • 在 Go 中,除非你在.proto 文件中显示声明了 option go_package,否则这个包名会被 Go 作为包名使用。
  • 在 Ruby 里面,所生成的类被包裹在嵌套的 Ruby 命名空间中,包名被转换为 Ruby 大写样式(第一个字母大写,如果第一个不是字母字符,添加PB_前缀)。例如,Open 类会出现在命名空间 Foo::Bar 中。
  • 在 JavaNano 中,除非你在.proto 文件中显示声明了 option java_package,否则这个包名会被作为 Java 包名使用。
包和名称解析

ProtoBuf 解析包和名称的方式与 C++语言类似:最内层的最先被查找,然后是次内层,以此类推。每个包对于其父包来说都是“内部”的。以一个'.'(英文句点)开头的包和名称 (例如 .foo.bar.Baz)表示从最外层开始查找。

protoc 编译器通过解析被导入的.proto 文件来解析所有的类型名称。 任何一种被支持的语言的代码生成器都知道如何正确地引用每一种类型,即使该语言具有不同的作用域规则。

定义服务

如果想在 RPC(远程过程调用)系统中使用自定义的消息类型,你可以在.proto 文件中定义一个 RPC 服务,protoc 编译器就会根据你选择的编程语言生成服务器接口和存根代码。例如,如果你想定义一个 RPC 服务,拥有一个方法来根据你的 SearchRequest 返回SearchResponse,可以在你的.proto 文件中这样定义:

service SearchService {
      rpc Search (SearchRequest) returns (SearchResponse);
}

与 ProtoBuf 最佳搭配的 RPC 系统是 gRPC :一个 Google 开发的平台无关语言无关的开源 RPC 系统。gRPC 和 ProtoBuf 能够非常完美的配合,你可以使用专门的 ProtoBuf 编译插件直接从.proto 文件生成相关 RPC 代码。

如果你不想使用 gRPC,你也可以用自己的 RPC 来实现和 ProtoBuf 协作。 更多的关于RPC 的信息请参考 Proto2 语言指南 。

现在也有很多第三方的采用 ProtoBuf 的 RPC 项目在开展中。 我们已知的这类项目列表,请参考 第三方插件 WIKI 主页

JSON 映射

Proto3 支持标准的 JSON 编码,在不同的系统直接共享数据变得简单。下表列出的是基础的类型对照。

在 JSON 编码中,如果某个值被设置为 null 或丢失,在映射为 ProtoBuf 的时候会转换为相应的 默认值 。 在 ProtoBuf 中如果一个字段是默认值,在映射为 JSON 编码的时候,这个默认值会被忽略以节省空间。可以通过选项设置,使得 JSON 编码输出中字段带有默认值。

选项

你可以在.proto 文件中可以声明若干选项。 选项不会改变整个声明的含意,但可能会影响在特定的上下文中它的处理方式。 完整的可用选项列表在文件google/protobuf/descriptor.proto 中定义。

一些选项是文件级的,它们应该声明在最顶级的作用域范围内,不要在消息、枚举或服务定义中使用它们。一些选项是消息级的,应该在消息结构内声明。一些选项是字段级的,它们应该声明在字段定义语句中。选项也可以声明在枚举类型、枚举值、服务类型、服务方法中。然而,目前没有任何有用的选项采用这种方式。

下面列出最常用的一些选项:

  • java_package (文件级选项):这个选项声明你想生成的 Java 类拥有的包名。 如果没有在.proto 文件中显示地声明 java_package 选项,默认采用 proto 的包名(在.proto 文件中用 package 关键字定义)作为生成的 Java 类的包名。 然而,通常情况下 proto 包名不是好的 Java 格式的包名,proto 包名一般不会是以 Java 包名所期望的反向域名的格式命名。如果没有生成 Java 代码,这个选项没有任何作用。
option java_package = "com.example.foo";
  • java_outer_classname (文件级选项):这个选项定义了你想要编译生成的 Java 输出类的最外层类名(也是文件名)。 如果没有在.proto文件中定义 java_outer_classname 选项,默认将.proto 文件名转换为驼峰格式的文件名(例如 foo_bar.proto 编译生成 FooBar.java) 。如果没有生成 Java 代码,这个选项没有任何作用。
option java_outer_classname = "Ponycopter";\
  • optimize_for (文件级选项): 它的取值可以是 SPEED,CODE_SIZE,或 LITE_RUNTIME。 这个选项按如下方式影响 C++和 Java 的代码生成
    器(也可能影响第三方的生成器):
  • SPEED --- 默认值: protoc 编译器会根据你定义的消息类型生成序列化、解析和执行其它常规操作的代码。这些代码是高度优化了的。
  • CODE_SIZE:protoc 编译器会采用共享、反射技术来实现序列化、解析和其它操作的代码,以生成最小的类。 因此,所生成的代码比采用选项 SPEED 的要小得多,但是操作性能降低了。类的公共成员函数依然是一样的(用 SPEED 优化选项也是如此)。 这种模式是最有用的应用情况是:程序包含很多.proto 文件,但并不追求所有模块都有极快的速度。
  • LITE_RUNTIME:protoc 编译器生成的类,仅依赖"lite"运行时库 (libprotobuf-lite , 而不是 libprotobuf)。"lite"运行时库比完整库 (约一个数量级小) 小得多,但省略了某些功能,如描述符和反射。这对于运行在像手机这样的有空间限制的平台上的应用程序特别有用。 protoc 编译器仍将为所有方法生成最快速的代码,这和在 SPEED 模式一样。生成的类将为每一种语言实现 MessageLite版本的接口,只提供了完整的 Message 接口的一个子集的实现。选项 optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件级选项): 为生成的 C++代码 启用 arena 内存管理功能。(译注:Arena Allocation,是一种 GC 优化技术,它可以有效地减少因内存碎片导致的 Full GC,从而提高系统的整体性能。)
  • objc_class_prefix(文件级选项):这个选项用来设置编译器从.proto 文件生成的类和枚举类型的名称前缀。这个选型没有默认值。您应该使用 3–5 个大写字母作为前缀来自 Apple 的建议 。 请注意:所有的 2 个字母的前缀由苹果公司保留使用。
  • packed (字段级选项): 这个选项 如果被设置为 true ,对于基本数值类型的可重复字段可以获得更紧凑的编码。使用此选项,没有负面影响。然而,请注意,在 2.3.0 版本之前是用此选项解析器会忽略被打包的数据。因此,更改现有字段为 packed,会破坏数据传输的兼容性。对于 2.3.0 及以后的版本,这种改变是安全的并且解析器能够同时接受这两种格式的打包字段的数据,但要小心,如果你要处理旧的程序使用了旧版本的 protobuf。
repeated int32 samples = 4 [packed=true];
  • deprecated (字段选项): 这个选项如果设置为 true ,表示该字段已被废弃,你不应该在后续的代码中使用它。 在大多数语言中这没有任何实际的影响。在 Java 中,它会变@Deprecated 注释。将来,其他特定于语言的代码生成器在为被标注为 deprecated 的字段生成操作函数的时候,编译器在尝试使用该字段的代码时发出警告。如果不希望将来有人使用使用这个字段,请考虑用 reserved 关键字 声明该字段。
int32 old_field = 6 [deprecated=true];
自定义选项

ProtoBuf 允许你使用自定义选项。这是一个 高级的功能,大多数人不需要。如果你认为你需要创建自定义选项,请参见 Proto2 语言指南 中 更详细的信息。请注意,创建自定义选项使用 extensions 关键字 ,只能用在 proto3 中的创建自定义选项。

编译创建类

要想从.proto 文件 生成 Java,Python,C++、 Go、 Ruby,JavaNano,Objective-C 或 C#代码,你需要在.proto 文件中定义自己的消息类型,并且使用 protoc 编译器来编译它。如果你还没有安装编译器,请下载安装包并按照自述文件中的说明来执行安装。

对于 Go 语言,还需要为编译器安装特殊的代码生成器插件: 请访问它在 Github 上的代码仓储 golang/protobuf ,下载并按照 安装提示操作。
编译器的调用格式如下:

--javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指在.proto 文件中解析 import 指令时的查找目录路径。如果省略,则使用当前目录。通过多次传递参数--proto_path,可以实现在多个导入目录中按顺序查找。-I=IMPORT_PATH 是 --proto_path 的缩写形式。
  • 您可以提供一个或多个 输出指令 :
  • --cpp_out 在目录 DST_DIR 中 生成 C++代码中 。更多细节,请参考 C++代码的生成。
  • --java_out 在目录 DST_DIR 中 生成 Java 代码。 更多细节,请参考 Java 代码的生成。
  • --python_out 在目录 DST_DIR 中 生成 Python 代码 。更多细节,请参考 Python 代码的生成。
  • --go_out 在目录 DST_DIR 中 生成 Go 代码 。 Go 代码的生成参考文档即将推出!
  • --ruby_out 在目录 DST_DIR 中生成 Ruby 代码。Ruby 代码的生成参考文档即将推出!
  • --javanano_out 在目录 DST_DIR 中 生成 JavaNano 代码 。 JavaNano 的代码生成器有很多选项,你可以自定义编译输出,欲知详情请参考代码生成器的自述文件 。 JavaNano 代码的生成参考文档即将推出!
  • --objc_out 在目录 DST_DIR 中生成 Objective-C 代码。Objective-C 代码的生成参考文档即将推出!
  • --csharp_out 在目录 DST_DIR 中生成 C#代码。C# 生成代码的参考文档即将推出!
    额外福利,如果 DST_DIR 以.zip 或 .jar 结尾,编译器会将代码输出写入到给定名称的单个 ZIP 格式压缩文档中。.jar 输出也将根据 Java JAR 规范的要求提供 manifest 清单文件。请注意,如果输出路径中具有与编译输出文件同名的文件,编译器将直接覆盖它而不会保存为另外一个名字。
  • 您必须提供一个或多个.proto 文件作为编译输入。多个.proto 文件可以同时编译。虽然文件是相对于当前目录来命名的,每个文件至少要在一个 IMPORT_PATH 指定的路径范围内,这样编译器可以为其确定规范的名称。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容