likes
comments
collection
share

跟着官方文档学习Protocol Buffers序列化结构数据

作者站长头像
站长
· 阅读数 5

在移动互联网时代,手机流量、电量是最为有限的资源,而移动端的即时通讯应用无疑必须得直面这两点。

解决流量过大的基本方法就是使用高度压缩的通信协议,而数据压缩后流量减小带来的自然结果也就是省电:因为大数据量的传输必然需要更久的网络操作、数据序列化及反序列化操作,这些都是电量消耗过快的根源。

当前即时通讯应用中最热门的通信协议无疑就是Google的Protobuf了,基于它的优秀表现,微信和手机QQ这样的主流IM应用也早已在使用它。接下来我们一起通过官网学习一下它吧~

跟着官方文档学习Protocol Buffers序列化结构数据

# 简介

Protocol Buffer又简称Protobuf。正如官网所介绍的,Protocol是用于序列化结构化数据的与语言无关、与平台无关的可扩展机制。

我们可以发现关键字,序列化。简单来说它和我们平常使用最多的Json格式一样,是用来各个平台、系统传输数据的。

跟着官方文档学习Protocol Buffers序列化结构数据

Protocol Buffers Documentation (protobuf.dev)

那么使用这种序列化结构传输数据,对比其他的,例如Json有什么优势呢?

跟着官方文档学习Protocol Buffers序列化结构数据

官网说明到,Protobuf类似XML,但是更小、更快、更简单。您只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地将结构化数据写入和读取到各种数据流,并使用各种语言。如果你有在数据库里面存储Json格式,那么你会发现它占用的存储容量是十分多的,而使用Protobuf则小的多。

更详细的我们可以查看overview页面

跟着官方文档学习Protocol Buffers序列化结构数据

  • 紧凑的数据存储(存储空间更小)
  • 快速解析(解析更快)
  • 多种编程语言的可用性
  • 通过自动生成的类优化功能
  • 跨语言兼容性

这里也有一个GitHub项目,对比了各种序列化工具的快慢。 Home · eishay/jvm-serializers Wiki · GitHub

跟着官方文档学习Protocol Buffers序列化结构数据

并且支持使用C++,C#,Dart,Go,Java,Kotlin,ObjectiveC,Python,Ruby。并且当我们使用proto3也可以支持PHP生成对应的Protobuf代码。

那么怎么开始呢?当然是先下载安装。。

安装编译器

按照官网的指示

跟着官方文档学习Protocol Buffers序列化结构数据 GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format

我们点击链接,跳转到Github页面。

跟着官方文档学习Protocol Buffers序列化结构数据

下滑点击Releases,进入。

跟着官方文档学习Protocol Buffers序列化结构数据

这里我们以最新的Protocol Buffers v26.0为例

跟着官方文档学习Protocol Buffers序列化结构数据

选择好对应的版本,windows再最下面,点击Show all 14assets。即可

跟着官方文档学习Protocol Buffers序列化结构数据

大多数人的电脑为windows,64位选择即可,点击下载。选择对应的安装路径,并配置环境变量。

跟着官方文档学习Protocol Buffers序列化结构数据

将安装目录下的,protoc-26.0-win64/bin配置为环境变量。

跟着官方文档学习Protocol Buffers序列化结构数据

接着打开CMD,输入protoc

跟着官方文档学习Protocol Buffers序列化结构数据

也可以通过protoc --version查看版本

跟着官方文档学习Protocol Buffers序列化结构数据

成功安装~

学习使用

那么成功安装之后,我们接下来应该怎么做呢?我们继续回到官网文档主界面。点击查看 Tutorials | Protocol Buffers Documentation (protobuf.dev)

跟着官方文档学习Protocol Buffers序列化结构数据

这里将介绍使用你想用的编程语言来实现一个简单的应用程序。

跟着官方文档学习Protocol Buffers序列化结构数据

我们以Java为例,下滑选择Java。Protocol Buffer Basics: Java | Protocol Buffers Documentation (protobuf.dev)

跟着官方文档学习Protocol Buffers序列化结构数据

跟着官方文档学习Protocol Buffers序列化结构数据

可以看到未来能使用Java编程语言来实现一个简单的Protobuf应用程序。

  1. .protofile定义消息格式。
  2. 使用protocol buffer 编译器。(即我们之前下载安装的)
  3. 使用Java的protocol buffer API去读写消息。

定义消息类型

所以我们首先要学习如何在.protofile定义消息格式。Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)我们学习最新的proto3 点击下方的proto3链接进行学习。

跟着官方文档学习Protocol Buffers序列化结构数据

我们通过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)

跟着官方文档学习Protocol Buffers序列化结构数据

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32Fixnum or Bignum (as required)intintegerint
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
uint32Uses variable-length encoding.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
uint64Uses variable-length encoding.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32Fixnum or Bignum (as required)intintegerint
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
fixed32Always four bytes. More efficient than uint32 if values are often greater than 228.uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 256.uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
sfixed32Always four bytes.int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64Always eight bytes.int64longint/long[4]int64Bignumlonginteger/string[6]Int64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
stringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.stringStringstr/unicode[5]stringString (UTF-8)stringstringString
bytesMay contain any arbitrary sequence of bytes no longer than 232.stringByteStringstr (Python 2) bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringList

通过这个表我们可知道,前面定义的三个类在Java里面的数据类型为:

  1. query: string -> String
  2. page_number: int32 -> int
  3. results_per_page: int32 -> int

分配字段编号

那么每个字段后面的数字又是什么呢?Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)

跟着官方文档学习Protocol Buffers序列化结构数据

这其实也是一种语法规定,我们必须要给每个message里面的字段定义一个1536870811的数字,并且要遵循下面这些规范:

  • 给定的数字在该消息的所有字段中必须是唯一的。
  • 字段编号19,00019,999 留给protocol buffer实现。如果您在message中使用这些保留字段编号之一,则协议缓冲区编译器将进行报错。
  • 不能使用任何先前保留的字段编号或已分配给extensions的字段编号。

需要注意的是:

  1. 一旦这个Message Type(前文中定义的SearchRequest)被使用了,就无法更改此编号。更改字段编号相当于删除该字段并创建了一个类型相同但编号为新编号的字段。
  2. 切勿重复使用字段编号。切勿从保留列表(例如19000-19999)中取出字段编号,定义为新字段的编号。
  3. 对于最常设置的字段,应该使用字段号1到15。较低的字段数值在wire format中占用的空间较少。例如,1到15范围内的字段号需要一个字节来编码。16到2047范围内的字段号占用两个。

添加更多的消息类型

跟着官方文档学习Protocol Buffers序列化结构数据

在我们之前定义的.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生成对应语言的代码。

跟着官方文档学习Protocol Buffers序列化结构数据

我们可以看到在java里面,编译器生成一个.java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊Builder类。

那么如何使用编译器生成对应的Classes呢?

Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)

跟着官方文档学习Protocol Buffers序列化结构数据 我们通过使用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即前文中「定义消息类型」章节所声明的)

跟着官方文档学习Protocol Buffers序列化结构数据

跟着官方文档学习Protocol Buffers序列化结构数据

得到了对应的java文件,点击进行查看

跟着官方文档学习Protocol Buffers序列化结构数据

我们可以查看这个生成类的目录结构。

跟着官方文档学习Protocol Buffers序列化结构数据

可以看到这个 SearchRequestOuterClass里面定义了个接口SearchRequestOrBUilder和一个类SearchRequest(等下会对比到)

我们点击protobuf.dev/getting-sta… 链接回到Java指南这个界面。往下拉,可以看到官网定义了一个proto文件。

跟着官方文档学习Protocol Buffers序列化结构数据

我们对其相关出现的参数进行学习。

跟着官方文档学习Protocol Buffers序列化结构数据

  • 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'

跟着官方文档学习Protocol Buffers序列化结构数据

可以看到它在com.kylin包下生成了三个文件,并且Outer Class的名称为SearchOuter

那么生成完我们对应的Classes之后就是第三步了。使用Java的protocol buffer API去读写消息。

读写消息

我们首先来看看怎么使用生成的类。

跟着官方文档学习Protocol Buffers序列化结构数据

protobuf编译器生成的消息类都是不可变的。一旦构造了消息对象,就无法修改它,就像 Java 字符串一样。要构造消息,您必须首先构造一个构建器,将要设置的任何字段设置为所选值,然后调用构建器的build()方法。

您可能已经注意到,修改消息的构建器的每个方法都会返回另一个构建器。返回的对象实际上是调用该方法的同一生成器。为方便起见,返回它以便您可以在一行代码中将多个 setter 串在一起。即链式调用。

官网后面的读写消息,其实就是通过控制台输入对对象进行赋值为Reading Message,然后遍历对象输出对应属性为Writing message。

跟着官方文档学习Protocol Buffers序列化结构数据

跟着官方文档学习Protocol Buffers序列化结构数据

我们可以把它简化一下。

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());  
    }  
}

我们点击运行结果发现了报错。

跟着官方文档学习Protocol Buffers序列化结构数据

发现了报错,其实可以看见我们缺少了类,即官方的jar包。于是我试着在这个文档里面查找,并没有找到相关的jar依赖说明。

于是我进入了官方的github仓库 github.com/protocolbuf…

跟着官方文档学习Protocol Buffers序列化结构数据

可以看到一个Protobuf Runtime 安装,选择Java。

跟着官方文档学习Protocol Buffers序列化结构数据

终于看到了!

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

选择自己的版本进行下载依赖,然后点击运行。26.0.RC1 ---> 4.26.0-RC1

跟着官方文档学习Protocol Buffers序列化结构数据

下载完jar包之后,点击运行。

跟着官方文档学习Protocol Buffers序列化结构数据

读写成功~

篇幅有限其实还是有很多细节没有在这一篇中讲到,更多详细的学习和操作会在以后一起学习~

跟着官方文档学习Protocol Buffers序列化结构数据