这是我参与「第五届青训营 」笔记创作活动的第6天
protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为protobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
protobuf拥有代码生成工具,因此,我们只需要定义好数据结构,就可生成代码。并且支持多种编程语言。
1. 快速开始
protobuf环境准备、快速上手与基础教程
1.1 准备protobuf环境
使用protobuf进行代码生成,需要两个工具:通用编辑器、代码生成器。
1.1.1 下载通用编译器 & 配置环境变量
Windows
下载完成后,解压到你的目标路径。(你自己愿意放哪就放哪)
将文件夹内的bin配置到环境变量
Ubuntu 20
-
一次下载不下来,就多下载几次。Linux下载完需要编译
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.tar.gz -
解压
tar -zxvf protobuf-all-21.12.tar.gz -
继续执行
./configure --prefix=/usr/local/protobuf-
如果报错
Can't exec "libtoolize": 没有那个文件或目录,这是因为你本地没有C++环境。执行如下命令。然后再次执行./configureapt-get install build-essential apt-get install g++
-
-
执行如下命令。
make、make check执行时间较长,make make check sudo make install sudo ldconfig -
配置环境变量
sudo vim /etc/profile添加如下内容
export PATH=$PATH:/usr/local/protobuf/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib刷新
source /etc/profile -
验证
protoc --version输出
libprotoc 3.21.12
1.1.2 安装生成器
这里展示的是GoLang的protoc生成器
这个地方有两个源,建议安装google的源,github那个有问题,不支持工具了。
go install google.golang.org/ProtoBuf/cmd/protoc-gen-go@latest
go get github.com/golang/protobuf/protoc-gen-go
1.2 编写proto文件
编写一个hello.proto文件
syntax = "proto3"; // proto版本
package pb; // 当前proto文件所在的包
// 分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包
option go_package = "./; hello";
message HelloRequest{
string RequestValue = 1;
}
message HelloResponse{
string ResponseValue = 1;
}
service HelloService{
rpc Hello(HelloRequest) returns (HelloResponse);
}
1.3 生成go文件
protoc --go_out=./ hello.proto
--go_out=./:是指出生成go文件的路径,这个和.proto文件中的那个路径是一个意思,只不过该命令可以重新指定生成go文件的路径
protoc --go_out=./后边跟的是要基于哪个.proto文件生成go文件,支持通配符。
执行命令后会在你指定路径下生成go文件。go文件内包含了基于.proto文件生成的go 的结构体,这样我们就不用自己写了。详细说明如下:
我们在.proto写了两个消息的结构,一个是请求的,一个是响应的。在对应的go文件里就会给我们生成这两个消息结构对应的go 结构体。
1.4 go结构序列化与反序列为proto
接下来,我么可以向使用json那样,对结构进行Marshal和Unmarshal
func main() {
helloRequest := &hello.HelloRequest{
RequestValue: "hello, my name is Anthony",
}
protoMessage, _ := proto.Marshal(helloRequest)
helloResponse := &hello.HelloResponse{}
proto.Unmarshal(protoMessage, helloResponse)
}
1.5 生成go GRPC文件
与生成普通的go文件类似,只不过是加上了plugins=grpc:
protoc --go_out=plugins=grpc:./ hello.proto
生成的go文件也比正常的文件在最后最后多了一些关于RPC调用的内容。重点的内容是,生成了一个接口。接口中定义了我们在.proto文件中定义的service中的方法。
我们只需要在我们的服务中实现这个接口,并将该实现与GRPC进行绑定就可以实现远程过程调用了。因为这篇笔记记录的是progobuf,而不是GRPC,所以就不详细展开了。
2. proto文件语法介绍
2.1 关键字
syntax:proto版本。必须写,而且要放在第一行。默认是proto2,但是现在主流是proto3。package:当前proto文件所在的包option go_package: 指定生成文件的路径及文件名。此处不指定也可以在执行protoc命令时指定。如:option go_package = "./; hello"。分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包。message:非常重要,用于指定消息的格式。后面会详细介绍。service:指定通信接口的函数
syntax = "proto3"; // proto版本
package pb; // 当前proto文件所在的包
// 分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包
option go_package = "./; hello";
message HelloRequest{
string RequestValue = 1;
}
message HelloResponse{
string ResponseValue = 1;
}
service HelloService{
rpc Hello(HelloRequest) returns (HelloResponse);
}
2.2 字段映射
| .proto Type | Notes | C++ Type | Python Type | Go Type |
|---|---|---|---|---|
| double | double | float | float64 | |
| float | float | float | float32 | |
| int32 | 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 | int32 | int | int32 |
| uint32 | 使用变长编码 | uint32 | int/long | uint32 |
| uint64 | 使用变长编码 | uint64 | int/long | uint64 |
| sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int32 |
| sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 | int64 | int/long | int64 |
| fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 | uint32 | int | uint32 |
| fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 | uint64 | int/long | uint64 |
| sfixed32 | 总是4个字节 | int32 | int | int32 |
| sfixed32 | 总是4个字节 | int32 | int | int32 |
| sfixed64 | 总是8个字节 | int64 | int/long | int64 |
| bool | bool | bool | bool | |
| string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 | string | str/unicode | string |
| bytes | 可能包含任意顺序的字节数据。 | string | str | []byte |
2.3 字段规则
required:消息体中必填字段,不设置会导致编解码异常。(例如位置1)optional: 消息体中可选字段。(例如位置2)repeated: 消息体中可重复字段,重复的值的顺序会被保留(例如位置3)在go中重复的会被定义为切片。
message User {
string username = 1;
int32 age = 2;
optional string password = 3;
repeated string address = 4;
}
2.4 0值
protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:
| 类型 | 默认值 |
|---|---|
| bool | false |
| 整型 | 0 |
| string | 空字符串"" |
| 枚举enum | 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0; |
| message | 不是null,而是DEFAULT_INSTANCE |
2.5 标识号
标识号:在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。
message Person {
string name = 1; // (位置1)
int32 id = 2;
optional string email = 3;
repeated string phones = 4; // (位置4)
}
以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。
小技巧:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。
2.6 消息嵌套
你也可以将消息嵌套任意多层,如 :
message Grandpa { // Level 0
message Father { // Level 1
message son { // Level 2
string name = 1;
int32 age = 2;
}
}
message Uncle { // Level 1
message Son { // Level 2
string name = 1;
int32 age = 2;
}
}
}
可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :
message PersonInfo {
message Person {
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:
message PersonMessage {
PersonInfo.Person info = 1;
}
2.7 import
我们也看到了,把所有的message写到一个文件中很不方便维护,嵌套层级过多也很乱。可以,将message拆分到不懂得文件中,然后通过import引入当前文件。
在userModel.proto文件中定义了message UserModel
syntax = "proto3";
package pb;
option go_package = "/internal/service; service";
message UserModel{
uint32 UserID = 1;
string UserName = 2;
string NickName = 3;
}
在userService.proto中引入
syntax = "proto3";
package pb;
import "userModels.proto";
option go_package = "/internal/service; service";
message UserDetailResponse{
UserModel UserDetail = 1;
uint32 Code = 2;
}
2.7 map类型
map<key_type, value_type> map_field = N;
语法非常简单和通用,但是有几个问题需要我们注意:
key_type可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。- 注意:枚举不是有效的
key_type。 value_type可以是除另一个映射之外的任何类型。- Map 字段不能使用
repeated关键字修饰。
我们举个典型的例子:学生的学科和分数就适合用map定义:
syntax = "proto3";
package map;
option go_package = "./;score";
message Student{
int64 id = 1; //id
string name = 2; //学生姓名
map<string, int32> score = 3; //学科 分数的map
}