目录
概述
RPC(Remote Procedure Call),顾名思义,指的是远程过程(方法)调用,与之相对应的就是本地方法调用。
本地方法调用
放到具体的代码中,可以理解为,被调用方(producer)和调用方(consumer)在同一个项目当中
远程方法调用
放到具体的代码中,可以理解为,被调用方法(producer)和调用方(consumer)在不同的项目当中
这里有一个问题,本地调用就可以实现了想要的功能了,为什么还要使用远程调用?
远程调用主要的目的就是为了降低机器的负载,把消耗资源比较大的操作放到一个独立的机器上。比如上图中 Provider 的 print 方法就是一个比较消耗资源的操作,这时我们把它放到机器 B 上,就可以降低机器 A 的负载,从而不会影响到机器 A 中其他重要业务的正常执行。
接下来,通过 java 实现的简易 RPC 框架说明其原理
实现关键点
想要实现远程的方法调用,下面的几个关键点是必不可少的。为了方便起见,在这里我把调用方称为 consumer ,被调用方称为 producer。假设 consumer 在机器 A,producer 在机器 B,consumer 想要调用 producer 中的 print 方法,该怎么实现呢?
简单说来就是以下流程:
- consumer 端获取 公共接口 中的一些属性(类名,方法 A 的名字,方法 A 的形参),并且序列化为对象 A
- 通过指定通信方式传输序列化对象 A 至 producer
- producer 端获取对象 A,反序列化后获取相关属性(类名,方法 A 的名字,方法 A 的形参)
- producer 端通过反射调用待执行的方法,然后把方法执行结果通过指定通信方式传回 consumer
公共接口
consumer 和 producer 中必须要有完全相同的接口(类),这里的完全相同,不仅仅是接口本身,还包括接口(类)的包名
例如 com.iflytek.yyzx.rpc.remoteMethod.HelloService.java。
package com.iflytek.yyzx.rpc.remoteMethod;
public interface HelloService {
public String sayHello(String words);
public String sayHello(String words, Integer time);
}
在 consumer 端和 producer 端都需要实现该接口,但是方法的执行逻辑不一样。
在 consumer 端的实现类 HelloServiceImpl.java
package com.iflytek.yyzx.rpc.client;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import com.iflytek.yyzx.rpc.remoteMethod.HelloService;
import com.iflytek.yyzx.rpc.remoteMethod.RpcRequest;
public class HelloServiceImpl implements HelloService {
public String sayHello(String words) {
try {
return (String) remoteCall(words);
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
@Override
public String sayHello(String words, Integer time) {
try {
return (String) remoteCall(words, time);
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
public static Object remoteCall(String words, Integer time) throws Exception {
// 获得类名
String className = HelloService.class.getName();
// 获得方法
Method method = HelloService.class.getMethod("sayHello", java.lang.String.class, java.lang.Integer.class);
// 获得传的参数
Object[] paramValue = { words, time };
Socket consumer = new Socket("127.0.0.1", 8765);
ObjectOutputStream oos = new ObjectOutputStream(consumer.getOutputStream());
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(className);
rpcRequest.setMethodName(method.getName());
rpcRequest.setParamType(method.getParameterTypes());
rpcRequest.setParamValue(paramValue);
System.out.println(rpcRequest.toString());
oos.writeObject(rpcRequest);
ObjectInputStream ois = new ObjectInputStream(consumer.getInputStream());
Object result = ois.readObject();
consumer.close();
return result;
}
public static Object remoteCall(String words) throws Exception {
// 获得类名
String className = HelloService.class.getName();
// 获得方法
Method method = HelloService.class.getMethod("sayHello", java.lang.String.class, java.lang.Integer.class);
// 获得传的参数
Object[] paramValue = { "Hello! I am consumer"};
Socket consumer = new Socket("127.0.0.1", 8765);
ObjectOutputStream oos = new ObjectOutputStream(consumer.getOutputStream());
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName(className);
rpcRequest.setMethodName(method.getName());
rpcRequest.setParamType(method.getParameterTypes());
rpcRequest.setParamValue(paramValue);
System.out.println(rpcRequest.toString());
oos.writeObject(rpcRequest);
ObjectInputStream ois = new ObjectInputStream(consumer.getInputStream());
Object result = ois.readObject();
System.out.println("consumer receive message from provider: " + result.toString());
consumer.close();
return result;
}
}
在 consumer 端实现 HelloService.java 的目的是 为了让用户感知上认为就是在调用本地的方法:sayHello(String words, Integer time),而实际上该方法里面的执行逻辑是在调用 producer 端 sayHello(String words, Integer time)。调用远程方法的细节在 remoteCall(String words, Integer time) 方法里面。
在 producer 端的实现类 HelloServiceImpl.java
例如 HelloServiceImpl.java
package com.iflytek.yyzx.rpc.server;
import com.iflytek.yyzx.rpc.remoteMethod.HelloService;
public class HelloServiceImpl implements HelloService{
public String sayHello(String words) {
System.out.println("provider receive message from consumer: " + words);
return "Hello! I am provider";
}
@Override
public String sayHello(String words, Integer time) {
System.out.println("provider receive message from consumer: " + words + ", time: " + time);
return "Hello! I am provider, time: " + time;
}
}
这样,consumer 在调用方法:sayHello(String words, Integer time) 的时候,实际上会执行 producer 中 HelloServiceImpl.java 的方法: sayHello(String words, Integer time) 的具体实现逻辑。具体原因参见后面
序列化 consumer 端待传输的方法信息
这里待传输的方法信息包括:
- 接口名(com.iflytek.yyzx.rpc.remoteMethod.HelloService)
- 方法名(sayHello)
- 方法的形参类型(java.lang.String.class,java.lang.Integer.class)
- 参数值("Hello! I am consumer", 1)
这里的形参类型和参数值是一 一 对应的,如果方法没有形参,则为空。其实,这些信息就是一个类的基本信息,如果获取到了这些信息,就可以通过 java 的反射动态加载对应的类,然后调用对应的方法。通常,在 consumer 端和 producer 端会各自申明一个完全相同的类,包含传输的方法信息,这样做使得信息的获取更加清晰,易懂。
例如 com.iflytek.yyzx.rpc.remoteMethod.RpcRequest.java
package com.iflytek.yyzx.rpc.remoteMethod;
import java.io.Serializable;
/**
* 用于存放远程调用需要的信息,例如类名,方法名等等
*
* @author hsyang
*
*/
public class RpcRequest implements Serializable{
private static final long serialVersionUID = -2865905900171967180L;
/** 类名 */
private String className;
/** 方法名 */
private String methodName;
/** 参数类型 */
private Class<?>[] paramType;
/** 参数值 */
private Object paramValue;
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class<?>[] getParamType() {
return paramType;
}
public void setParamType(Class<?>[] paramType) {
this.paramType = paramType;
}
public Object getParamValue() {
return paramValue;
}
public void setParamValue(Object paramValue) {
this.paramValue = paramValue;
}
@Override
public String toString() {
return "RpcRequest [className=" + className + ", methodName=" + methodName + ", paramType=" + paramType + "]";
}
}
有了这个 RpcRequest 类之后,consumer 端只需要序列化 RpcRequest,通过特定的通信方式把序列化后的 RpcRequest 字节流发送至 producer 端,然后 producer 端反序列化 RpcRequest,获取对应的参数信息。通过反射实现我们调用指定方法的目地。
通信方式
前面提到的通信方式,实际上就是:把 consumer 端的信息发送至 producer 端的一种方式。有很多方式都可以实现:使用 netty,使用 HttpClient,使用 RestTemplate 等等。在这里为了简便,直接使用了 jdk 提供 Socket 类(consumer端),ServerSocket 类(producer 端)来进行信息地传送的。
反射
producer 端在收到了 consumer 端通过特定通信方式发过来的信息 RpcRequest,反序列化获取 RpcRequest 里面的参数信息:
- 接口名(com.iflytek.yyzx.rpc.remoteMethod.HelloService)
- 方法名(sayHello)
- 方法的形参类型(java.lang.String.class,java.lang.Integer.class)
- 参数值("Hello! I am consumer", 1)
然后,通过 java 的反射技术,实现方法的调用,具体包括:
通过接口名加载对应的接口(com.iflytek.yyzx.rpc.remoteMethod.HelloService),同时获取对应接口在 producer 端提前声明好的实现类的对象
//加载接口类
Class<?> helloService = Class.forName(className);
// 获取接口实现类的对象
Object helloServiceImpl = serviceMap.get(className);
通过方法名和形参类型从已加载的 helloService 实例中获取方法对象
Method method = helloService.getMethod(methodName, paramType);
现在类的实例(helloServiceImpl)和 方法对象(method)都已经获取到了,接着就可以通过 java 的反射调用对应的方法了
Object result = method.invoke(helloServiceImpl, paramValues);
之后,会获得一个结果对象 result。紧接着,再把 result 通过指定通信方式传回 consumer。
至此,从客户端调用远程方法开始到结束的整个过程已经完毕了。