前言
在实际后台服务开发中,比如订单服务(开发者A负责)需要调用商品服务(开发者B负责),那么开发者B会和A约定调用API,以接口的形式提供给A。通常都是B把API上传到Maven私服,然后B开始写API的实现,A只需要引入API依赖进行开发即可。
订单服务调用商品服务
上图简单的描述了RPC在实际场景中的应用,我们在开发中当然是利用现有的RPC框架来快速实现业务需求,比如百度开源了baidu-rpc,阿里的Dubbo早已声名在外,腾讯自己玩TAF。本篇博客将实现一个迷你版的RPC,探索下RPC底层实现的奥秘!
动手实现RPC
商品服务工程
商品服务工程
注意,我将商品服务的API以及实现分为Maven的2个模块来开发。这里,我们想给定一个商品ID,查询得到商品对象信息。
商品对象
商品API
Product
要注意的是,Product是可以被序列化的,Why?
很显然,订单系统调用商品系统的时候,需要商品系统返回一个商品,必然涉及到发生网络传输,这就涉及对象的序列化和反序列化了。
商品查询API接口
商品查询API
订单系统调用商品服务
订单服务调用商品服务
在订单系统工程中需要引入商品服务API依赖。
在上图代码中,最重要的就是rpc方法了!
rpc实现方法
rpc
第一,我们看到了Proxy.newProxyInstance,很显然在进行动态代理。也即是说,在订单服务调用商品服务的代码中,我们先是通过动态代理返回一个代理的IProductService类型对象,这意味着当代理对象调用queryById方法的时候,会自动调用invoke方法!
第二,我们看看invoke到底做了些什么?
它本质上就是进行Socket通信,那么它需要传递什么信息给到商品服务呢?
我们知道订单系统就是想调用商品服务的某个类的某个方法,然后把这个方法的返回结果传输给订单系统!
想一想,如何调用某个类的某个方法呢?
只要我们能确定这个类的全限定类名、确定方法名、确定方法的参数类型,给定方法需要的具体参数,通过反射就能实现。
商品服务调用后得到的结果,我们序列化写入Socket流中,在订单系统中反序列化得到对象即可。
第三,这里需要思考一个问题:在订单系统中我们只知道商品服务的API,并不知道这背后的API到底是如何实现的,所以我们需要有一个映射,就是商品服务的API到商品服务的实现的一个映射关系,其实这就是所谓的服务的注册!
商品API的具体实现
商品服务
商品服务API实现
商品服务
商品服务
从这里,可以清晰的看到,商品服务读取了订单系统调用商品系统时发送的数据,利用反射机制,进行方法调用,并把调用结果写入Socket输出流。
运行结果
运行结果
启动商品服务后,通过订单系统发起对商品服务的调用。
以前总认为RPC是遥不可及的,感觉是个很神奇的东西,实际上它的底层实现不就是这样的么~
就这样我们实现了一个简陋而又详细的RPC。
说它简陋,是因为这个实现确实比较挫,在下一小节会说它为什么挫。
说它详细,是因为它一步一步的演示了一个RPC的执行流程,方便大家了解RPC的内部机制。
为什么说这个RPC实现很挫???
这个RPC实现只是为了给大家演示一下RPC的原理,要是想放到生产环境去用,那是绝对不行的。
1、缺乏通用性
我通过给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;
2、集成Spring
在实现了代理对象通用化之后,下一步就可以考虑集成Spring的IOC功能了,通过Spring来创建代理对象,这一点就需要对Spring的bean初始化有一定掌握了。
3、长连接or短连接
总不能每次要调用RPC接口时都去开启一个Socket建立连接吧?是不是可以保持若干个长连接,然后每次有rpc请求时,把请求放到任务队列中,然后由线程池去消费执行?只是一个思路,后续可以参考一下Dubbo是如何实现的。
4、 服务端线程池
我们现在的Server端,是单线程的,每次都要等一个请求处理完,才能去accept另一个socket的连接,这样性能肯定很差,是不是可以通过一个线程池,来实现同时处理多个RPC请求?同样只是一个思路。
5、服务注册中心
正如之前提到的,要调用服务,首先你需要一个服务注册中心,告诉你对方服务都有哪些实例。Dubbo的服务注册中心是可以配置的,官方推荐使用Zookeeper。如果使用Zookeeper的话,要怎样往上面注册实例,又要怎样获取实例,这些都是要实现的。
6、负载均衡
如何从多个实例里挑选一个出来,进行调用,这就要用到负载均衡了。负载均衡的策略肯定不只一种,要怎样把策略做成可配置的?又要如何实现这些策略?同样可以参考Dubbo
7、结果缓存
每次调用查询接口时都要真的去Server端查询吗?是不是要考虑一下支持缓存?
8、多版本控制
服务端接口修改了,旧的接口怎么办?
9、异步调用
客户端调用完接口之后,不想等待服务端返回,想去干点别的事,可以支持不?
10、优雅停机
服务端要停机了,还没处理完的请求,怎么办?
......
诸如此类的优化点还有很多,这也是为什么实现一个高性能高可用的RPC框架那么难的原因。