RPC框架实现原理
一、什么是 RPC 框架
RPC,全称为 Remote Procedure Call,即远程过程调用,是一种计算机通信协议。
比如现在有两台机器:A 机器和 B 机器,并且分别部署了应用 A 和应用 B。假设此时位于 A 机器上的 A 应用想要调用位于 B 机器上的 B 应用提供的函数或是方法,由于 A 应用和 B 应用不在一个内存空间里面,所以不能直接调用,此时就需要通过网络来表达调用的方式和传输调用的数据,也即所谓的远程调用。
二、RCP 框架的实现原理
1、建立通信
首先要解决通讯的问题:即 A 机器想要调用 B 机器,首先得建立起通信连接。主要是通过在客户端和服务器之间建立 TCP 连接,远程过程调用的所有相关的数据都在这个连接里面进行传输交换。
通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
2、服务寻址
解决寻址的问题:即 A 机器上的应用 A 要调用 B 机器上的应用 B,那么此时对于 A 来说如何告知底层的 RPC 框架所要调用的服务具体在哪里呢?
通常情况下我们需要提供 B 机器(主机名或 IP 地址)以及 特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。比如基于 WEB 服务协议栈的 RPC,就需要提供一个 endpoint URI,或者是从 UDDI 服务上进行查找。如果是 RMI 调用的话,还需要一个 RMI Registry 来注册服务的地址。
3、网络传输
3.1、序列化
当 A 机器上的应用发起一个 RCP 调用时,调用方法和其入参等信息需要通过底层的网络协议如 TCP 传输到 B 机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给 B 机器。
3.2、反序列化
当 B 机器接收到 A 机器的应用发来的请求之后,有需要对接收到的参数等信息进行反序列化操作(序列化的逆向操作),即将二进制信息恢复为内存的表达方式,然后再找到对用的方法(寻址的一部分)进行本地调用(一般是生成代理 Proxy 去调用,通常会有 JDK 动态代理、Cglib 动态代理,Javassist 生成字节码技术等),之后得到调用的返回值。
4、服务调用
B 机器进行本地调用(通过代理 Proxy)之后得到了返回值,此时还需要再把返回值发送回 A 机器,同样需要经过序列化操作,然后在经过网络传输将二进制数据发送回 A 机器,而当 A 机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后在交给 A 机器上的应用进行相关处理(一般是业务逻辑处理操作)。
通常,经过以上四个步骤之后,一次完整的 RPC 调用算是完成了,另外可能因为网络抖动等原因需要重试等。
三、为什么需要 RCP
主要就是因为在几个进程内(应用分布在不同的机器上),无法共用内存空间,或者在一台机器内通过本地调用无法完成相关的需求,比如不同的系统之间的通讯,甚至不同组织之间的通信。此外由于机器的横向扩展,需要多台机器组成的集群上部署应用等等。
四、RCP 哪些协议
最早的 CORBA、Java RMI,WebService 方式的 RPC 风格,Hessian,Thrift 甚至 Rest API
五、RCP 的实现基础
- 需要有非常高效的网络通信,比如一般选择 Netty 作为网络通信框架;
- 需要有比较高效的序列化框架,比如谷歌的 Protobuf 序列化框架;
- 可靠的寻址方式(主要是提供服务的发现),比如可以使用 Zookeeper、Nacos 来注册服务等;
- 如果是带会话(状态)的 RPC 调用,还需要有会话和状态保持的功能;
六、RCP 调用过程
6.1、一个基本的 RPC 架构里面应该至少包含一下四个组件
- 客户端(client):服务调用方(服务消费放);
- 客户端存根(client stub):存放服务器地址信息,将客户端的请求参数数据信息打包成网络信息,在通过网络传输发送给服务端;
- 服务端存根(Server Stub):接受客户端发送过来的请求消息并进行解包,然后在调用本地服务进行处理。
- 服务端(Server):服务的真正提供者;
6.2、具体的调用过程
- 服务消费者(Client 客户端)通过本地调用的方式调用服务;
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参的信息序列化(组装)成能够进行网络传输的消息体;
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
- 服务端存根(Server Stub)收到消息后进行解码(反序列化操作);
- 服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理;
- 本地服务执行具体业务逻辑并将处理结果返回给服务端存根(Server Stub);
- 服务端存根(Server Stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
- 客户端存根(Client Stub)接收到消息,并进行解码(反序列化);
- 服务消费方得到最终结果;
而 RPC 框架的实现目标则是将上面的第 2-10 步完好的封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉想调用本地服务一样的调用远程服务。
七、RPC 框架需要解决的问题
- 如何确定客户端与服务端之间的通信协议;
- 如何更高效的进行网络通讯;
- 服务端提供的服务如何暴露给客户端;
- 客户端如何发现这些暴露的服务;
- 如何更高效的对请求对象和相应结果进行序列化和反序列化的操作;
八、使用了那些技术
8.1、动态代理
生成 Client Stub(客户端存根)和 Server Stub(服务端存根)的时候需要用到 Java 动态代理技术,可以使用 JDK 提供的原生动态代理机制,也可以使用开源的:Cglib 代理,Javassist 字节码生成技术。
8.2、序列化
在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。
序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。
目前比较高效的开源序列化框架:如 Kryo、fastjson 和 Protobuf 等。
8.3、NIO 通信
出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。
Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。
可以选择 Netty 或者 mina 来解决 NIO 数据传输的问题。
8.4、注册中心
可选:Redis、Zookeeper、Consul、Etcd、Nacos
一般使用 Zookeeper 提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。