这篇博客介绍了如何使用 protocol buffer来构建我们的数据,编写 .proto
文件,以及如何从.proto
文件生成数据访问类。这是 proto3版本,有关proto2语法的信息,请参见官网的《 Proto2语言指南》
1. 定义消息类型
首先让我们看一个非常简单的例子。
这是官网给的一个经典示例:即,定义一个 SearchRequest(搜索请求)消息格式,其中有一个字符串类型的查询键值、int32类型的特定页面结果以及每页结果数。他的定义如下:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
解释:
- 文件的第一行指定使用的是proto3语法。如果不加的话,则protobuf 编译器将默认是使用proto2。这句话必须是文件的第一个非空行。
- SearchRequest 定义了3个字段。每个字段都有自己的数据类型、变量名称和标志号。
一般的,要定义一个消息类型,需要注意一下几点:
指定字段类型
Specifying Field Types
比如,在上面的例子中,string query = 1
,即是指定了变量query
的字段类型是string
。当然,我们可以指定其他的类型,下文将具体叙述。另外,我们也可以指定复合类型,包括枚举和其他消息类型。
分配字段编号
Assigning Field Numbers
比如,上面示例中,为字段 query
指定了编号 1
,这些字段号是用于标识消息二进制格式的字段,一旦确定了就不能改变。
编号可指定范围是: 1 − ( 2 29 − 1 ) 1 -(2^{29}-1) 1−(229−1),但是需要注意,我们不能使用编号为 19000 − 19999 19000 -19999 19000−19999 的数字。
指定字段规则
Specifying Field Rules
这是和 proto2 语法不一样的地方。proto3是默认不需要在前面限定规则的。proto3 默认前面是有 0或1 个限定字段。
另外,需要重点提出的是这个字段
repeated
:此字段可以在格式正确的消息中重复任意次(包括零次)。 是不是有点熟悉的感觉?对,这就是数组的定义!!如果想在 message 里定义一个数组的话,就用这个字段规则吧。
总结如下:
message 消息名称 {
string query = 1;
(字段规则) 数据类型 变量名称 = 标志号;
(field_rules) wire_type variable = field_number;
}
注释
如果要将注释添加到.proto文件中,则使用 C / C ++
样式 //
和 / * ... * /
:
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
编译成各版本
详情可以参看另一篇博客:一文掌握Proto Buffer的安装
2. 数据类型
protobuf定义了一下类型的消息字段类型,其与其他语言的对应关系如下所示:
.proto Type | Notes | C++ Type | Java Type | Go Type |
---|---|---|---|---|
double | double | double | float64 | |
float | float | float | float32 | |
int32 | 使用可变长度编码。负数编码效率低下,如果你的字段可能具有负值,请改用sint32。varints编码方式 | int32 | int | int32 |
int64 | 使用可变长度编码。负数编码效率低下,如果你的字段可能具有负值,请改用sint64。 | int64 | long | int64 |
uint32 | 使用可变长度编码。 | uint32 | int | uint32 |
uint64 | 使用可变长度编码。 | uint64 | long | uint64 |
sint32 | 使用可变长度编码。有符号的int值。与常规int32相比,它们更有效地编码负数。使用了Zigzag 编码方式 | int32 | int | int32 |
sint64 | 使用可变长度编码。有符号的int值。与常规int32相比,它们更有效地编码负数。 | int64 | long | int64 |
fixed32 | 始终为4个字节。如果值是经常大于 2 28 2^{28} 228 的话,则比uint32更有效。 | uint32 | int | uint32 |
fixed64 | 始终为8个字节。如果值是经常大于 2 56 2^{56} 256 的话,则比uint64更有效。 | uint64 | long | uint64 |
sfixed32 | 始终为4 个字节。 | int32 | int | int32 |
sfixed64 | 始终为8 个字节。 | int64 | long | int64 |
bool | bool | boolean | bool | |
string | 字符串是 UTF-8编码或 7位ASCII文本,并且长度不能超过 2 32 2^{32} 232。 | string | String | string |
bytes | 可以包含长度不超过 2 32 2^{32} 232 的任意字节序列。 | string | ByteString | []byte |
注释:在Java中,无符号的32位和64位整数使用带符号的对等体表示,最高位只是存储在符号位中。
3. 默认值
解析消息时,如果编码的消息不包含特定的元素,则已解析对象中的相应字段将设置为该字段的默认值。
这些默认值是特定于类型的:
-
string类型,默认值为空字符串。
-
bytes类型,默认值为空字节。
-
bools类型,默认值为false。
-
int 等数字类型,默认值为零。
-
enums枚举类型,默认值为第一个定义的枚举值,必须为0。
-
message 字段,未设置该字段。它的确切值取决于语言。 有关详细信息,请参见生成的代码指南。
4. 枚举类型
在定义消息类型时,你可能希望其一个字段仅具有一个预定义的值列表之一。
例如,假设你要为每个SearchRequest添加一个语料库字段,该语料库可以是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO。 您可以通过在消息定义中添加一个枚举以及每个可能值的常量来非常简单地完成此操作。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {//枚举消息类型,使用enum关键词定义
UNIVERSAL = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
----------------------------------------------------------------------------
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
## 规定,PhoneNumber 电话号码是哪种类型的
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
5. 消息嵌套
您可以将其他 message 类型用作字段类型。例如,假设你要在每条 SearchResponse 消息中包括 Result 消息,为此,我们可以在同一 .proto
中定义 Result 消息类型,然后在SearchResponse中指定结果类型的字段:
message SearchResponse {
repeated Result results = 1; //嵌套 Result的消息类型
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
6. Maps
如果要在数据定义中创建关联映射,则 protobuf 提供了方便的快捷方式语法:
map<key_type, value_type> map_field = N;
其中, key_type
可以是任何整数或字符串类型(浮点类型和 bytes 除外)
注意,枚举不是有效的 key_type
value_type
可以是除另一个映射以外的任何类型。
因此,例如,如果您想创建一个项目地图,其中每个 Project message 都与一个字符串键相关联,则可以这样定义它:
syntax = "proto3";
message MyMapTest {
// 定义一个k/v类型,key是string类型,value也是 Project消息类型
map<string, Project> projects = 3;
}
注意:Map 字段不能使用
repeated
关键字修饰
7. 使用ProtoBuf的一个示例
参考例子:
- ProtoBuf 的GitHub示例:https://github.com/protocolbuffers/protobuf/tree/master/examples
- ProtoBuf 的go语言官方使用文档:https://developers.google.cn/protocol-buffers/docs/gotutorial
下面,我们来看在 go 语言中如何使用 protobuf :
7.1 安装编译器,配置环境
如果按照编译器,可以参考我的另一篇博客:一文掌握Proto Buffer的安装
注意,go 语言需要安装插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go
7.2 编写proto文件
编写 proto
文件,命名为 hello.proto
:
syntax = "proto3";
// 用户信息,定义一个最简单的消息类型
message PersonInfo {
string name = 1;
int32 age = 2;
}
// 用户联系方式,里面尽量使用到了多样的消息定义方式
message PersonContact {
int32 id = 2;
string email = 3;
enum PhoneType { //枚举类型
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2; //使用枚举
}
repeated PhoneNumber phones = 4; //数组类型
}
7.3 编译成对应代码
官网给出的产生代码是:
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
我们直接切换到对应的目录,使用如下命名产生文件:
cd 对应目录
protoc --go_out=./ ./hello.proto
生成了 hello.pb.go
文件
7.4 集成到代码中
测试我们的代码,对 hello.proto
文件里的 PersonInfo
消息体进行序列化传输和解码 :
package main
import (
"fmt"
"github.com/golang/protobuf/proto" // proto包
c "grpc/proto" //引入 hello.pb.go 文件
)
func main() {
// PersonInfo 是 hello.pb.go 里的结构体
oldData := &c.PersonInfo{
Name: "testName",
Age: 21,
}
// 序列化该结构体
data, err := proto.Marshal(oldData) // 转换成二进制数据
if err != nil {
fmt.Println("marshal error: ", err.Error())
}
fmt.Println("序列化后的数据: ", data)
// 反序列化数据
newData := &c.PersonInfo{}
err = proto.Unmarshal(data, newData)
if err != nil {
fmt.Println("unmarshal err:", err)
}
fmt.Println("反序列化后的数据: ", newData)
}
输出:
序列化后的数据: [10 8 116 101 115 116 78 97 109 101 16 21]
反序列化后的数据: name:"testName" age:21
8. 总结
本文中,我们叙述了 protobuf 的初级用法,包括如何定义一个消息类型、应该注意什么,字段的数据类型有哪些,枚举类型、消息嵌套和Maps是如何定义和使用的等等。最后,本文给出了一个 protobuf 的 go 语言使用案例。
-
看完后,你应该明白了如果编写 proto文件了
-
后续可以观看:
参考资料
-
官网文档:https://developers.google.cn/protocol-buffers
-
梯子教程:https://www.tizi365.com/archives/386.html