概述

RPC是一个网际协议,用面向对象的方法实现RPC就是RMI。
RMI:远程方法调用
客户端提供给用户一个API,当用户调用其中某个方法时,客户端把要调用的方法和参数发送到服务器端,服务器根据这个方法名称在服务器端匹配到对应的对象,并且执行对应的方法。再把这个结果返回给客户端。

只有服务器端才真正存在拥有这个方法的对象,中间的网络通信过程用户是不知道的,用户就感觉是调用了一个本地方法而已。这样做的好处就是减轻了服务器的负担。

实现思路

客户端的流程

这个接口中的方法是用户需要调用的方法,真正调用方法的肯定是一个继承这个接口的真实对象,这个真实对象只存在于服务器端。但是为了提供给客户端调用,所以我们通过代理的方式,产生一个此接口的代理对象,为了供用户调用。

//用户需要调用的方法
public interface IDoSomething {
    public String doSomething(int a, int b) throws Exception;
}
//通过JDK代理返回一个接口的代理对象,可以看我之前写的代理博客
private <T> T interfaceJDKProxy(Class<?> klass) {
        ClassLoader classLoader = klass.getClassLoader();
        Class<?>[] interfaces = new Class[]{klass};
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object res = JDKProxy.this.methodInvoker.invoke((Object)null, method, args);
                return res;
            }
        });
    }

上述的methodInvoker是一个接口,接口中有一个invoke方法,是这个代理类执行某个代理方法时,具体的方法调用流程

这里用到了很牛逼的思想,面向接口编程。对象是接口类型,在具体需要的时候,set进去一个对应的接口实现类,这就实现了滞后。这也是框架中运用很多的思想。

那么根据我们的需求,这个代理对象执行方法时,要把所要执行的方法和对应参数发送到服务器,并且接受服务器执行结果。所以,我们自己编写的methodInvoke就要完成与服务器通信的部分

下一部分是真正提供给用户产生代理对象的方法,需要传入一个接口类型。其中setMethodInvoker就完成了和服务器的通信任务。我们把方法的全名先发送过去,然后在把参数封装到一个类中,发送这个类的gson字符串,服务器端只需要按照已经定好的规则来解析。

public <T> T getProxy(Class<?> klass) throws InstantiationException, IllegalAccessException
            , MethodInvokerNotSetException {
        this.startup();
        mecProxy.setMethodInvoker(new IMethodInvoker() {
            @Override
            public Object invoke(Object o, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
                Object result = null;
                DataOutputStream dos = null;
                DataInputStream dis = null;
                try {
                    dos = new DataOutputStream(socket.getOutputStream());
                    dis = new DataInputStream(socket.getInputStream());

                    //先发送方法的全名
                    dos.writeUTF(method.toString());

                    //在把参数包装在ArgumentMaker内发送其Gson字符串
                    ArgumentMaker argumentMaker = new ArgumentMaker();
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    for(int i = 0; i < parameterTypes.length;i++){
                        argumentMaker.add("arg"+i, args[i]);
                    }
                    dos.writeUTF(argumentMaker.toString());

                    Type type = method.getReturnType();
                    String res = dis.readUTF();
                    result = RMIServer.gson.fromJson(res, type);
                } catch (IOException e) {
                    if(dos != null){
                        try {
                            dos.close();
                        } catch (IOException ex) {
                        }
                    }
                    if(dis != null){
                        try {
                            dis.close();
                        } catch (IOException ex) {
                        }
                    }
                    if(socket != null){
                        try {
                            socket.close();
                        } catch (IOException ex) {
                        }
                    }

                }

                return result;
            }
        });

        return mecProxy.getProxy(klass);
    }
服务器端的大概流程

服务器端对于每一个客户端的解析程序。
解析方法的全名和参数,得到这个方法所在的类,方法的名称以及参数的类型。
方法所在的类必定是这个接口,所以我们还需要在服务器端匹配一个对应关系。就是该接口和具体实现类的对应关系,写在xml中并对其提前解析。然后根据所找到的类,再通过方法找到具体的类中方法,反射调用执行,结果再返回。

//接口和实现类对应关系,在客户端调用之前,就解析这个xml文件,把这个对应关系通过一个map存放好
<?xml version="1.0" encoding="UTF-8" ?>
<RMIMapping>
    <Mapping interface="rmi.Main.IDoSomething" impl="rmi.Main.DoSomethingImpl"></Mapping>
</RMIMapping>
public class RMIService implements Runnable{
    private Socket socket;

    public RMIService(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            //获得方法的全名
            String fullmethod = dis.readUTF();
            //参数封装的gson字符串
            String arguGson = dis.readUTF();

			//MethodInformation 是对方法的一个封装,包括方法所在类,方法名,变量的类型(暂时用String来表示类型)
            MethodInformation methodInformation = new MethodInformation(fullmethod);
            RMIDefinition definition = RMIFactory.getDefinition(methodInformation.getClassName());

            String[] argTypes = methodInformation.getArgTypes();
            //获得变量类型
            Class<?>[] parameterTypes = getParameterTypes(argTypes);
            //获得变量具体的值
            Object[] argValues = getArgValues(arguGson, parameterTypes);

            Class klass = definition.getKlass();
            Object obj = definition.getObj();
            Method method = klass.getDeclaredMethod(methodInformation.getMethodName(),parameterTypes);
            //反射调用对应方法
            Object res = method.invoke(obj, argValues);

            Type returnType = method.getGenericReturnType();
            //返回执行结果
            dos.writeUTF(RMIServer.gson.toJson(res,returnType));
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private Class<?>[] getParameterTypes(String[] argTypes){
        if(argTypes == null || argTypes.length <= 0){
            return new Class[]{};
        }

        Class<?>[] types = new Class[argTypes.length];
        for(int i = 0;i < argTypes.length;i++){
            types[i] = MecType.toType(argTypes[i]);
        }

        return types;
    }

    private Object[] getArgValues(String arguGson, Class<?>[] parameterTypes){
        Object[] args = new Object[parameterTypes.length];
        ArgumentMaker argumentMaker = new ArgumentMaker(arguGson);

        for (int i = 0;i < args.length;i++) {
            args[i] = argumentMaker.getValue("arg"+i,parameterTypes[i]);
        }
        return args;
    }
Server完整代码

用到了线程池,对于每一个解析程序都用一个线程new RMIService()完成。RMIService便是上面解析和返回的过程。

package rmi.core;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class RMIServer implements Runnable{
    public static int DEFAULT_PORT = 54187;
    public static Gson gson = new GsonBuilder().create();
    private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            10, 120,
            5000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

    private ServerSocket serverSocket;
    private int port;
    private volatile boolean goon;

    public RMIServer() {
        this.port = RMIServer.DEFAULT_PORT;
        this.goon = false;
    }

    public RMIServer(int port) {
        this();
        this.port = port;
    }

    public void startup(){
        try {
            if(null == this.serverSocket && !this.goon) {
                this.serverSocket = new ServerSocket(port);
                this.goon = true;
                new Thread(this,"RMIServer").start();
            }
        } catch (IOException e) {
        }
    }

    public void shutdown(){
        try {
            if(this.serverSocket != null && this.goon) {
                this.serverSocket.close();
                this.goon = false;
                this.serverSocket = null;
            }
        } catch (IOException e) {
            this.serverSocket = null;
        }
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }


    @Override
    public void run() {
        while(this.goon){
            try {
                Socket client = this.serverSocket.accept();
                threadPool.execute(new RMIService(client));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
客户端完整代码
package rmi.core;

import com.mec.util.ArgumentMaker;
import com.mec.util.proxy.IMethodInvoker;
import com.mec.util.proxy.MecProxy;
import com.mec.util.proxy.MethodInvokerNotSetException;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.Socket;

public class RMIClient {
    public static String DEFAULT_IP = "127.0.0.1";
    public static int PORT = 54187;

    private Socket socket;
    private String ip;
    private int port;

    private MecProxy mecProxy;

    public RMIClient() {
        this.mecProxy = new MecProxy();
        this.ip = RMIClient.DEFAULT_IP;
        this.port = RMIClient.PORT;
    }

    public RMIClient(String ip, int port){
        this();
        this.ip = ip;
        this.port = port;
    }

    /**
     * 获取远程代理对象
     * @param klass 要代理的接口
     * @param <T>
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws MethodInvokerNotSetException
     */
    public <T> T getProxy(Class<?> klass) throws InstantiationException, IllegalAccessException
            , MethodInvokerNotSetException {
        this.startup();
        mecProxy.setMethodInvoker(new IMethodInvoker() {
            @Override
            public Object invoke(Object o, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
                Object result = null;
                DataOutputStream dos = null;
                DataInputStream dis = null;
                try {
                    dos = new DataOutputStream(socket.getOutputStream());
                    dis = new DataInputStream(socket.getInputStream());

                    //先发送方法的全名
                    dos.writeUTF(method.toString());

                    //在把参数包装在ArgumentMaker内发送其Gson字符串
                    ArgumentMaker argumentMaker = new ArgumentMaker();
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    for(int i = 0; i < parameterTypes.length;i++){
                        argumentMaker.add("arg"+i, args[i]);
                    }
                    dos.writeUTF(argumentMaker.toString());

                    Type type = method.getReturnType();
                    String res = dis.readUTF();
                    result = RMIServer.gson.fromJson(res, type);
                } catch (IOException e) {
                    if(dos != null){
                        try {
                            dos.close();
                        } catch (IOException ex) {
                        }
                    }
                    if(dis != null){
                        try {
                            dis.close();
                        } catch (IOException ex) {
                        }
                    }
                    if(socket != null){
                        try {
                            socket.close();
                        } catch (IOException ex) {
                        }
                    }

                }

                return result;
            }
        });

        return mecProxy.getProxy(klass);
    }

    private void startup(){
        try {
            if(null == this.socket) {
                this.socket = new Socket(ip, port);
            }
        } catch (IOException e) {
            this.socket = null;
        }
    }

    private void shutdown(){
        try {
            this.socket.close();
            this.socket = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String getIp() {
        return ip;
    }

    public int getPort() {
        return port;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public void setPort(int port) {
        this.port = port;
    }

}

框架怎么用

  1. 在服务器端配置接口对应的实现类(xml文件中)
  2. 服务器设置port,启动服务器
  3. 客户端配置服务器的ip和port
  4. 通过getProxy()方法,传入接口类型,获得代理类
  5. 调用代理类其中的方法
public class TestForClient {
    public static void main(String[] args) {
        RMIClient client = new RMIClient();
        client.setIp("localhost");
        client.setPort(54185);
        try {
            IDoSomething proxy = client.getProxy(IDoSomething.class);
            String res = proxy.doSomething(1,2);
            System.out.println(res);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (MethodInvokerNotSetException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class TestForServer {
    public static void main(String[] args) {
        RMIServer server = new RMIServer();
        RMIFactory.scanMapping("/RMIMapping.xml");

        server.setPort(54185);
        server.startup();
    }
}

和java自带框架的区别

基本看了一下java实现的RMI,是根据一个约定的字符串获得服务器上真是存在的对象引用,然后调用这个对象中的方法。比我自己完成的,省去了参数传递和解析的过程。但是感觉内部还是会去传递参数,因为是俩个不同的jvm上的。对象都是不一样的,没有深究,只是猜测。