赞
踩
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。
Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
教程中将描述如何用protocol buffer语言构造你的protocol buffer数据,包括.proto文件的语法以及如何通过.proto文件生成数据访问类。教程中使用的是proto3版本的protocol buffer语言。
在回答这个问题之前,我们还是先给出一个在实际开发中经常会遇到的系统场景。比如:我们的客户端程序是使用Java开发的,可能运行自不同的平台,如:Linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序之间进行数据通讯时存在多种方式用于设计消息格式,如:
对于以上两种方式所产生的问题,Protocol Buffer均可以很好的解决,不仅如此,Protocol Buffer还有一个非常重要的优点就是可以保证同一消息报文新旧版本之间的兼容性。至于具体的方式我们将会在后续的博客中给出。
使用Protobuf的流程基本就是:先创建.proto文件定义消息格式,然后用内嵌的protoc编译。
创建.proto文件,其实就相当于定义数据结构,规定一下我们发消息的格式和内容是什么。
The definitions in a .proto file are simple: you add a message for each data structure you want to serialize, then specify a name and a type for each field in the message.
.proto文件以包声明开头,这有助于防止不同项目之间的命名冲突。
syntax = "proto3";//决定了proto文档的版本号
package GamePlayerTest;//命名空间
/*引用另一个.proto文件中定义*/
import "TestMsg2.proto";
message TestMsg1 { //浮点数 float testF = 1; double testD = 2; //变长编码 //Protobuf会自动优化,可以尽量少的使用字节数,来存储内容 int32 testInt32 = 3; //不太适用于负数 int64 testInt64 = 4; //更适用于负数 sint32 testSInt32 = 5; sint64 testSInt64 = 6; //无符号变长编码 uint32 testUInt32 = 7; uint64 testUInt64 = 8; //固定字节数类型 fixed32 testFixed32 = 9; //通常用于表示大于2的28次方的数 uint fixed64 testFixed64 = 10; //通常用于表示大于2的56次方的数 ulong sfixed32 testSFixed32 = 11; //int sfixed64 testSFixed64 = 12; //long }
这里将给出以上消息定义的关键性说明。
message
是消息定义的关键字,等同于C++/C#中的struct/class,或是Java中的class。TestMsg1
为消息的名字,等同于结构体名或类名。required
前缀叫字段规则或者限定符,表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional
和repeated
,带有这两种限定符的消息字段则没有required
字段这样的限制。相比于optional
,repeated
主要用于表示数组字段。
required
:不可增加或删除的字段,必须初始化optional
:可选字段,可删除,可以不初始化repeated
:可重复字段(相当于List)float
、double
、int64
和string
……分别表示消息的字段类型,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给出。testF
、testD
、testInt32
和testInt64
……分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。testD
字段编码后的数据一定位于testF
之后。需要注意的是该值在同一message中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既标签值和类型信息仅占有一个byte
,标签范围是16到2047的将占有两个bytes
,而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated
类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。enum TestEnum1
{
NORMAL = 0;
BOSS = 5;
}
message TestMsg1
{
//枚举
TestEnum1 test_enum1 = 16;
}
enum
是定义枚举类型的关键字,相当于C++中的enum
,TestEnum1
为枚举的名字,但不同点在于枚举值之间的分隔符是分号,不是逗号。test_enum1
为枚举变量的名字,NORMAL
和BOSS
都是枚举值,0和1表示枚举值所对应的实际整型值,可以为枚举值指定任意的整型数值,不是必须从0开始定义。但是,proto2语法中首行的枚举值总是默认值0,如果为了兼容,则0值必须作为定义的首行。TestMsg1.proto文件:
import "TestMsg2.proto";//引用另一个.proto文件中定义 //嵌套消息 message TestMsg2 { int32 test_int32 = 1; } message TestMsg1 { TestMsg2 test_msg2 = 17; //嵌套枚举 enum TestEnum2 { NORMAL = 0; BOSS = 1; } TestEnum2 test_enum2 = 18; GameSystemTest.HeartMsg heart_msg = 19; }
TestMsg2.proto文件:
syntax = "proto3";
package GameSystemTest;
message HeartMsg
{
int64 time = 1;
}
TestMsg1
消息的定义中包含另外一个消息类型作为其字段,如TestMsg2 test_msg2
。TestMsg1
和TestMsg2
被定义在同一个.proto文件中,那么我们是否可以包含在其他.proto文件中定义的message呢?Protocol Buffer提供了另外一个关键字import
,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他消息定义文件可以通过import
的方式将该文件中定义的消息包含进来,如:import "TestMsg2.proto";//引用另一个.proto文件中定义
message的字段必须符合以下规则:
当时一个被编码的message体中不存在某个message定义中的singular字段时,在message体解析成的对象中,相应字段会被设置为message定义中该字段的默认值。默认值依类型而定:
默认情况下,您只能使用直接导入的.proto文件中的定义。但是,有时你可能需要将.proto文件移动到新位置。现在,你可以在旧位置放置一个虚拟.proto文件,在文件中使用import public
语法将所有导入转发到新位置,而不是直接移动.proto文件并在一次更改中更新所有调用点。任何导入包含import public
语句的.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.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
可以导入proto2版本的消息类型到proto3的消息类型中使用,当然也可以在proto2消息类型中导入proto3的消息类型。但是proto2的枚举类型不能直接应用到proto3的语法中。
如果一个现存的消息类型不再满足你当前的需求,比如说你希望在消息中增加一个额外的字段–但是仍想使用由旧版的消息格式生成的代码,不用担心!只要记住下面的规则,在更新消息定义的同时又不破坏现有的代码就非常简单。
OBSOLETE_
前缀或者将字段编号设置为reserved
,这些未来其他用户就不会意外地重用该字段编号了。未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。
最初,proto3消息在解析期间总是丢弃未知字段,但在3.5版本中,我们重新引入了未知字段的保留以匹配proto2行为。在版本3.5及更高版本中,未知字段在解析期间保留,并包含在序列化输出中。
如果你想创建一个映射作为message定义的一部分,protocol buffers提供了一个简易便利的语法。
map<key_type, value_type> map_field = N;
key_type
可以是任意整数或者字符串(除了浮点数和bytes
以外的所有标量类型)。注意enum
不是一个有效的key_type
。value_type
可以是除了映射以外的任意类型(意思是protocol buffers的消息体中不允许有嵌套map)。
举例来说,假如你想创建一个名为projects的映射,每一个Project消息关联一个字符串键,你可以像如下来定义:
map<string, Project> projects = 15;
syntax = "proto3";//决定了proto文档的版本号 package GamePlayerTest;//命名空间 import "TestMsg2.proto";//引用另一个.proto文件中定义 enum TestEnum1 { NORMAL = 0; BOSS = 5; } //消息类 message TestMsg1 { //成员类型 成员名称 = 唯一编号 //浮点数 float testF = 1; double testD = 2; //变长编码 //Protobuf会自动优化,可以尽量少的使用字节数,来存储内容 int32 testInt32 = 3; //不太适用于负数 int64 testInt64 = 4; //更适用于负数 sint32 testSInt32 = 5; sint64 testSInt64 = 6; //无符号变长编码 uint32 testUInt32 = 7; uint64 testUInt64 = 8; //固定字节数类型 fixed32 testFixed32 = 9; //通常用于表示大于2的28次方的数 uint fixed64 testFixed64 = 10; //通常用于表示大于2的56次方的数 ulong sfixed32 testSFixed32 = 11; //int sfixed64 testSFixed64 = 12; //long //数组 repeated int32 arr_int32 = 13; repeated string arr_string = 14; //字典 map<int32,string> map1 = 15; //枚举 TestEnum1 test_enum1 = 16; //嵌套消息 message TestMsg2 { int32 test_int32 = 1; } TestMsg2 test_msg2 = 17; //嵌套枚举 enum TestEnum2 { NORMAL = 0; BOSS = 1; } TestEnum2 test_enum2 = 18; GameSystemTest.HeartMsg heart_msg = 19; }
syntax = "proto3";
package GameSystemTest;
message HeartMsg
{
int64 time = 1;
}
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP Type |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 总是4个字节 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sfixed64 | 总是8个字节 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | string | String | str/unicode | string | String(UTF-8) | string | string |
bytes | 可能包含任意顺序的字节数据。 | string | ByteString | str | []byte | String(ASCII-8BIT) | ByteString | string |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。