Protobuf编解码
Protobuf 是 Protocol Buffers 的简称,它是 Google 公司开发的一种数据描述语言,并于2008年对外开源。Protobuf 刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具
为什么选择Protobuf
一般而言我们需要一种编解码工具会参考:
- 编解码效率
- 高压缩比
- 多语言支持
其中压缩与效率 最被关注的点:
使用流程
首先需要定义我们的数据,通过编译器,来生成不同语言的代码
之前我们的 RPC 要么使用的 Gob, 要么使用的 json, 接下来我们将使用 probuf
首先创建 hello.proto 文件,其中包装 HelloService 服务中用到的字符串类型
syntax = "proto3";
package hello;
option go_package="micro/protobuf/hello";
message Hello{
string value = 1;
}
syntax: 表示采用proto3的语法。第三版的 Protobuf 对语言进行了提炼简化,所有成员均采用零值初始化。package:指明当前是main包(这样可以和Go的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称。option:protobuf的一些选项参数, 这里指定的是要生成的Go语言package路径, 其他语言参数各不相同message: 定义一个新的类型,在最终生成的Go语言代码中对应一个Hello结构体。Hello类型中只有一个字符串类型的value成员,1为该成员编码时用1编号代替名字
关于数据编码:
在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。
但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。
我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分
但是我们如何把这个定义文件(IDL: 接口描述语言), 编译成不同语言的数据结构喃? 这就需要我们安装protobuf的编译器
安装编译器
protobuf的编译器叫: protoc(protobuf compiler), 我们需要到这里下载编译器: Releases · protocolbuffers/protobuf
选择对应平台的二进制包下载:
这个压缩包里面有:
include:头文件或者库文件bin: protoc编译器readme.txt
安装编译器二进制
linux/unix系统直接:
mv bin/protoc usr/bin
windows系统:
找到自己 git-bash 上默认的 /usr/bin 目录。 大部分在:C:\Program Files\Git\usr\bin\
因此我们首先将bin下的 protoc 编译器 挪到 git-bash的/usr/bin 上 (本人的目录在D盘)
验证安装
$ protoc --version
libprotoc 3.21.9
安装Go语言插件
Protobuf 核心的工具集是 C++ 语言开发的,在官方的 protoc编译器 中并不支持 Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
安装该插件 增加语句提醒
接下来 我们就可以使用 protoc 来生成我们对于的Go语言的数据结构
Hello Protobuf
在day21/pb下创建hello.pb
syntax = "proto3";
package hello;
option go_package="micro/protobuf/hello";
message Blog {
string title = 1;
string summary = 2;
string content = 3;
string author = 4;
bool is_published = 5;
}
编译proto文件
$ cd micro/protobuf # 以protobuf作为编译的工作目录
$ protoc -I=. --go_out=./hello --go_opt=module="micro/protobuf/hello" hello/hello.proto
- -I:
-IPATH,即--proto_path=PATH, 指定proto文件搜索的路径, 如果有多个路径 可以多次使用-I 来指定, 如果不指定默认为当前目录-I=.表示从当前目录即protobuf目录搜索hello/hello.proto文件
--go_out:--go指插件的名称, 我们安装的插件为:protoc-gen-go, 而protoc-gen是插件命名规范,go是插件名称, 因此这里是--go,- 而
--go_out表示的是go插件的out参数, 这里指编译产物的存放目录
--go_opt:protoc-gen-go插件的opt参数,module指定了go module, 生成的go pkg会去除掉module路径,生成对应pkg- 这里若不写 module则在
hello文件夹下 生成micro/protobuf/hello/hello.pb.go
- 这里若不写 module则在
hello/hello.proto: 我们proto文件路径
这样我们就在当前目录下生成了Go语言对应的pkg, 我们的 message Hello 被生成为了一个Go Struct, 至于Proto3的语法和与Go语言数据结构的对应关系后面将讲到
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.21.9
// source: hello/hello.proto
package hello
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Blog struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"`
Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
Author string `protobuf:"bytes,4,opt,name=author,proto3" json:"author,omitempty"`
IsPublished bool `protobuf:"varint,5,opt,name=is_published,json=isPublished,proto3" json:"is_published,omitempty"`
}
...
然后我们就可以以Go语言的方式使用这个pkg
序列化与反序列化
基于上面生成的Go 数据结构, 我们就可以来进行 数据的交互了
我们使用 google.golang.org/protobuf/proto 工具提供的API来进行序列化与反序列化:
- Marshal: 序列化
- Unmarshal: 反序列化
在 hello 文件夹 下新建 hello_test.go 测试文件
package hello_test
import (
"fmt"
"testing"
"micro/protobuf/hello"
"google.golang.org/protobuf/proto"
)
func TestMarshal(t *testing.T) {
b := &hello.Blog{
Title: "GO 语言Protobuf讲解",
Summary: "GO 语言Protobuf讲解",
Content: "xxx",
Author: "lfd",
IsPublished: true,
}
// 序列化(编码)
encodedB, err := proto.Marshal(b)
if err != nil {
t.Fatal(err)
}
fmt.Println(encodedB)
// 解码
b2 := &hello.Blog{}
if err := proto.Unmarshal(encodedB, b2); err != nil {
t.Fatal(err)
}
fmt.Println(b2)
}