ProtoBuf 入门教程

在网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 或 XML,本教程介绍另外一个数据交换的协议的工具ProtoBuf。

1.简介

protocol buffers (ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

json\xml都是基于文本格式,protobuf是二进制格式。

你可以通过 ProtoBuf 定义数据结构,然后通过 ProtoBuf 工具生成各种语言版本的数据结构类库,用于操作 ProtoBuf 协议数据

本教程介绍的是最新的protobuf proto3版本的语法。

2. 使用ProtoBuf的例子

2.1. 创建 .proto 文件,定义数据结构

使用 ProtoBuf ,首先需要通过 ProtoBuf 语法定义数据结构(消息),这些定义好的数据结构保存在.proto为后缀的文件中。

例子:

文件名: response.proto

// 指定protobuf的版本,proto3是最新的语法版本
syntax = "proto3";

// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Response {
  string data = 1;   // 定义一个string类型的字段,字段名字为data, 序号为1
  int32 status = 2;   // 定义一个int32类型的字段,字段名字为status, 序号为2
}

说明:proto文件中,字段后面的序号,不能重复,定义了就不能修改,可以理解成字段的唯一ID。

2.2. 安装ProtoBuf编译器

protobuf的github发布地址: https://github.com/protocolbuffers/protobuf/releases

protobuf的编译器叫protoc,在上面的网址中找到最新版本的安装包,下载安装。

这里下载的是:protoc-3.9.1-win64.zip , windows 64位系统版本的编译器,下载后,解压到你想要的安装目录即可。

提示:安装完成后,将 [protoc安装目录]/bin 路径添加到PATH环境变量中

打开cmd,命令窗口执行protoc命令,没有报错的话,就已经安装成功。

2.3. 将.proto文件,编译成指定语言类库

protoc编译器支持将proto文件编译成多种语言版本的代码,我们这里以java为例。

切换到proto文件所在的目录, 执行下面命令

protoc --java_out=. response.proto

然后在当前目录生成了一个ResponseOuterClass.java的java类文件,这个就是我们刚才用protobuf语法定义的数据结构对应的java类文件,通过这个类文件我们就可以操作定义的数据结构。

2.4. 在代码中使用ProtoBuf对数据进行序列化和反序列化

因为上面的例子使用的是java, 我们先导入protobuf的基础类库。

maven:

<dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.9.1</version>
</dependency>

使用ProtoBuf的例子。

ResponseOuterClass.Response.Builder builder = ResponseOuterClass.Response.newBuilder();
// 设置字段值
builder.setData("hello www.tizi365.com");
builder.setStatus(200);

 ResponseOuterClass.Response response = builder.build();
 // 将数据根据protobuf格式,转化为字节数组
 byte[] byteArray  = response.toByteArray();

// 反序列化,二进制数据
try {
    ResponseOuterClass.Response newResponse = ResponseOuterClass.Response.parseFrom(byteArray);
    System.out.println(newResponse.getData());
    System.out.println(newResponse.getStatus());
} catch (Exception e) {
 }

提示:大家不必在意不同的语言具体怎么使用protobuf, 后面会有专门的章节介绍。

protobuf 定义消息

消息(message),在protobuf中指的就是我们要定义的数据结构。

1. 语法

syntax = "proto3";

message 消息名 {
    消息体
}

syntax关键词定义使用的是proto3语法版本,如果没有指定默认使用的是proto2。

message关键词,标记开始定义一个消息,消息体,用于定义各种字段类型。

提示: protobuf消息定义的语法结构,跟我们平时接触的各种语言的类定义,非常相似。

例子:

syntax = "proto3";
 
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

定义了一个SearchRequest消息,这个消息有3个字段,query是字符串类型,page_number和result_per_page是int32类型。

提示:我们通常将protobuf消息定义保存在.proto为后缀的文件中。

2. 字段类型

支持多种数据类型,例如:string、int32、double、float等等,下一章节会有详细的讲解。

3. 分配标识号

通过前面的例子,在消息定义中,每个字段后面都有一个唯一的数字,这个就是标识号。

这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变,每个消息内唯一即可,不同的消息定义可以拥有相同的标识号。

注意:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的字段预留一些标识号。

保留标识号(Reserved) 如果你想保留一些标识号,留给以后用,可以使用下面语法:

message Foo {
  reserved 2, 15, 9 to 11; // 保留2,15,9到11这些标识号
}

如果使用了这些保留的标识号,protocol buffer编译器会输出警告信息。

4. 注释

往.proto文件添加注释,支持C/C++/java风格的双斜杠(//) 语法格式。

例子:

// 定义SearchRequest消息
message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // 页码
  int32 result_per_page = 3;  // 分页大小
}

5. 为消息定义包

我们也可以为消息定义包。

例子:

package foo.bar;
message Open { ... }

定义了一个包:foo.bar

6. 选项

下面是一些常用的选项:

  • java_package 单独为java定义包名字。

  • java_outer_classname 单独为java定义,protobuf编译器生成的类名。

7. 将消息编译成各种语言版本的类库

编译器命令格式:

protoc [OPTION] PROTO_FILES

OPTION是命令的选项, PROTO_FILES是我们要编译的proto消息定义文件,支持多个。

常用的OPTION选项:

 --cpp_out=OUT_DIR           指定代码生成目录,生成 C++ 代码
  --csharp_out=OUT_DIR        指定代码生成目录,生成 C# 代码
  --java_out=OUT_DIR          指定代码生成目录,生成 java 代码
  --js_out=OUT_DIR            指定代码生成目录,生成 javascript 代码
  --objc_out=OUT_DIR          指定代码生成目录,生成 Objective C 代码
  --php_out=OUT_DIR           指定代码生成目录,生成 php 代码
  --python_out=OUT_DIR        指定代码生成目录,生成 python 代码
  --ruby_out=OUT_DIR          指定代码生成目录,生成 ruby 代码

例子:

protoc --java_out=. demo.proto

在当前目录导出java版本的代码,编译demo.proto消息。

有些语言需要单独安装插件才能编译proto,例如golang

安装go语言的protoc编译器插件
go get -u github.com/golang/protobuf/protoc-gen-go

注意: 安装go语言插件后,需要将 $GOPATH/bin 路径加入到PATH环境变量中。

编译成go语言版本

protoc --go_out=. helloworld.proto

protobuf 数据类型

 

Protobuf定义了一套基本数据类型,下表罗列出了protobuf类型和其他语言类型的映射表。

.proto TypeNotesC++ TypeJava TypePython Type[2]Go TypeRuby TypeC# TypePHP Type
double   double double float float64 Float double float
float   float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool   bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT)    

protobuf 枚举(enum)类型

 

当需要定义一个消息类型的时候,可能想为一个字段指定“预定义值序列”中的一个值,这时候可以通过枚举实现。

例子:

syntax = "proto3";//指定版本信息,不指定会报错

enum PhoneType //枚举消息类型,使用enum关键词定义,一个电话类型的枚举类型
{
    MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
    HOME = 1;
    WORK = 2;
}

// 定义一个电话消息
message PhoneNumber
{
    string number = 1; // 电话号码字段
    PhoneType type = 2; // 电话类型字段,电话类型使用PhoneType枚举类型
}

protobuf 数组类型

在protobuf消息中定义数组类型,是通过在字段前面增加repeated关键词实现,标记当前字段是一个数组。

1.整数数组的例子:

message Msg {
  // 只要使用repeated标记类型定义,就表示数组类型。
  repeated int32 arrays = 1;
}

2.字符串数组

message Msg {
  repeated string names = 1;
}

protobuf 消息嵌套

我们在各种语言开发中类的定义是可以互相嵌套的,也可以使用其他类作为自己的成员属性类型。

在protobuf中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。

1.引用其他消息类型的用法

// 定义Result消息
message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3; // 字符串数组类型
}

// 定义SearchResponse消息
message SearchResponse {
  // 引用上面定义的Result消息类型,作为results字段的类型
  repeated Result results = 1; // repeated关键词标记,说明results字段是一个数组
}

2.消息嵌套

类似类嵌套一样,消息也可以嵌套。

例子:

message SearchResponse {
  // 嵌套消息定义
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  // 引用嵌套的消息定义
  repeated Result results = 1;
}

3.import导入其他proto文件定义的消息

我们在开发一个项目的时候通常有很多消息定义,都写在一个proto文件,不方便维护,通常会将消息定义写在不同的proto文件中,在需要的时候可以通过import导入其他proto文件定义的消息。

例子:

保存文件: result.proto

syntax = "proto3";
// Result消息定义
message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3; // 字符串数组类型
}

保存文件: search_response.proto

syntax = "proto3";
// 导入Result消息定义
import "result.proto";

// 定义SearchResponse消息
message SearchResponse {
  // 使用导入的Result消息
  repeated Result results = 1; 
}

protobuf map类型

protocol buffers支持map类型定义。

1.map语法

map<key_type, value_type> map_field = N;

key_type可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。请注意,枚举不是有效的key_type

value_type 可以是除另一个映射之外的任何类型。

2.map的例子

syntax = "proto3";
message Product
{
    string name = 1; // 商品名
    // 定义一个k/v类型,key是string类型,value也是string类型
    map<string, string> attrs = 2; // 商品属性,键值对
}

Map 字段不能使用repeated关键字修饰。

golang 如何使用protobuf

本节介绍,在go语言中,如何是用protobuf对数据进行序列化和反序列化。

1.先参考protobuf快速入门章节安装protoc编译器

protoc快速入门

2.安装protoc-gen-go

安装针对go语言的编译器插件。

go get -u github.com/golang/protobuf/protoc-gen-go

安装好了之后, 在$GOPATH/bin下面会找到protoc-gen-go,编译器插件,将$GOPATH/bin路径添加到PATH环境变量中。

3.安装protobuf包

使用protobuf需要先安装对应的包。

go get -u github.com/golang/protobuf

4.定义proto消息

例子:

文件名:score_server/score_info.proto

syntax = "proto3";

package score_server;


// 基本的积分消息
message base_score_info_t{
    int32       win_count = 1;                  // 玩家胜局局数
    int32       lose_count = 2;                 // 玩家负局局数
    int32       exception_count = 3;            // 玩家异常局局数
    int32       kill_count = 4;                 // 总人头数
    int32       death_count = 5;                // 总死亡数
    int32       assist_count = 6;               // 总总助攻数
    int64       rating = 7;                     // 评价积分
}

5.编译proto文件,生成go代码

cd score_server
protoc --go_out=. score_info.proto

6.测试代码

package main

import (
    "fmt"
        // 导入protobuf依赖包
    "github.com/golang/protobuf/proto"
       // 导入我们刚才生成的go代码所在的包,注意你们自己的项目路径,可能跟本例子不一样
    "demo/score_server" 
)

func main() {
        // 初始化消息
    score_info := &score_server.BaseScoreInfoT{}
    score_info.WinCount = 10
    score_info.LoseCount = 1
    score_info.ExceptionCount = 2
    score_info.KillCount = 2
    score_info.DeathCount = 1
    score_info.AssistCount = 3
    score_info.Rating = 120

    // 以字符串的形式打印消息
    fmt.Println(score_info.String())

    // encode, 转换成二进制数据
    data, err := proto.Marshal(score_info)
    if err != nil {
        panic(err)
    }

    // decode, 将二进制数据转换成struct对象
    new_score_info := score_server.BaseScoreInfoT{}
    err = proto.Unmarshal(data, &new_score_info)
    if err != nil {
        panic(err)
    }

    // 以字符串的形式打印消息
    fmt.Println(new_score_info.String())
}

 

posted on 2022-08-10 13:42  root-123  阅读(1403)  评论(0编辑  收藏  举报