个人觉得,有谷歌翻译,百度翻译。加上自己的理解。自个看看官方文档也还不错。
下面98%是谷歌翻译跟百度翻译的。剩余2%是我自己按照自己的理解。
免责声明:粗糙翻译,随便看看哈。想纠正的。留言即可。本人水平有限。
Overview(概览)
这个页面的内容,你用谷歌翻译看下。大概意思就是什么是protobuf?怎么怎么diao,然后跟xml一番对比。然后谦虚了一把(阅读性不好、自描述性不好),后面介绍了proto3、protobuf历史(它要解决什么问题)。
由于第一次使用主要就看proto3了。
proto3语言指南 (Language Guide proto3)
定义一个消息类型(Defining A Message Type)
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 如果不指定 syntax 为proto3 会认为是proto2的语法。
- SearchRequest消息定义了三个字段(name/value)。每个字段对应一个要包含在此类型消息中的数据。每个字段都有名字跟类型。
指定字段类型 && 分配字段编号
- 除了指定标量类型,还可以指定复合类型(包括枚举和其他类型)
- 分配字段编号 编号用于以消息二进制格式标识字段,并在使用消息类型后不应更改。1~15占1个字节(包括字段编号和字段类型)。16~2047占两个字节。最小为1,最大为2^29-1或536,870,911不能使用19000到19999,这是保留字段
指定字段规则
单数:格式良好的消息可以有零个或一个字段(但不能超过一个)。
重复:此字段在格式良好的消息中可以重复任意次数(包括零)。将保留重复值的顺序。
proto3中,repeated 标量数字类型的字段使用packed作为默认编码。
关于编码参考https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn#packed
添加更多的消息类型
可以在一个.proto文件中定义多种消息类型。如果要定义与SearchResponse消息类型对应的答复消息格式,可以将其添加到相同的.proto:
注释格式 /**/ //
保留字段
如果通过完全删除字段或将其注释来更新消息类型,则未来用户可以在对类型进行自己的更新时重用字段编号。如果以后加载旧版本的旧版本.proto,包括数据损坏,隐私错误等,这可能会导致严重问题。指定已删除字段的字段编号reserved。如果之后使用这些被删除的字段,编译器会提示。
不能在同一reserved语句中混合字段名称和字段编号。
你的proto会生成什么
编译proto根据你所选择的语言,根据消息类型的的描述生成到文件中。包含getting 跟setting filed values,序列化消息输出流,通过输入流解析消息。
go生成的是.pb.go文件,每种消息都在这个文件中。
标量值类型
.proto TYpe | GO Type | PHP Type |
---|---|---|
double | float64 | float |
float | float32 | float |
int32(如果有负数请使用sint32) | int32 | integer |
int64 | int64 | integer/string |
uint32 | uint32 | integer |
uint64 | uint64 | integer/string |
sint32 | int32 | integer |
sint64 | int64 | integer/string |
fixed32 | uint32 | integer |
fixed64 | unit64 | integer/string[ |
sfixed32 | int32 | integer |
sfixed64 | int64 | integer/string |
bool | bool | boolean |
string | String | string |
bytes | []byte | string |
Integer用于64位计算机,字符串用于32位计算机。
默认值
字符串,默认为空字符串
字节,默认为空字节
bool 默认为false
数值类型,默认为0值
枚举类型,默认为第一个枚举值(必须为0)
对于消息字段,字段没有设置。具体的值取决于具体的语言。
重复的字段默认值为空。
请注意,对于标量消息字段,一旦解析了消息,就无法判断字段是否显式设置为默认值(例如布尔值是否设置为false),或者根本没有设置:在定义消息类型时,应该记住这一点。例如,如果您不希望某些行为在默认情况下也发生,则不要使用布尔值在设置为false时打开某些行为。还要注意,如果将标量消息字段设置为其默认值,则该值将不会在线路上序列化。
有关默认值如何在生成的代码中工作的更多详细信息,请参阅所选语言的生成代码指南。
枚举
在定义消息类型时,您可能希望其中一个字段只有一个预定义的值列表。
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;
}
每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:
必须有一个零值,以便我们可以使用0作为数字默认值。
零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。
您可以通过为不同的枚举常量指定相同的值来定义别名。为此,您需要将allow_alias选项设置为true,否则协议编译器将在找到别名时生成错误消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚举器常量必须在32位整数的范围内。由于枚举值在线路上使用变量编码,因此负值效率很低,因此不推荐使用。您可以在消息定义中定义枚举,如上面的示例中所示,也可以在外部定义-这些枚举可以在.proto文件中的任何消息定义中重用。还可以使用一条消息中声明的枚举类型作为另一条消息中字段的类型,使用语法message type.enum type。
在反序列化过程中,消息中将保留无法识别的枚举值,尽管反序列化消息时如何表示枚举值取决于语言。在支持具有指定符号范围之外的开放枚举类型的语言(如C++和GO)中,未知的枚举值仅作为其基础的整数表示形式存储。在具有封闭枚举类型的语言(如Java)中,枚举中的一个实例用于表示未识别的值,而基础的整数可以用特殊的访问器访问。在这两种情况下,如果消息被序列化,则无法识别的值仍将用消息序列化。
保留值
如果通过完全删除枚举项或将其注释掉来更新枚举类型,将来的用户可以在对该类型进行自己的更新时重用该数值。如果他们以后加载相同的.proto的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定保留已删除条目的数值(和/或名称,这也可能导致JSON序列化问题)。如果将来有任何用户试图使用这些标识符,协议缓冲区编译器将发出投诉。可以使用max关键字指定保留的数值范围达到最大可能值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设您希望在每个SearchResponse消息中包含结果消息–为此,可以在相同的.proto中定义结果消息类型,然后在SearchResponse中指定类型为result的字段:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
import
import "myproject/other_protos.proto";
默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。现在,您可以在旧位置放置一个虚拟的.proto文件,使用import public概念将所有导入转发到新位置,而不是直接移动.proto文件并在一次更改中更新所有调用站点。任何导入包含import public语句的协议的人都可以传递依赖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
protocol编译器(protoc命令) 根据 -I/--proto_path到指定的目录下找导入的文件。如果没有指定就到编译器目录下查找。通常你应该将项目的根目录设置给--proto_path,并对所有导入使用完全限定的名称。
可以导入proto2消息类型在proto3中使用。反之亦然。
嵌套类型
你可以在其他消息类型中定义和使用消息类型。
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;
}
}
}
更新消息类型
如果一个现有的消息类型不再满足您的所有需求——例如,您希望消息格式有一个额外的字段——但是您仍然希望使用用旧格式创建的代码,不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单。记住以下规则:
- 不要更改任何现有字段的字段编号。
- 如果添加新字段,则使用“旧”消息格式的代码序列化的任何消息仍然可以由新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,由新代码创建的消息可以由旧代码解析:旧二进制文件在解析时只忽略新字段。有关详细信息,请参阅“未知字段”部分。
- 只要在更新的消息类型中不再使用字段编号,就可以删除字段。您可能需要重命名字段,或者添加前缀“obsolete_u”,或者保留字段编号,这样.proto的未来用户就不能意外地重用该编号。
- int32、uint32、int64、uint64和bool都是兼容的——这意味着您可以在不中断向前或向后兼容性的情况下将字段从这些类型中的一种更改为另一种。如果从一个不适合于相应类型的线中解析一个数,那么您将得到与在C++中将该数转换为该类型的相同的效果(例如,如果一个64位的数字被读取为一个IT32,它将被截断到32位)。
- sint32和sint64彼此兼容,但与其他整数类型不兼容。
- 只要字节是有效的UTF-8,字符串和字节就可以兼容。
- 如果字节包含消息的编码版本,则嵌入消息与字节兼容。
- fixed32与sfixed32兼容,fixed64与sfixed64兼容。
- Enum在Wire格式方面与Int32、UInt32、Int64和UInt64兼容(请注意,如果值不匹配,则会被截断)。但是,请注意,当消息被反序列化时,客户端代码可能会对它们进行不同的处理:例如,消息中将保留无法识别的proto3枚举类型,但是当消息被反序列化时,如何表示这些枚举类型取决于语言。int字段总是保留其值。
- 将单个值更改为新的oneof成员是安全的,并且与二进制兼容。如果您确定一次没有设置多个代码,那么将多个字段移动到新的字段中可能是安全的。将任何字段移动到现有的某个字段中是不安全的。
未知字段
未知字段是格式良好的协议缓冲区序列化数据,表示解析程序无法识别的字段。例如,当一个旧的二进制文件用新字段解析新二进制文件发送的数据时,这些新字段就变成旧二进制文件中的未知字段。
最初,Proto3消息在解析过程中总是丢弃未知字段,但在3.5版中,我们重新引入了保留未知字段以匹配Proto2行为。在3.5及更高版本中,解析期间会保留未知字段,并包含在序列化输出中。
Any
any消息类型允许您将消息用作嵌入类型,而不需要它们的.proto定义。any包含一个任意的序列化消息(以字节为单位),以及一个充当该消息类型的全局唯一标识符并解析为该消息类型的URL。要使用任何类型,您需要导入google/protobuf/any.proto。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
不同语言提供了pack跟unpack 的运行时库用于any values。java中有pack() 、unpack(),C++ 有PackFrom() 、UnpackTo()。
如果您已熟悉proto2语法,则Any类型将替换扩展。
Oneof
如果您有一条包含多个字段的消息,并且在同一时间最多设置一个字段,则可以使用one-of功能强制执行此行为并保存内存。
Setting any member of the oneof automatically clears all the other members. You can check which value in a oneof is set (if any) using a special case() or WhichOneof() method, depending on your chosen language
除了所有的字段共享一个oneof内存外,Oneof字段跟常规字段类似,并最多同时设置一个字段。设置其中一个字段就会清理掉其他成员。根据所选语言,你可以使用case()或者WhichOneof方法检查oneof值的设置。
使用oneof
oneof后面跟名称
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后将一个字段添加到定义中。您可以添加任何类型的字段,但不能使用repeated field。
在生成的代码中,oneof字段与常规字段具有相同的getter和setter。还会有专门方法来检查oneof中的值.
Oneof 特点
- 设置一个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不能为 repeated
反射API适用于oneof字段。
向后兼容性问题
添加或删除其中一个字段时要小心。如果检查oneof的值返回None/NOT_SET,则可能意味着oneof尚未设置,或者已设置为oneof的其他版本中的字段。没有办法区别,因为没有办法知道线路上的未知字段是否是其中一个的成员。
标签复用问题
将字段移入或移出 oneof:在消息序列化和分析之后,可能会丢失某些信息(某些字段将被清除)。不管怎样,你可以安全的移动单个字段到oneof中,如果你知道只会有一个字段会被设置,也可以移动多个字段。
删除oneof字段并将其添加回:这可能会在序列化和分析消息后清除当前设置的oneof字段。
拆分或合并oneof:这与移动常规字段有类似的问题。
Map
如果要在数据定义中创建关联映射,Protobuf提供了一种方便的快捷方式语法:
map<key_type, value_type> map_field = N;
- map字段不能是repeated
- 报文格式排序和map值的迭代排序未定义,因此不能依赖于特定顺序中的map项。
- .proto生成文本格式时,map按键排序。数字键按数字顺序排列。
- 从报文中解析和合并时,如果存在重复的map key,则使用最后一个。当从文本中解析一个map,存在重复的key则可能会失败。
如果map字段提供键但没有值,则字段序列化时的行为取决于语言。在C++、Java和Python中,该类型的默认值被序列化,而在其他语言中,没有任何序列化。
向后兼容性问题
map语法在报文中等同于下面的内容,因此不支持map的protobuf也是可以处理你的数据的:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
包 (Packages)
可以向.proto文件添加可选的包说明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后,在定义消息类型的字段时,可以使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
包说明符影响生成代码的方式取决于所选语言(这里只看go):
- 在go中,包被用作go包的名称,除非在.proto文件中显式地提供一个选项go_package。
包和名称解析
包和名称解析
协议缓冲区中的类型名称解析像C++一样工作:首先搜索最内层的范围,然后搜索最内层的,等等,每个包都被认为是“内部的”到它的父包。前导“.”(例如,.foo.bar.baz)表示从最外面的作用域开始。
协议缓冲区编译器通过分析导入的.proto文件解析所有类型名。每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它有不同的作用域规则。
定义服务
如果要将消息类型与RPC(远程过程调用)系统一起使用,可以在.proto文件中定义一个RPC服务接口,协议缓冲区编译器将以所选语言生成服务接口代码和stub。因此,例如,如果要使用接收SearchRequest并返回SearchResponse的方法定义RPC服务,可以在.proto文件中定义它,如下所示:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
与协议缓冲区一起使用的最简单的RPC系统是GRPC:一个在谷歌开发的语言和平台无关的开源RPC系统。GRPC特别适用于协议缓冲区,允许您使用特殊的协议缓冲区编译器插件直接从.proto文件生成相关的RPC代码。
如果不想使用GRPC,也可以将协议缓冲区与您自己的RPC实现一起使用。你可以在Proto2语言指南中找到更多关于这个的信息。
还有许多正在进行的第三方项目,用于开发协议缓冲区的RPC实现。有关我们知道的项目的链接列表,请参阅第三方附加组件wiki页面。
JSON映射
Proto3支持JSON中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地描述编码。
如果JSON编码数据中缺少值或其值为null,则在解析为protobuf 时,它将被解释为适当的默认值。如果字段在protobuf 中具有默认值,则默认情况下将在JSON编码的数据中省略该字段以节省空间。实现可以提供用于在JSON编码的输出中发出具有默认值的字段的选项。
这个表我没粘贴,自个去官网上看吧。
JSON选项
Proto3 JSON实现可以提供以下选项:
- 使用默认值发出字段:Proto3 JSON输出中默认忽略具有默认值的字段。实现可以提供一个选项,用其默认值覆盖此行为和输出字段。
- 忽略未知字段:Proto3 JSON解析器默认情况下应拒绝未知字段,但可能提供在解析时忽略未知字段的选项。
- 使用proto字段名而不是lowercamelcase名称:默认情况下,proto3 json打印机应将字段名转换为lowercamelcase,并将其用作json名称。实现可以提供一个选项来使用proto字段名作为JSON名称。Proto3 JSON解析器需要接受转换后的lowercamelcase名称和Proto字段名称。
- 将枚举值作为整数而不是字符串发出:默认情况下,在JSON输出中使用枚举值的名称。可以提供一个选项来使用枚举值的数值。
选项
.proto文件中的单个声明可以用许多选项进行注释。选项不会改变声明的整体含义,但可能影响在特定上下文中处理声明的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。
有些选项是文件级选项,这意味着它们应该在顶级范围内写入,而不是在任何消息、枚举或服务定义内写入。有些选项是消息级选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。还可以在枚举类型、枚举值、服务类型和服务方法上编写选项;但是,目前没有任何有用的选项可用于这些类型。
生成你的类
编译示例:
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指定.proto解析import指令时在其中查找文件的目录。如果省略,则使用当前目录。可以通过--proto_path多次传递选项来指定多个导入目录; 他们将按顺序搜索。 可以用作简短的形式。 -I=IMPORT_PATH--proto_path
** --go_out生成Go代码DST_DIR。有关更多信息,请参阅Go生成代码参考。
** --php_out生成PHP代码DST_DIR。看到PHP生成的代码的参考更多。
- 您必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个文件中,IMPORT_PATH以便编译器可以确定其规范名称。
风格指南(style guide)
下面是.proto文件的风格指南。遵循这些约定,可以让你的protobuf 消息定义以及相应的类保持一致并易于阅读:
文件格式标准(Standard file formatting)
- 每行保持80个字符
- 缩进采用两个空格
文件结构(File structure)
文件命名如lower_snake_case.proto
(小写蛇形命名法,下划线命名)
文件中要保持如下的顺序(书写顺序):
- 许可证
2.文件概述
3.Syntax (指定proto版本)
4.包(package)
5.import
6.File options
7.其他
包(packages)
包名称应为小写,并且应与目录层次结构相对应。例如,如果文件在my/package/,则包名称应为my.package。
消息和字段名称(Message and field names)
消息名称采用大写驼峰(CamelCase)。字段名称采用下划线分割名称(underscore_separated_names)
message SongServerRequest {
required string song_name = 1;
}
据此生成后的代码中访问方法如下:
C++:
const string& song_name() { ... }
void set_song_name(const string& x) { ... }
Java:
public String getSongName() { ... }
public Builder setSongName(String v) { ... }
Go中类似如下:
func (m *Person_PhoneNumber) GetNumber() string {
if m != nil {
return m.Number
}
return ""
}
func (m *Person_PhoneNumber) GetType() Person_PhoneType {
if m != nil {
return m.Type
}
return Person_MOBILE
}
如果您的字段名称包含数字,则该数字应显示在字母后面而不是下划线之后。例如,用song_name1而不是song_name_1。
Repeated fields
对重复的字段使用复数名称(可以理解为是数组)。
repeated string keys = 1;
...
repeated MyMessage accounts = 17;
枚举
对于枚举类型名称使用CamelCase,值命名采用CAPITALS_WITH_UNDERSCORES
enum Foo {
FOO_UNSPECIFIED = 0;
FOO_FIRST_VALUE = 1;
FOO_SECOND_VALUE = 2;
}
每个枚举值应以分号结束,而不是逗号。更喜欢为枚举值添加前缀,而不是将其包围在封闭消息中。零值枚举应具有后缀缺省。
Services(服务)
如果您.proto定义了RPC服务,则应将CamelCase(首字母大写)用于服务名称和任何RPC方法名称:
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
参考资料:
- 《protoc2 与 protoc3 区别》https://www.jianshu.com/p/cdedcf696e9e