Loading...
墨滴

lyq

2021/11/06  阅读:39  主题:默认主题

手写一个简单的RPC框架

手写一个简单的 RPC 框架


前言

前段时间面试的时候,被问到怎么实现一个简单的 rpc 框架,所以今天抽空实现一个简单的 rpc 远程调用。

简述

RPC 即远程过程调用。在分布式环境中,客户机和服务器在不同的机器上运行,客户端通过网络通信,调用在服务器端运行的过程,并把结果发送回客户端。

程序调用本地方法时,在同一个进程,共享内存区域,但远程过程调用则发生在不过的机器,不同进程,客户端要调用服务端的方法,就需要解决一下几个关键点:

寻址

分布式应用一般都会部署在不同的服务器上,而且同一个应用会部署多个节点。这样客户端调用服务端就需要知道服务端的地址,端口等信息。在并发量较大的情况下,需要将大量请求均匀分发到多个服务端,以提升系统吞吐量。所以 PRC 框架需要注册中心来完成服务注册与发现,以及负载均衡的功能。常见的注册中心就是 zookeeper、Eureka。因为寻址其实就是基于观察者模式来实现,所以原则上 redis,消息队列等提供发布订阅功能的中间件也能做注册中心。

序列化、反序列化

要实现远程服务调用就需要通过网络来传输请求对象信息,那就要实现对象的序列化反序列化方法。序列化方法的实现方式很大程度上影响 RPC 框架的性能优劣。比如 JDK 自带的序列化、hession2、kyro、protostuff

网络协议

服务调用的网络通讯协议多种多样,有基于 socket 的、有用 NIO 的、也有用 http、tcp 协议的。网络协议也是 RPC 框架的性能决定性因素之一。所以有许多优秀的网络通讯的框架,比如 netty、mina。

主流 RPC 框架介绍

目前流行的 RPC 框架有

dubbo

阿里巴巴 2012 年开源的优秀轻量级 RPC 框架,后捐献到 Apache 基金会,2019 年 5 月毕业,成为 Apache 顶级项目。dubbo 在国内被广泛使用。支持的注册中心有 Muticast、Zookeeper、Redis、Simple。默认采用 Hessian 做序列化策略,网络通讯框架使用 Netty,默认使用 dubbo 协议。适用于服务消费者数量远大于消费提供者的场景。

thrift

由 facebook 推出,使用 socket 进行数据传输,数据以特定的格式发送,接收方进行解析,支持多种协议。是一个支持多语言的 RPC 框架。

gRPC

Google 开源的一个高性能、通用的 RPC 框架,主要面向移动应用开发并基于 HTTP/2 协议标准,基于 ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。

另外还有各大互联网公司开源的优秀 RPC 框架,诸如 Twitter 的 Finagle、新浪的 Motan、腾讯的 Tars、当当在 dubbo 基础上开源 dubbox、百度的 brpc 等。

手写简单 RPC 框架

为了深入理解 rpc 基本原理,下面手写一个最简单的 rpc 框架,用 java 的 BIO 做序列化、socket 网络通讯。负载均衡,服务注册功能就不写了,主要是理解 RPC 底层如何通过动态代理、反射技术实现远程服务调用。

公共 api

定义 api 接口

/**
 * 该接口为Dubbo的服务端、消费端公用的接口定义。
 * 当前案例中,使用的是直接复制代码的方式,这不是最优雅的使用方法。更好的建议是通过maven坐标的方式独立维护api。
 */

public interface EchoService {
    String echo(String message);
}

客户端

/**
 * 服务消费者
 *
 * @author leo
 * @date 2021/11/04
 */

public class Consumer {
    /**
     * 本地调用远程服务
     *
     * @param args
     */

    public static void main(String[] args) {
        //此处可以利用FactoryBean创建一个代理类并注册到spring容器中
        EchoService echoService = (EchoService) rpc(EchoService.class);
        //通过依赖注入的方式获取EchoService bean实例
        String str = echoService.echo("hello");
        System.out.println("本地输出远程调用结果:\n" + str);
    }

    /**
     * rpc
     * 动态创建代理对象
     * Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
     * 参数1:真实对象的类加载器
     * 参数2:真实对象实现的所有的接口,接口是特殊的类,使用Class[]装载多个接口
     * 参数3: 接口,传递一个匿名内部类对象
     *
     * @param clazz clazz
     * @return {@link Object}
     */

    public static Object rpc(final Class clazz) {
        return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
            /*
             * @param proxy  代理对象
             * @param method    代理的方法对象
             * @param args  方法调用时参数
             * @return
             * @throws Throwable
             */

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket socket = new Socket("127.0.0.1"8888);
                String className = clazz.getName();//api类名
                String methodName = method.getName();//api 类成员方法名
                Class<?>[] parameterTypes = method.getParameterTypes(); //类成员方法参数类型集合

                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectOutputStream.writeUTF(className);
                objectOutputStream.writeUTF(methodName);
                objectOutputStream.writeObject(parameterTypes);
                objectOutputStream.writeObject(args);

                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                Object o = objectInputStream.readObject();
                objectInputStream.close();
                objectOutputStream.close();
                return o;
            }
        });
    }

}

部分改进的点,已经在代码里做了说明,大家可以自行扩展。主要就是利用 jdk 的动态代理,创建一个代理类,在执行目标方法时会调用 invoke 方法,方法内部通过 socket 连接服务端,利用 java 反射获取 api 相关类名、方法名等并作为参数发送给服务端,并等待服务端的响应。

服务端

  1. 服务提供者实现 api 接口
public class EchoServiceImpl implements EchoService {
    @Override
    public String echo(String message) {
        return "[ECHO] " + message;
    }
}
  1. 定义服务端
/**
 * 服务提供者
 *
 * @author leo
 * @date 2021/11/04
 */

public class Provider {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8888);
            System.out.println("启动远程服务监听...");
            //监听客户端发来消息
            while (true) {
                Socket socket = serverSocket.accept();
                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                //读取客户端传输协议包
                String className = objectInputStream.readUTF();
                String methodName = objectInputStream.readUTF();
                Class<?>[] parameterTypes = (Class<?>[]) objectInputStream.readObject();
                Object[] arguments = (Object[]) objectInputStream.readObject();

                Class clazz = null;
                //服务注册:API到具体实现的映射
                //服务提供端可以将api实现类注册到spring容器中,然后这里可以根据类型进行依赖查找
                //Class clazz = Class.forName(className);
                if (className.equals(EchoService.class.getName())) {
                    clazz = EchoServiceImpl.class;
                }
                //传入方法名,方法参数类型获得方法对象
                Method method = clazz.getMethod(methodName, parameterTypes);
                //传入实现类对象,方法参数数组,方法对象执行获得返回结果对象
                Object result = method.invoke(clazz.newInstance(), arguments);

                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectOutputStream.writeObject(result);
                objectOutputStream.flush();

                objectInputStream.close();
                objectOutputStream.close();
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服务端通过 socket 等待客户端连接,当有连接到来时,获取请求参数,然后通过 java 反射,调用本地 api 实现类并将调用结果响应给服务端。

测试

先启动服务端

然后再启动客户端

以上就是一个最简单的 RPC 框架,不包含负载均衡,注册中心,服务治理,集群容错等高级用法。主要运用 socket、bio、动态代理、反射等基本知识点,实现 RPC 的基本功能,远程过程调用。

更多原创文章,请扫码关注我的微信公众号
更多原创文章,请扫码关注我的微信公众号

lyq

2021/11/06  阅读:39  主题:默认主题

作者介绍

lyq