【RPC】基于 BIO 手写简易 RPC 框架(version1)

RPC(Remote procedure call)远程调用技术,核心就是如何实现远程通信,在架构上类似 C/S(注:Tomcat 是 B/S 架构,底层基于 NIO/Netty。),即 Provider(Server) 持续提供服务, Consumer(Client) 远程调用服务。目前 Java 提供的网络编程方式有 BIO、NIO、AIO,本篇我们就基于 BIO 来实现一个简易的 RPC 框架。

PS:RPC 与 HTTP 的关系?RPC 是一种技术的概念名词,HTTP 是一种协议,RPC可以通过 HTTP 来实现,也可以通过Socket自己实现一套协议来实现。

先来看需求:

  1. Producer
    1. 提供一个要暴露的服务(接口)
    2. 通过框架的 publish() 方法就可以发布一个服务实例
  2. Consumer:通过框架的 clientProxy() 方法就可以远程调用 Producer 发布的服务,并且拿到返回结果

所以框架需要关注的几个问题:

  1. Producer 说是发布服务,到底什么叫发布服务?答:类似 C/S 架构,创建一个 SockerServer,持续接收请求
  2. Consumer 如何发起远程调用?答:可以通过 Socket 发送请求信息,然后将这部分发送逻辑封装到代理对象中
  3. Producer 与 Consumer 如何通信,即 Producer 如何知道 Consumer 调用什么服务的什么方法?答:自定义协议(RpcRequest)
  4. Producer 如何处理请求,并返回结果?答:解码 RpcRequest -> 反射执行方法 -> 编码结果
  5. Consumer 收到响应后如何解析?答:解码,返回给用户

框架整体结构如下

在这里插入图片描述

  • provider包:根据 Consumer 请求提供服务,即处理请求并返回结果
  • consumer包:构建服务代理对象,根据调用的服务的方法构建 RpcRequest,然后进行远程调用

我们先来看看 protocol包中的 RpcRequest 是什么。

1.RpcRequest

RpcRequest 封装了消费者要调用方法的具体信息,是我们的自定义协议,或者说是消息格式。

涉及两个过程:

  • Consumer 编码:RpcRequest -> 数据流(二进制) => 告诉 Provider 要执行哪个方法
  • Provider 解码:数据流(二进制)-> RpcRequest => 获取 Consumer 要调用哪个方法
// 注:只有实现了序列化接口,才能实现远程传输
public class RpcRequest implements Serializable {

    private String className;  // 类(接口/服务)
    private String methodName;  // 方法
    private Object[] parameters;  // 参数

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }

    private String version;

    public void setVersion(String version) {
        this.version = version;
    }

    public String getVersion() {
        return version;
    }
}

注意,下面是 provider包 的内容,目的是根据 consumer 包请求提供服务,即处理请求并返回结果…

2.RpcServer

负责将请求派发给不同线程

public class RpcServer {

    // 用线程池实现给所有连接都分配一个线程
    ExecutorService executorServic = Executors.newCachedThreadPool();

    // 发布服务
    // 入参是:服务的实现实例(单例,负责执行consumer要调用的方法),发布的端口
    // 注:如果要发布n个服务,要指定n个端口;线程池的线程为这n个服务的所有连接提供服务
    public void publisher(Object service, int port) {
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(port);
            while (true) {
                Socket socket = serverSocket.accept();
                // 每一个socket,交给一个processorHandler去处理
                executorServic.execute(new ProcessorHandler(socket, service));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


}

3.ProcessorHandler

Provider 线程的具体调用逻辑:

  1. 接收(解码):接收 Client 请求,将二进制流转换成 RpcRequest
  2. 执行:获取要执行的方法和参数,调用服务实现对象去执行方法
  3. 发送(编码):将方法执行结果返回,将基本类型/Java对象转换成 二进制流

PS:由于这里采用的是BIO,并且传输时涉及到对象的编解码,所以

  • 可以统一调用 ObjectOutputStream#writeObject() 编码
  • 可以统一调用 ObjectInputStream#readObject() 解码,然后再将 Object 转换成包装类/普通Java对象
public class ProcessorHandler implements Runnable {

    private Socket socket;
    private Object service;

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

    @Override
    public void run() {
        try {
            // Object~Stream也是包装类,其作用在于将字节流解析成java对象
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            // 解析出具体的请求信息(RPCRequest)
            RpcRequest rpcRequest = (RpcRequest)objectInputStream.readObject();

            // 反射调用本地服务
            Object res = invoke(rpcRequest);

            // 将执行结果返回
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(res);
            objectOutputStream.flush();  // 切记要flush手动刷新

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    // 通过反射具体执行provider提供的方法(即消费者要调用的方法)
    public Object invoke(RpcRequest request) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
    
        // 根据实参获取形参列表
        // 注:获取形参列表后才能确定一个方法
        Object[] args = request.getParameters();
        Class<?>[] types = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            types[i] = args[i].getClass();
        }

        // 通过全类名拿到具体具体Class对象
        Class<?> clazz = Class.forName(request.getClassName());

        // 获取 Method
        Method method = clazz.getMethod(request.getMethodName(), types);
        
        // 执行方法
        // 注:service 这里是单例模式,但是也可以new一个对象后再执行具体方法
        Object res = method.invoke(service, args);

        return res;
    }
}

注意,下面是 consumer包 内容,目的是构建服务代理对象,根据调用的服务的方法构建 RpcRequest,然后进行远程调用…

4.RpcProxyClient

代理对象,通过 JDK 动态代理生成一个代理对象

PS:这里创建一个代理对象是因为,服务的实现实例在 Provider,但 Consumer 调用服务的具体方法时也需要一个实例,而 Consumer 并没有这个实例。

public class RpcProxyClient {

    // 创建代理对象,代理的就是指定服务
    // 注:因为 Provider 发布时就是一个端口一个服务,所以这里代理的唯一标识就是 host:port
    public <T>T clientProxy(final Class<T> interfaceCls, final String host, final int port) {
        return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls},
                new RemoteInvocationHandler(host, port));
    }

}

5.RemoteInvocationHandler

代理对象的具体逻辑,核心是 invoke 方法,当 Consumer 调用了服务的方法时,就会走到 invoke():

  1. 构建请求信息 RpcRequest
  2. 发送(编码):将 RpcRequest 转换成二进制流,发送
  3. 线程阻塞等待 Provider 处理结果
  4. 接收(解码):接收 Provider 的处理结果,将二进制流转换成基本类型/Java对象,并返回给上层函数

注:234步的逻辑都是网络 IO 相关,所以后面单独封装了一个 RpcNetTransport 类去实现

public class RemoteInvocationHandler implements InvocationHandler {

    private String host;
    private int port;

    public RemoteInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    // 所有请求都会进入这里
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 构建调用Provider的请求参数
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameters(args);

        // 进行远程调用,并返回执行结果
        RpcNetTransport netTransport = new RpcNetTransport(host, port);
        Object res = netTransport.send(rpcRequest);

        return res;
    }
}

6.RpcNetTransport

/**
 * 实现网络调用
 */
public class RpcNetTransport {

    private String host;
    private int port;

    public RpcNetTransport(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public Object send(RpcRequest request) {
        Socket socket = null;
        Object result = null;
        ObjectInputStream inputStream = null;
        ObjectOutputStream outputStream = null;

        try {
            // 将调用的服务的具体信息通过网络写出
            socket = new Socket(host, port);

            outputStream = new ObjectOutputStream(socket.getOutputStream());
            // writeObject实际上是一种序列化
            outputStream.writeObject(request);
            outputStream.flush();

            // 阻塞等待Provider的返回结果
            inputStream = new ObjectInputStream(socket.getInputStream());
            result = inputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return result;
    }

}

好了,到此框架部分的内容就完成了,可以看到 RPC 也无非就是在 IO 基础上,多了个调用指定服务的逻辑而已(核心是:自定义协议+代理对象)。

结果测试

在这里插入图片描述

这里再说一下关于 api 包的内容,包括了服务(接口)和公共模块,是面向接口编程的基础,

  • Provider:提供接口的实现
  • Consumer:根据接口进行调用
public interface TestService {

    String test(String name);
}

Provider

服务实现类:

public class TestServiceImpl implements TestService {

    @Override
    public String test(String name) {
        System.out.println("new requst coming..." + name);

        Random random = new Random();
        String json = "{\"name\":" + "\"" + name + "\"" + ", \"age\":" + random.nextInt(40) + "}";
        return json;
    }
}

发布服务:

public class Provider {

    public static void main(String[] args) {

        RpcServer proxyServer = new RpcServer();
        // 创建一个服务实例
        proxyServer.publisher(new TestServiceImpl(), 8080);
    }
}

Consumer

远程调用服务:

public class Consumer {

    public static void main(String[] args) {

        // 创建代理对象
        RpcProxyClient rpcProxyClient = new RpcProxyClient();

        // 创建一个代理对象
        TestService service = rpcProxyClient.clientProxy(TestService.class, "localhost", 8080);
        // test() 会进行远程调用,阻塞式...
        String json = service.test("老五");
        System.out.println(json);
    }
}

结果如下:

在这里插入图片描述

完整代码我放到 GitHub 上了,有需要参考的同学点击这里跳转…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

A minor

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值