从零实现一个简易rpc框架

101 阅读5分钟

一、RPC是什么

Remote Procedure Call,远过程调用

所谓远过程调用通俗地讲,就是某个程序调用了远方另一个非本程序的方法,本质上就是进程之间的网络通信。更进一步说,每一个进程都有一个自己的地址空间,如果该进程调用的方法存在于本进程的地址空间,那么就是普通的本地调用;若该进程调用的方法不存在本进程的地址空间中,而是在另一个进程的地址空间中,那么就是远过程调用。

二、实现一个简单RPC

分析一下实现一个简单rpc的过程。

2.1 C/S模型

方法的请求者就是client方,方法的提供者就是我们的server方,两者通过网络通信来传递数据。

  • client端。client方要使用服务,最终就是要使用方法,所以client至少需要提供:

    • 调用方法的类,即谁来调用方法;
    • 方法名,方法参数,即确定调用哪一个方法;
  • server端。server端主要就是提供方法调用的服务,所以需要有以下的职责:

    • 注册可用方法,要提供方法首先就要有方法;

    • 获取服务,返回服务结果,即给client提供远程调用的服务;

      public interface ServiceCenter {
          // 开始监听网络端口,提供服务
          void start() throws IOException;
          
          // 停止服务
          void stop();
          
          // 注册服务接口及对应实现
          void register(Class<?> serviceInterface, Class<?> impl); 
          
          // 用于客户获取服务端口
          int getPort();
      }
      

2.2 网络通信

远程的通信需要通过网络来实现,网络的传输本质上就是字节序列的搬运,所以无论是客户端发出的请求还是服务端返回的服务,都需要序列化成字节序列发送。对象变成字节序列使用的是序列化方法,反之从字节序列还原一个对象使用的就是反射的方式了。

为了能够很好的解释来自客户端的rpc请求到底是什么含义,双方都需要对请求的结构达成一个协议:

public class RpcRequest implements Serializable {
    private String serviceName; // 服务对象的名称
    private String methodName;  // 要使用服务对象的哪个方法
    private Class<?>[] paraTypes;   // 方法参数类型
    private Object[] params;    // 方法参数列表
    
    // constructor, getters and setters ...
}

2.3 server服务提供

服务中心启动之后的主要任务就是:监听网络请求,获取到请求后,根据请求的参数从注册中心中找到方法并调用,接着通过网络返回结果。所以服务中心服务提供的主要逻辑如下:

public void start() throws IOException {
    // 1. 服务中心拥有了socket才打开了网络的通信的门
    try (ServerSocket serverSocket = new ServerSocket()) {
        serverSocket.bind(new InetSocketAddress(port)); // socket = (ip地址,端口)
        System.out.println("Service center start :)");
        while (true) {
            // 2. 阻塞等待客户连接
            // 3. 接收到tcp请求就交给一个线程进行处理
            threadPool.execute(new ServiceTask(serverSocket.accept()));
        }
    }
}

其中ServiceTask就是rpc请求处理的封装,里边的主要逻辑是:

  • 通过socket获取tcp的字节流输入,进而得到一个rpc请求对象;
  • 根据rpc请求的各个参数从注册中心获取服务对象;
  • 服务对象调用方法得到结果,再通过socket传回给客户端。
// 最终分配给线程的是可运行的任务
// 这里就将线程到底要干什么封装
private class ServiceTask implements Runnable {
    // 线程的任务就是要服务远程的客户
    // 网络通信就必须获取ip地址和端口,socket就是这两者的封装
    Socket clientSocket;

    public ServiceTask(Socket client) {
        this.clientSocket = client;
    }

    @Override
    public void run() {
        // TCP是字节流的传输,所以对象必然是序列化成字节码之后发过来
        // 所以需要获取对象的流输入输出
        try (ObjectInputStream ins = new ObjectInputStream(clientSocket.getInputStream());
             ObjectOutputStream outs = new ObjectOutputStream(clientSocket.getOutputStream())) {
            System.out.println("# Service center connected to client");
			
            // 1. 获取rpc请求对象的所有参数
            RpcRequest req = (RpcRequest) ins.readObject();
            String serviceName = req.getServiceName();
            String methodName = req.getMethodName();
            Class<?>[] paraTypes = req.getParaTypes();
            Object[] params = req.getParams();

            // 2. 根据参数从注册中心获取服务对象
            Class<?> serviceClass = registry.get(serviceName);
            if (serviceClass == null) {
                throw new ClassNotFoundException(serviceName + " not found in service center");
            }
            // 3. 获取服务对象的方法
            Method method = serviceClass.getMethod(methodName, paraTypes);
            // 4. 调用方法,获得结果
            Object result = method.invoke(serviceClass.newInstance(), params);
            // 5. 将结果返回给客户端
            outs.writeObject(result);

            System.out.println("# Service center do service and return result");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (clientSocket != null) {
                    clientSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

2.4 client服务获取

客户端最终是通过反射字节码的方式得到服务的,所以可以使用JDK提供的动态代理方法来动态获取一个服务对象的代理,接着通过该代理对象来间接调用远程服务器的方法。所以与服务器的网络通信实际上在代理对象内部进行完成,客户只是把这个代理对象当作其服务的提供者。

public class RpcClient {
    private Socket socket;

    /**
     * 获取远程服务对象
     * @param serviceInterface 服务接口
     * @param serviceAddr   服务中心地址
     * @return  服务代理对象
     */
    public Object getRemoteServiceProxy(
        final Class<?> serviceInterface,
        final InetSocketAddress serviceAddr
    ) {
        // 使用jdk动态代理
        return Proxy.newProxyInstance(
            serviceInterface.getClassLoader(),
            new Class<?>[]{serviceInterface},
            (proxy, method, args) -> {  // 定义代理对象要干啥
                System.out.println("> Proxy do work");
                // 1. 创建socket,与服务器建立连接
                socket = new Socket();
                socket.connect(serviceAddr);

                System.out.println("> Socket connected");

                // 2. 将服务请求序列化成字节流发送给服务中心
                try (ObjectOutputStream outs = new ObjectOutputStream(socket.getOutputStream());
                     ObjectInputStream ins = new ObjectInputStream(socket.getInputStream())) {

                    outs.writeObject(new RpcRequest(
                        serviceInterface.getName(),
                        method.getName(),
                        method.getParameterTypes(),
                        args
                    ));

                    System.out.println("> Proxy sent RPC request");

                    // 3. 阻塞等待服务中心返回服务结果
                    return ins.readObject();
                } finally {
                    if (socket != null) {
                        socket.close();
                    }
                }
            }
        );
    }
}

至此,一个极简的rpc框架就完成了!跑起来试试:

完整源码:gitee.com/bankarian/p…