深入Protobuf源码-Descriptor、Message、RPC框架

Descriptor框架

对非optimize_for为LITE_RUNTIME的proto文件,protobuf编译器会在编译出的Java代码文件末尾添加一个FileDescriptor静态字段以描述该proto文件定义时的所有元数据信息、为每个message对象定义一个Descriptor静态字段以描述该message定义时的元数据信息、为每个message对象定义一个FieldAccessorTable静态字段用于使用反射读取/设置某个字段的值等(以提供GeneratedMessage中方法的反射实现):    

private static Descriptor inter-nal_static_levin_protobuf_Result_descriptor;
private static FieldAccessorTable inter-nal_static_levin_protobuf_Result_fieldAccessorTable;
private static Descriptor inter-nal_static_levin_protobuf_SearchResponse_descriptor;
private static FieldAccessorTable inter-nal_static_levin_protobuf_SearchResponse_fieldAccessorTable;
private static FileDescriptor descriptor;

在protobuf中存在多种类型的元数据描述类:

1.     FileDescriptor:对一个proto文件的描述,它包含文件名、包名、选项(如java_package、java_outer_classname等)、文件中定义的所有message、文件中定义的所有enum、文件中定义的所有service、文件中所有定义的extension、文件中定义的所有依赖文件(import)等。在FileDescriptor中还存在一个DescriptorPool实例,它保存了所有的dependencies(依赖文件的FileDescriptor)、name到GenericDescriptor的映射、字段到FieldDescriptor的映射、枚举项到EnumValueDescriptor的映射,从而可以从该DescriptorPool中查找相关的信息,因而可以通过名字从FileDescriptor中查找Message、Enum、Service、Extensions等。

2.   Descriptor:对一个message定义的描述,它包含该message定义的名字、所有字段、内嵌message、内嵌enum、关联的FileDescriptor等。可以使用字段名或字段号查找FieldDescriptor。

3.   FieldDescriptor:对一个字段或扩展字段定义的描述,它包含字段名、字段号、字段类型、字段定义(required/optional/repeated/packed)、默认值、是否是扩展字段以及和它关联的Descriptor/FileDescriptor等。

4.   EnumDescriptor:对一个enum定义的描述,它包含enum名、全名、和它关联的FileDescriptor。可以使用枚举项或枚举值查找EnumValueDescriptor。

5.   EnumValueDescriptor:对一个枚举项定义的描述,它包含枚举名、枚举值、关联的EnumDescriptor/FileDescriptor等。

6.   ServiceDescriptor:对一个service定义的描述,它包含service名、全名、关联的FileDescriptor等。

7.   MethodDescriptor:对一个在service中的method的描述,它包含method名、全名、参数类型、返回类型、关联的FileDescriptor/ServiceDescriptor等。

最后,protobuf编译生成的代码末尾还有一个descriptorData字符串数组,它是序列化后的FileDescriptorProto数据,在静态初始化块中可以调用FileDescriptor.internalBuildGeneratedFileFrom()方法构造整个FileDescriptor实例,在完成FileDescriptor的构造后,还会回调传入的InternalDescriptorAssigner实例以初始化其他的静态字段,如以上提到的所有的静态字段。

在protobuf中Descriptor的类图:

Message、MessageLite框架

序列化和反序列化是protobuf最基础的框架,它使用MessageLite/Message接口来抽象一个可序列化的实例,并且使用Builder从字节数组或输入字节流中构建MessageLite/Message实例,MessageLite和Message内部都定义了自己的Builder类,他们个字继承自MessageLiteOrBuilder以及MessageOrBuiler,它们定义了MessageLite/Message和它们各自Builder类的共同接口。

MessageLiteOrBuilder接口只定义了MessageLite和MessageLite.Builder两个接口共有的两个方法:getDefaultInstanceForType()方法获取一个当前还未初始化的当前Message实例(没有字段被赋值,因而所有字段返回默认值,对repeat字段返回空,在当前protobuf 2.5.0的实现中,它返回的是一个单例,和每个生成的静态方法getDefaultInstance()返回相同的实例);isInitialized()方法用来判断是否所有required字段已经被赋值。MessageLite接口中定义了两个writeTo()方法分别将当前实例序列化并写入输出字节流中,而另一个writeDelimitedTo()方法则在写入之前将当前实例的总长度写入输出字节流中(以可变长32位Int编码方式),从而可以同时向一个输出字节流中写入多个Message实例;MessageLite中还定义了获取当前MessageLite在序列化成字节流后的总字节数的方法getSerializedSize(),两个直接返回字节数组的toByteArray()/toByteString()方法,以及获取它的Parser实例(getParserForType())和返回它的Builder实例(toBuilder()-创建一个新的Builder实例/newBuilderForType()-用当前MessageLite类初始化一个新的Builder实例并返回)方法。其中Builder接口用于从字节流或字节数组中解析并构造MessageLite对象(各种版本的mergeFrom()方法,如果发送端写入了MessageLite字节长度,则使用mergeDelimitedFrom()方法),最后Builder使用build()方法构造MessageLite对象,此时如果有required字段还未被设置,会抛出UninitializedMessageException,为了避免抛出异常,可以使用buildPartial()方法;另外Builder还定义了clone()和clear()方法;在生成的每个Message对象中都定义了一个newBuilder()静态方法,一般使用该静态方法初始化一个Builder实例。Parser接口也定义了各个版本的parseFrom()/parsePartialFrom()/parseDelimitedFrom()/parsePartialDelimitedFrom()方法用来从字节数组或字节流中解析出Message实例,在生成的代码中,Builder的实现直接调用Parser实现类中的方法。

在大部分情况下,MessageLite已经能完成所有的序列化和反序列化操作了,特别是一些资源有限额手持设备,它如果运行整个protobuf库会显得太耗资源;可以在.proto文件中加入一下指令来告诉protobuf编译器只需要生成实现MessageLite的类:

option optimize_for = LITE_RUNTIME

然而对一般的Server程序来说,我们并不在乎这点资源的损耗,因而会选择实现Message接口,它相比MessageLite,添加了Descriptors相关的支持,即支持使用FieldDescriptor来构建Message.Builder实例并最终构建Message实例。

MessageOrBuilder接口继承自MessageLiteOrBuilder接口,它定义了Message和Message.Builder共有的接口,即添加了Descriptor、FieldDescriptor等相关的扩展。由于实现Message和Message.Builder接口的类保存了所有Message定义时具有的信息(文件名、包名、字段列表等,使用各种Descriptor类来抽象),因而我们可以使用Message/Message.Builder类获取到更多的信息,如一个Message/Message.Builder没有赋值所有required的字段,可以使用findInitializationErrors()方法来获取所有未赋值的字段列表(字段的全路径名,getInitializationErrorString()是这个列表的字符串形式表达,为了提升性能,建议使用isInitialized()方法先做初步判断,因为它更快);另外在MessageOrBuilder中还定义了当前Message对应的Descriptor实例:getDescriptorForType()方法,获取所有已经赋值的FieldDescriptor到其值的一个Map:getAllFields(),通过FieldDescriptor取得其值:getField(),判断一个字段是否已经被赋值:hasField(),获取repeated字段的count:getRepeatedFieldCount(),通过FieldDescriptor以及index获取repeated字段在index处的值:getRepeatedField(),获取未知的字段:getUnknownFields()。Message接口除了继承自MessageOrBuilder接口的方法,并没有定义多余的方法,只是添加了equals、hashCode、toString方法的定义。而Message.Builder接口除了继承自MessageOrBuilder接口以外,它还定义了基于FieldDescriptor的方法,如通过FieldDescriptor创建/获取Builder实例:newBuilderForFileld()/getFieldBuilder(),通过FieldDescriptor设置/清除字段的值:setField()/clearField()/setRepeatedField()/addRepeatedField(),以及设置UnknownFields:setUnknownFields()/mergeUnknownFields()。

 

MessageLite/Message类图如下:

RPC框架

除了序列化框架,protobuf还定义了一套简单的RPC框架。之所以说简单是因为它定义的Service层接口的协议,而没有具体和传输相关的实现,而只是将传输相关的逻辑抽象成RpcChannel和BlockingRpcChannel分别用于表示同步和一步方式的Service方法调用,而至于底层用什么样的协议和框架,由用户自己决定并实现。

所谓RPC框架,从用户角度上最基本的就是定义客户端和服务器端的协议,即服务器端暴露出什么样的接口供客户端调用,这个接口定义了服务器在一个Host的某个(些)端口上接收某些请求数据,并期望能返回的响应。其中服务器和端口号属于传输实现的范畴,protobuf只是用RpcChannel/BlockingRpcChannel的概念做了抽象,而没有给出具体实现;而接收某个请求数据以及期待的响应数据,在protobuf使用Service/BlockingService抽象来定义,并且这也是protobuf中RPC框架的定义部分,其中Service和RpcChannel共同构成异步方式的RPC框架,而BlockingService和BlockingRpcChannel共同构成了同步(阻塞)方式的RPC框架。

从底层实现的角度,一个RPC调用就是客户端发送一些请求数据给服务器,服务器解析并处理这些请求数据,然后将响应数据返回给客户端。为了隐藏内部实现细节,提升写代码的效率,RPC将这一过程封装成方法调用,即不同的请求用不同的方法表达,这就是protobuf中RPC的定义。在protobuf中,定义一个PRC接口比较简单:首先开启RPC功能,然后用service关键字定义一个接口,在接口中使用rpc关键字定义一个方法,方法包含方法名、方法参数、返回值,其中方法参数和返回值都必须是一个message类型,并且只能有一个:

option java_generic_services = true;

service MyService {
    rpc request(SearchRequest) returns(SearchResponse);
}

在protobuf编译生成的代码中,它会生成一个MyService抽象类实现了Service接口,一般它只是作为一个命名空间,它内部定义了两个接口:Interface和BlockingInterface本别继承自Service接口和BlockingService接口,用于抽象异步和同步方式的RPC方法调用;这两个接口有两个实现类:Stub和BlockingStub,他们分别接收RpcChannel和BlockingRpcChannel实例作为构造函数参数,可以使用MyService中的静态方法newStub()和newBlockingStub()方法获取他们各自实例,他们主要用于客户端的调用。在生成的request方法中,除了request本身的参数,还有一个RpcController参数,它用于处理在RpcChannel/BlockingRpcChannel调用中的状态处理,如错误处理等,使用它可以获知此次调用是否出错,错误信息是什么等。在MyService中还定义了两个静态方法newReflectiveService/newReflectiveBlockingService,他们接收Interface/BlockingInterface实例,并返回Service/BlockingService的实现实例(暂时还没有想到使用他们的场景)。

在MyService的RPC框架实现中,在服务器端,实现MyService.Interface/MyService.BlockingInterface接口,然后将它注册到对RpcChannel/BlockingRpcChannel框架的实现中;在客户端则创建一个RpcChannel/BlockingRpcChannel实例,传入MyService.newStub()/MyService.newBlockingStub()方法获取对应的实例,然后使用这个Stub/BlockingStub实例调用相应的方法即可。

时间: 2024-10-01 14:09:35

深入Protobuf源码-Descriptor、Message、RPC框架的相关文章

Alluxio源码分析:RPC框架浅析(三)

        Alluxio源码分析是一个基于内存的分布式文件系统,和HDFS.HBase等一样,也是由主从节点构成的.而节点之间的通信,一般都是采用的RPC通讯模型.Alluxio中RPC是基于何种技术如何实现的呢?它对于RPC请求是如何处理的?都涉及到哪些组件?本文将针对这些问题,为您一一解答.         继<Alluxio源码分析:RPC框架浅析(二)>一文后,本文继续讲解Alluxio中RPC实现.         4.Client端实现        以FileSystemM

Alluxio源码分析:RPC框架浅析(二)

        Alluxio源码分析是一个基于内存的分布式文件系统,和HDFS.HBase等一样,也是由主从节点构成的.而节点之间的通信,一般都是采用的RPC通讯模型.Alluxio中RPC是基于何种技术如何实现的呢?它对于RPC请求是如何处理的?都涉及到哪些组件?本文将针对这些问题,为您一一解答.         继<Alluxio源码分析:RPC框架浅析(一)>一文后,本文继续讲解Alluxio中RPC实现.         3.Server端实现:RPC Server端口绑定.传输协议

Alluxio源码分析:RPC框架浅析(一)

        Alluxio源码分析是一个基于内存的分布式文件系统,和HDFS.HBase等一样,也是由主从节点构成的.而节点之间的通信,一般都是采用的RPC通讯模型.Alluxio中RPC是基于何种技术如何实现的呢?它对于RPC请求是如何处理的?都涉及到哪些组件?本文将针对这些问题,为您一一解答.         一.Alluxio中RPC实现技术支持         Alluxio中的RPC是依靠Thrift实现的,Apache Thrift 是 Facebook 实现的一种高效的.支持多

Hadoop2源码分析-RPC探索实战

1.概述 在<Hadoop2源码分析-RPC机制初识>博客中,我们对RPC机制有了初步的认识和了解,下面我们对Hadoop V2的RPC机制做进一步探索,在研究Hadoop V2的RPC机制,我们需要掌握相关的Java基础知识,如:Java NIO.动态代理与反射等.本篇博客介绍的内容目录如下所示: Java NIO简述 Java NIO实例演示 动态代理与反射简述 动态代理与反射实例演示 Hadoop V2 RPC框架使用实例 下面开始今天的博客介绍. 2.Java NIO简述 Java N

Hadoop源码学习:RPC

Hadoop源码学习:RPC Hadoop RPC使用java NIO编写,达到高性能,轻量级,可控性. 主要分为四层:序列化层,函数调用层,网络传输层,服务器端处理框架 序列化层:实现Writable接口 函数调用层:java反射机制和动态代理实现函数调用 网络传输层:使用Socket机制 服务器端处理框架:基于Reactor设计模式的事件驱动I/O模型 如何使用Hadoop RPC: RPC Server: 1.定义一个协议,实现VersionedProtocol接口, public int

Hadoop2源码分析-RPC机制初识

1.概述 上一篇博客,讲述Hadoop V2的序列化机制,这为我们学习Hadoop V2的RPC机制奠定了基础.RPC的内容涵盖的信息有点多,包含Hadoop的序列化机制,RPC,代理,NIO等.若对Hadoop序列化不了解的同学,可以参考<Hadoop2源码分析-序列化篇>.今天这篇博客为大家介绍的内容目录如下: RPC概述 第三方RPC Hadoop V2的RPC简述 那么,下面开始今天的学习之路. 2.RPC概述 首先,我们要弄明白,什么是RPC?RPC能用来做什么? 2.1什么是RPC

深入Protobuf源码-概述、使用以及代码生成实现

概述 捣鼓hdfs.yarn.hbase.zookeeper的代码一年多了,是时候整理一下了.在hadoop (2.5.2)中protobuf是节点之间以及客户端和各个节点通信的基础序列化框架(协议),而基于avro和Writable的序列化框架则是这个协议里的payload,因而这一系列的文章打算从protobuf这个框架开始入手(版本2.5.0). 从抽象的角度来说,protobuf框架是类实例序列化和远程调用的一种实现.所谓实例序列化简单来说就是将一个类实例转换成字节数组或字节流来表达,这

深入Protobuf源码-编码实现

基本类型编码 在前文有提到消息是一系列的基本类型以及其他消息类型的组合,因而基本类型是probobuf编码实现的基础,这些基本类型有: .proto Type Java Type C++ Type Wire Type double double double WIRETYPE_FIXED64(1) float float float WIRETYPE_FIXED32(5) int64 long int64 WIRETYPE_VARINT(0) int32 int int32 WIRETYPE_V

Hadoop2源码分析-YARN RPC 示例介绍

1.概述 之前在<Hadoop2源码分析-RPC探索实战>一文当中介绍了Hadoop的RPC机制,今天给大家分享关于YARN的RPC的机制.下面是今天的分享目录: YARN的RPC介绍 YARN的RPC示例 截图预览 下面开始今天的内容分享. 2.YARN的RPC介绍 我们知道在Hadoop的RPC当中,其主要由RPC,Client及Server这三个大类组成,分别实现对外提供编程接口.客户端实现及服务端实现.如下图所示:     图中是Hadoop的RPC的一个类的关系图,大家可以到<