找回密码
 立即注册
查看: 171|回复: 0

在Java法式中使用Protobuf

[复制链接]
发表于 2024-7-15 18:13 | 显示全部楼层 |阅读模式
Protocol Buffer是google出品的一种对象序列化的方式,它的体积小传输快,深得大师的喜爱。protobuf是一种平台无关和语言无关的协议,通过protobuf的定义文件,可以轻松的将其转换成多种语言的实现,非常便利。
今天将会给大师介绍一下,protobuf的基本使用和同java结合的具体案例。
为什么使用protobuf

我们知道数据在网络传输中是以二进制进行的,一般我们使用字节byte来暗示, 一个byte是8bits,如果要在网络上中传输对象,一般需要将对象序列化,序列化的目的就是将对象转换成byte数组在网络中传输,当接收方接收到byte数组之后,再对byte数组进行反序列化,最终转换成java中的对象。
那么将java对象序列化可能会有如下几种方式:

  • 使用JDK自带的对象序列化,但是JDK自带的序列化本身存在一些问题,而且这种序列化手段只适合在java法式之间进行传输,如果长短java法式,比如PHP或者GO,那么序列化就不通用了。
  • 你还可以自定义序列化协议,这种方式的灵活程度斗劲高,但是不够通用,而且实现起来也斗劲复杂,很可能呈现意想不到的问题。
  • 将数据转换成为XML或者JSON进行传输。XML和JSON的好处在于他们都有可以区分对象的起始符号,通过判断这些符号的位置就可以读取到完整的对象。但是不管是XML还是JSON的错误谬误都是转换成的数据斗劲大。在反序列化的时候对资源的消耗也斗劲多。
所以我们需要一种新的序列化的方式,这就是protobuf,它是一种灵活、高效、自动化的解决方案。
通过编写一个.proto的数据布局定义文件,然后调用protobuf的编译器,就会生成对应的类,该类以高效的二进制格式实现protobuf数据的自动编码和解析。 生成的类为定义文件中的数据字段提供了getter和setter方式,并提供了读写的措置细节。 重要的是,protobuf可以向前兼容,也就是说老的二进制代码也可以使用最新的协议进行读取。
定义.proto文件

.proto文件中定义的是你将要序列化的动静对象。我们来一个最基本的student.proto文件,这个文件定义了student这个对象中最基本的属性。
先看一个斗劲简单的.proto文件:
  1. syntax = ”proto3”;
  2. package com.flydean;
  3. option java_multiple_files = true;
  4. option java_package = ”com.flydean.tutorial.protos”;
  5. option java_outer_classname = ”StudentListProtos”;
  6. message Student {
  7.   optional string name = 1;
  8.   optional int32 id = 2;
  9.   optional string email = 3;
  10.   enum PhoneType {
  11.     MOBILE = 0;
  12.     HOME = 1;
  13.   }
  14.   message PhoneNumber {
  15.     optional string number = 1;
  16.     optional PhoneType type = 2;
  17.   }
  18.   repeated PhoneNumber phones = 4;
  19. }
  20. message StudentList {
  21.   repeated Student student = 1;
  22. }
复制代码
第一行定义的是protobuf中使用的syntax协议,默认情况下是proto2,因为目前最新的协议是proto3,所以这里我们使用proto3作为例子。
然后我们定义了地址的package,这个package是指编译的时候生成文件的包。这是一个定名空间,虽然我们在后面定义了java_package,但是为了和非java语言中的协议相冲突,所以定义package还长短常有必要的。
然后是三个专门给java法式使用的option。java_multiple_files, java_package, 和 java_outer_classname.
此中java_multiple_files指编译过后java文件的个数,如果是true,那么将会一个java对象一个类,如果是false,那么定义的java对象将会被包含在同一个文件中。
java_package指定生成的类应该使用的Java包名称。 如果没有明确的指定,则会使用之前定义的package的值。
java_outer_classname选项定义将暗示此文件的包装类的类名。 如果没有给java_outer_classname赋值,它将通过将文件名转换为大写驼峰来生成。 例如,默认情况下,“student.proto”将使用”Student”作为包装类名称。
接下来的部门是动静的定义,对于简单类型来说可以使用bool, int32, float, double, 和 string来定义字段的类型。
上例中我们还使用了复杂的组合属性,和嵌套类型。还定义了一个枚举类。
上面我们为每个属性值分配了ID,这个ID是二进制编码中使用的独一“标签”。因为在protobuf中标识表记标帜数字1-15比16以上的标识表记标帜数字占用的字节空间要更少,因此作为一种优化,凡是将1-15这些标识表记标帜用于常用或反复的元素,而将标识表记标帜16和更高的标识表记标帜用于不太常用的可选元素。
然后再来看看字段的修饰符,有三个修饰符分袂是optional,repeated和required。
optional暗示该字段是可选的,可以设置也可以不设置,如果没有设置,则会使使用默认值,对于简单类型来说,我们可以自定义默认值,如果不自定义,就会使用系统的默认值。对于系统的默认值来说,数字为0,字符串为空字符串,布尔值为false。
repeated暗示该字段是可以反复的,这种反复实际上就是一种数组的布局。
required暗示该字段是必需的,如果该字段没有值,那么该字段将会被认为是没有初始化,测验考试构建未初始化的动静将抛出 RuntimeException,解析未初始化的动静将抛出 IOException。
注意,在Proto3中不撑持required字段。
编译协议文件

定义好proto文件之后,就可以使用protoc命令对其进行编译了。
protoc是protobuf提供的编译器,一般情况下,可以从github的release库中直接下载即可。如果你不想直接下载,或者官方提供的库中并没有你需要的版本,则可以使用源代码直接进行编译。
protoc的使用的命令如下:
  1. protoc --experimental_allow_proto3_optional -I=SRC_DIR --java_out=DST_DIR $SRC_DIR/student.proto
复制代码
如果编译proto3,则需要添加–experimental_allow_proto3_optional选项。
我们运行一下上面的代码。会发此刻com.flydean.tutorial.protos包里面生成了5个文件。分袂是:
  1. Student.java              
  2. StudentList.java         
  3. StudentListOrBuilder.java
  4. StudentListProtos.java   
  5. StudentOrBuilder.java
复制代码
此中StudentListOrBuilder和StudentOrBuilder是两个接口,Student和StudentList是这两个类的实现。
详解生成的文件

在proto文件中,我们主要定义了两个类Student和StudentList, 他们中定义了一个内部类Builder,以Student为例,看下这个两个类的定义:
  1. public final class Student extends
  2.     com.google.protobuf.GeneratedMessageV3 implements
  3.     StudentOrBuilder
  4.   public static final class Builder extends
  5.       com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
  6.       com.flydean.tutorial.protos.StudentOrBuilder
复制代码
可以看到他们实现的接口都是一样的,暗示他们可能提供了不异的功能。实际上Builder是对动静的一个封装器,所有对Student的操作都可以由Builder来完成。
对于Student中的字段来说,Student类只有这些字段的get方式,而Builder中同时有get和set方式。
对于Student来说,对于字段的方式有:
  1. // required string name = 1;
  2. public boolean hasName();
  3. public String getName();
  4. // required int32 id = 2;
  5. public boolean hasId();
  6. public int getId();
  7. // optional string email = 3;
  8. public boolean hasEmail();
  9. public String getEmail();
  10. // repeated .tutorial.Person.PhoneNumber phones = 4;
  11. public List<PhoneNumber> getPhonesList();
  12. public int getPhonesCount();
  13. public PhoneNumber getPhones(int index);
复制代码
对于Builder来说,每个属性多了两个方式:
  1. // required string name = 1;
  2. public boolean hasName();
  3. public java.lang.String getName();
  4. public Builder setName(String value);
  5. public Builder clearName();
  6. // required int32 id = 2;
  7. public boolean hasId();
  8. public int getId();
  9. public Builder setId(int value);
  10. public Builder clearId();
  11. // optional string email = 3;
  12. public boolean hasEmail();
  13. public String getEmail();
  14. public Builder setEmail(String value);
  15. public Builder clearEmail();
  16. // repeated .tutorial.Person.PhoneNumber phones = 4;
  17. public List<PhoneNumber> getPhonesList();
  18. public int getPhonesCount();
  19. public PhoneNumber getPhones(int index);
  20. public Builder setPhones(int index, PhoneNumber value);
  21. public Builder addPhones(PhoneNumber value);
  22. public Builder addAllPhones(Iterable<PhoneNumber> value);
  23. public Builder clearPhones();
复制代码
多出的两个方式是set和clear方式。clear是清空字段的内容,让其变回初始状态。
我们还定义了一个枚举类PhoneType:
  1. public enum PhoneType
  2.       implements com.google.protobuf.ProtocolMessageEnum
复制代码
这个类的实现和普通的枚举类没太大区别。
Builders 和 Messages

如上一节所示,Message对应的类只有get和has方式,所以它是不成以变的,动静对象一旦被构造,就不能被改削。要构建动静,必需首先构建一个构建器,将要设置的任何字段设置为你选择的值,然后调用构建器的 build()方式。
每次调用Builder的方式城市返回一个新的Builder,当然这个返回的Builder和本来的Builder是同一个,返回Builder只是为了便利进行代码的连写。
下面的代码是如何创建一个Student实例:
  1. Student xiaoming =
  2.                 Student.newBuilder()
  3.                         .setId(1234)
  4.                         .setName(”小明”)
  5.                         .setEmail(”flydean@163.com”)
  6.                         .addPhones(
  7.                                 Student.PhoneNumber.newBuilder()
  8.                                         .setNumber(”010-1234567”)
  9.                                         .setType(Student.PhoneType.HOME))
  10.                         .build();
复制代码
Student中提供了一些常用的方式,如isInitialized()检测是否所有必需的字段都设置完毕。toString()将对象转换成为字符串。使用它的Builder还可以调用clear()用来断根已设置的状态,mergeFrom(Message other)用来对对象进行合并。
序列化和反序列化

生成的对象中提供了序列化和反序列化方式,我们只需要在需要的时候对其进行调用即可:

  • byte[] toByteArray();: 序列化动静并返回一个包含其原始字节的字节数组。
  • static Person parseFrom(byte[] data);: 从给定的字节数组中解析一条动静。
  • void writeTo(OutputStream output);: 序列化动静并将其写入 OutputStream.
  • static Person parseFrom(InputStream input);: 从一个动静中读取并解析动静 InputStream.
    通过使用上面的方式,可以很便利的将对象进行序列化和反序列化。
协议扩展

我们在定义好proto之后,假如后续还但愿对其进行改削,那么我们但愿新的协议对历史数据是兼容的。那么我们需要考虑下面几点:
1.不能更改现有字段的ID编号。
2.不能添加和删除任何必填字段。
3.可以 删除可选或反复的字段。
4.可以 添加新的可选字段或反复字段,但您必需使用新的ID编号。
好了,protocol buf的基本用法就介绍到这里,感觉有辅佐的小伙伴记得点赞保藏哦~
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-22 01:05 , Processed in 0.157656 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表