学习ProtoBuf | 青训营笔记

221 阅读7分钟

这是我参与「第五届青训营 」笔记创作活动的第6天

protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为protobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。

protobuf拥有代码生成工具,因此,我们只需要定义好数据结构,就可生成代码。并且支持多种编程语言。

1. 快速开始

protobuf环境准备、快速上手与基础教程

1.1 准备protobuf环境

使用protobuf进行代码生成,需要两个工具:通用编辑器、代码生成器。

1.1.1 下载通用编译器 & 配置环境变量

地址:github.com/protocolbuf…

Windows

下载完成后,解压到你的目标路径。(你自己愿意放哪就放哪)

将文件夹内的bin配置到环境变量

image-20230120141027061

Ubuntu 20

  • 一次下载不下来,就多下载几次。Linux下载完需要编译

    wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.tar.gz
    
  • 解压

    tar -zxvf protobuf-all-21.12.tar.gz
    
  • 继续执行

    ./configure --prefix=/usr/local/protobuf
    
    • 如果报错Can't exec "libtoolize": 没有那个文件或目录,这是因为你本地没有C++环境。执行如下命令。然后再次执行./configure

      apt-get install build-essential
      apt-get install g++
      
  • 执行如下命令。makemake check执行时间较长,

    make
    make check
    sudo make install
    sudo ldconfig
    
  • 配置环境变量

    sudo vim /etc/profile
    

    添加如下内容

    export PATH=$PATH:/usr/local/protobuf/bin
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
    

    刷新

    source /etc/profile
    
  • 验证

    protoc --version
    

    输出

    libprotoc 3.21.12
    

1.1.2 安装生成器

这里展示的是GoLang的protoc生成器

这个地方有两个源,建议安装google的源,github那个有问题,不支持工具了。

go install google.golang.org/ProtoBuf/cmd/protoc-gen-go@latest
go get github.com/golang/protobuf/protoc-gen-go

1.2 编写proto文件

编写一个hello.proto文件

syntax = "proto3"; // proto版本

package pb; // 当前proto文件所在的包

// 分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包
option go_package = "./; hello"; 

message HelloRequest{
  string RequestValue = 1;
}

message HelloResponse{
  string ResponseValue = 1;
}

service HelloService{
  rpc Hello(HelloRequest) returns (HelloResponse);
}

1.3 生成go文件

protoc --go_out=./ hello.proto

--go_out=./:是指出生成go文件的路径,这个和.proto文件中的那个路径是一个意思,只不过该命令可以重新指定生成go文件的路径

protoc --go_out=./后边跟的是要基于哪个.proto文件生成go文件,支持通配符。

执行命令后会在你指定路径下生成go文件。go文件内包含了基于.proto文件生成的go 的结构体,这样我们就不用自己写了。详细说明如下:

我们在.proto写了两个消息的结构,一个是请求的,一个是响应的。在对应的go文件里就会给我们生成这两个消息结构对应的go 结构体。

image-20230120164634273

1.4 go结构序列化与反序列为proto

接下来,我么可以向使用json那样,对结构进行MarshalUnmarshal

func main() {
	helloRequest := &hello.HelloRequest{
		RequestValue: "hello, my name is Anthony",
	}
	protoMessage, _ := proto.Marshal(helloRequest)

	helloResponse := &hello.HelloResponse{}
	proto.Unmarshal(protoMessage, helloResponse)
}

1.5 生成go GRPC文件

与生成普通的go文件类似,只不过是加上了plugins=grpc:

protoc --go_out=plugins=grpc:./ hello.proto

生成的go文件也比正常的文件在最后最后多了一些关于RPC调用的内容。重点的内容是,生成了一个接口。接口中定义了我们在.proto文件中定义的service中的方法。

image-20230120171856918

我们只需要在我们的服务中实现这个接口,并将该实现与GRPC进行绑定就可以实现远程过程调用了。因为这篇笔记记录的是progobuf,而不是GRPC,所以就不详细展开了。

2. proto文件语法介绍

2.1 关键字

  • syntax:proto版本。必须写,而且要放在第一行。默认是proto2,但是现在主流是proto3。
  • package:当前proto文件所在的包
  • option go_package: 指定生成文件的路径及文件名。此处不指定也可以在执行protoc命令时指定。如:option go_package = "./; hello"。分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包。
  • message:非常重要,用于指定消息的格式。后面会详细介绍。
  • service :指定通信接口的函数
syntax = "proto3"; // proto版本

package pb; // 当前proto文件所在的包

// 分号前是利用该proto文件生成go文件时的文件路径。分号之后是go文件所在的包
option go_package = "./; hello"; 

message HelloRequest{
  string RequestValue = 1;
}

message HelloResponse{
  string ResponseValue = 1;
}

service HelloService{
  rpc Hello(HelloRequest) returns (HelloResponse);
}

2.2 字段映射

.proto TypeNotesC++ TypePython TypeGo Type
doubledoublefloatfloat64
floatfloatfloatfloat32
int32使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代int32intint32
uint32使用变长编码uint32int/longuint32
uint64使用变长编码uint64int/longuint64
sint32使用变长编码,这些编码在负值时比int32高效的多int32intint32
sint64使用变长编码,有符号的整型值。编码时比通常的 int64高效。int64int/longint64
fixed32总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。uint32intuint32
fixed64总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。uint64int/longuint64
sfixed32总是4个字节int32intint32
sfixed32总是4个字节int32intint32
sfixed64总是8个字节int64int/longint64
boolboolboolbool
string一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。stringstr/unicodestring
bytes可能包含任意顺序的字节数据。stringstr[]byte

2.3 字段规则

  • required:消息体中必填字段,不设置会导致编解码异常。(例如位置1)
  • optional: 消息体中可选字段。(例如位置2)
  • repeated: 消息体中可重复字段,重复的值的顺序会被保留(例如位置3)在go中重复的会被定义为切片。
message User {
  string username = 1;
  int32 age = 2;
  optional string password = 3;
  repeated string address = 4;
}

2.4 0值

protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:

类型默认值
boolfalse
整型0
string空字符串""
枚举enum第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;
message不是null,而是DEFAULT_INSTANCE

2.5 标识号

标识号:在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[0,2^29-1]范围内的一个整数。

message Person { 
  string name = 1;  // (位置1)
  int32 id = 2;  
  optional string email = 3;  
  repeated string phones = 4; // (位置4)
}

以Person为例,name=1,id=2, email=3, phones=4 中的1-4就是标识号。

小技巧:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。

2.6 消息嵌套

你也可以将消息嵌套任意多层,如 :

message Grandpa { // Level 0
    message Father { // Level 1
        message son { // Level 2
            string name = 1;
            int32 age = 2;
    	}
	} 
    message Uncle { // Level 1
        message Son { // Level 2
            string name = 1;
            int32 age = 2;
        }
    }
}

可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person消息就定义在PersonInfo消息内,如 :

message PersonInfo {
    message Person {
        string name = 1;
        int32 height = 2;
        repeated int32 weight = 3;
    } 
	repeated Person info = 1;
}

如果你想在它的父消息类型的外部重用这个消息类型,你需要以PersonInfo.Person的形式使用它,如:

message PersonMessage {
	PersonInfo.Person info = 1;
}

2.7 import

我们也看到了,把所有的message写到一个文件中很不方便维护,嵌套层级过多也很乱。可以,将message拆分到不懂得文件中,然后通过import引入当前文件。

userModel.proto文件中定义了message UserModel

syntax = "proto3";
package pb;
option go_package = "/internal/service; service";

message UserModel{
  uint32 UserID = 1;
  string UserName = 2;
  string NickName = 3;
}

userService.proto中引入

syntax = "proto3";
package pb;

import "userModels.proto";
option go_package = "/internal/service; service";


message UserDetailResponse{
  UserModel UserDetail = 1;
  uint32 Code = 2;
}

2.7 map类型

map<key_type, value_type> map_field = N;

语法非常简单和通用,但是有几个问题需要我们注意:

  1. key_type 可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。
  2. 注意:枚举不是有效的 key_type
  3. value_type 可以是除另一个映射之外的任何类型。
  4. Map 字段不能使用 repeated 关键字修饰。

我们举个典型的例子:学生的学科和分数就适合用map定义:

syntax = "proto3";

package map;

option go_package = "./;score";

message Student{
  int64              id    = 1; //id
  string             name  = 2; //学生姓名
  map<string, int32> score = 3;  //学科 分数的map
}