美文网首页
Java 实现简单的RPC

Java 实现简单的RPC

作者: 东户舟先生 | 来源:发表于2019-10-16 18:51 被阅读0次

[TOC]

RPC解析

什么是RPC呢?RPC(Remote Procedure Call)是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。分布式系统各个几点之间通信通常只能依靠网络,因此RPC机制广泛的应用在分布式系统当中,典型的一个场景就是主从分布式架构系统中的主节点给工作节点部署任务。本文将通过一个的简单的例子讲解RPC的执行过程。

RPC涉及到Java网络通信,反射机制,代理模式(动态代理技术)。考虑到同学们可能对上述技术并不了解因此本文先简单介绍上述技术点,然后再通过例子讲解RPC的工作过程。

如果同学们已经了解相关技术可以直接跳至最后一节。

JavaSocket

Java网络通信技术请见前文JavaSocket

反射机制

Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 Java 的反射机制

反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)

我们用它可以做什么事呢?

  • 运行时生成实例并调用其某方法

  • 在类没有设置set方法时,修改类某实例的私有成员变量

  • 客户端向服务端传递类信息

  • 等等

获取类信息的三种方法:(Object表示任意类,object表示任意类的实例)

Class<?> clazz = Object.class // 类名.class 
Class<?> clazz = object.getClass() //对象.getClass()    
Class<?> clazz = Class.forName("ClassName") // 在编译时不能确定需要的是哪个类时使用

Class类中生成实例的方法:

Object newInstance() // 默认使用类的无参数构造器,如果创建有参数实例,需要先调用
    //getConstructors(),得到一个Constructor构造器的实例。   
object.getClass().newInstance()// 使用无参构造函数创建实例
clazz.getConstructor(String.class).newInstance(“233”) //获取String的构造函数,新建实例

Class类中获取类信息的方法:

Field[] getFields() // 获取成员变量
Method[] getMethods() // 获取方法
Constructor[] getConstructors() // 获取构造函数

java.lang.reflect类库中有三个类:

  • Field:描述类的域
  • Method:描述类的方法
  • Constructor:描述类的构造器

在Field类中:

public void set(Object obj, Object value) //可以修改任意对象的某成员变量值
    //参数1:对象  参数2:要修改的值

在Method类中:

 Public Object invoke(Object implicitPara,Object[] explicitPara) // 调用某类的任意方法
     //参数1:为实现类 参数2: 为方法参数

反射机制就是通过这些类库中类的方法实现动态获取类型信息。

在本文介紹RPC的过程客户端通过反射(Object.class.getMethod())获取接口代理类的方法信息,然后通过网络传递到服务端,服务端根据传来的方法名及参数(methodName``parameter)结合反射调用服务类(method.inovke())即被代理类的方法。

代理模式

给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问,。

这种方法有两个好处

  • 保证客户端对实现类的透明调用
  • 实现类扩展功能代码简洁

代理模式角色分为 3 种:

Subject(接口):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

RealSubject(接口的实现类):真正实现业务逻辑的类;

Proxy(代理类):用来代理和封装真实主题;

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层即接口。

代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理等等。

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 所谓静态也就是在程序运行前就已经存在代理类的.class字节码文件,代理类和真实主题角色的关系在运行前就确定了
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的.class字节码文件,即运行前代理类并不知道要代理谁

静态代理

现在学习一下静态代理,直观感受一下代理模式的用处,理解静态代理的缺点,之后再介绍动态代理。

下面的是一个打官司的例子。首先创建一个诉讼接口类Ilawsuit,以及一个具体诉讼人XiaoGang

public interface ILawsuit {
    //提交申请
    void submit();
    
    //进行举证
    void burden();
    
    //开始辩护
    void defend();
    
    //诉讼完成
    void finish();
}

4个方法表示诉讼的一般流程。

public class XiaoGang implements ILawsuit {
    @Override
    public void submit() {
        //老板拖欠小刚工资 小刚只好申请仲裁
        System.out.println("老板拖欠工资!特此申请仲裁!");
    }
    @Override
    public void burden() {
        System.out.println("这是合同书和过去一年的银行工资流水!");
    }
    @Override
    public void defend() {
        System.out.println("证据确凿,无需多言!");
    }
    @Override
    public void finish() {
        System.out.println("诉讼成功,老板即日起7天内结算工资");
    }
}

小刚胆子小不想自己直接面对法官的质询,因此找律师来帮他打官司。律师就是小刚的代理者,小刚即为被代理者。

public class Lawyer implements ILawsuit {
    //被代理者的引用
    private ILawsuit mLawsuit;
    
    public Lawyer(ILawsuit lawsuit) {
        before();
        mLawsuit = lawsuit;
        after();
    }
    
    @Override
    public void submit() {
        before();
        mLawsuit.submit();
        after();
    }
    
    @Override
    public void burden() {
        before();
        mLawsuit.submit();
        after();
    }
    
    @Override
    public void defend() {
        before();
        mLawsuit.defend();
        after();
    }
    
    @Override
    public void finish() {
        before();
        mLawsuit.finish();
        after();
    }
    
    private void before() {     // 在执行方法之前执行
        System.out.println("阅读材料");
    }
    private void after() {      // 在执行方法之后执行
        System.out.println("记录问题");
    }
}

律师类中的执行方法实质就是调用被代理者小刚的方法,但是律师每次过程之前都要阅读一下材料,完成之后要记录一下现场的问题(西方律师)。

那么相当于是对原有方法的扩充,这是并不需要修改被代理者小刚的代码,只需要在代理者代码执行操作前中添加before()方法和after()方法即可。

这即是代理模式的一个优点,增强功能不入侵被代理者代码。一个代理者可以代理多个类,通过代理类修改被代理类方法大大简化了修改工作,而且代码也更整洁(不必修改每个被代理类的代码)。

public class Client {
    public static void main() {
        // 构造一个小刚
        ILawsuit xiaogang = new XiaoGang();
        
        // 构造一个代理律师将小刚作为构造参数传递进去
        ILawsuit lawyer = new Lawyer(xiaogang);
        
        // 诉讼律师提交诉讼申请
        lawyer.submit();
        // 律师进行举证
        lawyer.burden();
        // 律师代替小刚进行辩护
        lawyer.defend();
        // 完成诉讼
        lawyer.finish();
    }
}

静态代理的缺点

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护

如何改进?

让代理类动态的生成,即下面要讲的动态代理

动态代理

为了让生成的代理类与目标对象(真实主题角色)保持一致性,通常有下面两种最常见的方式:

  1. 通过实现接口的方式 -> JDK动态代理
  2. 通过继承类的方式 -> CGLIB动态代理

本文介绍一下JDK动态代理模式,JDK动态代理主要涉及两个类:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler。还是上面的例子来学习动态代理。首先编写一个动态代理类实现 InVocationHandler,重写invoke方法,在invoke方法内部真正调用实现类的方法,这里用到了反射机制。

public class DynamicProxy implements InvocationHandler {
    private Object obj; // 被代理类的引用
    
    public DynamicProxy(Object) {
        this.obj = obj;        
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //调用被代理对象的方法
        Object result = method.invoke(obj, args);
        return result;
    }
}

之后修改客户端逻辑如下

public static void main(String[] args) {
    // 构造一个小刚...
    ILawsuit xiaogang = new XiaoGang();
    
    // 构造一个动态代理,被代理者是在运行时指定的
    DynamicProxy proxy = new DynamicProxy(xiaogang);
    
    // 获取被代理类小刚的ClassLoader
    Classloader loader = xiaogang.getClass().getClassLoader();
    
    // 动态构造一个代理者律师,这里会动态生成一个.class文件
    ILawsuit lawyer = (ILawsuit) Proxy.newProxyInstance(loader, new Class[] { ILawsuit.class }, proxy);
    
    // 律师提交诉讼申请
    lawyer.submit();
    
    // 律师进行举证
    lawyer.burden();
    
    // 律师代替小刚进行辩护
    lawyer.defend();
      
    // 完成诉讼
    lawyer.finish();
}

通过上述例子总结使用动态代理过程如下:

  • 定义一个方法接口 Interface
  • 被代理者实现上述定义接口 InterfaceImpl
  • 定义代理类实现InvocationHandler接口,重写invoke方法
  • 调用Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 方法生成代理类对象,
    • ClassLoader loader为被代理者的类加载器
    • Class<?>[] interfaces为被代理者实现的接口
    • InvocationHandler h 为自己实现的代理类
  • 用代理类调用上述实现类中的接口方法,实际上是调用InvocationHandler中的invoke方法
  • InvocationHandler的invoke方法根据反射获取的方法名和参数调用执行被代理类的方法(本地执行)
  • 返回执行结果

可以看到这里生成的动态代理类并没有显示的实现被代理者的方法,而是通过反射的形式获取到被代理类对象的方法并调用,因此也就没有了上述静态代理的缺点。

RPC

好了终于到了本文重点RPC了,现在有这样一个需求。

客户端机器A想要服务端机器B执行一个给定的任务(方法名),这个任务的执行环境(参数)由客户端A给出。任务的执行在服务端B发生,客户端A只关心任务结果并不关心执行过程。那么如何实现这个功能呢?

首先定义一个接口,表示服务端可以帮忙执行的任务列表

public interface HelloService {
    String hello(String name);
    String bye();
}

然后定义一个实现类,表示服务端真正提供的服务

public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "Hello" + name;
    }
    @Override
    public String bye() {
        return "Bye Bye!";
    }
}

定义服务端进程,接收每一个客户端发来的请求,根据网络传入参数调用服务类对应的服务

package rpc;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RpcServer {
    private ExecutorService threadPool;
    private static final int DEFAULT_THREAD_NUM = 10;

    public RpcServer() { //构造函数生成一个线程池
        threadPool = Executors.newFixedThreadPool(DEFAULT_THREAD_NUM);
    }

    public void register(Object service, int port) { //服务端主动注册服务

        try {
            System.out.println("server starts...");
            // TCP socket_server
            ServerSocket server = new ServerSocket(port);
            Socket socket = null; //接收客户端发来的请求
            while ((socket = server.accept()) != null) {
                System.out.println("client connected");
                threadPool.execute(new Processor(socket, service)); //线程池拿出一个线程执行任务  任务一定要实现Runnable接口
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    class Processor implements Runnable {
        Socket socket;
        Object service;

        public Processor(Socket socket, Object service) {
            this.socket = socket;
            this.service = service;
        }

        public void process() {

        }

        @Override
        public void run() { //真正的服务
            try {
                // 解码获取类信息
                ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
                String methodName = in.readUTF(); // 获取方法名
                Class<?>[] parameterTypes = (Class<?>[]) in.readObject(); // 参数类型
                Object[] parameters = (Object[]) in.readObject(); // 参数
                Method method = service.getClass().getMethod(methodName, parameterTypes);                 // 从注册的severice获取服务类,根据客户端传入的
                // 方法名以及参数调用服务类的相关方法
                try {
                    // 方法调用invoke
                    Object result = method.invoke(service, parameters);
                    // 初始化输出即返回值
                    ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
                    // 结果返回值
                    out.writeObject(result);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }
}

开启服务进程

package rpc;

public class Main {
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        RpcServer server = new RpcServer();
        server.register(helloService, 50001);
    }
}

定义客户端进程,通过接口的代理向服务端发送任务请求

package rpc;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

public class RpcClient {
    public static void main(String[] args) {
        // 生成接口的代理类对象 (代理类字节码文件是运行时生成的)
        HelloService helloService = getClient(HelloService.class, "127.0.0.1", 50001);
       
        // 代理类通过内部invoke方法调用接口方法hello,bye 
        System.out.println(helloService.hello("chenqihang"));
        System.out.println(helloService.bye());
    }

    @SuppressWarnings("unchecked")
    public static <T> T getClient(Class<T> clazz, String ip, int port) {
        return (T) Proxy.newProxyInstance(RpcClient.class.getClassLoader(), new Class<?>[]{clazz}, new InvocationHandler() {
            @Override
            public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable {

                Socket socket = new Socket(ip, port);
                // 将类信息编码发送到服务端
                ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
                
                out.writeUTF(arg1.getName()); //hello bye

                out.writeObject(arg1.getParameterTypes()); //方法参数

                out.writeObject(arg2);

                ObjectInputStream in = new ObjectInputStream(socket.getInputStream());                  //arg1.invoke(service,args)
                //这个方法在服务端被调用并返回结果给客户端

                return in.readObject();
            }
        });

    }
}

注意:先开启服务端进程,再执行客户端进程

通过上述代码就实现了客户端进程要求服务端进程Say Hello和Say Bye的功能,当然服务端可以做更复杂的操作。

总结一下如何利用Java实现RPC过程:

  • 定义一个方法接口

  • 在服务端实现接口,并注册服务(服务类

  • 在客户端用(JDK动态)代理机制生成接口的代理类

  • 客户端通过代理类调用接口的方法

  • 代理类自动将方法名和参数通过socket传递给服务端

  • 服务端通过反射接收客户端传递来的方法名和参数

  • 服务端根据参数调用服务类对应方法

  • 服务端将执行结果返回给客户端

当然这只是RPC的一个小的执行框架,相信聪明的同学们已经发现似乎上述过程有些技术是多余的呢,比如代理技术,直接通过反射机制获取信息再传递到服务端似乎也能完成需要的功能,没错这是对的,但是这样写代码是不是过于复杂了呢?一个好的RPC框架既需要满足需要的功能,还需要方便用户使用。更多RPC评价标准同学们可以参考这篇文章。RPC 介绍

为了提高效率和方便使用国内外的互联网公司及研究机构做了大量的工作。

国内

国外

Thrift from facebook https://thrift.apache.org
Avro from hadoop https://avro.apache.org
Finagle by twitter https://twitter.github.I/O/finagle
gRPC by Google http://www.grpc.I/O (Google inside use Stuppy)
Hessian from cuacho http://hessian.caucho.com
Coral Service inside amazon (not open sourced)

关于RPC的介绍到此结束,如有与文章中观点不一致的同学,欢迎与我交流。

相关文章

网友评论

      本文标题:Java 实现简单的RPC

      本文链接:https://www.haomeiwen.com/subject/dagumctx.html