首先 先把RPC的概念摆出来 RPC基本概念
RPC是远程过程调用(Remote Procedure Call)的缩写形式。SAP系统RPC调用的原理其实很简单,有一些类似于三层构架的C/S系统,第三方的客户程序通过接口调用SAP内部的标准或自定义函数,获得函数返回的数据进行处理后显示或打印。
假如你的应用是一个单体应用,那么你完全可以轻松的依赖本地函数调用来解决一切问题,而随着业务和技术的发展,企业级别的系统不可能永远停留在单体应用的层面,于是产生了分布式系统架构,这个也促使RPC的诞生。
其实传统的B/S架构的调用方式也能解决分布式系统架构的问题,比如我在A服务暴露一个Restful接口,然后通过B服务通过http协议调用这个restful接口一样可以实现分布式系统的架构;但是问题来了,每次调用都需要写一大串http请求的代码,于是你可能会提出一个问题,能不能像本地调用一样去调用远程的服务并且让用户无感是调用的远程服务呢?答案肯定是可以
RPC就是要解决这两个问题:
- 解决分布式系统中 服务之间的调用问题
- 远程调用服务时 如何让用户无感 像调用本地一样的方便
由于服务部署到不同的机器相互调用则避免不了网络通信,而服务消费方在调用远程服务时都要写一大坨网络通信的代码这无疑是糟糕的体验,那么要让通络通信对使用者透明,我们需要对网络通信的细节进行封装,首先我们先看下RPC的调用流程:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
RPC的目标就是要把2~8这些步骤都封装起来对用户透明化。那么怎么做才能封装这些细节让用户像调用本地一样调用远程服务呢?java中有一种代理模式(dubbo采用的就是这种方式)可以解决这个问题,我们本地生成一个远程服务的代理对象,将这个代理对象放进我们的容器内而在这个代理对象的内部去实现上述所说的对远程服务的调用过程,由此 就可以像调用本地一样调用远程了。
市面上已经有很多开源的RPC框架,比如阿里巴巴的Dubbo、Google 的gRPC等,这些现有的框架已经很完美的解决了我们上面聊到的问题,详情可以去相应的官网了解具体用法。
理论的东西千篇一律 网上可以找到很多,真正要理解和加深概念还是需要自己学动手写一下,下面我就把自己的理解转换成一个简单的demo
首先是client端发起RPC请求去调用远程服务:
public class RPCConsumerApp {
public static void main(String[] args) {
Shop shop = new ShopImpl();
String resp = shop.buy("颈椎病康复指南",1);
System.out.println("购买结果:" + resp);
}
}
/**
* 商店
*/
interface Shop{
/**
* 购买商品
* @param name
* @param count
* @return
*/
public String buy(String name,int count);
}
/**
* 商店实现类(使用代理模式)
* 其真正的实现类应该是部署到远端服务器上 本地只是个虚拟的实现类
* 其中封装了远程调用接口的细节
*/
class ShopImpl implements Shop{
//测试代码 端口暂时写死
private final static int PORT = 9090;
@Override
public String buy(String name, int count) {
String result = null;
try{
//根据服务名称 获取注册中心服务列表 默认是 接口.方法
List<String> providers = lookupProviders("Shop.buy");
//根据负载均衡策略筛选提供服务的节点
String providerAddress = chooseProvider(providers);
//通讯协议可选择 无论是http 还是socket 都可以
Socket socket = new Socket(providerAddress,PORT);
//将请求参数进行序列化
ShopRequest shopRequest = new ShopRequest(name,count);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//将请求发送给服务提供者
objectOutputStream.writeObject(shopRequest);
//接受响应
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object resp = objectInputStream.readObject();
result = resp.toString();
System.out.println("购买结果为:"+resp.toString());
}catch (Exception e){
e.printStackTrace();
System.out.println("购买异常:"+e.getMessage());
}
return result;
}
/**
* 获取指定服务的服务实例列表
*
* 分布式系统中会有多个服务节点提供服务 通常会有注册中心来管理实例列表
*
* @return
*/
public List<String> lookupProviders(String serveName){
List<String> providers = new ArrayList<>();
providers.add("127.0.0.1");
return providers;
}
/**
*
* 负载均衡算法 根据服务实例列表 筛选具体服务实例节点提供服务
* @return
*/
public String chooseProvider(List<String> providers){
return providers.get(0);
}
}
/**
* rpc 请求参数体
* 包括请求的接口名 参数 等
*/
@Data
@NoArgsConstructor
class ShopRequest implements Serializable{
private String method = "buy";
private String name;
private int count;
public ShopRequest(String name, int count) {
this.name = name;
this.count = count;
}
}
我们通过把RPC调用的细节进行封装(可以采用代理的方式)客户端使用远程服务时就像是调用本地一样方便
再看一下服务端是如果实现提供远程服务的:
public class RPCServerApp {
private final static int PORT = 9090;
private Shop shop = new RealShopImpl();
public static void main(String[] args) throws Exception{
RPCServerApp rpcServerApp = new RPCServerApp();
rpcServerApp.run();
}
public void run() throws Exception{
ServerSocket serverSocket = new ServerSocket(PORT);
try {
//循环接受客户端请求
while (true){
Socket socket = serverSocket.accept();
try{
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object request = objectInputStream.readObject();
System.out.println("客户端请求参数:"+request.toString());
String buyResult = null;
if(request instanceof ShopRequest){
ShopRequest shopRequest = (ShopRequest) request;
if("buy".equalsIgnoreCase(shopRequest.getMethod())){
buyResult = shop.buy(shopRequest.getName(),shopRequest.getCount());
}
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(buyResult);
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
socket.close();
}
}
}catch (Exception e){
System.out.println("exception :" + e.getMessage());
}finally {
serverSocket.close();
}
}
}
/**
* 商店实现类
*/
class RealShopImpl implements Shop {
@Override
public String buy(String name, int count) {
String result = null;
try {
result = "恭喜您!购买【" + name + "】成功,数量为【" + count + "】";
} catch (Exception e) {
System.out.println("购买异常:" + e.getMessage());
result = "商店打烊 暂时无法出售货物";
}
return result;
}
}
服务端接受客户端的请求,对参数进行反序列化>执行本地处理>序列化执行结果并返回
我们的demo比较简单 只是把自己的理解转换成代码,商用框架远比这个复杂的多,我们以dubbo举例,dubbo通过和spring的集成,在spring容器加载的时候便会加载我们使用 <dubbo:reference/>或者@Reference 胡姐配置的bean,为这些配置的对象生成一个代理对象,这个代理对象会负责进行远程通信调用远程服务,我们所需要的就是将这些代理对象注入到我们的服务中使用便可。
那我们怎么才能像dubbo那样不用自己手写代理对象而自动生成所需的代理对象呢?答案肯定是要遵循一套规范,我们要求所有远程调用的服务都遵循一套模板,我们把调用远程的所有信息放到一个RPCRequest对象里面,发给远程服务提供端,在服务端接收并解析之后他就知道我们到底想要调用哪个接口并且也知道我们传过来的参数列表分别是什么类型值是什么,就像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;
private transient Class<?> returnType;
private transient InvokeMode invokeMode;
一个好的RPC框架需要考虑的问题有很多,比如框架的通用性、通信的协议、服务端线程池、服务注册中心、负载均衡、服务多版本控制等等一系列的问题