通过protubuf文档先了解一下protobuf语法。
个人网站:https://linzyblog.netlify.app/
示例代码已经上传到github:点击跳转
Protocol Buffers ( Protobuf ) 是一种免费的开源 跨平台数据格式,用于序列化结构化数据。它是谷歌公司开发的一种数据描述语言,并于2008年开源。Protobuf刚开源时的定位类似于XML、JSON
等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。
Protocol Buffers 是一种与语言、平台无关,可扩展的序列化结构化数据的方法,常用于通信协议,数据存储等等。相较于 JSON、XML,它更小、更快、更简单,因此也更受开发人员的青眯。
protobuf官方文档:点击跳转
JSON:最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支持。
JSON数据格式:
{"title":"Protobuf article","status":"DRAFT","members" : [{"name" : "Molecule Man","age" : 29,"secretIdentity" : "Dan Jukes","powers" : ["Radiation resistance","Turning tiny","Radiation blast"]},
}
XML:现在基本很少使用XML。json使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
XML数据格式:
Protobuf articleDRAFT
Protobuf:适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码
。数据本身不具有可读性。因此只有在反序列化之后得到真正可读的数据。
Protobuf数据格式:
##.proto file
message Medium {required string title = 1;enum StatusType {DRAFT = 0;PUBLISHED = 1;}message Status {required StatusType type = 0[default = DRAFT];}required Status status = 2;
}
数据格式 | 数据保存方式 | 可读性/可编辑性 | 解析速度 | 语言支持 | 使用范围 |
---|---|---|---|---|---|
JSON | 文本 | 好 | 一般 | 所有语言 | 文件存储、数据交互 |
XML | 文本 | 好 | 慢 | 所有语言 | 文件存储、数据交互 |
Protobuf | 二进制 | 不可读 | 快 | 所有语言 | 文件存储、数据交互 |
解析器
检索时解析它们,这个过程在处理和内存消耗方面可能非常昂贵。但是使用protobuf,它使用预定义模式,使得解析逻辑高效而简单。当然也不能一味的使用Protobuf,JSON适用的场景远远大于Protobuf,在有些时候Protocol Buffers 仍然沒有 JSON 要来的方便。
// 指明当前使用proto3语法,如果不指定,编译器会使用proto2
syntax = "proto3";
// package声明符,用来防止消息类型有命名冲突
package msg;
// 选项信息,对应go的包路径
option go_package = "server/msg";
// message关键字,像go中的结构体
message FirstMsg {// 类型 字段名 标识号int32 id = 1;string name=2;string age=3;
}
syntax
: 用来标记当前使用proto的哪个版本。如果不指定,编译器会使用proto2。
package
: 指定包名,用来防止消息类型命名冲突。
option go_package
: 选项信息,代表生成后的go代码包路径。在生成 gRPC 代码时,必须指明。
message
: 声明消息的关键字,类似Go语言中的struct。
FirstMsg 消息定义指定了三个字段(名称/值对),每个字段都有一个名称和一个类型。
定义字段语法格式: 类型 字段名 编号
,例如repeated int32 nums = 1;在生成gRPC代码时会自动生成数组[]int32类型。
分配字段编号说明:
- 消息定义中的每个字段都有一个唯一的编号。这些字段编号用于在 消息二进制格式中标识您的字段,并且在使用消息类型后不应更改。
[1, 15]之内的标识号在编码的时候会占用一个字节。[16, 2047]之内的标识号则占用2个字节。
- 最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999],因为是预留信息,如果使用,编译时会报错。
.proto Type | Go Type | Notes |
---|---|---|
double | float64 | |
float | float32 | |
int32 | int32 | 使用变长编码。对于负值的效率很低,如果有负值,使用sint32 |
int64 | int64 | 使用变长编码。对于负值的效率很低,如果有负值,使用sint64 |
uint32 | uint32 | 使用变长编码 |
uint64 | uint64 | 使用变长编码 |
sint32 | int32 | 使用变长编码,负值时比int32高效的多 |
sint64 | int64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 |
fixed32 | uint32 | 总是4个字节,如果数值比228大的话,这个类型会比uint32高效。 |
fixed64 | uint64 | 总是8个字节,如果数值比256大的话,这个类型会比uint64高效。 |
bool | bool | |
string | string | 字符串必须包含UTF-8编码或7位ASCII文本,且长度不能超过232。 |
bytes | []byte | 可以包含不超过232的任意字节序列。 |
至多
存在一个该字段的数据。使用 proto3 语法时,当没有为给定字段指定其他字段规则时,这是默认字段规则。 什么是保留标示符?reserved 标记的编号、字段名,都不能在当前消息中使用。
保留标识符的作用:对于特殊的字段名或者编号通过完全删除字段或将其注释掉来更新消息类型,如果后面出现其他用户对该消息进行更新重用了特殊的字段名或者编号,可能会导致严重的错误,包括数据损坏、出现隐私漏洞等。
为了确保这种情况不会发生的一种方法就是用保留标识符指定保留已删除的字段名或者编号。如果有其他用户试图重用这些字段名或编号,protobuf则会报错预警。
syntax = "proto3";
package demo;// 在这个消息中标记
message DemoMsg {// 标示号:1,2,10,11,12,13 都不能用reserved 1, 2, 10 to 13;// 字段名 test、name 不能用reserved "test","name";// 不能使用字段名,提示:Field name 'name' is reservedstring name = 3;// 不能使用标示号,提示:Field 'id' uses reserved number 11int32 id = 11;
}// 另外一个消息还是可以正常使用
message Demo2Msg {// 标示号可以正常使用int32 id = 1;// 字段名可以正常使用string name = 2;
}
注意:不能在同一 reserved 语句中混合字段名称和字段编号。
枚举:在定义消息类型时,希望其中一个字段只是预定义值列表中的一个值。
例如,假设您想为每个SearchRequest
添加一个Corpus 字段,其中枚举预定义值可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。您可以通过在消息定义中添加一个枚举,为每个可能的值添加一个常量。
在下面的示例中,我们添加了一个包含所有可能值的 enum 调用Corpus,以及一个 type 字段Corpus:
enum Corpus {CORPUS_UNSPECIFIED = 0;CORPUS_UNIVERSAL = 1;CORPUS_WEB = 2;CORPUS_IMAGES = 3;CORPUS_LOCAL = 4;CORPUS_NEWS = 5;CORPUS_PRODUCTS = 6;CORPUS_VIDEO = 7;
}
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;Corpus corpus = 4;
}
每个枚举类型必须将其第一个类型映射为编号0, 原因有两个:
- 必须有一个零值,以便我们可以使用 0 作为数字 默认值。
- 零值必须是第一个元素,以便与第一个枚举值始终为默认值的
proto2
语义兼容 。
可以对相同的编号分配给不同的枚举常量来定义别名。只需要将allow_alias选项设置为true
,否则协议编译器将在找到别名时生成错误消息。尽管所有别名值在反序列化期间都有效,但在序列化时始终使用第一个值。
enum EnumAllowingAlias {option allow_alias = true;EAA_UNSPECIFIED = 0;EAA_STARTED = 1;EAA_RUNNING = 1;EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {ENAA_UNSPECIFIED = 0;ENAA_STARTED = 1;// ENAA_RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.ENAA_FINISHED = 2;
}
注意:
- 枚举器常量必须在 32 位整数范围内。
- 枚举类型同样可以使用保留标识符。
文件位置:proto/class.proto
syntax="proto3";
// 包名
package dto;
// 生成go后的文件路径
option go_package = "grpc/server/dto";message ClassMsg {int32 classId = 1;string className = 2;
}
文件位置:proto/user.proto
syntax = "proto3";// 导入其他proto文件
import "proto/class.proto";option go_package="grpc/server/dto";package dto;// 用户信息
message UserDetail{int32 id = 1;string name = 2;string address = 3;repeated string likes = 4;// 所属班级ClassMsg classInfo = 5;
}
如果Goland提示:Cannot resolve import...
可以使用其他消息类型作为字段类型,也可以在其他消息类型中定义和使用消息类型。
syntax = "proto3";
option go_package = "server/nested";
// 学员信息
message UserInfo {int32 userId = 1;string userName = 2;
}
message Common {// 班级信息message CLassInfo{int32 classId = 1;string className = 2;}
}
// 嵌套信息
message NestedDemoMsg {// 学员信息 (直接使用消息类型)UserInfo userInfo = 1;// 班级信息 (通过Parent.Type,调某个消息类型的子类型)Common.CLassInfo classInfo =2;
}
创建关联映射作为数据定义的一部分,map数据结构格式:
map map_field = N;
注意:
- key_type只能是任何整数或字符串类型(除浮点类型和任何标量bytes类型)。
- enum 不能作为key_type和value_type定义的类型。
- map字段不能是repeated。
示例:
//protobuf源码
syntax = "proto3";
option go_package = "server/demo";// map消息
message DemoMapMsg {int32 userId = 1;map like =2;
}//生成Go代码
type DemoMapMsg struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsUserId int32 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`Like map[string]string `protobuf:"bytes,2,rep,name=like,proto3" json:"like,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
向后兼容性
map 语法等效于以下内容,因此不支持 map 的Protobuf实现仍然可以处理你的数据:
message MapFieldEntry {key_type key = 1;value_type value = 2;
}repeated MapFieldEntry map_field = N;
任何支持映射的Protobuf实现都必须生成和接受上述定义可以接受的数据。
需要创建切片(数组)字段类型:
//protobuf源码
syntax = "proto3";
option go_package = "server/demo";// repeated允许字段重复,对于Go语言来说,它会编译成数组(slice of type)类型的格式
message DemoSliceMsg {// 会生成 []int32repeated int32 id = 1;// 会生成 []stringrepeated string name = 2;// 会生成 []float32repeated float price = 3;// 会生成 []float64repeated double money = 4;
}//生成Go代码
// repeated允许字段重复,对于Go语言来说,它会编译成数组(slice of type)类型的格式
type DemoSliceMsg struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFields// 会生成 []int32Id []int32 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`// 会生成 []stringName []string `protobuf:"bytes,2,rep,name=name,proto3" json:"name,omitempty"`// 会生成 []float32Price []float32 `protobuf:"fixed32,3,rep,packed,name=price,proto3" json:"price,omitempty"`Money []float64 `protobuf:"fixed64,4,rep,packed,name=money,proto3" json:"money,omitempty"`
}
如果需要一条包含多个字段的消息,并且最多同时设置一个字段,可以强制执行此行为并使用 oneof 功能节省内存。
oneof 字段与常规字段一样,在 oneof 共享内存中的所有字段,最多可以同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。
如果设置了多个值,则由 proto 中的 order 确定的最后一个设置的值将覆盖所有以前的设置值。
message SampleMessage {oneof test_oneof {string name = 4;SubMessage sub_message = 9;}
}
在生成的代码中,oneof 字段具有与常规字段相同的 getter 和 setter。还可以获得一种特殊的方法来检查 oneof 中设置了哪个值(如果有)。
Any消息类型允许您将消息作为嵌入类型使用,而不需要它们的.proto定义。
Any以字节的形式包含任意序列化的消息,以及作为该消息类型的全局唯一标识符并解析为该消息类型的URL。要使用Any类型,您需要 import google/protobuf/any.proto
import "google/protobuf/any.proto";message ErrorStatus {string message = 1;repeated google.protobuf.Any details = 2;
}
本章我们了解proto3的语法,下一章我会详细介绍gRPC以及如何加载 protoc-gen-go 插件达到生成 Go 代码的目的。