protobuf3笔记 | 青训营

81 阅读28分钟

定义消息类型

入门

一个.proto文件可以添加多个message

//指定proto3,默认proto2
syntax = "proto3";

//生成的go文件处在哪个目录哪个包中
// .代表当前目录 service代表go文件的包名是service
option go_package = ".;service"

//message 方法名{}
message SearchRequest {
  //字段类型 字段名 = 标识号;
  string query = 1;//查询字符串
  int32 page_number = 2;//页数
  int32 result_per_page = 3;//页数
}

指定字段规则

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

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

在proto3中,repeated的标量域默认情况均使用packed。

Reserved 保留标识符

用于确保在未来更新协议时,某些字段的标识符不会被重新使用或覆盖。当你在一个旧版本的协议中使用了保留标识符,它将告诉编译器和解析器在该字段上保持警惕,以便它在未来版本中不会被意外地重新定义。

举个简单的例子来解释:假设我们有一个 Protobuf 协议,用于定义电子邮件消息,其中包含发件人和收件人的姓名、邮件内容以及时间戳。在现有版本中,我们这样定义:

protoCopy codemessage Email {
 string sender_name = 1;
 string recipient_name = 2;
 string message_body = 3;
 int64 timestamp = 4;
}

然后,我们决定在未来的版本中添加一个新的字段,用于标识邮件的重要程度。我们可能会这样做:

protoCopy codemessage Email {
 string sender_name = 1;
 string recipient_name = 2;
 string message_body = 3;
 int64 timestamp = 4;
 // 新的字段,表示邮件的重要程度
 int32 importance_level = 5;
}

但这样做会有一个问题。假设在现有版本中,有其他人使用了字段编号 5,那么他们的解析器可能会认为这个字段是原先定义的字段,而不是新添加的重要程度字段。为了避免这种冲突,我们可以使用保留标识符来指示该字段在当前版本不可用:

protoCopy codemessage Email {
 string sender_name = 1;
 string recipient_name = 2;
 string message_body = 3;
 int64 timestamp = 4;
 // 保留字段编号 5,确保在当前版本中不被使用
 reserved 5;
 // 新的字段,表示邮件的重要程度
 int32 importance_level = 6;
}

这样,如果其他人在现有版本中使用了字段编号 5,他们的解析器会收到警告,避免意外冲突。

注意:虽然保留标识符可以避免字段编号的冲突,但是应该尽量避免在现有字段之间插入新字段,因为这可能会导致现有解析器无法正确处理新字段,除非它们进行了升级和更新。通常,向后兼容性是设计 Protobuf 协议时需要特别关注的问题。

从.proto文件生成了什么?

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

  • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
  • 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
  • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • 对go来说,编译器会位每个消息类型生成了一个.pd.go文件。
  • 对于Ruby来说,编译器会为每个消息类型生成了一个.rb文件。
  • javaNano来说,编译器输出类似域java但是没有Builder类
  • 对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。
  • 对于C#来说,编译器会为每个消息类型生成了一个.cs文件,.proto文件中的每一个消息有一个对应的类。

你可以从如下的文档链接中获取每种语言更多API(proto3版本的内容很快就公布)。API Reference

标量数值类型

一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:

.proto TypeNotesC++ TypeJava TypePython Type[2]Go TypeRuby TypeC# TypePHP Type
doubledoubledoublefloatfloat64Floatdoublefloat
floatfloatfloatfloatfloat32Floatfloatfloat
int32使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
uint32使用变长编码uint32intint/longuint32Fixnum 或者 Bignum(根据需要)uintinteger
uint64使用变长编码uint64longint/longuint64Bignumulonginteger/string
sint32使用变长编码,这些编码在负值时比int32高效的多int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
sint64使用变长编码,有符号的整型值。编码时比通常的int64高效。int64longint/longint64Bignumlonginteger/string
fixed32总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。uint32intintuint32Fixnum 或者 Bignum(根据需要)uintinteger
fixed64总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。uint64longint/longuint64Bignumulonginteger/string
sfixed32总是4个字节int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
sfixed64总是8个字节int64longint/longint64Bignumlonginteger/string
boolboolbooleanboolboolTrueClass/FalseClassboolboolean
string一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。stringStringstr/unicodestringString (UTF-8)stringstring
bytes可能包含任意顺序的字节数据。stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstring

默认值

当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:

  • 对于strings,默认是一个空string
  • 对于bytes,默认是一个空的bytes
  • 对于bools,默认是false
  • 对于数值类型,默认是0
  • 对于枚举,默认是第一个定义的枚举值,必须为0;
  • 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide

对于可重复域的默认值是空(通常情况下是对应语言中空列表)。

注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。

查看generated code guide选择你的语言的默认值的工作细节。

枚举

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语义,枚举类的第一个值总是默认值。
enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // 取消注释下面这一行将在Google内部导致编译错误,并在外部导致警告消息。
  // RUNNING = 1;  
}
  1. 首先,你可以为不同的枚举常量指定相同的值。在示例中,EnumAllowingAlias 枚举中的 STARTEDRUNNING 都被赋予了相同的值 1。
  2. 如果你想在枚举中使用别名,即不同的常量具有相同的值,你需要在该枚举上设置 allow_alias 选项为 true。在示例中,EnumAllowingAlias 设置了 allow_alias = true;,所以它可以使用别名。
  3. EnumNotAllowingAlias 枚举中,没有设置 allow_alias 选项,因此不能使用别名。如果你尝试为 STARTEDRUNNING 设置相同的值,编译器会产生一个错误信息。

使用其他消息类型

你可以将其他消息类型用作字段类型,相当于嵌套。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

message SearchResponse {
  repeated Result results = 1;
}

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

导入定义

上面是同文件的,如果是其他,proto文件呢?

在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢? 你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

import "myproject/other_protos.proto";

默认情况下你只能使用直接导入的.proto文件中的定义. 然而, 有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。import public 依赖性会通过任意导入包含import public声明的proto文件传递。例如:

// 这是新的proto
// 所有的定义都被移到了这里
// 这是久的proto
// 这是所有客户端正在导入的包
import public "new.proto";
import "other.proto";
//客户端proto

import "old.proto";
// 现在你可以使用新久两种包的proto定义了。

通过在编译器命令行参数中使用-I/--proto_pathprotocal 编译器会在指定目录搜索要导入的文件。如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。

使用proto2消息类型

在你的proto3消息中导入proto2的消息类型也是可以的,反之亦然,然后proto2枚举不可以直接在proto3的标识符中使用(如果仅仅在proto2消息中使用是可以的)。

嵌套类型

(类似java中的内部类)

你可以在其他消息类型中定义、使用消息类型,在下面的例子中,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;
    }
  }
}

更新一个消息类

如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。

  • 不要更改任何已有的字段的数值标识。
  • 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)
  • 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
  • int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
  • sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8编码。
  • 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
  • fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
  • 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的。

Any

(类似java中的泛型或Object)

Any 类型在 Protocol Buffers 中允许你在没有指定 .proto 定义的情况下使用消息作为一个嵌套类型。Any 类型包含一个可以被序列化为字节序列的任意消息,以及一个 URL 作为全局标识符,用于解析消息类型。

先导包import "google/protobuf/any.proto";

假设我们有两个消息类型,一个用于表示网络错误信息 NetworkErrorDetails,另一个用于表示错误状态 ErrorStatus

首先,我们需要在 ErrorStatus 消息中使用 Any 类型字段:

protoCopy codesyntax = "proto3";
import "google/protobuf/any.proto";

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

ErrorStatus 消息有两个字段:message 用于表示错误信息的文本,details 是一个重复的 Any 类型字段,用于存储任意类型的消息。

现在,我们假设在程序中创建了一个 NetworkErrorDetails 消息,并将它存储在 Any 字段中:

javaCopy codeimport "google/protobuf/any.proto";

message NetworkErrorDetails {
  string error_code = 1;
  string error_message = 2;
}

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = NetworkErrorDetails.newBuilder()
    .setErrorCode("404")
    .setErrorMessage("Page not found")
    .build();

ErrorStatus status = ErrorStatus.newBuilder()
    .setMessage("An error occurred")
    .addDetails(Any.pack(details)) // 将 NetworkErrorDetails 存储在 Any 字段中
    .build();

在上面的例子中,我们创建了一个 NetworkErrorDetails 消息,并将其存储在 Any 字段中,然后构建了一个 ErrorStatus 消息,并添加了 Any 字段。

接下来,我们演示如何读取 ErrorStatus 消息中的 Any 字段,并将其反序列化为具体的消息类型:

javaCopy code// Reading an arbitrary message from Any.
ErrorStatus status = ...; // 假设我们收到了 ErrorStatus 消息的字节序列

for (Any detail : status.getDetailsList()) {
    // 检查消息是否为 NetworkErrorDetails 类型
    if (detail.is(NetworkErrorDetails.class)) {
        // 反序列化消息为 NetworkErrorDetails 类型
        NetworkErrorDetails networkError = detail.unpack(NetworkErrorDetails.class);
        // 处理 networkError 消息
        System.out.println("Error Code: " + networkError.getErrorCode());
        System.out.println("Error Message: " + networkError.getErrorMessage());
    } else {
        // 处理其他类型的消息
        System.out.println("Unknown message type");
    }
}

在上面的示例中,我们使用 getDetailsList() 方法遍历 ErrorStatus 消息中的 Any 字段列表。然后,我们使用 is() 方法检查消息是否为我们期望的 NetworkErrorDetails 类型。如果是,我们使用 unpack() 方法将 Any 字段反序列化为具体的 NetworkErrorDetails 消息,并进行相应处理。

总结起来,Any 类型允许我们在不事先知道具体消息类型的情况下使用消息作为嵌套类型,从而更加灵活地处理不同类型的消息。注意目前 Any 类型的动态库仍在开发中,因此在不同语言中可能会有所差异。如果你熟悉 proto2 语法,使用 Any 类型可以替代使用拓展(extensions)。

Oneof

使用

Oneof 是 Protocol Buffers 中的一种特性,用于在一个消息中管理多个可选字段,并确保同时只有一个字段被设置。使用 Oneof 可以节省内存,因为它们会共享内存,只有一个字段会被设置,而其他字段会被清除。

让我们通过一个例子来解释和演示 Oneof 的用法:

假设我们有一个消息类型 Shape,表示几何图形,其中包含多个可选字段,如 circlerectangletriangle,分别表示圆形、矩形和三角形。我们可以使用 Oneof 特性来管理这些可选字段:

protoCopy codesyntax = "proto3";

message Shape {
  //多选一
  oneof shape_type {
    Circle circle = 1;
    Rectangle rectangle = 2;
    Triangle triangle = 3;
  }
}

message Circle {
  double radius = 1;
}

message Rectangle {
  double width = 1;
  double height = 2;
}

message Triangle {
  double side1 = 1;
  double side2 = 2;
  double side3 = 3;
}

在上面的示例中,我们使用 oneof 关键字定义了一个 shape_typeOneof 字段,它包含了 CircleRectangleTriangle 三个可选字段。

  • 在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字(如上面例子所示)
  • 可以增加任意类型的字段, 但是不能使用repeated 关键字.

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.
  • 反射API对oneof 字段有效.
  • 如果使用C++,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message 已经通过set_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消息,每个消息,两个消息将拥有对方的值,例如在下面的例子中,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字段。

Tage 重用问题:

  • 将字段移入或移除oneof:在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除)
  • 删除一个字段或者加入一个字段:在消息被序列号或者解析后,这也许会清除你现在设置的oneof字段
  • 分离或者融合oneof:行为与移动常规字段相似。

映射(Maps)

如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。

例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:

map<string, Project> projects = 3;

  • Map的字段可以是repeated。
  • 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
  • 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
  • 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。

生成map的API现在对于所有proto3支持的语言都可用了,你可以从API指南找到更多信息。

向后兼容性问题

map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的:

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


repeated MapFieldEntry map_field = N;

包(Packages)

定义

当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:

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

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

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

包的声明符会根据使用语言的不同影响生成的代码。

  • 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
  • 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
  • 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。
  • 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中。
  • 对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java_package。
  • 对于C#包可以转换为PascalCase后作为名称空间,除非你在你的文件中显式的提供一个option csharp_namespace,例如,Open会在Foo.Bar名称空间中

包及名称的解析

Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。

ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。

定义服务

定义了一个接口,具体还需要自己实现

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

//	service 接口名称 {
//		rpc 方法名称(接收参数) returns (返回参数) {}
//	}

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

最直观的使用protocol buffer的RPC系统是gRPC一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。

如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC实现,你可以从proto2语言指南中找到更多信息

还有一些第三方开发的PRC实现使用Protocol Buffer。参考第三方插件wiki查看这些实现的列表。

JSON 映射

Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。

如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。

proto3JSONJSON示例注意
messageobject{“fBar”: v, “g”: null, …}产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值
enumstring“FOO_BAR”枚举值的名字在proto文件中被指定
mapobject{“k”: v, …}所有的键都被转换成string
repeated Varray[v, …]null被视为空列表
booltrue, falsetrue, false
stringstring“Hello World!”
bytesbase64 string“YWJjMTIzIT8kKiYoKSctPUB+”
int32, fixed32, uint32number1, -10, 0JSON值会是一个十进制数,数值型或者string类型都会接受
int64, fixed64, uint64string“1”, “-10”JSON值会是一个十进制数,数值型或者string类型都会接受
float, doublenumber1.1, -10.0, 0, “NaN”, “Infinity”JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受
Anyobject{“@type”: “url”, “f”: v, … }如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{"@type": xxx, "value": yyy}否则,该值会被转换成一个JSON对象,@type字段会被插入所指定的确定的值
Timestampstring“1972-01-01T10:00:20.021Z”使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数
Durationstring“1.000340012s”, “1s”生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度
Structobject{ … }任意的JSON对象,见struct.proto
Wrapper typesvarious types2, “2”, “foo”, true, “true”, null, 0, …包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null
FieldMaskstring“f.fooBar,h”见fieldmask.proto
ListValuearray[foo, bar, …]
Valuevalue任意JSON值
NullValuenullJSON null

选项

在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。

一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。

如下就是一些常用的选择:

  • java_package (文件选项) :这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:

option java_package = "com.example.foo";

  • java_outer_classname (文件选项): 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:

option java_outer_classname = "Ponycopter";

  • optimize_for(文件选项): 可以被设置为 SPEED, CODE_SIZE,或者LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
    • SPEED (default): protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。
    • CODE_SIZE: protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。
    • LITE_RUNTIME: protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。

option optimize_for = CODE_SIZE;

  • cc_enable_arenas(文件选项):对于C++产生的代码启用arena allocation
  • objc_class_prefix(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是苹果推荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。
  • deprecated(字段选项):如果设置为true则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。在java中,这回变成@Deprecated注释,在未来,其他语言的代码生成器也许会在字标识符中产生废弃注释,废弃注释会在编译器尝试使用该字段时发出警告。如果字段没有被使用你也不希望有新用户使用它,尝试使用保留语句替换字段声明。

int32 old_field = 6 [deprecated=true];

自定义选项

ProtocolBuffers允许自定义并使用选项。该功能应该属于一个高级特性,对于大部分人是用不到的。如果你的确希望创建自己的选项,请参看Proto2 Language Guide。注意创建自定义选项使用了拓展,拓展只在proto3中可用。

生成你的类

可以通过定义好的.proto文件来生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。对于Go,你还需要安装一个特殊的代码生成器插件。你可以通过GitHub上的protobuf库找到安装过程

通过如下方式调用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 --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 中产生Go代码,参考正在制作中。

    • --javanano_out在目标目录DST_DIR中生成JavaNano,JavaNano代码生成器有一系列的选项用于定制自定义生成器的输出:你可以通过生成器的README查找更多信息,JavaNano参考正在制作中。

    • --objc_out在目标目录DST_DIR中产生Object代码,可以在Objective-C代码生成参考中查看更多。

    • --csharp_out在目标目录DST_DIR中产生Object代码,可以在C#代码生成参考中查看更多。

    • --php_out在目标目录DST_DIR中产生Object代码,可以在PHP代码生成参考中查看更多。

作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。

  • 你必须提议一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。

"hello.proto"是文件名称

protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto