RPC的实现原理

RPC的实现原理

正如上一讲所说,RPC主要是为了解决的两个问题:html

  • 解决分布式系统中,服务之间的调用问题。
  • 远程调用时,要可以像本地调用同样方便,让调用者感知不到远程调用的逻辑。

仍是以计算器Calculator为例,若是实现类CalculatorImpl是放在本地的,那么直接调用便可:
java

 
 

 

如今系统变成分布式了,CalculatorImpl和调用方不在同一个地址空间,那么就必需要进行远程过程调用:
git

 
 

 

那么如何实现远程过程调用,也就是RPC呢,一个完整的RPC流程,能够用下面这张图来描述:
github

 
 

 

其中左边的Client,对应的就是前面的Service A,而右边的Server,对应的则是Service B。
下面一步一步详细解释一下。apache

  1. Service A的应用层代码中,调用了Calculator的一个实现类的add方法,但愿执行一个加法运算;
  2. 这个Calculator实现类,内部并非直接实现计算器的加减乘除逻辑,而是经过远程调用Service B的RPC接口,来获取运算结果,所以称之为Stub
  3. Stub怎么和Service B创建远程通信呢?这时候就要用到远程通信工具了,也就是图中的Run-time Library,这个工具将帮你实现远程通信的功能,好比Java的Socket,就是这样一个库,固然,你也能够用基于Http协议的HttpClient,或者其余通信工具类,均可以,RPC并无规定说你要用何种协议进行通信
  4. Stub经过调用通信工具提供的方法,和Service B创建起了通信,而后将请求数据发给Service B。须要注意的是,因为底层的网络通信是基于二进制格式的,所以这里Stub传给通信工具类的数据也必须是二进制,好比calculator.add(1,2),你必须把参数值1和2放到一个Request对象里头(这个Request对象固然不仅这些信息,还包括要调用哪一个服务的哪一个RPC接口等其余信息),而后序列化为二进制,再传给通信工具类,这一点也将在下面的代码实现中体现;
  5. 二进制的数据传到Service B这一边了,Service B固然也有本身的通信工具,经过这个通信工具接收二进制的请求;
  6. 既然数据是二进制的,那么天然要进行反序列化了,将二进制的数据反序列化为请求对象,而后将这个请求对象交给Service B的Stub处理;
  7. 和以前的Service A的Stub同样,这里的Stub也一样是个“假玩意”,它所负责的,只是去解析请求对象,知道调用方要调的是哪一个RPC接口,传进来的参数又是什么,而后再把这些参数传给对应的RPC接口,也就是Calculator的实际实现类去执行。很明显,若是是Java,那这里确定用到了反射
  8. RPC接口执行完毕,返回执行结果,如今轮到Service B要把数据发给Service A了,怎么发?同样的道理,同样的流程,只是如今Service B变成了Client,Service A变成了Server而已:Service B反序列化执行结果->传输给Service A->Service A反序列化执行结果 -> 将结果返回给Application,完毕。

理论的讲完了,是时候把理论变成实践了。缓存

把理论变成实践

本文的示例代码,可到Github下载。网络

首先是Client端的应用层怎么发起RPC,ComsumerApp:负载均衡

public class ComsumerApp { public static void main(String[] args) { Calculator calculator = new CalculatorRemoteImpl(); int result = calculator.add(1, 2); } } 

经过一个CalculatorRemoteImpl,咱们把RPC的逻辑封装进去了,客户端调用时感知不到远程调用的麻烦。下面再来看看CalculatorRemoteImpl,代码有些多,可是其实就是把上面的二、三、4几个步骤用代码实现了而已,CalculatorRemoteImpl:框架

public class CalculatorRemoteImpl implements Calculator { public int add(int a, int b) { List<String> addressList = lookupProviders("Calculator.add"); String address = chooseTarget(addressList); try { Socket socket = new Socket(address, PORT); // 将请求序列化 CalculateRpcRequest calculateRpcRequest = generateRequest(a, b); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); // 将请求发给服务提供方 objectOutputStream.writeObject(calculateRpcRequest); // 将响应体反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object response = objectInputStream.readObject(); if (response instanceof Integer) { return (Integer) response; } else { throw new InternalError(); } } catch (Exception e) { log.error("fail", e); throw new InternalError(); } } } 

add方法的前面两行,lookupProviders和chooseTarget,可能你们会以为不明觉厉。异步

分布式应用下,一个服务可能有多个实例,好比Service B,可能有ip地址为198.168.1.11和198.168.1.13两个实例,lookupProviders,其实就是在寻找要调用的服务的实例列表。在分布式应用下,一般会有一个服务注册中心,来提供查询实例列表的功能。

查到实例列表以后要调用哪个实例呢,只时候就须要chooseTarget了,其实内部就是一个负载均衡策略。

因为咱们这里只是想实现一个简单的RPC,因此暂时不考虑服务注册中心和负载均衡,所以代码里写死了返回ip地址为127.0.0.1。

代码继续往下走,咱们这里用到了Socket来进行远程通信,同时利用ObjectOutputStream的writeObject和ObjectInputStream的readObject,来实现序列化和反序列化。

最后再来看看Server端的实现,和Client端很是相似,ProviderApp:

public class ProviderApp { private Calculator calculator = new CalculatorImpl(); public static void main(String[] args) throws IOException { new ProviderApp().run(); } private void run() throws IOException { ServerSocket listener = new ServerSocket(9090); try { while (true) { Socket socket = listener.accept(); try { // 将请求反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); Object object = objectInputStream.readObject(); log.info("request is {}", object); // 调用服务 int result = 0; if (object instanceof CalculateRpcRequest) { CalculateRpcRequest calculateRpcRequest = (CalculateRpcRequest) object; if ("add".equals(calculateRpcRequest.getMethod())) { result = calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB()); } else { throw new UnsupportedOperationException(); } } // 返回结果 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(new Integer(result)); } catch (Exception e) { log.error("fail", e); } finally { socket.close(); } } } finally { listener.close(); } } } 

Server端主要是经过ServerSocket的accept方法,来接收Client端的请求,接着就是反序列化请求->执行->序列化执行结果,最后将二进制格式的执行结果返回给Client。

就这样咱们实现了一个简陋而又详细的RPC。
说它简陋,是由于这个实现确实比较挫,在下一小节会说它为何挫。
说它详细,是由于它一步一步的演示了一个RPC的执行流程,方便你们了解RPC的内部机制。

为何说这个RPC实现很挫

这个RPC实现只是为了给你们演示一下RPC的原理,要是想放到生产环境去用,那是绝对不行的。

一、缺少通用性
我经过给Calculator接口写了一个CalculatorRemoteImpl,来实现计算器的远程调用,下一次要是有别的接口须要远程调用,是否是又得再写对应的远程调用实现类?这确定是很不方便的。

那该如何解决呢?先来看看使用Dubbo时是如何实现RPC调用的:

@Reference private Calculator calculator; ... calculator.add(1,2); ... 

Dubbo经过和Spring的集成,在Spring容器初始化的时候,若是扫描到对象加了@Reference注解,那么就给这个对象生成一个代理对象,这个代理对象会负责远程通信,而后将代理对象放进容器中。因此代码运行期用到的calculator就是那个代理对象了。

咱们能够先不和Spring集成,也就是先不采用依赖注入,可是咱们要作到像Dubbo同样,无需本身手动写代理对象,怎么作呢?那天然是要求全部的远程调用都遵循一套模板,把远程调用的信息放到一个RpcRequest对象里面,发给Server端,Server端解析以后就知道你要调用的是哪一个RPC接口、以及入参是什么类型、入参的值又是什么,就像Dubbo的RpcInvocation:

public class RpcInvocation implements Invocation, Serializable { private static final long serialVersionUID = -4355285085441097045L; private String methodName; private Class<?>[] parameterTypes; private Object[] arguments; private Map<String, String> attachments; private transient Invoker<?> invoker; 

二、集成Spring
在实现了代理对象通用化以后,下一步就能够考虑集成Spring的IOC功能了,经过Spring来建立代理对象,这一点就须要对Spring的bean初始化有必定掌握了。

三、长链接or短链接
总不能每次要调用RPC接口时都去开启一个Socket创建链接吧?是否是能够保持若干个长链接,而后每次有rpc请求时,把请求放到任务队列中,而后由线程池去消费执行?只是一个思路,后续能够参考一下Dubbo是如何实现的。

四、 服务端线程池
咱们如今的Server端,是单线程的,每次都要等一个请求处理完,才能去accept另外一个socket的链接,这样性能确定不好,是否是能够经过一个线程池,来实现同时处理多个RPC请求?一样只是一个思路。

五、服务注册中心
正如以前提到的,要调用服务,首先你须要一个服务注册中心,告诉你对方服务都有哪些实例。Dubbo的服务注册中心是能够配置的,官方推荐使用Zookeeper。若是使用Zookeeper的话,要怎样往上面注册实例,又要怎样获取实例,这些都是要实现的。

六、负载均衡
如何从多个实例里挑选一个出来,进行调用,这就要用到负载均衡了。负载均衡的策略确定不仅一种,要怎样把策略作成可配置的?又要如何实现这些策略?一样能够参考Dubbo,Dubbo - 负载均衡

七、结果缓存
每次调用查询接口时都要真的去Server端查询吗?是否是要考虑一下支持缓存?

八、多版本控制
服务端接口修改了,旧的接口怎么办?

九、异步调用
客户端调用完接口以后,不想等待服务端返回,想去干点别的事,能够支持不?

十、优雅停机
服务端要停机了,还没处理完的请求,怎么办?

......

诸如此类的优化点还有不少,这也是为何实现一个高性能高可用的RPC框架那么难的缘由。

固然,咱们如今已经有不少很不错的RPC框架能够参考了,咱们彻底能够借鉴一下前人的智慧。

后面若是有(dian)机(zan)会(duo)的话,也将和你们分享一下如何一步一步优化现有的这块RPC代码,把它作成一个小型RPC框架!

做者:柳树之 连接:https://www.jianshu.com/p/5b90a4e70783 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
相关文章
相关标签/搜索