Dubbo系列 之 浅谈RPC
本文将带你抽丝剥茧,揭开RPC神秘面纱的一角
一、什么是RPC?
RPC(Remote Procedure Call)远程过程调用。
与之对应的是 LPC(Local Procedure Call)本地方法调用。
举个例子:
public class CommonDTO implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "CommonDTO{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class CommonService {
public String sayHi(CommonDTO param) {
return "hi " + param.getName() + " ! You're " + param.getAge() + " years old!";
}
public String sayHi() {
return "hello world!";
}
}
现在项目A有一个CommonServie
类,包含两个个sayHi
方法,那么在项目A中,我们就能通过以下方式调用某一方法:
public class Test {
public static void main(String[] args) {
CommonService commonService = new CommonService();
CommonDTO param = new CommonDTO();
param.setName("zhangsan");
param.setAge(18);
commonService.sayHi(param);
}
}
这就是本地方法调用。
如果另一个项目B需要通过网络请求的方式来调用项目A中CommonServie
类中的sayHi
方法,就叫做远程过程调用。
如果项目B直接把项目A当做依赖引入,就是通过本地方法调用的方式调用该方法。
二、如何实现一个RPC框架?
通过RPC框架,我们可以像调用本地方法一样调用远程机器上的方法。
那么,问题来了,让我们一步一步的看。
2.1 什么是像调用本地方法一样调用远程机器上的方法?
先看看本地方法的实现:
CommonService commonService = new CommonService();
CommonDTO param = new CommonDTO();
param.setName("zhangsan");
param.setAge(18);
commonService.sayHi(param);
项目B中,我们肯定也希望通过commonService.sayHi(param);
的方式去调用项目A的这个方法。
我们很明显就能发现两个问题:
- 怎么定位到服务的提供端?
CommonService
类从哪来?- 怎么让服务提供端知道,需要调用的类是
CommonService
,调用的方法是sayHi
方法,并且该方法有一个实体类型的参数? - 怎么能在执行
sayHi
方法的时候,去调用服务提供端的这个方法?
带着疑问,我们继续。
2.2 如何准确的调用到远程服务器上的目标方法?
这个时候,必须要知道,远程调用的服务器地址、端口号、调用的类名、方法名,以及参数类型、参数值,才能准确的进行方法的调用。
需要参数类型,是因为方法存在重载的情况,比如本例当中,CommonService
类中就存在两个sayHi
方法,必须加上参数类型,才能准确的找到调用的方法。
2.3 如何保证服务消费端、提供端的类名、方法名与参数类型的一致性?
首先可以确定,调用的方法,肯定是一个抽象方法,因为我们并不知道该方法内部的实现逻辑,具体的实现肯定是在远程调用的另一端中。
我们常采用的一种方式,就是将远程调用的方法提取成公共的接口,将这些接口以及传参用的实体抽取到一个模块中,然后让服务提供端和消费端都引入该模块。
服务提供端、消费端共同引用的模块:
public interface CommonService {
/**
* 有参方法
* @param param
* @return
*/
String sayHi(CommonDTO param);
/**
* 无参方法
* @return
*/
String sayHi();
}
服务提供端的真实业务逻辑:
public class CommonServiceImpl implements CommonService {
@Override
public String sayHi(CommonDTO param) {
return "hi " + param.getName() + " ! You're " + param.getAge() + " years old!";
}
@Override
public String sayHi() {
return "hello world!";
}
}
这样,远程调用的时候,类名、方法名、参数类型就不存在不一致的情况了。
2.4 服务消费端与提供端之间,如何进行网络传输?
这就要求我们约定好网络传输使用的协议,是http
协议还是tcp
传输协议。
由于http
在应用层中完成,整个通信的代价较高,基于tcp
进行远程调用,数据传输在传输层完成,更适合对效率要求比较高的场景,本文以建立socket
链接举例。
数据传输时,无法直接传递参数,因此需要消费端把参数转换成字节流,传给服务提供端,然后服务提供端将字节流转换成自身能读取的格式,是一个序列化
和反序列化
的过程。
同时我们要约定好,序列化时各个参数的顺序。
服务消费端序列化方法如下:
Socket socket = new Socket(host, port);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
// 类名
objectOutputStream.writeUTF(clazz.getName());
// 方法名
objectOutputStream.writeUTF(method.getName());
// 参数类型
objectOutputStream.writeObject(method.getParameterTypes());
// 参数值
objectOutputStream.writeObject(args);
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object result = objectInputStream.readObject();
socket.close();
所以对应的服务提供端反序列方法如下:
ServerSocket serverSocket = new ServerSocket(port);
Socket socket = null;
try {
socket = serverSocket.accept();
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 类名
String className = objectInputStream.readUTF();
// 方法名
String methodName = objectInputStream.readUTF();
// 参数类型
Class<?>[] parameterType = (Class<?>[]) objectInputStream.readObject();
// 参数
Object[] arguments = (Object[]) objectInputStream.readObject();
......
// 返回结果
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(result);
} catch (Exception e){
System.out.println("处理失败!" + e.getMessage());
} finally {
if (socket != null){
try {
socket.close();
} catch (IOException e) {
socket = null;
System.out.println("关闭异常!" + e.getMessage());
}
}
}
这时又出现了两个新问题:
- 服务消费端,怎么将序列化的逻辑封装起来,真正做到像调用本地方法一样,去调用远程方法?
- 服务提供端,怎么根据传输的数据,找到真正执行的方法?
2.5 服务消费端,如何做到像调用本地方法一样,去调用远程方法?
在执行方法的时候,去进行远程调用,我们应该能联想到需要对方法进行增强。这个时候大家应该能联想到动态代理吧!
public class Center {
public static <T> T getProxy (Class<T> clazz, String host, int port){
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] {clazz}, (proxy, method, args) -> {
// 采用反射,像调用本地方法一样调用远程方法
Socket socket = new Socket(host, port);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeUTF(clazz.getName());
objectOutputStream.writeUTF(method.getName());
objectOutputStream.writeObject(method.getParameterTypes());
objectOutputStream.writeObject(args);
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object result = objectInputStream.readObject();
socket.close();
return result;
});
}
}
目前为止,服务消费端进行远程调用的逻辑已经完整了:
- 设置服务提供端的url地址以及端口
- 生成远程调用的类的一个代理类
- 执行方法的时候,通过代理类,去进行网络请求,进行远程调用
public static void main(String[] args) {
CommonService commonService = Center.getProxy(CommonService.class, "127.0.0.1", 8081);
CommonDTO param = new CommonDTO();
param.setName("zhangsan");
param.setAge(18);
System.out.println(commonService.sayHi(param));
}
2.6 服务提供端,怎么根据传输的数据,找到真正执行的方法?
服务提供端,真正执行的是接口的实现类,这就要求服务提供端,将接口的实现类暴露出来,通过CommonService
类映射到真正的实现类,去执行具体的方法。
public class Center {
// 接口和实现类的映射
public static ConcurrentHashMap<String, Class> service = new ConcurrentHashMap<>();
/**
* 注册
*
*
* @param commonService
* @param port
* @throws Exception
*/
public static void regist(HashMap<String, Class> commonService, int port) throws Exception {
service.putAll(commonService);
ServerSocket serverSocket = new ServerSocket(port);
Socket socket = null;
try {
socket = serverSocket.accept();
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 类名
String className = objectInputStream.readUTF();
// 方法名
String methodName = objectInputStream.readUTF();
// 参数类型
Class<?>[] parameterType = (Class<?>[]) objectInputStream.readObject();
// 参数
Object[] arguments = (Object[]) objectInputStream.readObject();
Class target = service.get(className);
// 反射执行
Method method = target.getMethod(methodName, parameterType);
Object result = method.invoke(target.newInstance(), arguments);
// 返回结果
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(result);
} catch (Exception e){
System.out.println("处理失败!" + e.getMessage());
} finally {
if (socket != null){
try {
socket.close();
} catch (IOException e) {
socket = null;
System.out.println("关闭异常!" + e.getMessage());
}
}
}
}
}
服务提供端的启动:
public static void main(String[] args) {
HashMap<String, Class> service = new HashMap<>();
service.put(CommonService.class.getName(), CommonServiceImpl.class);
try {
Center.regist(service, 8081);
} catch (Exception e) {
e.printStackTrace();
}
}
2.3 一个简易RPC框架结构
将服务消费端、提供端的Center
类提取到公共模块中。
整个项目的结构如下:
api
模块,存放远程调用的接口及参数:
center
模块,服务提供者的注册与启动,以及服务消费者的真正调用:
consumer
模块,消费者进行远程调用:
producer
模块,其实叫provider
更合适,进行服务的暴露:
码云仓库地址.
三、总结
大致上一个 RPC 框架需要一个注册中心,需要约定通信协议、序列化的格式,此外还需要负载均衡策略、容错机制、和监控运维。