第十四篇:手写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;
    }
 
    @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的源码:

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
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的),一般是使用反射调用。

金手指:RPC涉及的技术
三者必不可少:通信+通信内容Serializable和序列化+客户端动态代理与服务端反射调用;
其余两个:注册中心管理服务器地址 + Spring IOC管理bean(xml或者注解+扫描)。

7.3.4 手写RPC和RPC框架

这里是我们自己写RPC框架,所以底层五个技术都是自己实现,好的RPC框架都是封装好的,本文方便我们理解rpc框架。

八、尾声

手写RPC,深入底层理解整个RPC通信,完成了。

天天打码,天天进步!!!

工程文件:https://download.csdn.net/download/qq_36963950/12482263

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祖母绿宝石

打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值