Protobuf协议特色分析php
首先来看一个简单的例子,定义一个搜索请求的消息格式,每一个消息包含一个请求字符串,你感兴趣的页数和每页的结果数。下面是在.proto
文件中定义的消息。css
message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; }
SearchRequest
消息定义了3个特殊的字段(名字/值 对)对应着我须要的的消息内容。每一个字段有一个名字和类型。java
在上面的例子中,全部的字段都是标量类型 : 两个整形(page_number result_per_page
)和一个字符串query
。 固然你也可使用其余组合类型,好比枚举或者其余 消息类型。python
如你所见,消息中的每个字段都被定义了一个独一无二的数字标签。这个标签是用来在二进制的消息格式中区分字段的,一旦你的消息开始被使用,这些标签就不该该在被修改了。注意 1 到 15 标签在编码的时候仅占用1 byte ,16 - 2047 占用 2 byte 。所以你应该将 1 - 15 标签保留给最常常被使用的消息元素。另外为将来可能添加的经常使用元素预留位子。
你能定义的最小的标签是1, 最大是 2的29次方 -1 , 另外 19000 到 19999 (FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber
) 也不能用。他们是protobuf 的编译预留标签。另外你也不能使用被 reserved
的标签。c++
消息是字段必须是下面的一种编程
required
格式正确的消息必须有一个这个字段。optional
格式正确的消息能够有一个或者零个这样的消息。repeated
这个字段能够有任意多个。字段值的顺序被保留。因为历史缘由, repeated
字段的标量编码效率没有应有的效率高,新的代码可使用[packet=true]
来得到更高效的编码, 好比 :json
repeated int32 samples = 4 [packet=true]
Required 字段意味着永久,当你要标记一个字段为required 的时候你必须很是当心 —– 若是某个时刻你想要再也不使用这个字段,当你把它改为optional的时候就会出问题 : 使用旧的协议的人会由于认为这个字段缺失而认为消息不完整,进而拒收或者丢弃这个消息。谷歌的一些工程师得出这样的结论:使用required
形成的伤害比他们的好处多,他们更倾向于使用optional
的和repeated
的。然而,这种观点不是绝对的。安全
多个消息类型能够在一个.proto
文件中定义。当你定义多个相关联的消息的时候就用的上了 —— 好比我要定义一个返回消息格式来回应SearchRequest
消息,那么我在同一个文件中 :bash
message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; } message SearchResponse { //。。。
添加注释
在.proto
文件中添加注释,使用C/C++风格的 //
语法服务器
message SearchRequest { required string query = 1; optional int32 page_number = 2;// Which page number do we want? optional int32 result_per_page = 3;// Number of results to return per page. }
当你在某次更新消息中屏蔽或者删除了一个字段的话,将来的使用着可能在他们的更新中重用这个标签数字来标记他们本身的字段。而后当他们加载旧的消息的时候就会出现不少问题,包括数据冲突,隐藏的bug等等。指定这个字段的标签数字(或者名字,名字可能在序列化为JSON的时候可能冲突)标记为reserved
来保证他们不会再次被使用。若是之后的人试用的话protobuf编译器会提示出错。
message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
注意一个reserved字段不能既有标签数字又有名字。
.proto
文件最终生成什么当你使用protoc
来编译一个.proto
文件的时候,编译器将利用你在文件中定义的类型生成你打算使用的语言的代码文件。生成的代码包括getting setting
接口和序列化,反序列化接口。
.proto
文件生成一个.h
和一个.cc
文件。 每一个消息生成一个class。.java
文件,外加一个特殊的Builder
类来生成消息实例。.pb.go
文件。proto | Note | C++ | Java | Python | Go |
---|---|---|---|---|---|
float | float | float | float | *float32 | |
double | double | double | float | *float64 | |
int32 | 变长编码. 编码负数效率底下– 打算使用负数的话请使用 sint32. | int32 | int | int | *int32 |
int64 | 变长编码. 编码负数效率底下– 打算使用负数的话请使用 sint64. | int64 | long | int/long | *int64 |
uint32 | 变长编码. | uint32 | int | int/long | *uint32 |
uint64 | 变长编码. | uint64 | long | int/long | *uint64 |
sint32 | U变长编码. 数值有符号,负数编码效率高于int32 | int32 | int | int | *int32 |
sint64 | U变长编码. 数值有符号,负数编码效率高于int64 | int64 | long | int/long | *int64 |
fixed32 | 固定4byte, 若是数值常常大于2的28次方的话效率高于uint32. | uint32 | int | int | *uint32 |
fixed64 | 固定8byte, 若是数值常常大于2的56次方的话效率高于uint64. | uint64 | long | int/long | *uint64 |
sfixed32 | 固定4byte. | int32 | int | int | *int32 |
sfixed64 | 固定8byte. | int64 | long | int/long | *int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串内容应该是 UTF-8 编码或者7-bit ASCII 文本. | string | String | str/unicode | *string |
bytes | 任意二进制数据. | string | ByteString | str | []byte |
optional
字段和默认初始值按照上面提到的,元素能够被标记为optional
的。一个正确格式的消息能够有也能够没有包含这个可选的字段。再解析消息的时候,若是个可选的字段没有被设置,那么他的值就会被设置成默认值。默认值能够做为消息描述的一不部分 :
optional int32 result_per_page = 3 [default = 10];
若是没有明确指明默认值,那么这个字段的值就是这个字段的类型默认值。好比 : 字符串的默认值就是空串。数字类型的默认值就是0。枚举类型的默认值是枚举定义表的第一个值,这意味着枚举的第一个值须要被格外注意。
当你定义一个消息的时候,你可能但愿它其中的某个字段必定是预先定义好的一组值中的一个。你如说我要在SearchRequest
中添加corpus
字段。它只能是 UNIVERSAL, WEB , IMAGES , LOCAL, NEWS ,PRODUCTS, 或者 VIDEO
。你能够很简单的在你的消息中定义一个枚举而且定义corpus
字段为枚举类型,若是这个字段给出了一个再也不枚举中的值,那么解析器就会把它看成一个未知的字段。
1 message SearchRequest { 2 required string query = 1; 3 optional int32 page_number = 2; 4 optional int32 result_per_page = 3 [default = 10]; 5 enum Corpus { 6 UNIVERSAL = 0; 7 WEB = 1; 8 IMAGES = 2; 9 LOCAL = 3; 10 NEWS = 4; 11 PRODUCTS = 5; 12 VIDEO = 6; 13 } 14 optional Corpus corpus = 4 [default = UNIVERSAL]; 15 }
只须要将相同的值赋值给不一样的枚举项名字,你就在枚举中你能够定义别名 。固然你得先将allow_alias
选项设置为true
, 不然编译器遇到别名的时候就报错。
1 enum EnumAllowingAlias { 2 option allow_alias = true; 3 UNKNOWN = 0; 4 STARTED = 1; 5 RUNNING = 1; 6 } 7 enum EnumNotAllowingAlias { 8 UNKNOWN = 0; 9 STARTED = 1; 10 // RUNNING = 1; //取消这一行的屏蔽的话,编译器报错。 11 }
枚举常数必须是一个32为的整数。因为枚举值在通信的时候使用变长编码,因此负数的效率很低,不推荐使用。你能够在(像上面这样)在一个消息内定义枚举,也能够在消息外定义 —– 这样枚举就在全文件可见了。若是你想要使用在消息内定义的枚举的话,使用语法 MessageType.EnumType
。
在你编译带有枚举的.proto
文件的时候,若是生成的是C++或者Java代码, 那么生成的代码中会有对应的枚举。
你可使用其余的消息类型做为字段的类型。好比咱们打算在SearchResponse
消息中包含一个Result
类型的消息 :
message SearchResponse { repeated Result result = 1; } message Result { required string url = 1; optional string title = 2; repeated string snippets = 3; }
在上面的例子中, Result
消息类型是和SearchResponse
定义在同一个文件中,若是你想使用的消息类型已经在另外一个.proto
文件中定义的话怎么办 ?
只要你导入一个文件就可使用这个文件内定义的消息。在你的文件头部加上这样的语句来导入其余文件: import "myproject/other_protos.proto";
默认状况下你只能使用直接导入的文件中的定义。然而有的时候你须要将一个文件从一个路径移动到另外一个路径的时候,与其将全部的引用这个文件的地方都更新到新的路径,不如在原来的路径上留下一个假的文件,使用import public
来指向新的路径。import public
语句能够将它导入的文件简介传递给导入本文减的文件。好比 :
// new.proto // 新的定义都在这里
// old.proto // 其余的文件其实导入的都是这个文件 import public "new.proto"; import "other.proto";
// client.proto import "old.proto"; // 你可使用 old.proto 和 new.proto 的定义, 可是不能使用other.proto的定义
在命令行中试用-I/--proto_path
来指定一系列的编译器搜索路径,若是这个参数没有被设置,那么默认在命令执行的路径查找。一般状况下使用-I/--proto_path
来指定到你项目的根目录,而后使用完整的路径来导入所需的文件。
你能够将proto3的消息类型导入并在proto2的消息中使用,反之亦然。不过proto2的枚举不能在proto3中使用。
你能够在一个消息中定义并使用其余消息类型,好比下面的例子 —— Result
消息是在SearchResponse
中定义的 :
1 message SearchResponse { 2 message Result { 3 required string url = 1; 4 optional string title = 2; 5 repeated string snippets = 3; 6 } 7 repeated Result result = 1; 8 }
若是你打算在这个消息的父消息以外重用这个消息的话,你能够这样引用它 : Parent.Type
message SomeOtherMessage { optional SearchResponse.Result result = 1; }
你想嵌套多深就嵌套多深,没有限制 :
1 message Outer { // Level 0 2 message MiddleAA { // Level 1 3 message Inner { // Level 2 4 required int64 ival = 1; 5 optional bool booly = 2; 6 } 7 } 8 message MiddleBB { // Level 1 9 message Inner { // Level 2 10 required int32 ival = 1; 11 optional bool booly = 2; 12 } 13 } 14 }
注意这是一个被废弃的特性,若是你建立一个新的消息的话,不要使用这个,请直接使用内嵌消息。
Groups是另外的一种在你的消息中内嵌信息的方式。例如 :
1 message SearchResponse { 2 repeated group Result = 1 { 3 required string url = 2; 4 optional string title = 3; 5 repeated string snippets = 4; 6 } 7 }
Group其实将内嵌消息的定义和字段声明合并在一块儿了。在你的生成代码中,你会发现这个消息有一个Result类型的result字段(字段名字自动小写来防止冲突)。 所以这个例子和上面的第一个内嵌的例子是等价的。除了这个消息的通信格式不大同样外。
若是一个现有的消息类型再也不知足你的需求,好比你须要额外的字段,可是你仍然但愿兼容旧代码生成的消息的话,不要担忧! 在不破坏现有代码的前提下更新消息是很简单的。请铭记下面的规则 :
optional
或者repeated
。因为任何required
字段都没有丢失,这意味着你的旧代码序列化的消息可以被新代码解析经过。你应该给新的字段设置合理的默认值,这样新的代码能够合适解析使用旧的消息。一样的,新的代码产生的消息包也能够被旧的代码解析经过,旧的代码在解析的时候会忽略新的字段。不过新的字段并无被丢弃,若是这个消息在旧的代码中再次被序列化,这些未知的字段还会在里面 —— 这样这些消息被传递回新的代码的时候,解析仍然有效。required
字段能够被移除,可是对应的数字标签不能被重用。或许你能够经过重命名这个字段,加上前缀OBSOLETE_
来表示废弃。或者你能够标记reserverd
。这样你将来就不会不当心重用这些字段了。required
字段能够被转化扩展字段,反之亦然。int32, uint32, int64, uint64, 和 bool
这些类型是兼容的 —— 这意味着你能够将一个字段的类型从其中的一种转化为另外一种,不会打破向前向后兼容! 若是通讯的时候传输的数字不符合对应类型的那么你会获得和C++中强制类型转化同样的效果(64bit数字会被截断)。sint32 sint64
相互兼容,可是不和其余的数字类型兼容。string bytes
相互兼容 ,前提是二进制内容是有效的UTF-8 。optional repeated
是兼容的。当给定的输入字段是repeated
的时候,若是接收方期待的是一个optional
的字段的话,对与原始类型的字段,他会取最后一个值,对于消息类型的字段,他会将全部的输入合并起来。int32, uint32, int64, and uint64
在传输格式中相互兼容(注意若是不合适会被 截断),可是接收方在发序列化的时候处理他们可不大同样。请注意: 反序列化的时候不正确的枚举数字会被丢弃,这样这个字段的has_xxx
接口就返回false
而且get_xxx
接口返回枚举的第一个值。不过若是是一个整形字段的话,这个数值会一致保留。因此当你打算把一个整形更新为枚举的时候,请务必注意整数的值不要超出接收方枚举的值。extensions
让你定义一段可用的数字标签来供第三方扩展你的消息。其余人能够在他们本身的文件里面使用这些标签数字来扩展你的下消息(无需修改你的消息文件)。 举个例子:
message Foo { //,,, extensions 100 to 199; }
这意味着Foo
消息在[ 100 , 199 ]区间的标签数字被保留作扩展使用。其余的使用者能够在他们本身的文件中导入你的文件,而后在他们本身的文件中给你的消息添加新的字段 :
extend Foo { optional int32 bar = 126; }
这样就意味着Foo
消息如今有一个叫作bar
的int32
字段了。在编码的时候,通信格式和使用者定义的新的消息同样。不过你的程序访问扩展字段的方式和访问常规字段的方式不太同样, 这里以C++代码为例 :
Foo foo; foo.SetExtension(bar, 15);
相似的,Foo
类有如下接口HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()
。
注意扩展字段能够是除了oneof
或者map
外的其余任何类型,包括消息类型。
你能够在其余类型的做用域内定义扩展字段 :
message Baz { extend Foo { optional int32 bar = 126; } //。。。 }
在这种状况下,扩展的字段以下访问 ( C++ )
Foo foo; foo.SetExtension(Baz::bar, 15);
这里有一个很常见的疑惑 : 在一个消息类型内定义另外一个类型的扩展并不会致使被扩展消息类型和包含类型的任何关系。实际上,在上面的例子中,Baz类不是Foo类的子类。上面仅仅意味着bar
这个变量其实是Baz
的一个static变量,仅此而已。
一个常规的使用方法是当咱们要扩展一个类型的字段的时候,将它写在这个类型里面, 好比我要扩展一个Baz类型的Foo字段的时候 :
message Baz { extend Foo { optional Baz foo_ext = 127; } ... }
然而,这并非必要的。你彻底能够这样作 :
message Baz { ... } // This can even be in a different file. extend Foo { optional Baz foo_baz_ext = 127; }
事实上这个语法是用来避免疑惑的。正如上面提到的,嵌套语法常常会不熟悉扩展的人被误觉得是子类。
重要的是,要确保两个使用者不会向同一个消息内扩展同一个数字的字段。不然若是类型刚好不兼容的话数据就混乱了。你须要为你的项目定义合适的扩展数字来避免这种事。
若是你打算使用一些很是大的数字来做为你的扩展的话,你可让你的扩展字段区间一直到最大值,你能够max
关键字 :
message Foo { extensions 1000 to max; }
max 是 2的29次方 - 1, 536,870,911.
一样的你不能使用19000-19999 。 你能够定义扩展空间包含他们,不过当你定义扩展字段的时候不能真的使用这些数字。
若是你的消息中有不少可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可使用oneof
来强化这个特性而且节约存储空间。 oneof
字段相似optional
字段只不过oneof
里面全部的字段共享内存,并且统一时刻只有一个字段能够被设着。设置其中任意一个字段都自动清理其余字段。在你的代码中,你可使用case()或者 WhichOneOf()
接口来查看究竟是哪一个字段被设置了。
使用Oneof特性你只须要在oneof
关键字后面加上它的名字就行 :
message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; } }
你能够在oneof
中使用oneof
, 你可使用任何类型的字段,可是你不能使用required, optional, 或者 repeated
关键字。
在你的代码中,oneof内的字段和其余常规字段有同样的getter setter 接口。你还能够经过接口(取决于你的语言)判断哪一个字段被设置。
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); //清理name字段. CHECK(!message.has_name());
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
Swap()
接口的话,每一个消息会带有对方的oneof字段。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到底哪里不同了。
重用的注意事项:
若是你打算在你的数据结构中建立一个关联表的话,咱们提供了很方便的语法:
map<key_type, value_type> map_field = N;
这里key_type能够是任意整形或者字符串。而value_tpye 能够是任意类型。
举个例子,若是你打算建立一个Project表,每一个Project关联到一个字符串上的话 :
map<string, Project> projects = 3;
如今生成Map的API对于全部支持proto2的语言均可用了。
repeated, optional, 或者 required
.在通信中,map等价与下面的定义, 这样不支持Map的版本也能够解析你的消息:
message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
为了防止不一样消息之间的命名冲突,你能够对特定的.proto
文件提指定packet 名字 。
package foo.bar; message Open { ... }
在定义你的消息字段类型的时候你能够指定包的名字:
message Foo { ... required foo.bar.Open open = 1; ... }
包名字的实现取决于你工做的具体编程语言:
java_package
,不然这个包名字就是Java的包名字。protobuf的名字解析方式和C++很像。首先是最里面的做用域被搜索,而后是外面的一层。。。 没一个包都从他本身到它的父辈。可是若是前面有.
号的话就(好比foo.bar.Baz
)意味着从最外面开始。
protobuf 编译器经过全部导入.proto
文件来解析全部的名字。代码生成器为每一个语言生成对应的合适的类型。
若是打算将你的消息配合一个RPC(Remote Procedure Call 远程调用)系统联合使用的话,你能够在.proto
文件中定义一个RPC 服务接口而后protobuf就会给你生成一个服务接口和其余必要代码。好比你打算定义一个远程调用,接收SearchRequest返回SearchResponse, 那么你在你的文件中这样定义 :
service SearchService { rpc Search (SearchRequest) returns (SearchResponse); }
默认状况下,编译器给你生成一个纯虚接口名叫SearchRequest
和一个对应的桩实现。这个桩实现直接调用RpcChannel,这个是你本身实现的具体RPC代码。好比你打算实现一个RpcChannel来序列化消息而且使用HTTP发送。换句话说,生成的代码提供了一个基于你的RPC的类型的安全的协议接口实现,它 不须要知晓你的PRC 的任何实现细节。所以最后你的代码大致是这样的 :
1 using google::protobuf; 2 3 protobuf::RpcChannel* channel; 4 protobuf::RpcController* controller; 5 SearchService* service; 6 SearchRequest request; 7 SearchResponse response; 8 9 void DoSearch() { 10 // 你本身提供MyRpcChannel和MyRpcController两个类,这两个类分别实现了纯虚接口 11 // s protobuf::RpcChannel 和protobuf::RpcController. 12 channel = new MyRpcChannel("somehost.example.com:1234"); 13 controller = new MyRpcController; 14 service = new SearchService::Stub(channel); 15 16 // Set up the request. 17 request.set_query("protocol buffers"); 18 19 // Execute the RPC. 20 service->Search(controller, request, response, protobuf::NewCallback(&Done)); 21 } 22 23 void Done() { 24 delete service; 25 delete channel; 26 delete controller; 27 }
全部的服务器类一样实现服务接口。这提供了一种在不知道方法名字和参数的状况下调用方法的途径。在服务器这边,你须要实现一个能够注册服务的PRC服务器。
1 using google::protobuf; 2 3 class ExampleSearchService : public SearchService { 4 public: 5 void Search(protobuf::RpcController* controller, 6 const SearchRequest* request, 7 SearchResponse* response, 8 protobuf::Closure* done) { 9 if (request->query() == "google") { 10 response->add_result()->set_url("http://www.google.com"); 11 } else if (request->query() == "protocol buffers") { 12 response->add_result()->set_url("http://protobuf.googlecode.com"); 13 } 14 done->Run(); 15 } 16 }; 17 18 int main() { 19 //你本身提供的MyRpcServer类,它不须要实现任何接口,这里意思意思就行。 20 MyRpcServer server; 21 22 protobuf::Service* service = new ExampleSearchService; 23 server.ExportOnPort(1234, service); 24 server.Run(); 25 26 delete service; 27 return 0; 28 }
若是你不想嵌入你本身的已经存在的RPC系统,你如今可使用gRPC : 这是一种谷歌开发的语言和平台无关的开源RPC系统。gPRC和protobuf配合的格外方便。在添加了特定的插件后,它能够从你的.proto
文件直接生成对应的RPC代码。不过因为proto2和proto3之间存在兼容问题,咱们推荐你使用proto3来定义你的gPRC服务。若是你打算使用gPRC配合protobuf , 你须要3.0.0以上的版本。
每一个.proto
文件中的独立的定义均可以被一系列的选项说明。选项不改变任何定义的总体意义,可是在特定的上下文下它们能有特定的效果。选项列表在google/protobuf/descriptor.proto
中.
有的选项是文件等级的,意味着它必须在文件最顶端写,不能在任何消息,枚举或者服务的定义中。也有写选项是消息级别的,意味着它们应该写在消息定义内,有些选项是字段级别的,意味着他们应该被写在字段定义中。选项能够被写在枚举,服务中,可是目前尚未对应的有意义的选项。
这是一些经常使用的选项:
java_package
(file option): 生成的Java的包名字。若是没有指定这个选项那么使用packet关键字的参数。不过packet关键字没有办法生成优雅的Java包名字,由于packet关键字不支持.
号。非Java语言忽略。option java_package = "com.example.foo";
java_outer_classname
(file option): Java最外围的类名字和文件名。若是没有设置,文件名就死协议文件名转化成驼峰式的名字 : (foo_bar.proto 变成 FooBar.java
) , 非java语言忽略。option java_outer_classname = "Ponycopter";
optimize_for
(file option): 能够是SPEED, CODE_SIZE, or LITE_RUNTIME
. 对 C++ 、Java (或者其余三方代码生成器)代码生成有以下影响: option optimize_for = CODE_SIZE;
cc_generic_services
,java_generic_services
, py_generic_services
(file options): 是否生成抽象的服务代码 分别对应C++, Java, 和Python。 因为历史遗留缘由,这些被默认设置为true。// This file relies on plugins to generate service code. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false;
cc_enable_arenas
(file option): 容许 arena allocation ,C++有效.
packed
(field option): 当你对一个repeated的整形字段设置true 的时,会使用一种更有效的编码方式。 没有坏处。不过在2.3.0以前的版本,若是解析器发现期待这个字段不是packed而接收的数据是packed,那么数据会被忽略。以后的版本是安全的。若是你使用好久的版本的话请当心。
repeated int32 samples = 4 [packed=true];
deprecated
(field option): 若是被设置为true,那么这个字段被标记为废弃,新的代码不该该使用它。在大多数语言中这个没有实际的意义,Java会使用@Deprecated
.optional int32 old_field = 6 [deprecated=true];
Protocol Buffers 甚至容许你自定义你本身的选项。注意这是高级用法,大多数人用不到。既然选项是在google/protobuf/descriptor.proto (like FileOptions or FieldOptions)
中定义的,你只须要扩展他们定义你本身的选项。好比:
import "google/protobuf/descriptor.proto"; extend google.protobuf.MessageOptions { optional string my_option = 51234; } message MyMessage { option (my_option) = "Hello world!"; }
这里咱们经过扩展MessageOptions
定义了一个消息级别的选项。咱们在C++中这样读取这个选项的值:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
这里,MyMessage::descriptor()->options()
返回了MessageOptions消息。读取扩展选项和读取其余的扩展字段没什么区别。
Java代码:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions() .getExtension(MyProtoFile.myOption);
Python代码:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions() .Extensions[my_proto_file_pb2.my_option]
各类类型的选项都能被扩展。
import "google/protobuf/descriptor.proto"; extend google.protobuf.FileOptions { optional string my_file_option = 50000; } extend google.protobuf.MessageOptions { optional int32 my_message_option = 50001; } extend google.protobuf.FieldOptions { optional float my_field_option = 50002; } extend google.protobuf.EnumOptions { optional bool my_enum_option = 50003; } extend google.protobuf.EnumValueOptions { optional uint32 my_enum_value_option = 50004; } extend google.protobuf.ServiceOptions { optional MyEnum my_service_option = 50005; } extend google.protobuf.MethodOptions { optional MyMessage my_method_option = 50006; } option (my_file_option) = "Hello world!"; message MyMessage { option (my_message_option) = 1234; optional int32 foo = 1 [(my_field_option) = 4.5]; optional string bar = 2; } enum MyEnum { option (my_enum_option) = true; FOO = 1 [(my_enum_value_option) = 321]; BAR = 2; } message RequestType {} message ResponseType {} service MyService { option (my_service_option) = FOO; rpc MyMethod(RequestType) returns(ResponseType) { // Note: my_method_option has type MyMessage. We can set each field // within it using a separate "option" line. option (my_method_option).foo = 567; option (my_method_option).bar = "Some string"; } }
注意若是你在另外一个包中使用这个包定义的选项的话,你必须使用包名字做为前缀:
// foo.proto import "google/protobuf/descriptor.proto"; package foo; extend google.protobuf.MessageOptions { optional string my_option = 51234; } // bar.proto import "foo.proto"; package bar; message MyMessage { option (foo.my_option) = "Hello world!"; }
若是你要用.proto
文件生成 C++ , Java, Python的代码的话,你须要使用protoc来编译.proto
文件。若是你还没安装这个编译器的话,去下载一个吧。
以下执行协议的编译:
protoc –proto_path=IMPORT_PATH –cpp_out=DST_DIR –java_out=DST_DIR –python_out=DST_DIR path/to/file.proto
.proto
文件的搜索目录,默认是当前的工做目录。能够屡次使用这个参数来指定多个目录,他们会按照顺序被检索, -I=IMPORT_PATH
是 --proto_path
的简写。--cpp_out
C++ code in DST_DIR.--java_out
generates Java code in DST_DIR.--python_out
generates Python code in DST_DIR.做为一个额外的便利,若是DST_DIR
以.zip
或者.jar
来结尾的话,编译器会自动给你打包。注意若是指定路径已经存在的话会被覆盖。
.proto
文件。多个文件能够一次全给定。文件名必须是相对当前目录的相对路径名。每一个文件都应该在IMPORT_PATHs
指定的某个路径下!