protocol 协议语言介绍
Protocol Buffer是Google提供的一种数据序列化协议,是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
指定版本
protocol 语言文件后面名为.proto
。
文件第一行指定版本。必须在文件首行指定,之前不能有任何空行和注释。可以不指定,默认为proto2。
syntax = "proto3";
定义Message
以message关键字开头,然后指定名称。消息体中时字段的定义,分别指定类型、名称和字段编号。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
示例中只展示了基础类型字段定义,同样可以指定其他枚举类型或者定义好的Message类型。
在二进制格式中,字段编号与类型标识符结合使用。 1到15范围内的字段编号需要一个字节来编码。 从 16 到 2,047 的数字需要 2 个字节。 所以字段编号 1 到 15 的单字节标识符提供更好的性能,所以应将其用于最基本、最常用的字段。
protocol 基础类型与其他语言类型对应关系 Scalar Value Types
关于字段编号是1到2的29次方减一之间的数。不能使用19000到19999之间的数 (FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber))。
复杂类型字段声明
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated关键字表面该字段是一个重复的值,生成对应语言代码时,会是一个集合字段或属性。
保留字段
如果更新服务的消息删除某些字段,应保证不应重复使用该字段编号。如从现有的message中删除字段应该保留其编号。
message Foo {
reserved 2, 15, 9 to 11; //to表示一个连续的范围值
reserved "foo", "bar";
int32 id = 1;
string name = 3;
}
也可以将reserved 关键字用作未来可能添加字段的占位符。
可以通过编号和名称的方式保留字段,但是不能混合使用
添加注释
protocol 和很多主流语言注释方式相同,使用 //
和 /* ... */
的注释方式。
默认值
对于声明的message编码过后,其中定义的字段,会有一个默认的零值。如:
- string:empty
- byte:empty bytes
- bool:false
- numeric type:0
- enum:枚举中定义的第一个值,且必须是0
- For message fields, the field is not set. Its exact value is language-dependent. See the generated code guide for details.
枚举声明
通过enum关键字定义枚举,并什么可以有哪些值。
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
可以看到枚举内的第一个常量定义为0,这是必须的,proto3中所有的字段都是必须的(proto3移除了proto2中required和optional的声明),需要定义一个0的常量作为默认值。
如果枚举不需要共用,可以直接在message内声明并定义,如:
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;
}
allow_alias
如果需要一个枚举内不同的变量声明相同的值,需要开启allow_alias
选项。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
保留值
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
和message定义保留字段一样,不支持序号和名称混合使用。
包(命名空间)声明
通过package关键字指定包名,方便工程化管理,避免命名冲突。对应Go中包名,C#的命名空间。
package foo.bar;
包引入
import引入其他proto文件,对应Go的import,C#的using。
import "google/api/annotations.proto";
proto3 和 proto2 不同版本间定义的message类型可以相互引用,但是proto2 定义的枚举不能被proto3 引入使用。
import public
默认情况下,您只能使用直接导入的 .proto文件定义。然而,有时需要移动 .proto文件到一个新的位置,但不想为此更新所有引用它的.proto文件,此时可以在文件原始位置放置一个仿造的 .proto文件,使用import public将所有导入转发到新位置。任何包含import public语句的proto文件都可以临时依赖import public依赖。例如:
// 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
标志设置为项目的根目录,并对所有导入使用完全限定的名称。
嵌套类型
可以在一个message内部定义一个message,类似C#、java中的内部类
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跟新的需求,例如在不破坏现有代码的前提下新增字段。应遵循如下规则:
- 不要更改现有字段的编号。
- 如果新增消息字段应提供合理的默认值以保证旧服务的代码与新生成的服务代码之间能正常交互。
- 如果确保一个消息字段不会再使用可以,可以删除。或者重命名字段怎加
OBSOLETE_
前缀进行标识,也可以通过预留字段保留字段编号,确保不会重复使用该编号。 - int32, uint32, int64, uint64和bool类型之间时相互兼容的,意味着可以直接修改相应的字段类型而且不破坏兼容性。应该注意的是类似int64改为int32类型是,超出的数据部分会被截断。
- sint32 和sint64 彼此兼容,但与其他整数类型不兼容。
- 如果字节包含消息的编码版本,则嵌入消息与字节兼容。
- 只要字节是有效的UTF-8类型,字符串和字节之间也是是兼容的。
- enum与int32、uint32、int64和uint64协议格式兼容(如果这些值不匹配,它们将被截断)。然而,需要注意的是,当消息被反序列化时,客户端代码可能会以不同的方式对待它们:如,无法识别的proto3枚举类型将保留在消息中,但是当消息被反序列化时,如何表示取决于不同的语言。int字段总是保留其值。
- fixed32与sfixed32兼容, fixed64与sfixed64兼容。
- 单个字段修改为新的oneof成员也是允许的,如果能明确多个字段没有被同时设置,那么多个字段修改为新的onof成员也是相对安全的。任何字段移入现有的oneof成员都是不安全的。
未知字段
未知字段是格式良好的协议缓冲区序列化数据,用于表示解析器无法识别的字段。proto3早期版本中会丢弃未知字段。3.5之后的版本会在解析期间保留未知字段,并包含在序列化输出中以兼容proto2。
Any 类型
Any类型允许将消息作为嵌入类型使用不需要在proto内定义。 类型 Any 可以表示任何已知的 Protobuf 消息类型。使用Any类型需要引入google/protobuf/any.proto
包。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
OneOf
如果消息中包含多个字段,但是最多同时只会设置一个值,可以借助Oneof强制约束并节省内存(oneof
集中所有的字段共享内存)。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
设置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
字段。但是可以在repeated
字段类型内使用oneof
关键字。
Maps
map关键字可以很方便的声明一个map类型字段:
//map<key_type, value_type> map_field = N;
map<string, Project> projects = 3;
key_type可以是任何整数或字符串类型(除float和bytes外的任何标量类型)。
声明map时不能和repeated
一起使用,可以通过如下方式变相定义一个重复的map
message Order {
message Attributes {
map<string, string> values = 1;
}
repeated Attributes attributes = 1;
}
或者
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
服务定义
定义Rpc约束,即声明传入和返回值,可以理解为其他语言中接口(抽象)的定义。
service BlogService {
rpc CreateArticle (CreateArticleRequest) returns (CreateArticleReply) {
option (google.api.http) = {
post: "/v1/article/"
body: "*"
};
}
}
Options
选项(Options)不会影响整体声明,改变的时编译时的处理方式,在google/protobuf/descriptor.proto
查看完整支持的options。
Options分为文件级(只能声明在文件最顶级)、消息级(什么在message内)、字段级(声明field)。
选项也可以声明在enum 、service等类型上。官网文档原话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.刚接触还不是很理解后面这段话的含义。
如果有需要自定义选项,参考文档 。