1.什么是RPC框架?
现代后端系统需要向上提供众多API,为了简化系统、去耦合,提出了分布式应用。然而,分布式应用的各个子系统并不是完全独立的,它们需要进行相互通信。为了实现系统间通信,我们有两种手段:
RPCMessageQueue当我们讨论RPC时,通常会强调其如下几个特性:
隐藏网络通信过程(或者将其称为良好的封装性):RPC需要将方法调用对应的消息包从一个主机传递到另一个主机,因此涉及到了网络通信过程。然而,RPC暴露的接口需要完全隐藏网络传输过程,即在调用者看来,RPC调用与普通方法调用没有区别。同步性:在面向对象语言中,方法调用栈通常时同步入栈出栈的,即直到方法通过return返回指向结果,方法才会出栈。虽然没有任何标准要求RPC一定要实现同步性,但最常见的RPC就是同步调用的;RPC与REST的区别?
RPC与REST最大的区别就在于RPC提供了更好的抽象,RPC甚至将网络传输细节彻底隐藏了,而REST没有。具体来说,REST至少要求用于提供URL以及请求参数,而RPC隐藏了与网络传输的相关实现细节。另一方面,RPC可以基于任何网络通信协议,而REST通常基于HTTP(或者HTTPS)协议。RPC调用者并不会关心具体的协议是:HTTP、TCP还是其他任何自定义协议。
RPC内部结构如下图所示:
2.RPC框架的组件设计
RPC框架可以很复杂,例如ApacheDubbo,其提供了非常多的功能,不过RPC框架的核心为如下几个组件:
代理层:RPC接口向上暴露一个普通的方法调用,但是其内部却隐藏了复杂的序列化、协议解析、网络传输等过程。代理层就可以完成这样一件事:隐藏复杂实现细节,对外暴露一个简单的API;序列化层:RPC中通过网络来完成消息传递,例如调用具体哪个接口的哪个方法,方法入口参数是什么。我们无法直接传输面向对象世界中的类与对象,因此需要序列化层将它们转换为字节数据来进行传输;协议层:协议层的存在是基于TCP层开发自定义通信协议的内在要求。由于TCP协议不一定按照应用层的数据分包来传递,存在所谓的TCP粘包、“TCP拆包”现象,而自定义协议的目的就是为了解决TCP的这些问题;总之,协议层主要为TCP传输的数据包提供元数据;服务注册层:当分布式应用的主机数达到一定规模后,主机的下线以及上线将非常频繁。服务注册层能够很好地起到集群元数据信息中转站的作用,当客户端通过代理层进行RPC调用时,能够通过读取服务注册层的注册信息来得知哪个服务器能够提供此RPC调用对应的服务;网络传输层:网络传输层对应于第一节RPC内部结构图的sockets层,其负责网络数据的传输,其需要负责TCP连接的管理、TCP数据包的编解码等工作;下面将按照框架各个层进行展开说明。
2.1代理层
代理层的实现策略有多种方式,例如Dubbo基于Spring容器在加载Bean时为接口实例织入网络传输RPC调用请求包的逻辑。当然,不借助于Spring容器,我们也能够实现代理逻辑,例如借助于CGLIB、Javassist等代理框架实现动态代理。
本项目基于CGLIB实现动态代理逻辑,代理层完成如下逻辑:
将接口的方法调用抽象为:接口完全限定名、方法名、方法入口参数类型、方法实际的入口参数,并将这些数据封装为RpcRequest实例;将RpcRequest通过NettyRpcClient进行网络传输;2.2序列化层
Java的序列化框架有非常多的选择,甚至你可以使用自定义的序列化框架。良好的RPC框架对序列化框架需要考虑非常多的因素,例如:
序列化效率;序列化协议的向前以及向后兼容性;跨语言性:各个语言的方法栈不同,如果分布式集群涉及由不同语言写成的组件,那么跨语言将是非常重要的一个特性;出于简单易用的目的,本项目选择Kryo,而没有详细考虑上述因素。
关于Kryo的序列化实现,不妨直接查看本项目cool.spongecaptain.serialize.kyro包下的KryoSerialization类的具体实现。
2.3协议层
协议层是必要的,试想这样一个问题:客户端连续发送了两个RpcRequest消息,而且在接收端同时接收到了这两个RpcRequest实例对应的序列化后的字节数据,那么:接收端如何将该字节数组数据解析为两个RpcRequest实例呢?如果没有协议层,接收端将无法完成此任务。当然,对于服务端向客户端回复的响应RpcReponse实例也有着完全一样的道理。
关于协议层起到的作用,我们可以用下图来表示(以RpcRequest为例):
metaData是TCP数据包的元数据,本项目中设计为由如下字段组成:
MagicNumber:魔数,用于说明此TCP数据包为RPC请求,用于向消息接收方说明消息类型;versionNumber:协议版本号;serializaitonID:序列化号,用于支持序列化协议扩展;