Protobuf 简介
Protobuf 的全称是 Protocol Buffers,是 Google 开发的,诞生之初是为了解决服务器端新旧协议(高低版本)兼容性问题,所以取名叫做「协议缓冲区」,现在已经演变为语言中立、平台无关、可扩展的序列化数据的格式,可用于通信协议(尤其是 RPC 通信)、数据存储等。与 XML 和 JSON 相比,Protobuf 更小巧、更快、更简单,一旦定义了要处理的数据结构后(保存在 .proto
文件中),就可以利用 Protobuf 代码生成工具 protoc
生成相应的代码,就像我们在基于 Go Micro 框架创建第一个微服务接口中所做的那样。此外,只需使用 Protobuf 对数据结构进行一次描述,即可通过不同语言或从不同数据流中对结构化数据进行读写。通过前面对 Codec 组件的介绍我们也已经知道,Go Micro 默认的编码格式就是 Protobuf。
Protobuf 快速入门
下面我们使用 Protobuf 和 Go 开发一个简单的示例程序,并通过这个示例来介绍 Protobuf 的基本使用。这个程序由两部分组成,一部分是 Writer,负责将结构化数据写入磁盘文件,另一部分是 Reader,负责从该磁盘文件读取结构化数据并打印。
用于演示的结构化数据是 HelloWorld
,我们编写对应的包含结构化数据的 hello.proto
文件如下:
syntax = "proto3";
package hello;
message HelloWorld {
int32 id = 1;
string str = 2;
int32 opt = 3;
}
在第一行代码中我们通过 syntax = "proto3"
指定使用 proto3,然后声明代码所在的包,接下来,才正式开始结构化数据的定义,在 Protobuf 中,我们通过 message
来定义结构化数据(驼峰式命名法),在这个结构化数据中包含三个字段,分别是 id、str 和 opt,每个字段需要声明类型,以及唯一的编号,这些字段编号用于标识消息二进制格式中的字段,在结构化数据中,字段名不能重复。
下面是 Protobuf 中的数据类型与所支持语言的对应关系:
此外,Protobuf 还支持嵌入枚举类型:
message HelloWorld {
int32 id = 1;
string str = 2;
enum OptType {
READ = 0;
WRITE = 1;
}
OptType opt = 3;
}
使用枚举的时候需要注意,一定要有零值,它是枚举类型字段的默认值。
此外,Protobuf 还支持类型嵌套,从而构建更复杂的结构化数据,比如我们之前在通过 Broker 组件实现基于事件驱动的异步通信中定义 User 相关数据结构时就使用了类型嵌套:
message User {
string id = 1;
string name = 2;
string email = 3;
string password = 4;
}
message Error {
int32 code = 1;
string description = 2;
}
message Request {}
message Response {
User user = 1;
repeated User users = 2;
repeated Error errors = 3;
}
我们在 Response
中引入了 User 和 Error 类型,如果 User 和 Error 定义在其他文件,还可以通过 import
引入:
import "package/other.proto" // package 表示包名,other 表示文件名
这里的 repeated
表示可能包含多个 User 和 Error 类型(即返回的是 User 或 Error 数组)。
既然有数组,那就有字典(Map),Protobuf 也支持字典类型:
map<key_type, value_type> map_field = N;
定义好结构化的数据类型之后,如果你想将其用于 RPC 通信,可以通过 service
来定义 RPC 服务:
message HelloWorld {
int32 id = 1;
string str = 2;
enum OptType {
READ = 0;
WRITE = 1;
}
OptType opt = 3;
}
message Request {}
message Response {
HellWorld hello = 1;
}
service ReaderService {
rpc read(Request) returns Response {}
}
service WriterService {
rpc write(HelloWorld) returns Response {}
}
但由于我们本示例只是与本地磁盘文件进行交互,所以就没有必要定义 Service 了。
自动生成相关代码
接下来,我们通过 Protobuf 命令行工具 protoc
基于上面定义的 hello.proto
文件来生成相关的代码,在此之前,需要先安装这个工具,可以参考基于 Go Micro 框架创建第一个微服务接口中的安装方式安装,由于我们基于 Go 语言进行编码所以还要安装对应的 proto-gen-go
,前者需要全局安装,后者可以在当前项目中安装:
go get -u github.com/golang/protobuf/protoc-gen-go
安装完成后,执行如下命令生成代码:
protoc --proto_path=. --go_out=. proto/hello.proto
注:假设你的目录结构和我的一致,我在
$GOPATH/src
目录下新建了一个hello
目录存放本示例代码,然后在hello
目录下创建了proto
目录来存放.proto
文件,上述命令在hello
目录下执行。
运行成功后我们就可以在 hello.proto
所在目录下看到新生成的 hello.pb.go
文件。
编写 Writer 和 Reader 实现
接下来,我们在 hello
目录下创建 writer.go
,并编写消息编码代码并编码后消息写入日志文件 log
中:
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
hello "hello/proto"
"io/ioutil"
)
func main() {
message := &hello.HelloWorld{}
message.Id = 1
message.Str = "学院君"
message.Opt = hello.HelloWorld_WRITE
out, err := proto.Marshal(message)
if err != nil {
fmt.Printf("消息编码失败: %v\n", err)
return
}
err = ioutil.WriteFile("log", out, 0644)
if err != nil {
fmt.Printf("文件写入 log 失败: %v\n", err)
return
}
fmt.Printf("将消息编码并写入到文件成功: %s\n", message)
}
然后在同一级目录下创建 reader.go
,并编写从文件中读取消息代码并将其解码打印出来:
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
hello "hello/proto"
"io/ioutil"
)
func main() {
in, err := ioutil.ReadFile("log")
if err != nil {
fmt.Printf("文件读取失败: %v\n", err)
return
}
message := &hello.HelloWorld{}
if err := proto.Unmarshal(in, message); err != nil {
fmt.Printf("消息解码失败: %v\n", err)
return
}
fmt.Println("读取文件内容并解码消息成功: ", message)
}
在上述代码中,我们使用了 golang/protobuf/proto
包提供的 API 对消息进行编解码处理,这是 Go 语言官方对 Protobuf 序列化数据进行支持的底层库。
最后我们来执行这两个文件:
说明消息编码和解码功能都是 OK 的,以上就是 Protobuf 编码的基本用法以及在 Go 语言中的简单实现,更多细节和高级功能,请参考 Protobuf 官方指南:Developer Guide。