protobuf 学习指南(proto3)

8,375 阅读31分钟

定义消息类型

首先,创建一个*.proto文件

//声明使用proto3语法;如果没有指定这个,编译器会使用proto2。这个指定语法必须是文件的非空非注释的第一行
syntax = "proto3";

//我们定义一个SearchRequest消息格式有3个字段
message SearchRequest{
  //字符串(string)类型的 query 字段
  string query = 1;
  //32位整型(int32)类型的 page_number 字段
  int32  page_number = 2;
  //32位整型(int32)类型的result_per_page 字段
  int32 result_per_page = 3;
}

指定字段类型

在上面的例子中,所有字段都是标量类型(scalar types)

  • 两个整型变量:page_numberresult_per_page
  • 一个字符串变量:query

当然还可以为字段指定其他和成类型,包括枚举(enumerations)或其他消息类型

分配标识号

在消息定义的时候每个字段都有一个唯一的编号。这些字段编号用于在消息二进制格式中标识字段,音消息类型被使用,就不应更改。

请注意:1-15范围内的字段编号占一个字节进行编码,包括字段编号和字段类型

16-2047范围内的字段编号占用两个字节

因此建议应该更多的使用1-15

可以指定最小字段编号为1,最大字段编号为2^29 - 1536,870,911

不能使用19000-19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber)的标识号,protobuf协议实现对这些进行了预留,如果非要使用,编译时就会报警,同样,也不能使用任何以前保留(reserved)的字段编号。

指定字段规则

所指定的消息字段修饰符必须是一下几种:

  • sigular:一个格式良好的消息应该有0个或者1个这种字段(但不能超过1个)。这是proto3语法的默认字段规则。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

proto3中,repeated的标量域默认情况下使用packed

添加更多的消息类型

在一个.proto文件中可以定义多个消息类型

syntax = "proto3";

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

message SearchResponse{
    string result = 1;
}

添加注释

.proto文件中添加注释,可以使用C/C++/Java风格的(//)和(/** ... */)语法格式

//声明使用proto3语法;如果没有指定这个,编译器会使用proto2。这个指定语法必须是文件的非空非注释的第一行
syntax = "proto3";

//我们定义一个SearchRequest消息格式有3个字段
message SearchRequest{
  //字符串(string)类型的 query 字段
  string query = 1;
  //32位整型(int32)类型的 page_number 字段
  int32  page_number = 2;
  //32位整型(int32)类型的result_per_page 字段
  int32 result_per_page = 3;
}

message SearchResponse{
  /**
    字符串(string)类型的 result 字段
   */
  string result = 1;
}

保留字段(Reserved)

如果通过完全删除字段或将其注释掉来更新消息类型,那么用户可以在对该类型进行自己的更新时可能会重用字段号。如果他们以后加载相同.proto的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。为了确保不会发生这种情况,可以指定保留已删除字段的字段编号(或是名称,这也可能导致JSON序列化问题)。用户试图使用这些字段标识符,编译器将会报错。

message Foo {
  reserved 2, 15, 9 to 11; //保留字段号
  reserved "foo", "bar"; //保留字段名
  string a = 2; // 编译报错,因为 2 已经被标为保留字段
  string foo = 1; //编译报错,因为 "foo" 已经被标位保留字段
}

注意,不能在同一个reserved语句中同时使用字段名和字段号。

.proto文件生成了什么?

当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

  • C++:编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
  • Java:编译器为每个消息类型生成一个.java的文件,以及Builder用于创建消息类实例的特殊类。
  • Kotlin:除了Java生成的代码之外,编译器还未每个消息生成一个.kt文件,其中包含可用于简化创建消息实例的DSL
  • Python:有点不太一样,Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • go:编译器会位每个消息类型生成了一个.pd.go文件。
  • Ruby:编译器会为每个消息类型生成了一个.rb文件。
  • Objective-C:编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
  • C#:编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。
  • Dart:编译器会为.pb.dart文件中的每种消息类型生成一个带有类的文件。

标量类型(scalar types)

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32使用可变长度编码。 编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint32。Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32Fixnum or Bignum (as required)intintegerint
int64使用可变长度编码。 编码负数效率低下 - 如果您的字段可能具有负值,请改用 sint64。Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
uint32使用可变长度编码。Uses variable-length encoding.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
uint64使用可变长度编码。Uses variable-length encoding.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sint32使用可变长度编码。 有符号整数值。 这些比常规 int32 更有效地编码负数。Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32Fixnum or Bignum (as required)intintegerint
sint64使用可变长度编码。 有符号整数值。 这些比常规 int64 更有效地编码负数。Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
fixed32总是四个字节。 如果值通常大于 228,则比 uint32 更有效。Always four bytes. More efficient than uint32 if values are often greater than 228.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
fixed64总是八个字节。 如果值通常大于 256,则比 uint64 更有效。Always eight bytes. More efficient than uint64 if values are often greater than 256.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sfixed32总是四个字节。Always four bytes.int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64总是八个字节。Always eight bytes.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.stringStringstr/unicode[5]stringString (UTF-8)stringstringString
bytes可以包含不超过 232 的任意字节序列。May contain any arbitrary sequence of bytes no longer than 232.stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstringList
  • [1] Kotlin 使用来自 Java 的相应类型,即使是无符号类型,以确保在混合 Java/Kotlin 代码库中的兼容性。
  • [2] 在 Java 中,无符号 32 位和 64 位整数使用它们的有符号对应物表示,最高位简单地存储在符号位中。
  • [3] 在所有情况下,为字段设置值将执行类型检查以确保其有效。
  • [4] 64 位或无符号 32 位整数在解码时总是表示为 long,但如果在设置字段时给出 int,则可以是 int。在所有情况下,该值必须适合设置时表示的类型。见[2]。
  • [5] Python 字符串在解码时表示为 unicode,但如果给出 ASCII 字符串,则可以是 str(这可能会发生变化)。
  • [6] 64 位机器上使用整数,32 位机器上使用字符串。

默认值

在解析消息时,如果编码的消息不包含特定的singular元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值与类型有关:

  • 对于string,默认值为空字符串。
  • 对于bytes,默认值是空bytes
  • 对于bool,默认值为false
  • 对于数字类型,默认值为0
  • 对于枚举,默认值为第一个定义的枚举变量,其值必须为0
  • 对于消息字段,未设置。它的值取决于语言。 注意: 对于标量消息字段来说,一旦消息被解析,就无法判断该字段是真实被设为默认值(例如bool变量被设为false)还是就没有设置:在定义消息类型时需要牢记这一点。例如,如果你不想在默认情况向执行某种行为,那么就不要用boole被设置为false来切换这些行为。同时,如果一个标量消息字段被设为它的默认值,那么改值在传输时将不会被序列化。

枚举

当你在定义消息类型时,你可能想让它的字段只是用预定义列表中的一个值。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  
  enum Role {
    RESIDENT = 0;
    CLEANER = 1;
    ADMIN = 2;
    OWNER = 3;
    SU = 4;
  }
  Role role = 4;
}
  • 必须有一个零值,这样我们可以把0作为数字的默认值
  • 零值小时第一个元素,以便于proto2语义兼容,其实第一个枚举值始终是默认值。

还可以对枚举常量设置别名。需要设置option allow_aliastrue, 否则 protocol编译器会产生错误信息。

enum Role {
  option allow_alias = true;
  RESIDENT = 0;
  HOUSEHOLDER = 0;
  ENFORCER = 1;
  GATHERER = 2;
  PC_ADMIN = 3;
}

enum UserRole {
  RESIDENT = 0;
  HOUSEHOLDER = 0;
  ENFORCER = 1;
  GATHERER = 2;
  PC_ADMIN = 3;
}
  • 枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
  • 你可以在一个消息定义的内部定义枚举,你也可以在消息的外部定义枚举类型,这样这些枚举值可以在同一.proto文件中定义的任何消息中重复使用。当然也可以在一个消息使用在另一个消息中定义的枚举类型——采用MessageType.EnumType的语法格式。但是在同一个.proto文件中定义枚举类型,枚举类型的值不能相同。编译器会认为已经存在。
  • 当你编译一个使用了enum.proto文件时,生成的代码中会包含JavaC++对应的enum,针对Python的特定的EnumDescriptor类,用来在执行生成的类中创建一系列包含数值的符号常量。

在反序列化期间,无法识别的enum值将保留在消息中,尽管在反序列化消息时如何表示该值取决于语言。在支持指定符号范围之外使用值的开放枚举类型的语言,如c++Go,未知的枚举值只是作为其基础整数表示形式存储。在具有封闭枚举类型的语言,如Java,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问底层整数。在任何一种情况下,如果消息被序列化,未被识别的值仍将与消息一起序列化。

保留变量

如果你通过完全删除或注释一个字段来更新枚举类型时,那么之后的用户在更新他们自己的类型时将可以重用该字段的序号。如果之后他们使用旧版的.proto时,会引起严重的问题,包括数据损坏、隐私bug等。避免给问题的途径之一就是指明你要删除的字段需要(或者会在JSON序列化时会引起问题的名称)是reserved的,这样将来用户在使用这些字段时protocol buffer编译器就会告警。你可以指明你要保留的数字到可能的最大值(通过max关键字)得范围。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注意,不能在同一个reserved语句中混用字段名称和字段序号。

使用其他消息类型

你也可以使用其它消息类型作为字段类型。

例如,假如你想在SearchResponse消息中包含一个Result消息,你可以在同一个.proto文件中定义一个Result消息类型,然后在SearchResponse中声明一个Result类型的字段。

message SearchResponse {
  repeated Result results = 1;
}

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

导入定义

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

你可以通过从其它.proto文件中导入它们来使用这些定义。要使用其它.proto的定义,你需要在你的文件头部导入声明:

import "myproject/other_protos.proto";

默认情况下你只能使用直接导入的.proto文件中的定义。然而, 有时候你需要移动一个.proto文件到一个新的位置。现在,你可以在旧位置放置一个虚拟 .proto 文件,以使用命令 import public将所有导入转发到新位置,而不是直接移动 .proto 文件并在一次更改中更新所有调用点。导入包含 import public 语句的 proto 的任何人都可以导入公共依赖项。例如:

// 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

编译器在一系列指定的目录(命令行下通过-I / --proto_path标志指定)下查找导入的文件。如果没有指定,编译器将在当前目录下查找。通常你应该将--proto_path标志设为项目的根目录,并且使用全路径导入。

使用proto2消息类型

可以在你的proto3消息中导入并使用proto2的消息类型,反之亦可。然而proto2的枚举不能再proto3中直接使用(可以在导入的proto2的消息中使用)。

嵌套类型

你可以在一个消息类型中定义并使用其它的消息类型,就像下面的例子 Result消息定义在SearchResponse中:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 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;
    }
  }
}

更新消息类型

如果已存在的消息类型不再满足你的需求 例如,你想在消息格式中添加新的字段,但还想使用就格式生成的代码。别担心!在不破坏你现有代码的基础上更新消息类型很简单。只需要记住下面的规则:

  • 不要修改已有字段的序号。
  • 如果你新增了字段,任何使用旧格式序列化的消息仍能被新生成的代码解析。你应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。类似地,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时简单地忽略新字段。
  • 字段可以被移除,只要它的序号不再被你更新的消息类型使用。你可以重命名字段,或者添加前缀"OBSOLETE_",或者保留字段号,这样.proto的未来用户就不会意外地重用该号码。
  • int32uint32int64uint64bool都是兼容的——这意味着你可以将一个字段从这些类型中的一种更改为另一种,而不会中断向前或向后兼容。如果从连线中解析出一个不适合相应类型的数字,那么你将获得与在c++中将该数字强制转换为该类型相同的效果(例如,如果将64位数字读取为int32,那么它将被截断为32位)。
  • sint32sint64是相互兼容的,但不与其它整型兼容。
  • stringbytes兼容,bytesUTF-8兼容。
  • 如果字节包含消息的编码版本,则嵌入的消息与bytes兼容。
  • fixed32sfixed32fixed64sfixed64兼容。
  • 在传输格式中enumint32uint32int64uint64兼容(注意变量不兼容的部分将被截断)。然而需要留意的是在消息反序列化时,客户端代码会被区别对待:例如,尽管无法识别的proto3中的enum类型会被保存在消息中,但是在消息反序列化时,它是如何表示这取决于语言。int字段总会保留它的值。修改new oneof成员中的单个变量是安全且二进制兼容的。如果您确定没有代码一次设置多个字段,那么将多个字段移动到一个新的字段中可能是安全的。将任何字段移动到现有字段中都是不安全的。

未知字段

未知字段是protocol buffer在序列化数据时无法解析的数据。例如,当旧的二进制代码在解析带有新字段的新二进制代码发送的数据时,这些新字段将成为旧二进制代码中的未知字段。

最初,在解析时proto3总是丢弃未知字段,但在3.5版本之后,重新引入了未知字段的保留来匹配proto2的行为。在3.5及之后的版本中,在解析时未知字段会被保留并将其包含的序列化的输出中。

Any

Any消息类型允许你在没有.proto定义的情况下将你的消息类型作为嵌入类型使用。Any包含作为bytes的任意序列化消息,以及充当全局惟一标识符并解析为该消息类型的URL要使用Any类型,你需要导入google/protobuf/any.proto

import "google/protobuf/any.proto";

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

给定消息类型的默认类型 URLtype.googleapis.com/_packagename_._messagename_

不同的语言实现将支持运行时库助手以类型安全的方式打包和解包 Any 值——例如,在 Java 中,Any 类型将具有特殊的pack()unpack(),而在 C++ 中有PackFrom()UnpackTo()方法:

// 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.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

目前,用于处理 Any 类型的运行时库正在开发中。

如果您已经熟悉proto2 语法,则Any可以保存任意 proto3 消息,类似于可以允许扩展的proto2 消息。

Oneof

如果有有一个包含多个字段的消息,在同一时间最多只能设置一个字段,那么你可以通过使用oneof特性强制执行此行为并节省内存。

除所有字段共享同一个Oneof内存和最多同时只能设置一个字段外,Oneof字段与常规字段类似。设置oneof字段中的任何成员都将自动清除其它成员。根据你所使用的的语言不同,你可以使用(必要时)特定的case()WhichOneof()方法来检查Oneof中的哪个变量被设置。

使用OneOf

要在你的.proto文件中定义一个Oneof字段,你可以在的oneof关键字后跟上你的oneof名称,就如下面的test_oneof

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

之后你可以添加你的oneof字段到oneof定义中。除了不能使用repeated字段,你可以使用任意字段。

在你生成的代码中,oneof字段有着与常规字段一样的getterssetters。必要时,你也可以使用特定的方法来确定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的多个成员,在解析消息时仅使用最后看到的成员。
  • 不能使用repeated
  • oneof字段使用反射 APIs
  • 如果你设置oneof字段为默认值(比如设置int32字段为0),该字段的case将被设置,且在传输时被序列化。
  • 如果你使用C++,请确保你的代码不会引起内存崩溃。下面的代码会引起崩溃,因为在调用set_name()方法时sub_message已经删除。
    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • 同样是在C++中,如果你使用Swap()来交换两个带有oneofs的消息,每个消息会以另一个的oneof case结束:在下面的例子中,msg1将拥有sub_messagemsg2将拥有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的成员。

Tag重用问题

  • 移入/移出字段到oneof:在消息序列化和解析后,你可能会丢失部分消息(有些字段被清理了)。但是,你可以安全地将单个字段移动到一个新的oneof字段中,如果知道只设置了一个字段,则可以移动多个字段。
  • 删除一个oneof字段后有添加:在消息序列化和解析后,可能会将你当前的设置清零。
  • 切割/合并 oneof:与移动常规字段问题相似。

Maps

如果你想创建一个关联映射作为你的数据定义的一部分,protocol buffers提供了一个方便快捷的语法:

map<key_type, value_type> map_field = N;

key_type可以是任意的integralstring类型(即除了浮点型和bytes外的所有标量类型)。注意enum不是有效的key_typevalue_type可以是除了其它Map外的所有类型。

那么,假如你想创建一个项目映射,每个项目关联一个string键,定义如下:

map<string, Project> projects = 3;
  • Map字段不可以是repeated
  • 映射值的网络格式排序和映射迭代排序是未定义的,所以在特定的排序中你不能依赖你的映射元素组成。
  • .proto生成文本格式时,映射根据键排序。数字键按数字大小排序。
  • 从网络解析/合并时,如果键有多个副本,那么使用最后遇到的键。当从文本格式中解析映射时,如果键存在副本,则可能解析失败。
  • 如果你仅提供了Map字段的键而没有提供值,字段序列化时的行为因语言而异。在C++JavaPython中,值会被序列化为该类型的默认值,在其它语言中并不会被序列化。

向后兼容

map 语法等效于以下内容,因此不支持 map 的协议缓冲区实现仍然可以处理您的数据:

message MapFieldEntry {
  optional key_type key = 1;
  optional value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成和接受上述定义可以接受的数据。

packages

你可以在.proto文件中添加package说明符来避免协议消息类型键的名称冲突。

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

之后在定义你的消息类型字段时,你可以使用package说明符:

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

package说明符影响生成代码的方式依赖于你所选的语言:

  • C++中,生成的类会被打包到C++的命名空间中。例如:Open位于foo::bar命名空间中。
  • Java中,package作为Java包使用,除非在.proto文件中额外提供option java_package
  • Python中,package指令会被忽略,Python模块是根据它们在文件系统中的位置来组织的。
  • Go中,package将被用作Go的包名,除非在.proto文件中额外提供option go_package
  • Ruby中,生成的类会被打包嵌入到Ruby的命名空间中,并转换为所需的Ruby大小写样式(第一个字母大写;如果第一个字符不是字母,PB_是前缀)。例如:Open位于foo::bar命名空间中。
  • C# 中,package在被转换为PascalCase后作为命名空间使用,除非在.proto文件中额外提供option csharp_namespace

包和名称解析

Protocol buffer语言中的类型名称解析类似于C++:首先在最内层查找,之后是下一层,一次类推,每个包在其父包的“内部”。“.”开头(例如,.foo.bar.Baz)意味着从最外层作用域开始查找。

Protocol buffer编译器通过导入的.proto文件来解析所有的类型名称。即使有着不同的作用域规则,各语言生成的代码也知道如何每种类型该如何使用。

定义服务

如果你现在RPC(远程调用)系统中使用你的消息类型,你可以在.proto文件中定义RPC服务接口,之后protocol buffer编译器会生成所选语言的服务接口代码和存根。比如,你要定义一个RPC服务,它使用你的SearchRequest并返回SearchResponse,在.proto文件中你可以这样定义:

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

使用protocol buffer最直接的RPC系统是gRPC:由Google开发的,与语言和平台无关的开源RPC系统。gRPCprotocol buffer协同良好,它允许你使用特殊的protocol buffer插件直接从.proto文件中生成相关的RPC代码。

如果你不想使用gRPC,你也可以在你自己的RPC实现中使用protocol buffer

也有一些正在进行的第三方项目来为protocol buffer开发RPC实现。

JSON Mapping

Proto3支持Json编码规范,这使得在不同系统间共享数据变得更加方便。在下面的表中,将逐个类型地描述编码。

如果一个值在JSON编码中丢失或为null,在解析到protocol buffer时它会被解释为合适的默认值。如果protocol buffer中的字段有默认值,那么在Json编码的数据中将默认省略该字段,以节省空间。在Json编码输出中,实现可以提供带有默认字段的选项。

proto3JsonJson示例备注
messageobject{"fooBar":v,"g":null,_}生成Json对象。消息字段名称被映射为lowerCamelCase并成为Json对象的键。如果指定了json_name字段选项,则指定的值将被作为键使用。解析器既接受lowerCamelCase名称(或使用json_name指定的名称),也接受原生的proto字段名称。所有字段类型都可接受null,并被视为该类型的默认值。
enumstring"FOO_BAR"使用proto中指定的enum值名称。解析器既接受枚举名称,也接受整数值。
map<K,V>object{"K":v,_}所有的键都被转换成string。
repeated Varray[v, ...]null被当做空列表[]。
booltrue,falsetrue,false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"Json值会变成使用添加padding的标准base64编码的string。标准的或url安全的base64编码,带/不带padding也都可以接受。
int32,fixed32,uint32number1,-10,0Json值会变成十进制的数字。数字或string都可被接受。
int64,fixed64,uint64string"1","-10"Json值会变成十进制的string。数字或string都可被接受。
float,doublenumber1.1,-10.0,0,"NaN","Infinity"Json值会变成数字或"NaN"、"Infinity"、"-Infinity"其中之一。数字或string都可被接受。指数表示法也被接受。
Anyobject{"@type":"url","f":v,...}如果Any包含的值有特定的Json映射,它将被转换为如下格式:{"@type": xxx, "value": yyy}。否则,该值会被转换为Json对象,且”@type“字段会被插入以指示实际数据类型。
Timestampstring"1972-01-01T10:00:20.021Z"使用RFC 3339,其生成的输出总是Z-normalized后的,并使用0、3、6或9位小数。除“Z”以外的偏移量也可以接受。
Durationstring"1.000340012s","1s"根据所需的精度,生成的输出总是包含0、3、6或9位小数,跟后缀”s“。只要符合纳秒精度和后缀“s”的要求,任何小数(也可以没有)都可以接受。
Structobject{ ... }任意的Json对象。
Wrapper typesvarious types2,"2","foo",true,"true",null,0,...包装器使用与包装的原始类型相同的JSON表示,但在数据转换和传输期间允许并保留null。
FieldMaskstring"f.fooBar,h"
ListValuearray[foo,bar, ...]
Valuevalue任意的Json值
NullValuenullJson null
Emptyobject{}任意的空Json对象。

JSON 选项

Proto3Json实现可支持下列选项:

  • 带默认值得空字段:默认情况下,在proto3 JSON输出中会省略具有默认值的字段。实现可以提供一个选项来覆盖此行为,并使用其默认值输出字段。
  • 忽略未知类型:默认情况下,Proto3 Json解析器会驳回未知字段,但在解析时可以提供选项来忽略未知字段。
  • 使用proto字段来代替lowerCamelCase名称:默认情况下,proto3 Json的输出应该将字段名转换为lowerCamelCase并作为Json名称使用。该实现可以通过提供选项来使用proto字段作为Json名称。Proto3 Json解析器被设计为可同时接受转换后的lowerCamelCase名称和proto字段名称。
  • 指明enum值作为整数而不是string:默认情况下,在Json输出中使用枚举值的名称。通过选项可指定使用数字代替枚举值。

options

.proto文件中的各个声明可以用许多选项进行注释。选项不会改变声明的总体含义,但可能影响在特定上下文中处理它的方式。可用选项的完整列表在google/protobuf/description.proto中定义。

有些选项是文件级别的,意味着它们应该写在开头位置,而不是在消息、枚举或服务定义中。有些选项是消息级别的,意味着它们应该写在消息定义中。有些选项是字段选项,意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法中,然而,当前不存在对Any有用的选项。 以下是一些最常用的选项:

  • java_package(文件级):这个包你想用来生成Java类。如果.proto文件中没有额外给出java_package选项,默认情况下使用proto包(在.proto文件中使用package关键字指明的)。然而通常情况下proto包并不适合Java包,因为不希望proto包以反向域名展开。如果不生成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";
    
  • java_multiple_files(文件级):如果为 false,则只.java为该.proto文件生成一个文件,所有 Java 类/枚举/等。为顶级消息、服务和枚举生成的消息将嵌套在外部类中。如果为 true.java将为每个 Java 类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto文件生成的包装器 Java 类将不包含任何嵌套类/枚举/等。这是一个布尔选项,默认为false。如果不生成 Java 代码,则此选项无效。

    option java_multiple_files = true;
    
  • optimize_for(文件选项):可以设置为SPEED、CODE_SIZE、 或LITE_RUNTIME。这会通过以下方式影响 C++Java 代码生成器(以及可能的第三方生成器):

    • SPEED(默认):Protocol buffer编译器会为你的消息类型生成序列化、解析和其它常用操作的代码。此代码高度优化。
    • CODE_SIZEProtocol buffer编译器会生成最小的类,其依赖共享、反射的代码来实现序列化、解析和其它操作。因此生成的代码比SPEED小很多,但操作也会比较慢。Classes仍会实现与SPEED模式相同的公共API。这种模式在包含大量.proto文件且不是所有文件都需要快速生成的应用程序中最有用。
    • LITE_RUNTIMEProtocol buffer编译器依赖“轻量的”运行时库(使用libprotobuf-lite而不是libprotobuf)。lite运行时比完整的库小得多(大约小一个数量级),但是忽略了某些特性,比如描述符和反射。这对于在受限平台(如手机)上运行的应用程序尤其有用。编译器仍然会像在SPEED模式下那样生成所有方法的快速实现。生成的类将仅用每种语言实现MessageLite接口,该接口只提供完整Message接口方法的一个子集。
    option optimize_for = CODE_SIZE;
    
  • cc_enable_arenas(文件级):为C++代码生成启用arena allocation

  • objc_class_prefix(文件级):为.proto文件生成的所有Objective-C类设置前缀。没有默认值。你应该使用苹果推荐的前缀,即3-5个大写字母。注意苹果保留所有的2个字母的前缀。

  • deprecated(文件级):如果设置为true,则表示该字段已被废弃,新代码不应使用该字段。在大多数语言中,该选项并没有实际效果。在Java中,会变成一个@Deprecated注释。将来,其它语言的代码生成器可能会在字段访问器上生成弃用注释,这将使得编译器在尝试使用该字段时发出警告。如果该字段将不再使用且你也不希望有新的用户使用它,那么可以考虑使用保留语句替换字段声明。

    int32 old_field = 6 [deprecated=true]
    

自定义option

Protocol buffer也允许你定义并使用自定义的选项。这是大多数人用不到的高级功能。如果你真的想创建自定义选项,查看Proto2 。注意,用扩展来创建自定义选项,这是proto3中唯一允许使用的自定义选项

编译生成

要从.proto文件中包括你定义的消息类型的JavaPythonC++GoRubyObjective-CC#代码,你需要允许protocol buffer编译器protoc。如果你还没安装编译器,需要先安装。对于Go,你还需要安装特定的生成插件。

Protocol编译器使用如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH指明解决import命令时查找.proto文件的路径。缺省使用当前目录。多个导入命令可以通过多次使用--proto_path选项指明,它们将按顺序检索。--proto_path可简写为-I=IMPORT_PATH

  • 你可以提供一个或多个输出命令:

  • 你必须提供一个或者多个.proto文件作为输入。多个.proto文件可以一次指定。虽然这些文件是相对于当前目录命名的,但是每个文件必须驻留在IMPORT_PATH中的一个,以便编译器可以确定它的规范名称。