本文是博主看到的一篇比较好博文,语言通俗易懂,远离那些不接地气的官方话语。
html
一个阳光明媚的早晨,老婆又在翻看我订阅的技术杂志。java
“老公,什么是RPC呀,为何大家程序员那么多黑话!”,老婆仍是一如既往的好奇。
“RPC,就是Remote Procedure Call的简称呀,翻译成中文就是远程过程调用嘛”,我一边看着书,一边漫不经心的回答着。
“啥?你在说啥?谁不知道翻译成中文是什么意思?你个废柴,快给我滚去洗碗!”
“我去。。。”,我如梦初醒,我对面坐着的可不是一个程序员,为了避免去洗碗,我瞬间调动起所有脑细胞,星辰大海在我脑中汇聚,灵感涌现......git
"是这样,远程过程调用,天然是相对于本地过程调用来讲的嘛。"
“嗯哼,那先给老娘讲讲,本地过程调用是啥子?”
“本地过程调用,就比如你如今在家里,你要想洗碗,那你直接把碗放进洗碗机,打开洗碗机开关就能够洗了。这就叫本地过程调用。”程序员
“哎呦,我可不干,那啥是远程过程调用?”
“远程嘛,那就是你如今不在家,跟姐妹们浪去了,忽然发现碗还没洗,打了个电话过来,叫我去洗碗,这就是远程过程调用啦”,多么通俗易懂的解释,我真是天才!github
“哦!我明白了”,说着,老婆开始收拾包包。
“你这是干啥去哦”
“我?我要出门浪去呀,待会记得接收个人远程调用哦,哦不,我们要专业点,应该说,待会记得接收个人RPC哦!”
......apache
提及RPC,就不能不提到分布式,这个促使RPC诞生的领域。编程
假设你有一个计算器接口,Calculator,以及它的实现类CalculatorImpl,那么在系统仍是单体应用时,你要调用Calculator的add方法来执行一个加运算,直接new一个CalculatorImpl,而后调用add方法就好了,这其实就是很是普通的本地函数调用,由于在同一个地址空间,或者说在同一块内存,因此经过方法栈和参数栈就能够实现。缓存
如今,基于高性能和高可靠等因素的考虑,你决定将系统改造为分布式应用,将不少能够共享的功能都单独拎出来,好比上面说到的计算器,你单独把它放到一个服务里头,让别的服务去调用它。restful
这下问题来了,服务A里头并无CalculatorImpl这个类,那它要怎样调用服务B的CalculatorImpl的add方法呢?网络
有同窗会说,能够模仿B/S架构的调用方式呀,在B服务暴露一个Restful接口,而后A服务经过调用这个Restful接口来间接调用CalculatorImpl的add方法。
很好,这已经很接近RPC了,不过若是是这样,那每次调用时,是否是都须要写一串发起http请求的代码呢?好比httpClient.sendRequest...之类的,能不能像本地调用同样,去发起远程调用,让使用者感知不到远程调用的过程呢,像这样:
@Reference private Calculator calculator; ... calculator.add(1,2); ...
这时候,有同窗就会说,用代理模式呀!并且最好是结合Spring IoC一块儿使用,经过Spring注入calculator对象,注入时,若是扫描到对象加了@Reference注解,那么就给它生成一个代理对象,将这个代理对象放进容器中。而这个代理对象的内部,就是经过httpClient来实现RPC远程过程调用的。
可能上面这段描述比较抽象,不过这就是不少RPC框架要解决的问题和解决的思路,好比阿里的Dubbo。
总结一下,RPC要解决的两个问题:
实际状况下,RPC不多用到http协议来进行数据传输,毕竟我只是想传输一下数据而已,何须动用到一个文本传输的应用层协议呢,我为何不直接使用二进制传输?好比直接用Java的Socket协议进行传输?
无论你用何种协议进行数据传输,一个完整的RPC过程,均可以用下面这张图来描述:
以左边的Client端为例,Application就是rpc的调用方,Client Stub就是咱们上面说到的代理对象,也就是那个看起来像是Calculator的实现类,其实内部是经过rpc方式来进行远程调用的代理对象,至于Client Run-time Library,则是实现远程调用的工具包,好比jdk的Socket,最后经过底层网络实现实现数据的传输。
这个过程当中最重要的就是序列化和反序列化了,由于数据传输的数据包必须是二进制的,你直接丢一个Java对象过去,人家可不认识,你必须把Java对象序列化为二进制格式,传给Server端,Server端接收到以后,再反序列化为Java对象。
下一次我也将经过代码,给你们演示一下,如何实现一个简单的RPC。
其实这二者并非一个维度的概念,总得来讲RPC涉及的维度更广。
若是硬要比较,那么能够从RPC风格的url和Restful风格的url上进行比较。
好比你提供一个查询订单的接口,用RPC风格,你可能会这样写:
/queryOrder?orderId=123
用Restful风格呢?
Get /order?orderId=123
RPC是面向过程,Restful是面向资源,而且使用了Http动词。从这个维度上看,Restful风格的url在表述的精简性、可读性上都要更好。
严格来讲这二者也不是一个维度的。
RMI是Java提供的一种访问远程对象的协议,是已经实现好了的,能够直接用了。
而RPC呢?人家只是一种编程模型,并无规定你具体要怎样实现,你甚至均可以在你的RPC框架里面使用RMI来实现数据的传输,好比Dubbo:Dubbo - rmi协议
要实现一个RPC不算难,难的是实现一个高性能高可靠的RPC框架。
好比,既然是分布式了,那么一个服务可能有多个实例,你在调用时,要如何获取这些实例的地址呢?
这时候就须要一个服务注册中心,好比在Dubbo里头,就可使用Zookeeper做为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用。
那么选哪一个调用好呢?这时候就须要负载均衡了,因而你又得考虑如何实现复杂均衡,好比Dubbo就提供了好几种负载均衡策略。
这还没完,总不能每次调用时都去注册中心查询实例列表吧,这样效率多低呀,因而又有了缓存,有了缓存,就要考虑缓存的更新问题,blablabla......
你觉得就这样结束了,没呢,还有这些:
如此种种,都是一个优秀的RPC框架须要考虑的问题。
固然,接下来咱们仍是先实现一个简单的RPC,再在上面一步步优化!
正如上一讲所说,RPC主要是为了解决的两个问题:
仍是以计算器Calculator为例,若是实现类CalculatorImpl是放在本地的,那么直接调用便可:
如今系统变成分布式了,CalculatorImpl和调用方不在同一个地址空间,那么就必需要进行远程过程调用:
那么如何实现远程过程调用,也就是RPC呢,一个完整的RPC流程,能够用下面这张图来描述:
其中左边的Client,对应的就是前面的Service A,而右边的Server,对应的则是Service B。
下面一步一步详细解释一下。
理论的讲完了,是时候把理论变成实践了。
本文的示例代码,可到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的原理,要是想放到生产环境去用,那是绝对不行的。
一、缺少通用性
我经过给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/2accc2840a1b