Go Micro 框架增补篇:Protobuf 快速入门

protocol buffers

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 中的数据类型与所支持语言的对应关系

此外,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 中引入了 UserError 类型,如果 User 和 Error 定义在其他文件,还可以通过 import 引入:

import "package/other.proto"  // package 表示包名,other 表示文件名

这里的 repeated 表示可能包含多个 UserError 类型(即返回的是 UserError 数组)。

既然有数组,那就有字典(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 read(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 序列化数据进行支持的底层库。

最后我们来执行这两个文件:

基于 Protobuf 进行编解码的演示代码示例

说明消息编码和解码功能都是 OK 的,以上就是 Protobuf 编码的基本用法以及在 Go 语言中的简单实现,更多细节和高级功能,请参考 Protobuf 官方指南:Developer Guide

上一篇: Go Micro 框架底层组件篇 —— Broker 底层源码剖析

下一篇: Go Micro 框架增补篇:集成 gRPC 网关对外提供服务