Protobuf 编解码

932 阅读4分钟

Protobuf编解码

ProtobufProtocol 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编号代替名字

关于数据编码:

XMLJSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。
但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。
我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分

但是我们如何把这个定义文件(IDL: 接口描述语言), 编译成不同语言的数据结构喃? 这就需要我们安装protobuf的编译器

安装编译器

protobuf的编译器叫: protoc(protobuf compiler), 我们需要到这里下载编译器: Releases · protocolbuffers/protobuf

选择对应平台的二进制包下载:

image.png

这个压缩包里面有:

  • 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盘)

image.png

image.png

验证安装

$ 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

安装该插件 增加语句提醒 image.png
接下来 我们就可以使用 protoc 来生成我们对于的Go语言的数据结构

Hello Protobuf

在day21/pb下创建hello.pb

image.png

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
  • 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)
}