RPC三大要点
RPC全称是Remote Procedure Call,远程过程调用。也就是调用其他机器的方法。
我们可以自己想一下要怎么实现去调用其他机器的方法。
首先我们需要知道去哪里调用我们需要的服务。
因为服务端可能提供了很多个远程服务,也有可能多个进程部署在一个机器,为了防止端口冲突,往往使用随机端口,那我们要怎么知道访问哪个端口呢?这就涉及到服务发现的问题。
然后是如何传递参数呢?
如果有两个参数,要先传第一个还是第二个?TCP一端发送出去的数据,另外一端不能全部读取,怎么才算读完?还有一个类里面有各种数据,这些数据要怎么表示?这就需要有一个统一的方案,称之为协议约定问题。
最后其实就是传输问题,因为我们远程调用肯定需要解决网络问题,如果发生了错误、重传、丢包、性能等问题怎么办?
下面来看一下各个要点的实现。
协议约定
自定义一个RPC协议的话,可以把它涉及成协议头和协议体,协议头包括协议版本,消息id标识这次rpc请求,消息类型,序列化方式等等,协议体由请求接口和参数构成,长度是可变的。
这里的参数,因为要通过网络传输,所以肯定不能使用UFT-8这类的编码,我们都应该转化成二进制后再传输,所以这里涉及到了序列化。
序列化
常用的序列化包括jdk原生序列化,hessian,protobuf(google使用的)等等。
选择序列化方式要从哪些方面考虑呢?
-
首先要保证安全性。
-
然后很重要的是通用性和兼容性,因为要考虑是否支持比较多的对象类型,是否跨平台、跨语言的,是否很多人使用并且踩过坑了。
-
其次才考虑性能,效率和开销。
综合比较,我们首选的还是hessian和protobuf。
那么,序列化在使用时我们还需要注意什么问题吗?
首先要避免构造复杂的对象,因为越复杂的对象,在序列化和反序列化过程中就越容易出错
还要避免对象过于庞大,数据太大会很耗时的。
避免使用序列化不支持的类作为入参。比如hessian框架,他不支持LinkedHashMap、LinkedHashSet,避免使用第三方集合,尽量选择原生的、常用的集合类,如HashMap,ArrayList。
还要避免对象有复杂的继承关系,因为父类的属性也会被序列化,造成对象复杂且庞大,既影响性能,也容易出问题。
像dubbo中默认的RPC协议就是 Hessian2协议,Hessian2协议是自描述的,只要拿到二进制数据,根据本身的规则,就能解析出来,不需要另外的协议。
服务发现
服务发现是使用注册中心实现的,当一个应用启动之后,就向服务中心注册自己的服务,调用方先去注册中心找到要调用的服务,再去发起请求。
像Dubbo 会从注册中心获取服务端的列表,根据路由规则和负载均衡规则,在多个服务端中选择一个最合适的服务端进行调用。
传输问题
有很多的优秀的网络框架可以使用,在 Dubbo 里面,使用了 Netty 的网络传输框架。
如何设计RPC框架
这里我们来设计一个简单的rpc框架。首先,正如前面所说,我们需要一个注册中心,这个注册中心我们可以用zk,也可以自己做一个简单的注册中心。
这个注册中心用什么作用呢?前面我们介绍是用来注册自己的服务的,我们这里可以简单一点,我们可以自己配置自己项目的访问地址,配置发布的服务名称,bean的名称,方法名称。这样就完成了一个服务的注册。通过访问路径,可以找到服务所在的机器,然后通过bean的名称,方法的名称,可以找到具体的业务逻辑实现。
所以整个过程就是,先在注册中心注册好自己的服务。然后调用方根据服务名称获取注册中心的服务提供者的访问地址,这样就知道要访问哪台机器了。
之后,我们统一使用一个方法来调用rpc,这里就叫做execute(),这个方法传入的对象里面包括服务名称,参数的值。在调用execute()的时候,其实有用一个代理类去实现具体的网络请求。这里我们使用hessian序列化,将服务名称和参数通过网络框架传过去。
然后是服务提供端,收到了这个数据后,解析出数据,获取到服务名称,和参数。再去注册中心获取该服务的配置,之前配置了bean的名称,根据这个bean的名称去spring容器中获取bean,再通过方法名,使用反射去调用该方法,得到结果,再通过网络IO返回结果。
到这里,这个rpc调用就结束了,当然一个完整的rpc框架应该还包括限流、降级、限权等等功能,有机会再去研究。