当前位置: 首页 > 后端技术 > Java

java程序中protobuf

时间:2023-04-02 00:05:25 Java

的使用介绍ProtocolBuffer是Google出品的一种对象序列化方法。它体积小,传输速度快,深受大家的喜爱。Protobuf是一个独立于平台和语言的协议。通过protobuf的定义文件,可以方便的转换成多种语言的实现,非常方便。今天给大家介绍一下protobuf的基本使用以及结合java的具体案例。为什么要使用protobuf?我们知道数据在网络中是以二进制形式传输的。一般我们用字节来表示。一个字节是8位。如果要在网络上传输对象,一般需要对对象进行序列化。序列化的目的是将对象转换成字节数组,在网络上传输。接收方接收到字节数组后,将字节数组反序列化,最后转成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文件,它定义了学生对象最基本的属性。先看一个比较简单的.proto文件:syntax="proto3";packagecom.flydean;optionjava_multiple_files=true;optionjava_package="com.flydean.tutorial.protos";optionjava_outer_classname="StudentListProtos";messageStudent{optional字符串名称=1;可选的int32id=2;可选字符串email=3;枚举PhoneType{MOBILE=0;家=1;}messagePhoneNumber{可选字符串number=1;可选PhoneType类型=2;}repeatedPhoneNumberphones=4;}messageStudentList{repeatedStudentstudent=1;}第一行定义了protobuf中使用的语法协议,默认是proto2,因为最新的协议是proto3,所以这里以proto3为例。然后我们定义我们所在的包,指的是编译时生成文件的包。这是一个命名空间。虽然后面定义了java_package,但是为了和非java语言的协议冲突,还是需要定义package的。然后是java程序专用的三个选项。java_multiple_files、java_package和java_outer_classname。其中,java_multiple_files是指编译后的java文件个数。如果为真,那么一个java对象就是一个类。如果为false,则定义的java对象将包含在同一个文件中。java_package指定生成的类应该使用的Java包名称。如果未明确指定,将使用先前定义的包的值。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不支持必填字段。编译协议文件定义好proto文件后,就可以使用protoc命令编译了。protoc是protobuf提供的编译器。一般情况下可以直接从github的release库下载。如果不想直接下载,或者官方库中没有你需要的版本,可以使用源码直接编译。protoc使用的命令如下: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个文件。它们是:Student.javaStudentList.javaStudentListOrBuilder.javaStudentListProtos.javaStudentOrBuilder.java其中,StudentListOrBuilder和StudentOrBuilder是两个接口,Student和StudentList是这两个类的实现。生成文件详解在proto文件中,我们主要定义了两个类Student和StudentList,其中定义了一个内部类Builder。以Student为例,看这两个类的定义:publicfinalclassStudentextendscom.google.protobuf.GeneratedMessageV3implementsStudentOrBuilderpublicstaticfinalclassBuilderextendscom.google.protobuf.GeneratedMessageV3.Builderimplementscom.flydean.tutorial.protos.StudentOrBuilder可以看到他们实现的接口是一样的,说明他们可以提供相同的功能。Builder其实就是消息的包装器,所有对Student的操作都可以由Builder完成。对于Student中的字段,Student类只有这些字段的get方法,而Builder类有get和set两种方法。对于Student来说,对于字段的方法有://requiredstringname=1;publicbooleanhasName();publicStringgetName();//requiredint32id=2;publicbooleanhasId();publicintgetId();//可选字符串email=3;publicbooleanhasEmail();publicStringgetEmail();//repeated.tutorial.Person.PhoneNumberphones=4;publicListgetPhonesList();publicintgetPhonesCount();publicPhoneNumbergetPhones(intindex);对Builder来说,每个属性多了两个方法://requiredstringname=1;publicbooleanhasName();publicjava.lang.StringgetName();publicBuildersetName(Stringvalue);publicBuilderclearName();//requiredint32id=2;publicbooleanhasId();publicintgetId();publicBuildersetId(intvalue);publicBuilderclearId();//可选字符串email=3;publicbooleanhasEmail();publicStringgetEmail();publicBuildersetEmail(Stringvalue);publicBuilderclearEmail();//repeated.tutorial.Person.PhoneNumberphones=4;publicListgetPhonesList();publicintgetPhonesCount();publicPhoneNumbergetPhones(intindex);publicBuildersetPhones(intindex,PhoneNumbervalue);publicBuilderaddPhones(PhoneNumbervalue);publicBuilderaddAllPhones(Iterablevalue);publicBuilderclearPhones();两个额外的方法是set和clear方法。clear是清除字段内容,恢复到初始状态。我们还定义了一个枚举类PhoneType:publicenumPhoneTypeimplementscom.google.protobuf.ProtocolMessageEnum这个类的实现和普通的枚举类没有太大区别。Builders和Messages上一节中,Message对应的类只有get,有methods,不能改。一旦构造了消息对象,就不能对其进行修改。要构建一条消息,您必须首先构建一个构建器,将您想要设置的任何字段设置为您选择的值,然后调用构建器的build()方法。每次调用Builder方法时,都会返回一个新的Builder。当然返回的Builder和原来的Builder是一样的。返回Builder只是为了方便后续的代码编写。下面的代码是如何创建Student实例的:Studentxiaoming=Student.newBuilder().setId(1234).setName("Xiaoming").setEmail("flydean@163.com").addPhones(Student.PhoneNumber.newBuilder().setNumber("010-1234567").setType(Student.PhoneType.HOME)).build();Student中提供了一些常用的方法,例如isInitialized()来检测是否设置了所有必要的字段。toString()将对象转换为字符串。使用它的Builder也可以调用clear()清除设置状态,调用mergeFrom(Messageother)合并对象。序列化和反序列化生成的对象中提供了序列化和反序列化方法,我们只需要在需要时调用它们:byte[]toByteArray();:序列化消息并返回包含其原始字节字节数组的消息。staticPersonparseFrom(byte[]data);:从给定的字节数组中解析一条消息。voidwriteTo(OutputStreamoutput);:将消息序列化并写入OutputStream.staticPersonparseFrom(InputStreaminput);:从消息InputStream中读取并解析消息。通过使用上面的方法,可以很方便的对对象进行序列化和反序列化。Protocolextension我们定义了proto之后,如果以后要修改它,我们希望新的protocol能够兼容历史数据。那么我们需要考虑以下几点:现有字段的ID号不能更改。无法添加和删除必填字段。可以删除可选或重复字段。可以添加新的可选或重复字段,但您必须使用新的ID号。综上,protocolbuf的基本用法就介绍到这里。在下一篇文章中,我们将更详细地介绍proto协议的具体内容,敬请期待。本文示例可参考:learn-java-base-9-to-20本文已收录于http://www.flydean.com/01-protocolbuf-guide/最通俗的解读,最深刻的干货货,最简洁的教程,很多你不知道的小技巧等你来发现!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!