ProtoBuf 入门教程
在网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 或 XML,本教程介绍另外一个数据交换的协议的工具ProtoBuf。
1.简介
protocol buffers (ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
json\xml都是基于文本格式,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 Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | PHP 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; }
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编译器
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; // 评价积分 }
cd score_server
protoc --go_out=. score_info.proto
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()) }