手写RPC,深入底层理解整个RPC通信
Posted 毛奇志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写RPC,深入底层理解整个RPC通信相关的知识,希望对你有一定的参考价值。
无论对谁太过热情,就增加了不被珍惜的概率。
一、前言
RPC,远程过程调用,调用远程方法像调用本地方法一样。RPC交互分为客户端和服务端,客户端调用服务端方法,服务端接收数据并打印到控制台,并response响应给客户端。
RPC和HTTP的联系与区别
联系:都是远程通信的网络协议,任何网络协议都包括语法、语义、时序三个特征。
区别:
HTTP协议是应用层协议,更多地用于前后端通信或移动端、PC端与后端通信,其他的常用应用层协议还包括SSH协议、ftp协议。
RPC是远程过程调用,更多地用于分布式架构中各个服务模块之间的通信,其中的知识涉及socket通信、序列化和反序列化、动态代理、反射调用。如下程序,服务端要发布socket服务+线程池io处理多个客户端连接+反射调用,客户端要动态代理实例化对象,参与网络传输的Bean要实现Serializable接口变为可序列化+IO流序列化/反序列化。
本文意义:我们会使用到很多RPC框架,这些RPC框架底层都是封装好了的,socket通信、序列化(本文使用JavaIO实现序列化)、反射、代理,本文意义在于自己手写一个RPC通信,理解RPC通信底层,以后看RPC框架的源码就简单多了。
tip:手写RPC,一步步来看,由于没有两台电脑,就用一个电脑上的两个工程进行交互。
二、客户端动态代理
2.1 构建工程结构
分布式就是在两个JVM进程的通信,现实中环境中是两个电脑上的两个JVM进程,笔者只有一个电脑,所以使用一个电脑上的两个Java程序,本质上是一样的,只要是两个JVM程序通信即可(唯一不同的是,两个电脑的通信就是要将服务端的api模块发布到私服上供客户端maven依赖使用,但是一个电脑上的上两个应用程序是将api模块maven install到本地仓库供客户端maven依赖使用)。
使用idea新建两个maven Project,架构为quickstart(因为我们只是应用程序,不是web程序),分别为rpcServer 和 rpcClient ,在rpcServer中新建两个Modul,也都是maven quickstart,分别为rpcServerApi和rpcServerProvider。
如图:
注意:无论新建Maven Project还是Maven Modul,事先要设置好idea中的maven home,user settings file,maven repository,类似笔者电脑如下:
2.2 rpcServerApi被rpcServerProvider 和 rpcClient 引用
将rpcServerApi 作为依赖在rpcServerProvider中使用,然后,将rpcServerApi Maven clean,再Maven install,就可以生成jar包安装到本地的maven repository中,这样,让rpcClient再次引入rpcServerApi作为依赖。
如图:
注意1:maven quickstart生成为jar包,maven webapp生成为war包,这里rpcServerApi是quickstart,所以maven install是生成jar包(到本地maven repository)。
注意2:pom.xml依赖来源于两种,一种是远程依赖,一种是本地依赖,这里的rpcServerApi经过maven install就是本地依赖了。
2.3 客户端动态代理
2.3.1 rpcServerApi 提供接口
2.3.2 rpcServerProvider
publisher()方法为服务端发布一个服务
2.3.3 rpcClient 动态代理使用IHelloService
由于客户端和服务端是两个JVM中的程序,即使现在将服务端工程的api模块maven install到本地仓库,客户端程序可以通过导入依赖找到IHelloService,但是如何实例化IHelloService呢?毕竟IHelloService是接口,无法自己实例化,api模块中也没有其子类实现(provider模块中倒是有IHelloService的子类实现,但是客户端程序并未导入其依赖)。所以,由于api模块本身无法实例化IHelloService,所以客户端无法通过new 实例化IHelloService,这里采用动态代理的方式。即客户端拿到的IHelloService只是一个引用,需要使用远程代理。
2.4 客户端动态代理成功
先运行rpcServerProvider工程的Main类的main方法,启动服务端,绑定8080端口;然后,启动rpcClient工程的App类的main方法,去连接8080端口,运行成功:
代理对象调用sayhello()方法就是执行invoke()方法里面的逻辑,这里执行了,打印成功。
三、客户端连接服务端并数据传送
3.1 服务端 rpcServerProvider
服务端rpcServerApi模块,提供网络通信Bean类 RpcRequest,因为这个Bean类用于网络通信,所以变为可序列化,实现Serializable接口。
将 rpcServerProvider maven install 更新下,这样更改对于rpcClient就可见了。
3.2 客户端 rpcClient
客户端提供RpcNetTransport类,用于Socket连接,newSocket()方法提供连接服务端socket,send()提供调用newSocket()连接服务端。
注意到,服务端api模块中,RpcRequest是一个实体类,所以客户端可以直接使用new新建实例,但是刚才的IHelloService不行,只能使用动态代理。
四、多线程、传送数据Bean的序列化和反序列化
4.1 服务端 rpcServerProvider 接收客户端发送的数据
4.2 客户端 rpcClient 写数据到服务端
五、服务端反射调用并返回给客户端
5.1 rpcServerProvider 服务端返回数据给客户端
服务端收到客户端发来的数据反序列为RpcRequest,这里面装载着classname methodname type Parameters,服务端invoke()根据这些信息反射调用方法,将得到的结果记为result,并发送给客户端。
5.2 rpcClient 客户端接收数据
客户端的send()方法中应该完善与服务端的交互。
客户端的动态代理invoke()方法是新建一个RpcRequest类,制定好classname methodname type parameters ,绑定端口号,发送给服务端。
六、成功交互
整个流程如下:
服务端运行,然后客户端运行连接上服务端,然后客户端 helloService.sayHello(“Mic”);客户端组装好数据报文,发送给服务端,服务端接收数据报文,根据数据报文反射调用方法,则服务端打印 “服务端收到一个请求: Mic”,然后将返回值 “这里是服务端sayHello的实现” 发送给客户端,客户端将其打印出来。
整个过程涉及到socket通信、序列化和反序列化、动态代理、反射调用,其中,服务端要发布socket服务+线程池io处理多个客户端连接+反射调用,客户端要动态代理实例化对象,参与网络传输的Bean要实现Serializable接口变为可序列化+IO流序列化/反序列化。
七、面试金手指
7.1 JDK反射调用(InvocationHandler接口)+动态代理(Proxy.newInstance())(一)
7.1.1 JDK动态代理:实际接口
首先是要被代理的接口:
/**
* 被代理的主体需要实现的接口
*/
public interface Subject {
String doSomething(String thingsNeedParm);
String doOtherNotImportantThing(String otherThingsNeedParm);
}
7.1.2 JDK动态代理:实际接口的实现类
然后是代理接口的实现类:
public class SubjectIpml implements Subject {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String doSomething(String thingsNeedParm) {
System.out.println("使用" + thingsNeedParm + "做了一些事情");
return "调用成功";
}
public String doOtherNotImportantThing(String otherThingsNeedParm) {
System.out.println("使用" + otherThingsNeedParm + "做了一些事情");
return "调用成功";
}
}
7.1.3 JDK动态代理:代理类(实现InvocationHandler接口重写invoke()方法)+动态代理Proxy.newInstance()
然后就是代理类:
public class SubjectProxy implements InvocationHandler {
private Subject subject;
SubjectProxy(Subject subject){
this.subject = subject;
}
/**
* @param proxy 调用这个方法的代理实例
* @param method 要调用的方法
* @param args 方法调用时所需要的参数
* @return 方法调用的结果
* @throws Throwable 异常
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//进行method过滤,如果是其他方法就不调用
if (method.getName().equals("doSomething")){
System.out.println("做某些事前的准备");
Object object = method.invoke(subject,args);
System.out.println("做某些事后期收尾");
return object;
}
return "调用失败";
}
/**
* 获取被代理接口实例对象
*/
public Subject getProxy() {
return (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), this);
}
}
7.1.4 测试类:动态代理+反射调用
然后是测试类:
public class ProxyTest {
public static void main(String[] args) {
Subject subject = new SubjectIpml();
SubjectProxy subjectProxy = new SubjectProxy(subject); // 实际类构造注入到代理类
Subject proxy = subjectProxy.getProxy(); // 获取代理对象
proxy.doSomething("改锥"); // 代理对象反射调用方法
proxy.doOtherNotImportantThing("纸片"); // 代理对象反射调用方法就是执行invoke()里面的逻辑,比真实对象调用方法多了一个前打印和后打印
}
}
7.1.5 测试结果
测试结果:
做某些事前的准备
使用改锥做了一些事情
做某些事后期收尾
做某些事前的准备
使用纸片做了一些事情
做某些事后期收尾
实现JDK动态代理很简单,只要实现InvocationHandler接口重写invoke()方法,就好了。
调用invoke()方法的时候,返回代理对象,然后使用代理对象来调用方法就好。
7.1.6 InvocationHandler接口的invoke()方法(三个参数+返回值),以Mybatis源码为例
InvocationHandler的invoke()的三个参数到底有什么用呢,经过笔者翻看Mybatis的MapperProxy动态代理的源码,笔者发现了他的用处。
先贴上Mybatis的MapperProxy的源码:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
}
看invokeDefaultMethod这个方法,就用到了invoke() 的第一个参数proxy,这个invokeDefaultMethod只是为了修复Mybatis早期版本不能调用Java8默认方法的bug。
他把默认方法绑定到了调用方法的代理实例,然后再传入参数,调用默认方法
金手指:InvocationHandler接口的invoke()方法(三个参数+返回值)
第一个参数proxy,可以用来绑定实例接口的方法;
第二个参数method ,是调用的方法,可以用来方法过滤,得到方法的声明类等等;
第三个参数就仅仅是被调用方法的参数罢了;
返回值:类型为Object,可以自己任意返回,可以返回invoke()调用的函数的返回值,也可以返回字符串。
7.1.7 Proxy.newInstance()方法
newProxyInstance方法(调用对象+三个参数+返回值)
调用对象:在需要创建代理实例的时候才调用,这里是测试类中调用,因为是静态方法,所以是直接类名调用,就是Proxy.newInstance()这样用。
三个参数:
loader: 用哪个类加载器去加载代理对象;
interfaces:动态代理类需要实现的接口;
h:动态代理方法在执行时,会调用h里面的invoke方法去执行。
返回值:返回类型为Object,但是如果去接收的话,返回的就是一个代理对象,第一个测试类(封装一层)和第二个测试类都是使用一个引用接收代理对象。
7.2 JDK反射调用(InvocationHandler接口)+动态代理(Proxy.newInstance())(二)
利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(这个类就是“动态代理类”,代理的是接口,创建一个实现了该接口的类,所以代理类和实际类是实现同一个接口)及其实例(对象),代理的是接口(Interfaces),不是类(Class),也不是抽象类。在运行时才知道具体的实现,spring aop就是此原理。
金手指:Proxy.newInstance()方法三个点
1、运行时才创建;
2、创建一个实现了给定接口的类,这个类就是代理类,所以代理类和实际类实现同一个接口;
3、并创建代理类的实例,返回代理类实例的引用。
金手指:第一个测试类和第二个测试类区别
Subject proxy = subjectProxy.getProxy(); // 获取代理对象
proxy.doSomething("改锥"); // 代理对象反射调用方法
proxy.doOtherNotImportantThing("纸片"); // 代理对象反射调用方法
第一个测试类,通过调用封装好的getProxy()方法调用底层的Proxy.newInstance(),在运行时创建实现接口的代理类的对象,然后return返回引用,使用Subject proxy接收引用。然后使用代理对象的引用调用方法,实际上是调用实际实现类的方法。
IVehical vehical = (IVehical)Proxy.newProxyInstance(car.getClass().getClassLoader(), Car.class.getInterfaces(), new VehicalInvacationHandler(car));
vehical.run();
第二个测试类,Main类中调用Proxy.newInstance()返回,在运行时创建实现接口的代理类的对象,然后return返回引用,使用IVehical vehical接收引用,然后使用代理对象的引用调用方法,实际上是调用实际实现类的方法。
小结:两者是一样的,只是第一个实现类包了一层。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
7.2.1 JDK动态代理:实际接口
public interface IVehical {
void run();
}
7.2.2 JDK动态代理:实际接口的实现类
public class Car implements IVehical {
public void run() {
System.out.println("Car会跑");
}
}
7.2.3 JDK动态代理:代理类,实现Invocationhandler接口,重写invoke()方法,在invoke()方法中使用反射调用接口实现类中的方法
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class VehicalInvacationHandler implements InvocationHandler {
private final IVehical vehical;
public VehicalInvacationHandler(IVehical vehical){
this.vehical = vehical;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---------before-------");
Object invoke = method.invoke(vehical, args);
System.out.println("---------after-------");
return invoke;
}
}
7.2.4 测试类
import java.lang.reflect.Proxy;
public class App {
public static void main(String[] args) {
IVehical car = new Car();
IVehical vehical = (IVehical)Proxy.newProxyInstance(car.getClass().getClassLoader(), Car.class.getInterfaces(), new VehicalInvacationHandler(car)); // 获取代理对象
vehical.run(); //代理对象调用run()方法就是执行invoke()里面的逻辑,比真实对象调用run()方法多了一个前打印和后打印
}
}
上面代码中,代理car对象,调用run方法时,自动执行invocationhandler中的invoke方法。
7.2.5 测试结果
---------before-------
Car会跑
---------after--------
7.3 RPC总结
7.3.1 RPC全体过程(动态代理)
金手指1:客户端动态代理
服务端只有一个api模块maven install,客户端可以识别,但是客户端拿到的api模块的不是只是一个接口,没实例化,服务端的provider模块虽然实例化了,但是没有maven install,现在客户端实现动态代理,使用Proxy.newInstance()实现动态代理,得到代理对象,然后再用代理对象调用实现类的方法,就是执行invoke()方法。
金手指2:Proxy.newInstance()
Proxy.newInstance()该函数要传递三个参数
loader: 用哪个类加载器去加载代理对象,意义:接口的类加载器;
interfaces:动态代理类需要实现的接口,意义:接口的字节码,代理和实现是同一个接口;
h:动态代理方法在执行时,会调用h里面的invoke方法去执行,意义:实参是传入InvocationHandler接口实现类,要执行里面的invoke()方法。
金手指3:Proxy.newInstance()
Proxy.newInstance()第三个参数需要一个InvocationHandler接口实现类,所以要提供一个InvocationHandler接口实现类,实现invoke()方法。
invoke()方法三个参数,第一个proxy,一般没用,第二个是method,用来调用方法method.invoke(),既然是调用方法,就要传递对象和参数,第三个是args参数。
这样,Proxy.newInstance()得到代理对象,然后代理对象.functionXxx(“参数”)就调用了invoke(),代理对象 functionXxx “参数” 分别确定invoke()中的三个参数。
金手指4:InvocationHandler接口的invoke()方法(三个参数+返回值)
第一个参数proxy,可以用来绑定实例接口的方法;
第二个参数method ,是调用的方法,可以用来方法过滤,得到方法的声明类等等;
第三个参数就仅仅是被调用方法的参数罢了。
返回值:类型为Object,可以自己任意返回,可以返回invoke()调用的函数的返回值,也可以返回字符串。
金手指5:newProxyInstance方法(调用对象+三个参数+返回值)
调用对象:在需要创建代理实例的时候才调用,这里是测试类中调用,因为是静态方法,所以是直接类名调用,就是Proxy.newInstance()这样用。
三个参数:
loader: 用哪个类加载器去加载代理对象
interfaces:动态代理类需要实现的接口
h:动态代理方法在执行时,会调用h里面的invoke方法去执行
返回值:返回类型为Object,但是如果去接收的话,返回的就是一个代理对象,第一个测试类(封装一层)和第二个测试类都是使用一个引用接收代理对象。
金手指6:Proxy.newInstance()方法和 InvocationHandler接口的invoke()方法的关系
Proxy.newInstance()方法的第三个参数是InvocationHandler接口的实现类,用来新建 InvocationHandler接口实现类的实例对象,后面动态代理调用方法的时候,就是调用这个 InvocationHandler接口实现类的对象invoke()方法了,而不是其他 InvocationHandler接口实现类的对象的invoke()方法。
7.3.2 RPC全体过程
RPC整体过程:
1、先启动服务端,服务端publisher服务,等待连接,因为是服务端不能断,所以用while(true);
2、然后启动客户端,Proxy.newInstance()得到代理对象,然后代理对象.functionXxx(“参数”)就调用了invoke(),先打印一句 System.out.println("Hello Proxy:invoke");
,然后发送网络请求;
3、客户端中的invoke()方法中的method参数没有用来调用客户端自己的方法(因为不需要调用自己方法,要完成网络通信,去调用服务端的方法(就是使用ip:port建立连接,然后生成代理对象,开始传递类名、方法名、参数类型列表,args参数用来传递参数,传递给服务端,让服务端根据提供的信息发射调用自己的方法,然后将反射调用方法的返回值传递过来,客户端接收)),而是用来设置类名、方法名、参数类型列表,客户端中的invoke()方法中的args参数也是用来传递参数,传递给服务端,让服务端根据提供的信息发射调用自己的方法(金手指:客户端传递之前将数据包装成一个RpcRequest对象,所以服务端getOutputStream()直接强转(RpcRequest));
4、服务端将反射调用方法的返回值传递过来,客户端接收,客户端打印"这里是服务端sayHello的实现",这样完成客户端调用服务端的方法就像调用自己的方法一样,这就是RPC整个过程。
值得注意的是,客户端调用的这个sayhello()是有参数的,在客户端的InvocationHandler接口实现类的invoke()方法中,使用 request.setParameters(args); 将实参传递过去,对于客户端传递的四个参数,服务端的invoke()方法中对其接收处理,如下:
Class clazz = Class.forName(rpcRequest.getClassName()); // className用于确定字节码
Method method = clazz.getMethod(rpcRequest.getMethodName(), rpcRequest.getType()); // getMethod需要方法签名 methodname和getType设置
Object result = method.invoke(service, rpcRequest.getParameters()); // invoke需要对象和实参 service是new HelloService(); 实参使用 args 传递过来的
四个参数与反射:
四个参数:classname得到字节码,methodname+type 方法签名 args 方法参数;
反射:因为是反射调用,所以对象必须是实现类,客户端中动态代理的invoke中的对象参数也是实现类。
7.3.3 RPC涉及的技术(五个)
RPC目的:客户端调用服务端的方法就像调用自己的一样。
要完成RPC的目的,整个RPC涉及的技术:
1、通信:客户端给服务端通信,服务端给客户端通信,这里用socket通信
2、通信的内容,既要通信,一定不能没有通信内容,通信内容类一定要是可序列化的,实现Serializable接口,通信发送和接收一定要序列化和反序列化,这是使用javaIO流实现。
3、通信的内容,通信内容类要实现Serialable接口,就要指定调用服务端的哪个类的哪个方法,包括确定方法+调用方法:
确定方法:类使用字节码作为一个标识,方法使用方法签名作为唯一标识,使用客户端InvocationHandler实现类中的invoke()方法中的method参数来完成。
调用方法:给出方法实参列表,使用客户端InvocationHandler实现类中的invoke()方法中的args参数来完成。
4、客户端动态代理:代理分为两种静态代理和动态代理,编译时确定代理的实际类和运行时确定代理的实际类。这里使用Proxy.newInstance()完成。
5、服务端反射调用:客户端要调用服务端的方法,传递方法的唯一坐标,服务端不大可能new一个对应的对象来调用方法(也可以这样做,只是不优雅,RPC框架源码没有new的),一般是使用反射调用。
7.3.4 手写RPC和RPC框架
这里是我们自己写RPC框架,所以底层五个技术都是自己实现,好的RPC框架都是封装好的,本文方便我们理解rpc框架。
八、尾声
手写RPC,深入底层理解整个RPC通信,完成了。
天天打码,天天进步!!!
以上是关于手写RPC,深入底层理解整个RPC通信的主要内容,如果未能解决你的问题,请参考以下文章