微服务架构系列 - 增补篇:数据压缩编码工具 Protobuf

- 5 mins

微服务架构系列(三十五)

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

img

此外,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 序列化数据进行支持的底层库。

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

img

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

rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora