入门ProtoBuf:从零开始的序列化与反序列化
0.简介
ProtoBuf是由Google开发的一种轻量级的数据序列化协议
,其全称为Protocol Buffers
。ProtoBuf是一种跨语言、跨平台的数据交换格式,可以用于数据的存储和网络传输。ProtoBuf的主要特点包括:
-
简洁高效:ProtoBuf使用二进制编码,序列化后的数据非常紧凑,占用空间小,传输速度快。同时,ProtoBuf支持对数据进行压缩,进一步减小数据传输的开销。
-
可扩展:ProtoBuf支持对数据结构进行版本控制和扩展,可以方便地向已有的数据结构中添加新的字段和数据类型。
-
跨平台:ProtoBuf定义的数据结构可以被多种编程语言所解析和生成,例如Java、C++、Python、Go等,实现了不同语言之间的数据交换。
-
代码自动生成:通过ProtoBuf提供的编译器工具,可以根据ProtoBuf定义的数据结构自动生成对应的代码,大大简化了开发人员的工作。
总之,ProtoBuf是一种高效、可扩展、跨平台的数据序列化协议,广泛应用于分布式系统、网络通信、数据存储等场景。
1.安装
官方下载地址
注意,不同的电脑系统安装包是不一样的:
注意:我们需要将下载得到的可执行文件protoc
所在的 bin 目录加到我们电脑的环境变量中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFFpTrND-1678628859826)(https://gitee.com/xzxwbb/cloud-image/raw/master/img/image-20230312210450307.png)]
编译go语言的工具包
这个protoc可以将proto文件编译为任何语言的文件,想要编译为go语言的,还需要下载另外一个可执行文件
命令是这样的:
go install google.golang.org/ProtoBuf/cmd/protoc-gen-go@latest
或者
go get github.com/golang/protobuf/protoc-gen-go
win10下protoc-gen-go不是内外部命令的方案
git clone https://github.com/golang/protobuf
-
进入protobuf/protoc-gen-go目录并进入到cmd中
-
go build -o protoc-gen-go.exe main.go
-
将protoc-gen-go.exe拷贝到windows/System32目录
2.简单入门
-
创建hello.proto
syntax = "proto3";
package hello;
option go_package = "./;hello";
message Say{
int64 id = 1;
string hello = 2;
repeated string word = 3;
}
-
在hello.proto所在目录下运行protoc命令
protoc --go_out=. hello.proto
-
生成hello.pb.go
3.进阶
关键字
-
message:定义一个消息类型,即一组相关的字段。
-
field:定义消息类型中的一个字段,包括字段的名称、编号和类型。
-
enum:定义一个枚举类型,用于在一组有限的值中进行选择。
-
option:为ProtoBuf编译器提供指令,用于控制如何生成代码。
-
package:定义一组相关消息类型的命名空间。
-
import:引用其他ProtoBuf文件中定义的消息类型。
-
oneof:定义一组互斥的字段,只能设置其中的一个字段。
-
repeated:定义一个字段可以包含多个值。
-
map:定义一个映射类型,可以将一组键值对映射到另一组值。
-
service:定义一个RPC服务,包括一组RPC方法和其参数。
以下是一个简单的ProtoBuf消息类型定义示例,展示了上述关键字的用法:
syntax = "proto3";
package example;
// 定义一个枚举类型
enum Fruit {
APPLE = 0;
BANANA = 1;
ORANGE = 2;
}
// 定义一个消息类型
message FruitBasket {
string owner = 1;
repeated Fruit fruits = 2;
map<string, int32> fruit_counts = 3;
oneof favorite_fruit {
Fruit favorite = 4;
string favorite_other = 5;
}
}
// 定义一个RPC服务
service FruitService {
rpc GetFruitBasket (FruitBasketRequest) returns (FruitBasket);
}
message FruitBasketRequest {
string owner = 1;
}
上面的代码定义了一个名为“FruitBasket”的消息类型,它包含了一些字段:
-
owner
:一个字符串类型的字段,编号为1。
-
fruits
:一个枚举类型的重复字段,编号为2。
-
fruit_counts
:一个映射类型的字段,将字符串键映射到32位整数值,编号为3。
-
favorite
和favorite_other
:一组互斥的字段,只能设置其中一个,分别为枚举类型Fruit
的值或字符串类型的值,编号分别为4和5。
该代码还定义了一个名为“FruitService”的RPC服务,其中包括一个名为“GetFruitBasket”的方法,该方法接受一个“FruitBasketRequest”消息类型的参数,并返回一个“FruitBasket”消息类型的响应。
分配标识号
在ProtoBuf中,每个字段都需要分配一个唯一的编号(tag),用于标识该字段。该编号用于在消息二进制编码期间标识字段,以便解析器可以识别和处理消息中的字段。
这些编号的使用可以使ProtoBuf消息具有更小的尺寸、更快的解析速度和更好的向后兼容性。由于ProtoBuf编码是紧凑的二进制格式,字段的编号将直接存储在编码数据流中,这意味着即使字段顺序发生变化,解析器仍然可以正确识别和解析数据。
在ProtoBuf中,字段的编号必须为正整数,且不能重复或跳过。通常,推荐将较小的编号分配给消息中经常使用的字段,以减小消息的尺寸和解析时间。编号的范围为1到536,870,911(在proto3语法中)或1到2^29-1(在proto2语法中)。
示例代码中,owner
字段的编号为1,fruits
字段的编号为2,fruit_counts
字段的编号为3,favorite
字段的编号为4,favorite_other
字段的编号为5。在消息编码过程中,这些编号将用于标识和定位这些字段,以便能够准确地解析和构造ProtoBuf消息。
字段类型
在ProtoBuf中,每个字段都必须指定一种类型。
在Go语言中,可以使用proto
包来生成和解析ProtoBuf消息。该包将ProtoBuf消息的字段类型映射到Go语言中的数据类型。
以下是常见的ProtoBuf字段类型和它们在Go语言中的映射,同时附有每种类型的含义:
ProtoBuf类型 |
Go语言类型 |
含义 |
double |
float64 |
64位浮点数 |
float |
float32 |
32位浮点数 |
int32 |
int32 |
32位有符号整数 |
int64 |
int64 |
64位有符号整数 |
uint32 |
uint32 |
32位无符号整数 |
uint64 |
uint64 |
64位无符号整数 |
sint32 |
int32 |
32位有符号整数,使用可变长度编码 |
sint64 |
int64 |
64位有符号整数,使用可变长度编码 |
fixed32 |
uint32 |
32位无符号整数,使用固定长度编码 |
fixed64 |
uint64 |
64位无符号整数,使用固定长度编码 |
sfixed32 |
int32 |
32位有符号整数,使用固定长度编码 |
sfixed64 |
int64 |
64位有符号整数,使用固定长度编码 |
bool |
bool |
布尔类型 |
string |
string |
UTF-8编码的字符串 |
bytes |
[]byte |
字节数组 |
enum |
枚举类型 |
枚举类型 |
message |
消息类型 |
消息类型 |
repeated |
切片类型 |
重复的字段,可以包含多个值 |
map |
map 类型 |
映射类型,将一组键值对映射到另一组值 |
需要注意的是,由于ProtoBuf和Go语言中的数据类型有些细微差别,因此在处理某些类型时可能需要进行一些类型转换。
protoc的常用命令
-
--proto_path
:指定ProtoBuf文件的搜索路径,可以指定多个路径。
-
--proto_path=
:清空默认的ProtoBuf文件搜索路径。
-
--go_out
:将ProtoBuf文件编译成Go语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--java_out
:将ProtoBuf文件编译成Java语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--python_out
:将ProtoBuf文件编译成Python语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--cpp_out
:将ProtoBuf文件编译成C++语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--csharp_out
:将ProtoBuf文件编译成C#语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--objc_out
:将ProtoBuf文件编译成Objective-C语言的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--grpc_out
:将ProtoBuf文件编译成支持gRPC的源代码文件。该选项后面需要指定生成的源代码文件的输出目录。
-
--plugin
:指定一个插件来处理生成的源代码文件。该选项后面需要指定插件的路径和文件名。
-
--descriptor_set_out
:将ProtoBuf文件编译成二进制的ProtoBuf描述符文件。该选项后面需要指定输出的文件名和路径。
这些选项中,最常用的是--proto_path
和--[language]_out
。使用这些选项,可以将ProtoBuf文件编译成多种不同语言的源代码文件,并且可以通过--proto_path
选项来指定ProtoBuf文件的搜索路径,以便protoc
能够找到需要编译的文件。
枚举类型
创建enum.proto
syntax = "proto3";//指定版本信息,非注释的第一行
enum SexType //枚举消息类型,使用enum关键词定义,一个性别类型的枚举类型
{
UNKONW = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
MALE = 1; //1男
FEMALE = 2; //2女 0未知
}
// 定义一个用户消息
message UserInfo
{
string name = 1; // 姓名字段
SexType sex = 2; // 性别字段,使用SexType枚举类型
}
生成.pb.go
protoc --go_out=. enum.proto
消息嵌套
外部引用
// 定义Article消息
message Article {
string url = 1;
string title = 2;
repeated string tags = 3; // 字符串数组类型
}
// 定义ListArticle消息
message ListArticle {
// 引用上面定义的Article消息类型,作为results字段的类型
repeated Article articles = 1; // repeated关键词标记,说明articles字段是一个数组
}
内部嵌套
message ListArticle {
// 嵌套消息定义
message Article {
string url = 1;
string title = 2;
repeated string tags = 3;
}
// 引用嵌套的消息定义
repeated Article articles = 1;
}
import导入其他proto文件定义的消息
创建article.proto
syntax = "proto3";
package nesting;
option go_package = "./;article";
message Article {
string url = 1;
string title = 2;
repeated string tags = 3; // 字符串数组类型
}
创建list_article.proto
syntax = "proto3";
// 导入Article消息定义
import "article.proto";
package nesting;
option go_package = "./;article";
// 定义ListArticle消息
message ListArticle {
// 使用导入的Result消息
repeated Article articles = 1;
}
生成.pb.go
protoc --go_out=. article.proto
protoc --go_out=. list_article.proto
map类型
创建score.proto
syntax = "proto3";
package map;
option go_package = "./;score";
message Student{
int64 id = 1; //id
string name = 2; //学生姓名
map<string, int32> score = 3; //学科 分数的map
}
生成.pb.go
protoc --go_out=. score.proto
实战
- 创建study_info.proto
syntax = "proto3";
package demo;
option go_package = "./;study";
message StudyInfo {
int64 id = 1; //id
string name = 2; //学习的科目名称
int32 duration = 3; //学习的时长 单位秒
map<string, int32> score = 4; //学习的分数
}
- 生成.pb.go
protoc --go_out=. study_info.proto
- 编写go文件,读取ProtoBuf中定义的字段,进行赋值,取值,转成结构体等操作
proto编码和解码的操作和json是非常像的,都使用“Marshal”和“Unmarshal”关键字
package main
import (
"fmt"
"google.golang.org/ProtoBuf/proto"
study "juejin/ProtoBuf/proto/demo"
)
func main() {
// 初始化proto中的消息
studyInfo := &study.StudyInfo{}
//常规赋值
studyInfo.Id = 1
studyInfo.Name = "学习ProtoBuf"
studyInfo.Duration = 180
//在go中声明实例化map赋值给ProtoBuf消息中定义的map
score := make(map[string]int32)
score["实战"] = 100
studyInfo.Score = score
//用字符串的方式:打印ProtoBuf消息
fmt.Printf("字符串输出结果:%v\n", studyInfo.String())
//转成二进制文件
marshal, err := proto.Marshal(studyInfo)
if err != nil {
return
}
fmt.Printf("Marshal转成二进制结果:%v\n", marshal)
//将二进制文件转成结构体
newStudyInfo := study.StudyInfo{}
err = proto.Unmarshal(marshal, &newStudyInfo)
if err != nil {
return
}
fmt.Printf("二进制转成结构体的结果:%v\n", &newStudyInfo)
}
控制台输出
字符串输出结果: id:1 name:"学习ProtoBuf" duration:180 score:{key:"实战" value:100}
Marshal转化二进制结果:[8 1 18 14 229 173 166 228 185 160 80 114 111 116 111 66 117 102 24 180 1 34 10 10 6 229 174 158 230 136 152 16 100]
二进制转化为结构体的结果:id:1 name:"学习ProtoBuf" duration:180 score:{key:"实战" value:100}