跟着官方文档学习Protocol Buffers序列化结构数据
在移动互联网时代,手机流量、电量是最为有限的资源,而移动端的即时通讯应用无疑必须得直面这两点。
解决流量过大的基本方法就是使用高度压缩的通信协议,而数据压缩后流量减小带来的自然结果也就是省电:因为大数据量的传输必然需要更久的网络操作、数据序列化及反序列化操作,这些都是电量消耗过快的根源。
当前即时通讯应用中最热门的通信协议无疑就是Google的Protobuf了,基于它的优秀表现,微信和手机QQ这样的主流IM应用也早已在使用它。接下来我们一起通过官网学习一下它吧~
# 简介Protocol Buffer又简称Protobuf。正如官网所介绍的,Protocol是用于序列化结构化数据的与语言无关、与平台无关的可扩展机制。
我们可以发现关键字,序列化。简单来说它和我们平常使用最多的Json格式一样,是用来各个平台、系统传输数据的。
Protocol Buffers Documentation (protobuf.dev)
那么使用这种序列化结构传输数据,对比其他的,例如Json有什么优势呢?
官网说明到,Protobuf类似XML,但是更小、更快、更简单。您只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取到各种数据流,并使用各种语言。如果你有在数据库里面存储Json格式,那么你会发现它占用的存储容量是十分多的,而使用Protobuf则小的多。
更详细的我们可以查看overview页面
- 紧凑的数据存储(存储空间更小)
- 快速解析(解析更快)
- 多种编程语言的可用性
- 通过自动生成的类优化功能
- 跨语言兼容性
这里也有一个GitHub项目,对比了各种序列化工具的快慢。 Home · eishay/jvm-serializers Wiki · GitHub
并且支持使用C++,C#,Dart,Go,Java,Kotlin,ObjectiveC,Python,Ruby。并且当我们使用proto3也可以支持PHP生成对应的Protobuf代码。
那么怎么开始呢?当然是先下载安装。。
安装编译器
按照官网的指示
GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format
我们点击链接,跳转到Github页面。
下滑点击Releases,进入。
这里我们以最新的Protocol Buffers v26.0为例
选择好对应的版本,windows再最下面,点击Show all 14assets。即可
大多数人的电脑为windows,64位选择即可,点击下载。选择对应的安装路径,并配置环境变量。
将安装目录下的,protoc-26.0-win64/bin配置为环境变量。
接着打开CMD,输入protoc
。
也可以通过protoc --version
查看版本
成功安装~
学习使用
那么成功安装之后,我们接下来应该怎么做呢?我们继续回到官网文档主界面。点击查看 Tutorials | Protocol Buffers Documentation (protobuf.dev)
这里将介绍使用你想用的编程语言来实现一个简单的应用程序。
我们以Java为例,下滑选择Java。Protocol Buffer Basics: Java | Protocol Buffers Documentation (protobuf.dev)
可以看到未来能使用Java编程语言来实现一个简单的Protobuf应用程序。
- 在
.proto
file定义消息格式。 - 使用protocol buffer 编译器。(即我们之前下载安装的)
- 使用Java的protocol buffer API去读写消息。
定义消息类型
所以我们首先要学习如何在.proto
file定义消息格式。Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)我们学习最新的proto3
点击下方的proto3链接进行学习。
我们通过proto3语法定义一个消息类型。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
- 第一行
syntax = "proto3"
则是定义了使用的是proto3语法。如果没有这条声明语句,那么protocol buffer 编译器则会假定你使用的是proto2语法。并且必须要写在文件的第一行,不能为空,且不能有注释。 message SearchRequest
则定义了一个名为SearchRequest
的message,并且声明了3个字段(name/valued对)。每个字段对应您想要包含在这种类型的消息中的数据。每个字段都有一个名称和类型。例如字段query是string类型,page_number和results_per_page是int32类型。
指定字段类型
我们定义了这3个数据类型在.proto文件中,那么它和我们的编程语言数据类型是有什么对应关系吗?如何明确我们需要的数据类型呢?我们查看文档 Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
uint32 | Uses variable-length encoding. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | Uses variable-length encoding. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | Always eight bytes. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str (Python 2) bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List |
通过这个表我们可知道,前面定义的三个类在Java里面的数据类型为:
- query: string -> String
- page_number: int32 -> int
- results_per_page: int32 -> int
分配字段编号
那么每个字段后面的数字又是什么呢?Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
这其实也是一种语法规定,我们必须要给每个message里面的字段定义一个1
到536870811
的数字,并且要遵循下面这些规范:
- 给定的数字在该消息的所有字段中必须是唯一的。
- 字段编号
19,000
到19,999
留给protocol buffer实现。如果您在message中使用这些保留字段编号之一,则协议缓冲区编译器将进行报错。 - 不能使用任何先前保留的字段编号或已分配给extensions的字段编号。
需要注意的是:
- 一旦这个Message Type(前文中定义的SearchRequest)被使用了,就无法更改此编号。更改字段编号相当于删除该字段并创建了一个类型相同但编号为新编号的字段。
- 切勿重复使用字段编号。切勿从保留列表(例如19000-19999)中取出字段编号,定义为新字段的编号。
- 对于最常设置的字段,应该使用字段号1到15。较低的字段数值在wire format中占用的空间较少。例如,1到15范围内的字段号需要一个字节来编码。16到2047范围内的字段号占用两个。
添加更多的消息类型
在我们之前定义的.proto
文件里面,我们这是定义了一个message名为SearchRequest,让我们也可以在一个.proto文件里面定义多个message。当然我最好是定义相关的message在一个文件里面。例如我们可以在下面定义一个searchResponse。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
虽然在一个文件里面定义多个message,但是当里面具有了多个不同的依赖关系时,会导致太过于膨胀。所以建议每个.proto文件包含尽可能少的消息类型。
添加注释
在.proto文件添加注释,使用的是C/C++格式的//和/.../语法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 results_per_page = 3; // Number of results to return per page.
}
生成Classes
简单的学习了相关的语法,那么我们就要进行前面说的第二步了。使用protobuf compiler生成对应语言的代码。
我们可以看到在java里面,编译器生成一个.java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊Builder类。
那么如何使用编译器生成对应的Classes呢?
Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
我们通过使用protoc
编译对应的.proto文件。
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
其中IMPORT_PATH
指明了去哪里寻找.proto
文件,即文件存放在哪个路径下。如果省略,则默认为当前目录。其中--proto_path
参数可以缩写为-I
. 当然指定的文件路径也可以为多个路径,会按顺序进行搜寻。
其他的--xxx_out
则是生成指定编程语言代码到指定目录中。例如--java-out=DST_DIR
则是生成对应的java代码到指定的DST_DIR目录中。
我们进行使用D:\proto>protoc --java_out=D:\proto "SearchRequest.proto"
将D盘下的proto文件中的SearchRequest.proto
文件进行编译。(SearchRequest.proto即前文中「定义消息类型」章节所声明的)
得到了对应的java文件,点击进行查看
我们可以查看这个生成类的目录结构。
可以看到这个
SearchRequestOuterClass
里面定义了个接口SearchRequestOrBUilder
和一个类SearchRequest
(等下会对比到)
我们点击protobuf.dev/getting-sta… 链接回到Java指南这个界面。往下拉,可以看到官网定义了一个proto文件。
我们对其相关出现的参数进行学习。
- java_package:定义一个包。为了防止不同项目之间的命名冲突。当定义了该参数,则会在指定的目录下,生成这个包结构。
- java_outer_classname:其实就是定义这个Outer Class的名字。对照前文,则是
SearchRequestOuterClass
这个是默认生成的名字。 - java_multiple_files = true:当为true的时候,则不会像前文一样,生成的只有一个Outer Class对象,里面包含了其他的Class。而是每一个接口和Class都是一个文件。
我们可以修改一下我们的proto文件,添加这三个参数。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.kylin";
option java_outer_classname = "SearchOuter";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
接下来运行以下命令protoc --java_out=D:\proto\demo\ '.\SearchRequest .proto'
可以看到它在com.kylin
包下生成了三个文件,并且Outer Class的名称为SearchOuter
那么生成完我们对应的Classes之后就是第三步了。使用Java的protocol buffer API去读写消息。
读写消息
我们首先来看看怎么使用生成的类。
protobuf编译器生成的消息类都是不可变的。一旦构造了消息对象,就无法修改它,就像 Java 字符串一样。要构造消息,您必须首先构造一个构建器,将要设置的任何字段设置为所选值,然后调用构建器的build()方法。
您可能已经注意到,修改消息的构建器的每个方法都会返回另一个构建器。返回的对象实际上是调用该方法的同一生成器。为方便起见,返回它以便您可以在一行代码中将多个 setter 串在一起。即链式调用。
官网后面的读写消息,其实就是通过控制台输入对对象进行赋值为Reading Message,然后遍历对象输出对应属性为Writing message。
我们可以把它简化一下。
import com.kylin.SearchRequest;
public class Demo {
public static void main(String[] args) {
//Reading Message
SearchRequest searchRequest = SearchRequest.newBuilder()
.setQuery("1")
.setPageNumber(1)
.setResultsPerPage(100)
.build();
//Writing Message
System.out.println(searchRequest.getQuery());
System.out.println(searchRequest.getPageNumber());
System.out.println(searchRequest.getResultsPerPage());
}
}
我们点击运行结果发现了报错。
发现了报错,其实可以看见我们缺少了类,即官方的jar包。于是我试着在这个文档里面查找,并没有找到相关的jar依赖说明。
于是我进入了官方的github仓库 github.com/protocolbuf…
可以看到一个Protobuf Runtime 安装,选择Java。
终于看到了!
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version><!--version--></version>
</dependency>
选择自己的版本进行下载依赖,然后点击运行。26.0.RC1 ---> 4.26.0-RC1
下载完jar包之后,点击运行。
读写成功~
篇幅有限其实还是有很多细节没有在这一篇中讲到,更多详细的学习和操作会在以后一起学习~