Protobuf
proto3
编写.proto文件来构造我们需要的信息体结构
定义一个信息类型
syntax="proto3"
message SearchRequest{
string query=1;
int32 page_number=2;
int32 result_per_page=3;
}
首先第一行必须指定proto文件用的是哪个版本的语言,syntax="proto3"表明本文件使用proto3.
一个信息类型由message关键字开头声明,括号内是类型、字段名的组合,每个字段都有一个唯一的数字标识,是字段名后的那个数字。这个数字区分每个字段编码后的二进制形式,这个数字表示该字段的类型和该数字在编码时对应的字节数,1-15代表字段类型和数字由一个字节编码,16-2047则需要两个字节编码。
因此,为了节省编码空间,经常需要使用到的字段,应指定为1-15,同时在设计时要考虑预留1-15中的数字给未来可能常用到的字段。
最小的字段编号为1,最大为 536,870,911 (2的29次方减1)。19000-19999是pb预留的数字,这部分数字是不能使用的;同时,由于这个数字对于字段而言是唯一的,因此不能重复使用;对于用reserved指定的保留数字,也不能再使用。
message的字段有两种类型:
1.字段类型前没有任何关键词的,这是proto3默认的规则,这种字段其实叫做singular字段,类似与proto2中的optional,message中可以有,也可以没有该字段的信息。
message m1{
int testNum=1;
}
2.字段类型前指定为repeated,这个字段可以被重复任意次数,且值的顺序会被保存
message m3{
repeated int testNum=1;
}
在编码时,repeated字段采用packed方式进行编码。
同一个.proto文件中,可以定义多个message;protobuf语言的注释方式和C++一样。
保留字段 reserved field
在开发过程中可能会涉及到对proto文件中message各个fields的修改,可能是更新、删除某个field及其表示,这样可能会导致调用的服务失败。其中一个防止这种问题的方式是,确保你要删除的field的标识(或是名字)是reserved,具体protobuf的编译器会决定未来这个field表示能否被使用
当我们想要通过删除message中某个字段,或者直接注释掉来修改/更新这个message时,应当将这些字段对应的字段编号和/或字段名通过reserved关键字进行保留,使得别人用到这些字段时,protobuf编译器可提前发现问题。
message Foo{
reserved 2,5,9 to 11;
reserved "foo","bar";
}
字段编号和字段名的保留声明需要分开,不能一句reserved语言同时含有字段名和字段编号。
reserved的字段编号和字段名都不允许再出现在message定义中:
message re{
int32 a=1;
reserved 1;
}
**上述代码是错误的,**reserved了1之后,就不能再用1了。
.proto文件生成什么?
.proto文件在protobuf编译器编译后,会根据不同的语言生成不同语言所需的依赖文件:
C++,则会生成.h和.cpp文件,用户include头文件来使用pb定义的信息
GO,则生成.go文件,是一个package,用户直接import这个包来使用pb定义的信息
不同语言生成的代码API不同,需要根据语言查询相关文档。
pb中scalar字段类型和具体语言对应的数据类型
scalar type(标量类型) 表示只有一个值;复合类型则表示包含多个值,例如结构体。
下面的表格去除没学到的其他语言,想要这些语言的信息可在官网查询。
https://developers.google.com/protocol-buffers/docs/proto3#scalar
| .proto Type | Notes | C++ Type | Python Type[3] | Go Type |
|---|---|---|---|---|
| double | double | float | float64 | |
| float | float | float | float32 | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int32 |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | int/long[4] | int64 |
| uint32 | Uses variable-length encoding. | uint32 | int/long[4] | uint32 |
| uint64 | Uses variable-length encoding. | uint64 | int/long[4] | uint64 |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int32 |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | int/long[4] | int64 |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int/long[4] | uint32 |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | int/long[4] | uint64 |
| sfixed32 | Always four bytes. | int32 | int | int32 |
| sfixed64 | Always eight bytes. | int64 | int/long[4] | int64 |
| bool | bool | bool | bool | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | str/unicode[5] | string |
| bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | str | []byte |
字段默认值
如果message中的字段没有赋值,则这些字段的值会被赋为和字段类型相对应的默认值:
-
For strings, the default value is the empty string.
字符串字段默认值为空串
-
For bytes, the default value is empty bytes.
字节型字段默认值为空
-
For bools, the default value is false.
布尔型默认为false
-
For numeric types, the default value is zero.
数字型默认为0
-
For enums, the default value is the first defined enum value, which must be 0.
枚举型,默认值为定义时的第一个枚举值,必须为0
-
The default value for repeated fields is empty (generally an empty list in the appropriate language).
repeated字段默认值为空,一般来说是个空列表,这个需要根据目标导出语言决定。
-
For message fields, the field is not set. Its exact value is language-dependent. See the generated code guide for details.
对于嵌套的message字段,该字段的默认值没有设置,具体值和要编译的语言相关(C++,GO)
如果scalar type的字段被设置为默认值,则不会进行编码。( Also note that if a scalar message field is set to its default, the value will not be serialized on the wire. )
枚举类型
枚举类型通常用于指定某个字段的值为我们定义的枚举类型中的值:
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;
}
上述的message中,corpus字段是一个Corpus枚举类型,这个枚举类型在message内部定义,外部别的message是不可见的。
如果外部确实想用这个枚举类型,则在别的message定义中,使用MessageType.EnumType:
message MyMessage2{
enum NotAllowAlias{
UNKNOWN=0;
STARTED=1;
}
}
message MyMessage3{
MyMessage2.NotAllowAlias nested=1;
}
枚举类型的第一个值必须是0,一是为了满足默认值的设置,二是为了兼容proto2。
枚举类型中每个名称的值默认是不同的,如果想要不同名称有相同的值,需要在定义中设置allow_alias为true:
message myMessage1{
enum AllowAliases{
option allow_alias=true //允许重复的枚举值
UNKNOWN=0;
STARTED=1;
RUNNING=1; //注意就是这用了重复的枚举值
}
}
message myMessage2{
enum NoAllowAliases{
UNKNOWN=0;
STARTED=1;
RUNNING=1;//由于没有设置allow_alias为true,这个定义是错误的,编译器会报警。
}
}
枚举值必须在32位有符号整型范围内,但由于负数编码效率低,推荐使用正数的枚举值。
对于C++和GO而言,如果反序列化后,发现值不在定义的枚举值内,仍然会将得到的值存在底层的整型数据中。
同理,删除枚举类型中某些枚举值和枚举名,应采用reserved保证安全性:
enum myEnum{
reserved 2,5,7,9 to max
reserved "FOO","BAR"
}
规则与前面提到的reserved一致。
使用别的message作为字段
message Event{
string name=1;
Time eventTime=2;
}
message Time{
string hour=1;
string minute=2;
string second=3;
}
导入定义
在protobuf中,如果想要用到别的.proto文件中定义的message,可以使用:
import "path/needed.proto",import语句必须在proto文件的顶端
默认情况下,我们只能使用直接import的proto文件中的定义,编译器不会寻找多层依赖,只会使用到一层依赖。但有时候可能多个proto文件import了proto文件A,而文件A可能发生了路径的变化,这时候如果每个proto文件都去修改import语句,则工作量较大。因此,通过使用import public来实现间接import,但也只有这一层间接:
//new.proto
//all definitions are moved here
所有的定义从old.proto复制到了new.proto
//old.proto
import public "path/new.proto"
import "other.proto"
old.proto就是其他proto文件原本所import的文件,只不过定义都移到new.proto中了
//client.proto
import "path/old.proto"
client.proto就是我们import别人的proto
通过old.proto中的import public,实现client.proto引用new.proto中的定义,但要注意的是,因为只能使用直接import的定义,所以client.proto无法使用other.proto中的定义。
具体来说,如果A文件原本import了B文件,当B文件目录发生变化时,我们可以cp B.proto newpath/C.proto,然后将原位置的B文件的最顶端插入import public "newpath/C.proto",就能实现依赖的变化,而无需改变所有处于A文件相同地位的文件中的import语句。
通过命令行 -I/--proto_path 指定protobuf编译器搜索路径,protobuf编译器会在这些目录下去寻找import的Proto文件。如果没有指定,则默认是在执行编译器时所在的目录下和默认搜索路径下搜索。
proto2和proto3的文件可以互相import,但是proto2的枚举类型无法在proto3中直接使用。
如果protoA导入了protoB,在编译protoA时,编译器并不会根据依赖去编译protoB,而在使用protoA.pb.go文件时,前提是protoB.pb.go已存在,因为编译出来的protoA.pb.go文件在头部import了protoB对应的包。
嵌套的message
message Nes{
message bird{
int32 a=1;
int32 b=2;
}
}
message Kid{
Nes.bird a=1;
}
通过类似作用域的访问方式来使用别的message内部嵌套的message类型,类似Outer.Middle.Inner。
更新message类型 结合例子补充
如果现有的message定义不满足我们未来的需求,需要进行修改时,只要满足下面的条件,就不需要去修改使用旧版本proto编写的代码。
-
Don't change the field numbers for any existing fields.
不要更改任何已有字段的编号
-
If you add new fields, any messages serialized by code using your "old" message format can still be parsed by your new generated code. You should keep in mind the default values for these elements so that new code can properly interact with messages generated by old code. Similarly, messages created by your new code can be parsed by your old code: old binaries simply ignore the new field when parsing. See the Unknown Fields section for details.
添加新的字段后,使用旧的proto的代码也可以正常使用,只是这些新的字段会被当成Unknown Field,不影响使用。
-
Fields can be removed, as long as the field number is not used again in your updated message type. You may want to rename the field instead, perhaps adding the prefix "OBSOLETE_", or make the field number reserved, so that future users of your
.protocan't accidentally reuse the number.字段可以删除,可以使用reserved来保证以后用不到该字段编号;只要保证更新的message定义中不再使用该字段的编号。
由于reserved字段编号后,message定义中不再有该字段编号的字段,所以猜测pb解码时是先获取字段的编号,再按照字段的类型去解码的,猜想如果指定是reserved,则解码时不对这些字段反序列化。(因为确实难找到案例,需要后续跟进)
-
int32,uint32,int64,uint64, andboolare all compatible – this means you can change a field from one of these types to another without breaking forwards- or backwards-compatibility. If a number is parsed from the wire which doesn't fit in the corresponding type, you will get the same effect as if you had cast the number to that type in C++ (e.g. if a 64-bit number is read as an int32, it will be truncated to 32 bits). -
sint32andsint64are compatible with each other but are not compatible with the other integer types. -
stringandbytesare compatible as long as the bytes are valid UTF-8. -
Embedded messages are compatible with
bytesif the bytes contain an encoded version of the message. -
fixed32is compatible withsfixed32, andfixed64withsfixed64. -
For
string,bytes, and message fields,optionalis compatible withrepeated. Given serialized data of a repeated field as input, clients that expect this field to beoptionalwill take the last input value if it's a primitive type field or merge all input elements if it's a message type field. Note that this is not generally safe for numeric types, including bools and enums. Repeated fields of numeric types can be serialized in the packed format, which will not be parsed correctly when anoptionalfield is expected. -
enumis compatible withint32,uint32,int64, anduint64in terms of wire format (note that values will be truncated if they don't fit). However be aware that client code may treat them differently when the message is deserialized: for example, unrecognized proto3enumtypes will be preserved in the message, but how this is represented when the message is deserialized is language-dependent. Int fields always just preserve their value. -
Changing a single value into a member of a new
oneofis safe and binary compatible. Moving multiple fields into a newoneofmay be safe if you are sure that no code sets more than one at a time. Moving any fields into an existingoneofis not safe.
Unknown Field
未知字段指在反序列化时无法识别的字段,这通常见于对端发来了一个按照新版本message编码的数据包,而本地使用的是旧版本的message来反序列化解析,这些新的字段会在本地反序列化后当成UnknownField。
Any 特性比较特殊,后面结合例子补充
https://developers.google.com/protocol-buffers/docs/proto3#any
The Any message type lets you use messages as embedded types without having their .proto definition. An Any contains an arbitrary serialized message as bytes, along with a URL that acts as a globally unique identifier for and resolves to that message's type. To use the Any type, you need to import google/protobuf/any.proto.
Any类型可以使用户在没有某个message定义的情况下,用作另一个message的字段;Any字段的值包含任意序列化的message,格式为bytes,因为message和bytes是互相兼容的,所以Any可以是任何一种message类型的值。 Any包含任意序列化的消息(字节),以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。
The default type URL for a given message type is type.googleapis.com/_packagename_._messagename_.
要使用Any类型的字段,必须import "google/protobuf/any.proto"
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2; //here
}
Different language implementations will support runtime library helpers to pack and unpack Any values in a typesafe manner – for example, in Java, the Any type will have special pack() and unpack() accessors, while in C++ there are PackFrom() and UnpackTo() methods:
不同语言编译出的代码文件有不同的API来支持从Any字段转换成具体的message类型,以Java为例:
// 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 ...
}
}
**困惑点:**没有message的定义,Any是如何提供api来判断Any字段的值是哪个message类型呢?(也即detail.Is
**A:**目前看到的例子,大致理解为,使用proto的用户,知道某个message的定义,例如通过include(C++)或import(go)protoc编译后的代码文件,知道了message A的定义,然后通过编译后提供的api来测试Any字段是否是这个message A类型,再通过类似UnpackTo的API来将Any字段的值(底层是bytes类型)反序列化成具体的message。
C++的例子:https://blog.csdn.net/u011573853/article/details/73060934
oneof
oneof字段类型由多个字段组成,在序列化和反序列化后只有其中的一个字段是有值的,类似于C++中的union类。如果想要在某一时刻只有一个字段被设置值了,则需要使用oneof:
message ONEOF{
oneof choice{
string name=1;
int32 id=2;
}
}
要注意语法上,oneof的的花括号最后不加分号,而内部的字段具有字段编号。oneof中的字段可以是除了map和repeated外的所有类型。可以通过编译出来的具体语言的api来判断oneof字段到底设置了哪个字段的值,oneof字段也有setter和getter的api。
oneof字段的特点
1.后面设置的oneof的值会覆盖前面设置的值,多次setter则oneof字段的值是最后一次设置的值
ONEOF msg;
msg.set_name("halo");
CHECK(msg.has_name())
msg.set_id(2) //这个会清除掉之前设置的name
CHECK(!msg.has_name())
2.If the parser encounters multiple members of the same oneof on the wire, only the last member seen is used in the parsed message.
如果解析器遇到了同一个oneof字段的多个成员,则会取最后一个成员用于解析message
3.Reflection反射API可以用于oneof
4.如果对oneof中的字段设置了默认值,例如对其中的int32设置了0,该值会被序列化,并能通过api判断设置了int32的字段
5.如果在C++中使用含有oneof的message,需要注意内存冲突,因为本质上是同一块内存,如果前面设置了字段A后面用字段B的方式去访问就会crash:
ONEOF msg;
int* id=msg.mutable_id();
msg.set_name("hello");
*id=10; //crash 因为msg的oneof字段已经是string型,这时候对那片空间赋整型值是错误的
6.同样,在C++中,如果对同一个含有oneof字段的message类型的两个实例采用Swap(),则要注意的是oneof字段互换,因此存在msgA的oneof是个int,msgB的oneof是个string,swap后,oneof的类型也跟着换,后续操作需要特别注意别出现类型错误。
ONEOF msgA,msgB;
msgA.set_name("hello");
msgB.set_id(5);
msgA.Swap(&msgB);
CHECK(msgA.have_id());
CHECK(msgB.have_name());
兼容性问题
oneof字段也可以删除和添加来更新message,但如果使用api检车oneof字段是否被设置,返回了NONE/NOT_SET,这意味着要么oneof没有被设置,要么使用了不同版本的oneof,对于用户而言,用户无法感知,因为没办法知道一个Unknown Field是不是oneof中的字段。
Tag Reuse Issues
- Move fields into or out of a oneof: You may lose some of your information (some fields will be cleared) after the message is serialized and parsed. However, you can safely move a single field into a new oneof and may be able to move multiple fields if it is known that only one is ever set.
- Delete a oneof field and add it back: This may clear your currently set oneof field after the message is serialized and parsed.
- Split or merge oneof: This has similar issues to moving regular fields.
map字段
map字段的定义方式:
message havemap{
map<key_type,value_type> mymap=1;
}
其中key_type的类型可以是除了浮点数和bytes类型外的任何标量类型(scalar type);要注意枚举类型不是有效的key_type;value_type可以是除了map类型以外的所有类型。
特性:
1.map字段不能是repeated的。
2.序列化后的map中元素的顺序是不确定的,不要有任何的预想。(猜测,底层应当是个哈希表实现)
3.当从.proto文件中生成文本格式的map时,map是按照key的值排序的。
4.当从二进制数据解析或message进行合并(merge)的时候,如果出现了重复的key,则使用最后的那个key-value对。而如果是生成文本格式的map,出现重复的key则报错。
5.如果提供了map的key,但没有指定value,该字段序列化的行为是语言决定的,若是C++,JAVA,Kotlin,Python,则会赋予默认值来序列化。
兼容性
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何方式的pb实现,都要能够产生和接受按照上述定义的map,以实现兼容。
package
package关键字是用来指定.proto文件属于哪个包,这是用来避免message重名的情况,通过包名的作用域访问,来避免该现象。
syntax="proto3";
package foo.bar
message Open{
string open=1;
}
在别的.proto文件中,通过作用域访问方式来使用这些类型:
message AnotherMSG{
foo.bar.Open open=1;
}
- In C++ the generated classes are wrapped inside a C++ namespace. For example,
Openwould be in the namespacefoo::bar. - In Go, the package is used as the Go package name, unless you explicitly provide an
option go_packagein your.protofile.
以go为例,想要使用package的话,.proto文件应该这样写:
//packagename.proto
syntax = "proto3";
package pblearn.havename;
option go_package="pblearn/havename/";
message Hello {
string say=1;
}
要注意作用域的名字是pblearn.havename,而编译出来的go文件的包名为havename(这是go_package指定的),必须要有go_package这一行,否则不能编译。
//client.proto
syntax="proto3";
option go_package="/home/rex/helloworld/pkname";
import "packagename.proto";
message MessageName {
pblearn.havename.Hello he=1;
}
定义服务define service 入职后看公司代码
如果想要在一个rpc系统中使用pb定义的message类型,可以通过在.proto定义RPC service接口实现,pb会编译出对应语言的接口代码。
service SearchService{
rpc Search(SearchRequest) returns(SearchResponse);
}
能够直接使用这种service是gRPC(谷歌的rpc),也可以自己开发的rpc来适配这种通信方式,也有许多第三方的rpc支持该特性。
If you don't want to use gRPC, it's also possible to use protocol buffers with your own RPC implementation. You can find out more about this in the Proto2 Language Guide.
JSON mapping pb与json互转
protobuf提供了完整的从pb序列化为json格式的能力,并给出了pb中的类型与json的类型的一一对应关系。需要注意的是,如果pb的message中含有字段A,而json的数据没有该字段的数据或者值为null,那么从json数据转为pb数据时,这些字段会使用默认值。相应的,如果message中的字段是默认值,为了减小数据包大小,默认值的字段不会编码为json格式,编码后的json输出不包含这些字段的信息。
| proto3 | JSON | JSON example | Notes |
|---|---|---|---|
| message | object | {"fooBar": v, "g": null, …} |
Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type. |
| enum | string | "FOO_BAR" |
The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values. |
| map<K,V> | object | {"k": v, …} |
All keys are converted to strings. |
| repeated V | array | [v, …] |
null is accepted as the empty list []. |
| bool | true, false | true, false |
|
| string | string | "Hello World!" |
|
| bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted. |
| int32, fixed32, uint32 | number | 1, -10, 0 |
JSON value will be a decimal number. Either numbers or strings are accepted. |
| int64, fixed64, uint64 | string | "1", "-10" |
JSON value will be a decimal string. Either numbers or strings are accepted. |
| float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0. |
| Any | object |
{"@type": "url", "f": v, … } |
If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy}. Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
| Timestamp | string | "1972-01-01T10:00:20.021Z" |
Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than "Z" are also accepted. |
| Duration | string | "1.000340012s", "1s" |
Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required. |
| Struct | object |
{ … } |
Any JSON object. See struct.proto. |
| Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … |
Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
| FieldMask | string | "f.fooBar,h" |
See field_mask.proto. |
| ListValue | array | [foo, bar, …] |
|
| Value | value | Any JSON value. Check google.protobuf.Value for details. | |
| NullValue | null | JSON null | |
| Empty | object | {} |
An empty JSON object |
json选项
pb导出json格式时,可以指定以下的选项:
1.忽略取值为默认值的字段信息,如果字段的值是默认值,则该字段被忽略,不会编码输出。
2.忽略未知字段,pb json解析器可以忽略未知字段
3.使用指定的json字段名,而不是lowerCamelCase名字;默认情况下,pb json解析器使用pb字段名的lowerCamelCase形式的名字,可以通过选项来指定某个字段输出到json后的字段名,pb json解析器能够解析这两种方式的名字。
4.枚举类型转换成json时,既可以用枚举值的名字作为输出,也可以用枚举值作为输出,这可以通过选项进行调整。
以上四种都可以通过选项进行调整,改变pb json解析器的行为。
options pb选项
pb的所有选项都定义在 google/protobuf/descriptor.proto 文件中。
Some options are file-level options, meaning they should be written at the top-level scope, not inside any message, enum, or service definition. Some options are message-level options, meaning they should be written inside message definitions. Some options are field-level options, meaning they should be written inside field definitions. Options can also be written on enum types, enum values, oneof fields, service types, and service methods; however, no useful options currently exist for any of these.
选项有文件水平的选项、message水平的选项、字段水平的选项等等。
常用的一些选项:
-
optimize_for(file option): Can be set toSPEED,CODE_SIZE, orLITE_RUNTIME. This affects the C++ and Java code generators (and possibly third-party generators) in the following ways:SPEED(default): The protocol buffer compiler will generate code for serializing, parsing, and performing other common operations on your message types. This code is highly optimized.CODE_SIZE: The protocol buffer compiler will generate minimal classes and will rely on shared, reflection-based code to implement serialialization, parsing, and various other operations. The generated code will thus be much smaller than withSPEED, but operations will be slower. Classes will still implement exactly the same public API as they do inSPEEDmode. This mode is most useful in apps that contain a very large number.protofiles and do not need all of them to be blindingly fast.LITE_RUNTIME: The protocol buffer compiler will generate classes that depend only on the "lite" runtime library (libprotobuf-liteinstead oflibprotobuf). The lite runtime is much smaller than the full library (around an order of magnitude smaller) but omits certain features like descriptors and reflection. This is particularly useful for apps running on constrained platforms like mobile phones. The compiler will still generate fast implementations of all methods as it does inSPEEDmode. Generated classes will only implement theMessageLiteinterface in each language, which provides only a subset of the methods of the fullMessageinterface.
option optimize_for = CODE_SIZE; -
cc_enable_arenas(file option): Enables arena allocation for C++ generated code. -
deprecated(field option): If set totrue, indicates that the field is deprecated and should not be used by new code. In most languages this has no actual effect. In Java, this becomes a@Deprecatedannotation. In the future, other language-specific code generators may generate deprecation annotations on the field's accessors, which will in turn cause a warning to be emitted when compiling code which attempts to use the field. If the field is not used by anyone and you want to prevent new users from using it, consider replacing the field declaration with a reserved statement.int32 old_field = 6 [deprecated = true];deprecated和reserved功能相似,推荐使用reserved而不是deprecated选项。
编写.proto文件后编译go代码
编写.proto文件时,需要在syntax行后指定一个选项:
option go_package="path/" 注意一定要有最后一个/
不然无法编译,该选项的意义为,指定生成go代码文件的目录,如果没有改目录,编译器会自动创建。
编译出来的go代码文件名、package名的关系:
1.假设proto文件名为protofile.proto,则编译出来的go代码文件名为protofile.pb.go
2.如果go_package的值为option go_package="dirname/pkname"
则会创建一个dirname/pkname的目录,该目录下放置编译的go代码文件,且go代码的package名为pkname
protoc编译生成go代码文件的命令行指令(最简单的版本):
protoc --go_out=path file1.proto file2.proto
go_out指定的是go代码文件输出的前缀路径,而后缀路径由option go_package指定;举例,若go_out指定为/home/rex,go_package指定为helloworld/nihao,则go文件最终的完整目录为/home/rex/helloworld/nihao
关于更新message的实践
经测试发现,protobuf中message最重要的信息是字段编号,只要字段类型相互兼容(例如enum类型和int类型是兼容的),以及服务器和客户端各自使用的.proto文件中字段编号是相同的,那么就可以正常编解码。具体的字段名只会影响生成的.pb.go文件中相关的API的名字而已,与编解码无关。
//client使用的proto文件
message Test{
int32 i1=1;
enum NUMBER{
DEFAULT=0;
ACTIVATION=1;
SHUTDOWN=2;
}
NUMBER num=2;
}
//server使用的proto文件
message Test{
int32 ii1=1;
enum NUMBER{
DEFAULT=0;
ACTIVATION=1;
SHUTDOWN=2;
}
// NUMBER num=2;
}
即使两边使用的message的字段名不同,一样可以解码,只是Server端调用的是msg.GetIi1(),并不影响编解码。
如果将server的proto文件中第一个字段改成:
int32 ii1=2;
会发现也可以GetIi1(),这其实是将client传来的num字段解码成ii1字段了,因为在client中该字段的编号为2,这整明了protobuf就是根据字段编号和字段类型兼容性来编解码的。
此外,只要是解码端不认识的字段,都会自动变为未知字段(UnknownField),解码端可以正常解码,并且会将这些不认识的字段数据存到未知字段中,不会丢弃。因此,Server的proto文件中直接注掉num字段也能正常进行交互。其实写不写reserved效果是一样的,只要字段在message定义中不存在,解码时都会自动放到未知字段中;而reserved更像是一种提醒,告诉使用者这个字段名或者字段编号在传来的数据中可能是存在的,且如果存在的话解码后都放进了未知字段中。
如果解码端的message定义比对端传来的message多出别的字段,这部分字段解码后会被赋为默认值。
再者,因为pb编解码是根据字段编号来处理二进制数据的,对于两种message,假设分别名为Test和NewTest,只要这两个定义中用到了兼容的字段类型和对应字段编号相同,那么也可以解码:
//假设server的proto文件为
message NewTest{
int32 ii1=1;
enum NUMBER{
DEFAULT=0;
ACTIVATION=1;
SHUTDOWN=2;
}
// NUMBER num=2;
}
server中涉及解码的go代码为:
var buf [512]byte
n,_:=conn.Read(buf[0:])
var res pbtest.NewTest
proto.Unmarshal(buf[0:n],&res)
fmt.Println((&res).GetIi1())
这样的两者通信是没有问题的,虽然pb提供了这种兼容性,但最好两端使用同样的proto文件,不然以后出bug了都不知都怎么回事。
嵌套message案例
//proto文件
message Test{
int32 i1=1;
enum NUMBER{
DEFAULT=0;
ACTIVATION=1;
SHUTDOWN=2;
}
NUMBER num=2;
message inner {
string FirstName=1;
string LastName=2;
}
inner inner_msg=3;
}
client端代码:
func main() {
conn, err := net.Dial("tcp4", "127.0.0.1:9528")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
defer conn.Close()
msg := pbtest.Test{}
msg.I1 = 2
msg.Num = 3
msg.InnerMsg = &pbtest.TestInner{FirstName: "feng", LastName: "zhou"}
res := proto.MessageName(&msg)
fmt.Println(res)
mar_opt := proto.MarshalOptions{AllowPartial: true}
data, _ := mar_opt.Marshal(&msg)
conn.Write(data)
}
server端代码:
func main() {
listener, err := net.Listen("tcp4", "127.0.0.1:9528")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
for {
conn, _ := listener.Accept()
defer conn.Close()
var buf [512]byte
n, _ := conn.Read(buf[0:])
var res pbtest.Test
proto.Unmarshal(buf[0:n], &res)
fmt.Println(res)
fmt.Println((&res).GetInnerMsg().GetFirstName(),
(&res).GetInnerMsg().GetLastName())
}
oneof字段案例
//proto文件
message Test{
int32 i1=1;
enum NUMBER{
DEFAULT=0;
ACTIVATION=1;
SHUTDOWN=2;
}
NUMBER num=2;
message inner {
string FirstName=1;
string LastName=2;
}
inner inner_msg=3;
oneof File{
string FileName=4;
string Date=5;
}
}
client端代码:
func main() {
conn, err := net.Dial("tcp4", "127.0.0.1:9528")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
defer conn.Close()
msg := pbtest.Test{}
fmt.Println(msg)
msg.I1 = 2
fmt.Println(msg)
msg.Num = 3
msg.InnerMsg = &pbtest.TestInner{FirstName: "feng", LastName: "zhou"}
msg.File = &pbtest.Test_FileName{"nmd"} //因为是以指针方式实现的File接口,这里必须有取址
res := proto.MessageName(&msg)
fmt.Println(res)
mar_opt := proto.MarshalOptions{AllowPartial: true}
data, _ := mar_opt.Marshal(&msg)
conn.Write(data)
}
server端代码:
func main() {
listener, err := net.Listen("tcp4", "127.0.0.1:9528")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
for {
conn, _ := listener.Accept()
defer conn.Close()
var buf [512]byte
n, _ := conn.Read(buf[0:])
var res pbtest.Test
proto.Unmarshal(buf[0:n], &res)
fmt.Println(res)
fmt.Println((&res).GetInnerMsg().GetFirstName(),
(&res).GetInnerMsg().GetLastName())
fmt.Println((&res).GetFileName()) //注意这里直接是GetFileName
}
}
编译出来的Test结构体为:
type Test struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
I1 int32 `protobuf:"varint,1,opt,name=i1,proto3" json:"i1,omitempty"`
Num Test_NUMBER `protobuf:"varint,2,opt,name=num,proto3,enum=Test_NUMBER" json:"num,omitempty"`
InnerMsg *TestInner `protobuf:"bytes,3,opt,name=inner_msg,json=innerMsg,proto3" json:"inner_msg,omitempty"`
// Types that are assignable to File:
// *Test_FileName
// *Test_Date
File isTest_File `protobuf_oneof:"File"`
}
需要注意的是,上面的proto文件编译后,得到类型Test,Test_FileName,Test_Date(这里只讨论这几个类型,其他生成的类型这里不提及)和一个名为File的接口类型,Test_FileName,Test_Date以指针的方式实现了File接口。
因此在创建message内容时,直接对File字段赋Test_FileName,Test_Date对象的指针即可。
在反序列化后,使用getter函数获取oneof中字段的值时,直接GetFileName,而不是GetFile().GetFileName(),这是因为oneof花括号包括的字段都是message的字段,所以直接Get这些字段名就行。GetFile返回的是Test结构体中的File字段,只是返回一个接口对象而言。说的可能比较含糊,看代码吧。。