手写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。

如图:
手写RPC,深入底层理解整个RPC通信

注意:无论新建Maven Project还是Maven Modul,事先要设置好idea中的maven home,user settings file,maven repository,类似笔者电脑如下:
手写RPC,深入底层理解整个RPC通信

2.2 rpcServerApi被rpcServerProvider 和 rpcClient 引用

将rpcServerApi 作为依赖在rpcServerProvider中使用,然后,将rpcServerApi Maven clean,再Maven install,就可以生成jar包安装到本地的maven repository中,这样,让rpcClient再次引入rpcServerApi作为依赖。

如图:
手写RPC,深入底层理解整个RPC通信

注意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 提供接口

手写RPC,深入底层理解整个RPC通信

2.3.2 rpcServerProvider

手写RPC,深入底层理解整个RPC通信
publisher()方法为服务端发布一个服务
手写RPC,深入底层理解整个RPC通信
手写RPC,深入底层理解整个RPC通信

2.3.3 rpcClient 动态代理使用IHelloService

由于客户端和服务端是两个JVM中的程序,即使现在将服务端工程的api模块maven install到本地仓库,客户端程序可以通过导入依赖找到IHelloService,但是如何实例化IHelloService呢?毕竟IHelloService是接口,无法自己实例化,api模块中也没有其子类实现(provider模块中倒是有IHelloService的子类实现,但是客户端程序并未导入其依赖)。所以,由于api模块本身无法实例化IHelloService,所以客户端无法通过new 实例化IHelloService,这里采用动态代理的方式。即客户端拿到的IHelloService只是一个引用,需要使用远程代理。
手写RPC,深入底层理解整个RPC通信

手写RPC,深入底层理解整个RPC通信

手写RPC,深入底层理解整个RPC通信

2.4 客户端动态代理成功

先运行rpcServerProvider工程的Main类的main方法,启动服务端,绑定8080端口;然后,启动rpcClient工程的App类的main方法,去连接8080端口,运行成功:
手写RPC,深入底层理解整个RPC通信

代理对象调用sayhello()方法就是执行invoke()方法里面的逻辑,这里执行了,打印成功。

三、客户端连接服务端并数据传送

3.1 服务端 rpcServerProvider

服务端rpcServerApi模块,提供网络通信Bean类 RpcRequest,因为这个Bean类用于网络通信,所以变为可序列化,实现Serializable接口。
手写RPC,深入底层理解整个RPC通信
将 rpcServerProvider maven install 更新下,这样更改对于rpcClient就可见了。

3.2 客户端 rpcClient

客户端提供RpcNetTransport类,用于Socket连接,newSocket()方法提供连接服务端socket,send()提供调用newSocket()连接服务端。
手写RPC,深入底层理解整个RPC通信

手写RPC,深入底层理解整个RPC通信

注意到,服务端api模块中,RpcRequest是一个实体类,所以客户端可以直接使用new新建实例,但是刚才的IHelloService不行,只能使用动态代理。

四、多线程、传送数据Bean的序列化和反序列化

4.1 服务端 rpcServerProvider 接收客户端发送的数据

手写RPC,深入底层理解整个RPC通信

手写RPC,深入底层理解整个RPC通信

4.2 客户端 rpcClient 写数据到服务端

手写RPC,深入底层理解整个RPC通信

五、服务端反射调用并返回给客户端

5.1 rpcServerProvider 服务端返回数据给客户端

服务端收到客户端发来的数据反序列为RpcRequest,这里面装载着classname methodname type Parameters,服务端invoke()根据这些信息反射调用方法,将得到的结果记为result,并发送给客户端。
手写RPC,深入底层理解整个RPC通信

5.2 rpcClient 客户端接收数据

客户端的send()方法中应该完善与服务端的交互。
手写RPC,深入底层理解整个RPC通信

客户端的动态代理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; }  @Override public String doSomething(String thingsNeedParm) { System.out.println("使用" + thingsNeedParm + "做了一些事情"); return "调用成功"; }  @Override 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 异常 */ @Override 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; }  @Override 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通信的主要内容,如果未能解决你的问题,请参考以下文章

带你手写基于 Spring 的可插拔式 RPC 框架整体结构

手写简易版rpc框架,理解远程过程调用原理

手写简易版rpc框架,理解远程过程调用原理

手写简易版rpc框架,理解远程过程调用原理

手写一个自己的 RPC 框架?

深入浅出掌握grpc通信框架