以服务于中国广大创业者为己任,立志于做最好的创业网站。

标签云创业博客联系我们

导航菜单

西瓜视频最新版本下载 西瓜视频旧版不升级ie=utf-8

  

  # 1\.protobuf语法-(proto3)   

  

  本文介绍了如何使用prototol。   

  

  缓冲区(pb)语言用于构建您的pb数据,包括:带后缀的文件语法。proto(语法:语法)以及如何从。proto文件(数据访问类:数据)。   

  

  访问类).在本文档中,使用了pb的proto3版本,并且使用了proto2的pb语法。请参见Proto2语言指南。   

  

  本文是一个参考指南,用于逐步展示文档中的各种功能。   

  

  # 1.1.定义消息。   

  

  首先,看一个非常简单的例子。假设您想要定义一个搜索请求(搜索请求:search。   

  

  Request),那么您需要查询的字符串字段,您感兴趣的结果所在的数据页面的page_number,以及每页的结果数,result_per_page。   

  

  语法='proto3';消息SearchRequest {字符串查询=1;int 32 page _ number=2;int 32 result _ per _ page=3;}   

  

  *文件的第一行表明您使用了proto3语法:如果您不编写此语句,pb编译器将假设您使用proto2。它必须是。原型文件。   

  

  * SearchRequest类型中声明了三个字段,每个字段都是键值对,用于存储search request中的相应信息。每个字段都有一个名称和一个类型。   

  

  # 1.1.1.声明字段类型。   

  

  在上面的代码中,所有字段都是按比例的。   

  

  类型:两个整数类型(page_number和result_per_page)和一个字符串类型(query)。但是,您也可以将字段定义为复合类型(复合类型:composite。   

  

  类型),包括枚举类型(枚举类型:enumerations)和其他消息类型。   

  

  # 1.1.2.分配字段编号。   

  

  如您所见,消息定义中的每个字段都有一个唯一的编号。这些数字用于在消息编码后区分二进制数据中的每个字段。一旦您的消息开始使用,您就不能更改其字段的数量。1-15的字段编号用一个字节编码,包括字段编号和字段类型(参见协议。   

  

  缓冲器   

  

  您可以在“编码”一章中找到更多编码信息。字段编号16-2047占用两个字节进行编码。因此,您应该保留1-15作为经常使用的字段编号。记住为后面添加的常用字段保留几个1-15的数字。   

  

  可以使用的最小字段数是1,最大值是2 {29}-1或536870911。您不能使用19000到19999作为字段编号,因为它们是协议。   

  

  缓冲区,如果您在。proto文件,pb的编译器会报告一个错误。同样,您不能使用之前标有保留的保留字段编号。   

  

  # 1.1.3.指定字段规则。   

  

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

  

  *单数规则:一条消息可以有零个或一个单数字段(但不能超过一个)。这也是proto3语法的默认字段规则。   

  

  *重复规则:用repeat修饰的字段可以任意重复(包括零次)。(实际上是一个数组)。重复数组中元素的顺序是固定的。   

  

  在proto3,重复修饰的字段默认情况下是打包编码的。   

  

  您可以在协议缓冲区编码中找到有关打包编码的更多信息。   

  

  # 1.1.4.添加更多消息。   

  

  您可以在. proto文件中定义多个消息。如果要定义多个相关消息,此功能非常有用。例如,如果您想将SearchResponse定义为您的响应消息,可以将其添加到该消息中。原型文件。   

  

  消息SearchRequest {字符串查询=1;int 32 page _ number=2;int 32 result _ per _ page=3;}消息搜索响应{.}   

  

  # 1.1.5.添加评论。   

  

  在。原型文件,使用C/C风格注释语法://或/*.*/   

  

  /* SearchRequest表示一个搜索查询,带有分页选项*以指示要在   

response. */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.}

  

# 1.1.6. 保留字段

  

如果你通过删除或注释字段修改了一个message,之后的开发者在修改这个message的时候可能会重新使用这个字段号。如果他们之后加载旧版本的.proto文件,可能导致严重的问题,例如数据污染、权限漏洞等。避免这个问题的方法就是指定你删除的字段号(和字段名,因为字段名重复使用也可能导致JSON序列化的问题)是reserved。后续的开发者如果想使用这些字段标识,pb编译器都会报错。

  

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

  

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

  

# 1.1.7. 从你的.proto文件生成什么东西

  

当你在.proto文件上运行pb编译器,编译器以所选语言生成代码。你是用这种语言来操作这些message类型,包括获取和设置字段值,将message序列化为输出流,以及从输入流解析message。

  

* 对于C ++,编译器从每个.proto生成一个.h和.ccc文件,以及文件中描述的每条message的类。

  

* 对于Java,编译器为每个message的都生成一个类并写在.java文件,以及用于创建message类实例的特殊Builder类。

  

* 对于Kotlin,除了Java生成的代码之外,编译器还为每个message生成一个.kt文件,其中包含可用于简化创建消息实例的DSL。

  

* Python有点不同 - Python编译器为.proto中的每一个message生成一个具有静态描述符的模块,然后与metaclass一起使用,以在运行时创建必要的python数据访问类。

  

* 对于Go,编译器在.pb.go文件中为每一个message生成对应的类型。

  

* 对于Ruby,编译器生成一个.rb文件,内容是包括每个message的Ruby模块。

  

* ...

  

文档后续有相关语言的API,具体可见API reference

  

# 1.2. Scalar值类型

  

一个message的字段可以是以下的类型 - 这个表中显示了.proto文件中可以使用的类型以及在不同语言中生成对应类型。

  

.proto Type

  

Notes

  

C++ Type

  

Java Type

  

Python Type[2]

  

Go Type

  

---|---|---|---|---|---

  

double

  

double

  

double

  

float

  

*float64

  

float

  

float

  

float

  

float

  

*float32

  

int32

  

使用动态长度编码,编码负数时效率很低。如果你的字段可能有负数,使用sint32。

  

int32

  

int

  

int

  

*int32

  

int64

  

使用动态长度编码,编码负数时效率很低。如果你的字段可能有负数,使用sint64。

  

int64

  

long

  

int/long[3]

  

*int64

  

uint32

  

使用动态长度编码。

  

uint32

  

int[1]

  

int/long[3]

  

*uint32

  

uint64

  

使用动态长度编码。

  

uint64

  

long[1]

  

int/long[3]

  

*uint64

  

sint32

  

使用动态长度编码。有符号整型。编码负数的效率要高于int32。

  

int32

  

int

  

int

  

*int32

  

sint64

  

使用动态长度编码。有符号整型。编码负数的效率要高于int64。

  

int64

  

long

  

int/long[3]

  

*int64

  

fixed32

  

使用四个字节编码。如果值大多大于228,编码效率要高于uint32。

  

uint32

  

int[1]

  

int/long[3]

  

*uint32

  

fixed64

  

使用四个字节编码。如果值大多大于256,编码效率要高于uint64。

  

uint64

  

long[1]

  

int/long[3]

  

*uint64

  

sfixed32

  

使用四个字节编码。

  

int32

  

int

  

int

  

*int32

  

sfixed64

  

使用八个字节编码。

  

int64

  

long

  

int/long[3]

  

*int64

  

bool

  

bool

  

boolean

  

bool

  

*bool

  

string

  

字符串应该是UTF-8编码或者七位ASCII编码。

  

string

  

String

  

unicode (Python 2) or str (Python 3)

  

*string

  

bytes

  

任意的字节数组。

  

string

  

ByteString

  

bytes

  

[]byte

  

在Protocol Buffer Encoding中你可以看到这些类型在序列化时是如何编码的。

  

# 1.3. 默认值

  

解析编码后的message时,如果message有个singular类型的字段没有指定值,那么解析出来的相关字段都会设置为默认值。

  

* string:默认值是空字符串

  

* bytes:默认值是空byte数组

  

* bool:默认是false

  

* 数字:默认是0

  

* enum:默认值是定义的第一个枚举值,肯定是0

  

* message:它的默认值取决于语言的实现。

  

repeated字段的默认值为空(通常是一个空数组)。

  

注意:message的字段中,一旦一个message被解析出来,我们就不知道它是被设置为默认值(例如有个bool被设置为false)或者它并没有被设置;你在定义message类型的时候需要注意这一点。例如,不要定义一个在某些情况需要设置为false的bool类型。同时注意,如果一个message字段设置为默认值,它的值在传输时就不会被序列化。

  

在generated code guide可以查看你使用的语言如何生成代码。

  

# 1.4. Enumerations

  

当你定义一个message,可能需要某一个字段的值在一个预定义的列表中。例如,你想要为SearchRequest添加一个corpus字段,corpus有这几种值:UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCES或VIDEO。通过在你的message定义处添加一个具有多个常量的枚举类型就可以实现这个功能。

  

下面代码中,我们添加了一个Corpus的枚举类型,这个类型具有上面提到的七种值;同时在message中添加了一个类型为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作为默认值

  

* 零值需要是第一个元素,这是为了与proto2语义兼容,proto2中第一个枚举值始终是默认值

  

你可以通过给不同的枚举变量赋予相同的值来定义别名(别名:alias)。前提是你需要设置allow_alias选项为true,否则pb编译器会报error异常。

  

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

  

枚举变量必须是32位的integer。因为枚举使用varint

  

encoding进行编码,负数在编码时的效率很低因此是不推荐使用的。如上面代码所示,你可以在message内部定义枚举,也可以在message外部定义。你也可以在把一个message的枚举作为另一个message中的字段,使用这个语法:_MessageType_._EnumType_。

  

当使用pb编译器编译.proto文件中的枚举类型时,生成的代码中将具有对应的 Java、Kotlin 或 C++ 枚举,或用于 Python 的特殊

  

EnumDescriptor 类,用于在运行时生成的类中创建一组具有整数值的符号常量。

  

> 注意: 生成的代码可能会受到特定语言的枚举数限制(一种语言低至数千)。请查看你使用的语言的限制。

  

反序列化时,message中会保留未识别出来的枚举值,具体的表现形式取决于语言的实现。在C++和golang中,枚举类型的值可以在其指定范围之外,因此未识别出来的值会简单存储为一个integer。在Java等的枚举类型中,枚举中专门有一种情况用来标识未识别的值,它可以通过特殊的访问机制来获取具体内容。在其他情况下,如果message被序列化了,这个无法识别的值也会随message序列化。

  

# 1.4.1. 保留值

  

如果你通过删除或注释掉枚举的一个条目来更新这个枚举类型,未来的开发者可能会重新使用这个枚举值来实现他们对这个枚举类型的修改。如果他们在之后使用了旧版本的.proto文件会导致严重问题,例如数据污染,权限漏洞等。保证这个不再发生的方法就是枚举条目的值和名字都声明为reserved。如果未来开发者使用这些条目名或者值,pb编译器就会报错。你可以使用max关键字来声明你保留的条目值的范围一直到最大值。

  

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

  

注意:不能在同一行reserved语句中同时添加枚举成员的名和值。

  

# 1.5. 使用其他message类型

  

你可以使用其他的message作为当前message字段的类型。例如,你想在SearchResponse中包括Result,你可以在当前.proto文件中定义一个Result,然后在SearchResponse中声明一个Result字段。

  

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

  

# 1.5.1. 导入已定义的数据类型

  

本节的特性不适合Java。

  

在上面的例子中,Result和SearchResponse定义在同一个.proto文件中,如果你想使用的message已经定义在另一个.proto文件中呢?

  

你可以通过在当前.proto文件中import其他的.proto文件以使用已有的类型,例如,在文件的开头可以这么写:

  

import "myproject/other_protos.proto";

  

这样,你只能使用other_protos.proto中的类型定义。然而,你可能需要把一个.proto文件移动到一个新的位置。你可以把.proto文件移动到新的位置并在所有import它的.proto文件中更新其路径;但是还有更简单的方法,就是在原来的路径上放一个假的.proto文件,使用import

  

public来将import重定向到新的.proto文件中。任何导入具有import

  

public声明的.proto文件的.proto文件,可以进行依赖的传递。(这么说有点不明白,看下面的代码。new.proto就是原来的.proto文件的新名字,而在.proto文件的原来位置即old.proto中使用了import

  

public声明。这样,当client.proto中import了old.proto文件,client.proto也可以使用new.proto的定义。从另一个角度说,old.proto继承了new.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.protoimport "old.proto";// You use definitions from old.proto and new.proto, but not other.proto

  

pb编译器通过-I/--proto_path标志获取的路径下查找导入的.proto文件。如果没有这个标志的参数,它会在运行编译器的当前路径下查找。通常情况下,你应该将

  

--proto_path标志设置为项目的根目录,并为所有的需要导入的.proto文件提供完全的路径名。

  

# 1.5.2. 使用proto2的message类型

  

proto3的message中可以导入使用proto2的message,反之亦然。然而,proto2的枚举无法直接在proto3语法中使用,除非一个ptoro2的message使用了它们。

  

# 1.6. 嵌套类型

  

你可以在一个message定义内部再定义或使用另一个message,如下代码所示。这里,这个Result就是定义在SearchResponse内部。

  

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

  

如果你想在与SearchResponse同级的message中使用Result,就需要使用SearchResponse.Result来进行定义。

  

message SomeOtherMessage { SearchResponse.Result result = 1;}

  

在message中你想嵌套多少就能嵌套多少层message。

  

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

  

# 1.7. 更新一个message

  

如果已有的message已经无法满足你的需求了,例如,你需要这个message增加一个额外的字段,但是你还需要使用原来的message来开发。不要怕,更新你的message是很简单的,而且不会破坏你原有的代码。只要记住以下几条规则:

  

* 不要改变已有字段的字段号

  

* 如果你新增一个字段,新代码也可以解析你的旧message序列化后的内容。你应该记住这些字段的默认值,以便新代码与旧代码生成的message能够正确交互。同样,由你的新代码创建的消息可以由旧代码解析:旧代码在解析时会忽略新的字段。具体内容见未知字段的具体内容。

  

* 字段可以被删除,但是它的字段号在更新的message不可以使用。你可能想重命名这些字段,例如在字段名开头加OBSULETE,或者让字段编号保留(保留:reserved),所以未来的开发者不会使用这些字段号。

  

* int32,uint32,int64,uint64和bool是互相兼容的,这意味着你可以把一个这些类型的字段任意转换成另一种类型。如果从线上获取的一个数字无法满足这个字段的类型,你得到的结果会和C++中把这个数字转换成那个字段类型的结果一致。

  

* sint32和sint64是可以互相转换的,但是无法与其他的integer类型相互转换。

  

* string和bytes是相互转换的,前提是bytes是utf-8编码。

  

* 嵌套的message与bytes是互相转换的,前提是bytes中包含这个message的编码版本。

  

* fixed32与sfixed32可以互相转换,fixed64和sfixed64互相转换。

  

* 对于string和bytes以及message的字段来说,optional和repeated声明是可以相互转换的。假设现在有repeated的字段被序列化后作为输入,对于想要optional字段的客户端来说,如果字段是原始数据类型,它就会只取最后一个值;然而如果它是一个message类型,则会合并所有的输入值。注意,这种方式对于bool、枚举等类型并不安全。repeated字段和bool、枚举等类型使用packed格式进行序列化,但是它们不会正确的解析到optional字段中。

  

* 枚举和int32、uint32、int64、uint64是可相互转换的,注意它们转换时可能存在截断的情况。然而,客户端在反序列化message时可能会出现不同的情况:例如,一个未识别的proto3枚举类型储存在message中,但是message在反序列化时这个类型的表现形式取决于具体语言实现。

  

* 把一个字段值转换为新的oneof的一个成员这种操作是安全的,同时是可以互相转换的。把多个字段移动到一个新的oneof中可能是安全的,前提是你确定没有其他的代码同时设置它们中的多个字段值。把任何的字段移动到现有的oneof中是不安全的。

  

# 1.8. 未知的字段

  

未知字段(未知字段:unknown

  

field)是序列化的数据中,pb解析器无法识别的内容。例如,当使用基于旧.proto文件的解析器解析具有新增字段的数据时,这些新字段就是未知字段。

  

起初,proto3的message在解析时会丢弃未知字段,但是3.5版本我们重新引入了对未知字段的保留以兼容proto2。在3.5或更高版本中,未知字段在解析过程中会保留并且包含在序列化的输出中。

  

# 1.9. Any类型

  

Any类型可以让你在不知道message定义的情况下把一个message作为字段的类型。一个Any类型中包含任何类型message序列化后的数据,同时还有一个URL作为标识符并且用于解析该数据的类型。使用Any,需要导入google/protobuf/any.proto。

  

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

  

对于给定message类型的URL是:type.googleapis.com/_packagename_._messagename_。

  

不同的语言实现将支持runtime library

  

helper以类型安全的方式打包和解包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 network_error; detail.UnpackTo(&network_error); ... processing network_error ... }}

  

目前,关于Any的runtime libraries还在开发中。

  

如果你已经熟悉了proto2的语法,Any可以持有任意proto3的message与proto2的message可以允许extension是相似的。

  

# 1.10. Oneof

  

如果你在一个message中有很多个字段,但是同时最多只有一个字段被设置,你可以通过oneof来解决这个问题同时节省内存。

  

oneof与普通的字段相似,但是oneof中的所有字段共享一块内存,并且最多只有一个字段可以被设置。设置oneof中的任意字段都会清除其他的字段。你可以使用case()或者WhichOneof()方法来判断哪个字段被设置,具体实现依赖于具体的语言。

  

# 1.10.1. 使用Oneof

  

使用oneof关键字后面跟oneof名来定义一个oneof类型。

  

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

  

接下来可以向oneof定义中添加oneof字段。除了map和repeated字段,其他类型都可以作为oneof的字段。

  

在生成的代码中,oneof字段与普通的字段都有getter和setter方法。你也会有一个特殊的方法来检查oneof中哪个字段被设置了。具体查看API

  

文档。

  

# 1.10.2. 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的多个字段,那么解析message的时候只使用遇到的最后一个字段。

  

* oneof不能被repeated修饰。

  

* oneof字段使用反射的API。

  

* 如果你设置了oneof某一个字段为默认值(例如设置一个int32字段为0),这个字段的"case"就会被设置,同时它的值在传输时会被序列化。

  

* 如果你是用C++,请确保你的代码不会导致内存崩溃。下面的代码会有这个问题因为sub_message已经通过set_name()方法删除掉了。

  

SampleMessage message;SubMessage* sub_message = message.mutable_sub_message();message.set_name("name"); // Will delete sub_messagesub_message->set_... // Crashes here

  

* 还是在C++中,如果你Swap()两个具有oneof的message,每个message最后都会有对方的oneof字段:下面的代码中,msg1会有sub_message,msg2会有name。

  

# 1.10.3. 向后兼容性问题

  

当删除或增加oneof的字段时需要小心。如果检查一个oneof字段返回None或者NOT_SET,这说明这个字段没有被设置或者在oneof的之前某一个版本中是被设置的。这是很难区分的,因为无法知道传输过来的一个未知字段是不是oneof的成员。

  

# 1.10.3.1. 标签重用问题

  

* 从一个oneof移出或移入字段:message序列化和解析后可能会丢失一部分信息,因为有的字段会被清除掉。然而,单个字段是可以安全的添加到一个新的oneof中。多个字段的话,如果这些字段同时最多只有一个字段被设置,那么也是可以添加到oneof中的。

  

* 删除一个oneof中的字段然后添加回去:当message被序列化和解析后,可能会清除掉当前设置的字段。

  

* 拆分或者合并oneof:这与移动字段产生的问题相同。

  

# 1.11. 字典(字典:map)

  

如果你想使用字典,pb提供了一个语法:

  

map map_field = N;

  

其中,key_type可以是数字或者string类型(可以是任何的scalar类型,除了浮点类型和bytes)。注意,枚举不是一个有效的key_type。value_type可以是除了map以外的任何的类型。

  

例如,你想创建一个map,其中每个Project都和一个string相关联:

  

map projects = 3;

  

* map作为字段时不能用repeated修饰。即没有[]map。

  

* map在传输前后,其元素的顺序不是固定的。因此你不能指望你的map元素在传输后也按照固定的顺序进行遍历。

  

* 当为.proto文件生成代码时,map会根据key进行排序。数字key会按照数字顺序排序。

  

* 当解析传输的数据或者合并map时,如果有重复的key,map只会使用最后一个遇到的key。当从文件中解析map时,如果遇到了重复的key可能会导致解析失败。

  

* 如果你提供了一个没有value的key作为map的字段,这个字段在序列化时的行为取决于语言实现。在C++、Java、Kotlin和Python中,这个类型的默认值是会被序列化的,但是其他的语言中不会序列化这个值。

  

map的API见API文档。

  

# 1.11.1 向后兼容

  

map在传输时等效于下面的代码,因此不支持map的pb实现仍然可以解析map数据。

  

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

  

任意支持map的pb实现在构造和接收的数据时都必须与上述代码中定义生成的数据相同。

  

# 1.12. package

  

在一个.proto文件中,你可以添加一个可选的package标识来防止message重名。

  

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

  

在你自己的message中定义字段时可以使用上述的包定义:

  

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

  

package标识符对后续生成的代码的影响取决于你使用的语言:

  

* 在C++中,生成的类都在C++的同一个命名空间下。例如,Open会在foo::bar里面。

  

* 在Java和Kotlin中,package默认用作java包,除非你显式提供.proto文件中的选项java_package。

  

* 在Python中,忽略package指令,因为Python模块根据文件系统中的位置组织。

  

* 在Go中,package默认用作Go包名,除非你显式提供.proto文件中的选项go_package。

  

* 在Ruby中,生成的类被包装在嵌套的Ruby命名空间内,转换为所需的Ruby大写样式(首字母大写;如果第一个字符不是字母,则使用PB_)。例如,Open将在Foo :: Bar中。

  

* 在C#中,package默认被用作转换为PascalCase之后的命名空间,除非你在.proto文件中明确提供选项csharp_namespace。例如,Open将在命名空间Foo.Bar中。

  

# 1.12.1. package和名称解析

  

在pb语言中类型名称的解析工作和C++一样:首先搜索最内层的范围,然后是下一个最内层,依此类推,每个包都比它的父包更靠内。一个前置的'.'(例如,.foo.bar.baz)意味着从最外面的包开始。

  

pb编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何引用该语言的每种类型,即使它具有不同的范围规则。

  

# 1.13. 定义service

  

如果你想在RPC系统中使用你定义的message,你可以在.proto文件中定义一个RPC服务接口,pb编译器会根据你使用的语言生成一个服务接口代码与桩代码(桩代码:stub)。例如,如果你想定义一个RPC服务,里面有一个方法,输入SearchRequest,输出SearchResponse,你可以在.proto文件中这么写:

  

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

  

最直接了当使用pb的RPC系统是gRPC:Google实现的语言和平台中立性的开源RPC系统。gRPC特别适用于pb,并且通过一个特殊的pb编译器插件让你直接从.proto文件中生成RPC代码。

  

如果你不想使用gRPC,也可以在你自己的RPC实现上使用pb。具体看Proto2 Language Guide。

  

目前还有很多第三方工程在开发适用于pb的RPC实现。具体见third-party add-on wiki page。

  

# 1.14. JSON映射

  

proto3支持JSON的规范编码,使系统之间的数据传输更加容易。编码方式在下表中。

  

如果JSON数据中一个值丢失或者是null,转换成pb时会使用该类型默认值。如果pb中一个字段是默认值,转换成JSON时就会被省略掉。有的实现可能会提供选项,从而可以在JSON中保留具有默认值的字段。

  

proto3

  

JSON

  

JSON example

  

Notes

  

---|---|---|---

  

message

  

object

  

{"fooBar": v, "g": null, …}

  

生成JSON对象。message的字段名映射成小写驼峰格式成为JSON对象的键。如果json_name字段定义了,它的值就被设置为键。解析器可以解析小驼峰名和原始的proto字段名。JSON的null对所有的字段类型都适用,并且映射成该类型的默认值。

  

enum

  

string

  

"FOO_BAR"

  

JSON中使用在.proto文件中定义的枚举名。枚举名和整型值都可以被解析出来。

  

map

  

object

  

{"k": v, …}

  

所有的键都转为字符串。

  

repeated V

  

array

  

[v, …]

  

null被识别为空数组。

  

bool

  

true, false

  

true, false

  

string

  

string

  

"Hello World!"

  

bytes

  

base64 string

  

"YWJjMTIzIT8kKiYoKSctPUB+"

  

JSON 值将是使用带填充的标准 base64 编码后的字符串数据。带/不带填充的标准或 URL安全的base64编码都可以解析。

  

int32, fixed32, uint32

  

number

  

1, -10, 0

  

JSON值是整数。数字或字符串都可解析。

  

int64, fixed64, uint64

  

string

  

"1", "-10"

  

JSON值是整数构成的字符串。数字或字符串都可解析。

  

float, double

  

number

  

1.1, -10.0, 0, "NaN", "Infinity"

  

JSON值是数字或者以下几种特殊字符串值:NaN、Infinity和-Infinity。数字和字符串都可以解析为JSON。指数符号也可以使用。-0等效为0。

  

Any

  

object

  

{"@type": "url", "f": v, … }

  

如果Any中的内容是有JSON映射的值,它会转换成 {"@type": xxx, "value":

  

yyy}。否则,它会被转换成JSON对象,@type字段会插入JSON对象中。

  

Timestamp

  

string

  

"1972-01-01T10:00:20.021Z"

  

遵循RFC 3339规范,生成的输出是Z规范化的并且使用0、3、6、9个小数。除了Z以外的其他偏移也是可用的。

  

Duration

  

string

  

"1.000340012s", "1s"

  

产生的输出经常含有0、3、6、9个小数,这取决于具体的精度,后面会跟"s"(秒单位)。解析器接收任何小数位数(也没有),只要它们适合纳秒精度并且有后缀“s”。

  

Struct

  

object

  

{ … }

  

转换为JSON对象。

  

Wrapper types

  

various types

  

2, "2", "foo", true, "true", null, 0, …

  

JSON中,包装器使用与包装的原始类型一样的表示形式,并且null在数据转换和传输时不会被忽略。

  

FieldMask

  

string

  

"f.fooBar,h"

  

See field_mask.proto.

  

ListValue

  

array

  

[foo, bar, …]

  

Value

  

value

  

转换为JSON的value。具体见:google.protobuf.Value 。

  

NullValue

  

null

  

对应JSON的null

  

Empty

  

object

  

{}

  

对应一个空的JSON对象。

  

# 1.14.1. JSON选项

  

一个proto3JSON实现可能会提供如下选项:

  

* 传输具有默认值的字段:在proto3JSON输出中,pb中带有默认值的字段在序列化时会被省略掉。有的pb实现可能会提供一个选项来重载这个行为并且输出具有默认值的字段。

  

* 忽略未知字段:proto3JSON解析器默认会驳回未知字段,但是也可以提供一个选项在解析时忽略这些字段。

  

* 不使用小写驼峰名,使用proto的字段名:proto3默认的在序列化时应该把字段名转换成小写驼峰名并用做JSON的键。有的实现可能会直接使用message的字段名作为JSON的名字。proto3JSON解析器必须都能够转换小写驼峰名和proto字段名。

  

* 以整型而不是字符串形式传输枚举类型:pb默认序列化后的JSON输出中使用枚举的名字进行传输。其他的实现可能会使用枚举值而不是枚举名进行传输。

  

# 1.15. 选项

  

可以使用多个选项注释.proto文件中的单个声明。选项不会更改声明的整体含义,但可能影响其在特定上下文中处理的方式。完整的可用选项定义在:google/protobuf/descriptor.proto。

  

有些选项是文件级别的,意味着它们应该写在最顶层的范围内,而不是在message、枚举或者service定义中。有些选项是message级的,意味着他们应该写在message定义内部。有些选项是字段级的,意味着它们应该被写在字段定义的内部。选项也可以写在枚举类型、枚举值、oneof、service类型和service方法上,然而,现在没有选项提供使用。

  

具体的选项在生成各种语言的桩代码中用的比较多,这个用到再查就可以了。

  

# 1.16. 生成你的类代码

  

通过.proto文件中的message定义,你可以生成Java、Kotlin、Python、C++、Go、Ruby等的类代码,这需要在.proto文件上执行pb编译器protoc得到。如果你没有安装这个编译器,下载安装包然后按照README的指引进行。对于Go语言,你还需要安装一个特殊的代码生成插件:你可以在github的golang/protobuf上找到它和它的安装指南。

  

这个pb编译器的运行指令如下:

  

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文件。如果省略了,就会使用当前文件夹。多个import文件夹可以通过多次声明--proto_path来添加;它们会被按照顺序依次查找。-I=_IMPORT_PATH_可以用作缩写的--proto_path。

  

你可以提供一个或多个输出文件夹:

  

* \--cpp_out将生成的C++代码放到DST_DIR。更多信息见:C++ generated code reference

  

* \--java_out 在 DST_DIR 中生成 Java 代码。有关更多信息,请参阅Java generated code reference。

  

* \--kotlin_out 在 DST_DIR 中生成额外的 Kotlin 代码。有关更多信息,请参阅Kotlin generated code reference。

  

* \--python_out 在 DST_DIR 中生成 Python 代码。有关更多信息,请参阅Python generated code reference。

  

* \--go_out 在 DST_DIR 中生成 Go 代码。有关更多信息,请参阅 Go generated code reference。

  

* \--ruby_out 在 DST_DIR 中生成 Ruby 代码。 Ruby 生成的代码参考即将推出!

  

* \--objc_out 在 DST_DIR 中生成 Objective-C 代码。有关更多信息,请参阅Objective-C generated code reference。

  

* \--csharp_out 在 DST_DIR 中生成 C# 代码。有关更多信息,请参阅C# generated code reference。

  

* \--php_out 在 DST_DIR 中生成 PHP 代码。有关更多信息,请参阅 PHP generated code reference。为方便起见,如果 DST_DIR 以 .zip 或 .jar 结尾,则编译器会将输出写入具有给定名称的单个 ZIP 格式存档文件。 .jar 输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已经存在,它将被覆盖;编译器不够聪明,无法将文件添加到现有存档中。

  

* 你必须提供一个或多个.proto 文件作为输入。可以一次指定多个 .proto 文件。尽管文件是相对于当前目录命名的,但每个文件都必须在IMPORT_PATH之一中,以便编译器可以确定其规范名称。