手写一个简单的RPC框架

1、简述

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

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

寻址

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

序列化、反序列化

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

网络协议

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

2、主流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等。

3、手写简单RPC框架

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

代码结构:

[外链图片转存失败(img-uhhmccIJ-1562756759414)(C:\Users\Administrator\Desktop\rpc.jpg)]

核心代码就是框架服务端和客户端的实现。

请求类:RpcRequest

public class RpcRequest implements Serializable {
    //接口全限定名
    private String className;
    //方法名
    private String methodName;
    //方法参数类型
    private Class<?>[] paramTypes;
    //方法参数
    private Object[] params;

    /*getter and setter*/
}

客户端通过该请求告诉服务端要调用哪个服务、哪个方法、方法参数类型及参数。

响应类:RpcResponse

public class RpcResponse implements Serializable {
    //异常
    private Throwable error;
    //调用结果
    private Object result;

    /*getter and setter*/
}

响应类保存了服务调用的结果,和调用失败的错误信息。请求和响应都要进行网络传输,所以要实现Serializable接口。以便序列化。

客户端:RpcClient

public class RpcClient {
    public Object execute(RpcRequest request,String host,int port) throws Throwable {
        Socket server = new Socket(host, port);
        ObjectInputStream ois = null;
        ObjectOutputStream oos = null;
        try{
            //将请求写到连接服务端socket的输出流
            oos = new ObjectOutputStream(server.getOutputStream());
            oos.writeObject(request);
            oos.flush();

            //读取输入流的内容
            ois = new ObjectInputStream(server.getInputStream());
            Object res = ois.readObject();
            RpcResponse response = null;
            if (!(res instanceof RpcResponse)){
                throw new InvalidClassException("相应类型不正确,应当为"+RpcResponse.class+"类型");
            }else{
                response = (RpcResponse) res;
            }
            if (response.getError()!=null){
                throw response.getError();
            }
            return response.getResult();

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw e;
        } finally {
            if (ois!=null)ois.close();
            if (oos != null) oos.close();
            if (server != null) server.close();
        }
    }
}

客户端通过socket连接服务端,将request序列化发送到服务端,等待响应,然后反序列化,读取调用结果。这里要通过动态代理来发送请求,实现基于接口远程调用。客户端本地定义接口,具体的实现类在服务端。客户端代理类:

客户端代理类:RpcClientProxy

public class RpcClientProxy implements InvocationHandler {
    private String host;
    private int port;

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

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> clazz){//clazz必须是接口类型
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(),new Class[]{clazz},RpcClientProxy.this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //这里可以功能增强,比如负载均衡,过滤器
        System.out.println("调用远程方法前,xxxxxx");
        RpcRequest request = new RpcRequest();
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParamTypes(method.getParameterTypes());
        request.setParams(args);
        Object result = new RpcClient().execute(request, host, port);
        //功能增强,比如记录流水信息
        System.out.println("调用远程方法后,xxxxxx");
        return result;
    }
}

通过getProxy方法获取服务接口代理对象,执行目标方法时会调用invoke方法,invoke方法调用了客户端execute方法,发送请求到服务端。

自定义注解:Service

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
    /**
     * 注解所属接口类型
     * @return
     */
    Class<?> value();
}

使用这个注解标记类为服务类,有这个注解的类将被实例化,存进map。value参数为服务的接口类型。

服务端:RpcServer

对于服务端来说,就是五个步骤:

  1. 根据指定端口号,创建socket连接

  2. 通过参数获取要扫描的包路径,扫描之。也就是遍历目录,找到所有有@Service注解的类,实例化它,存在map里。key是类的全限定名,value是实例。

  3. 阻塞等待获取客户端连接,

  4. 拿到连接后,具体的操作放到线程池中去执行

代码如下:

public class RpcServer {
    public void start(int port,String clazz){
        ServerSocket server = null;
        try {
            //1、创建socket连接
            server = new ServerSocket(port);
            //2、获取所有服务类
            Map<String,Object> services = getService(clazz);
            //3、创建线程池
            Executor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>() {
            });
            while(true){
                //4、获取客户端连接
                Socket client = server.accept();
                //5、将服务端被调用的服务放到线程池中异步执行
                RpcServerHandler service = new RpcServerHandler(client,services);
                executor.execute(service);
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (server!=null){
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*扫描包路径获取rpc服务类,并构建实例,与全限定名对应存在map里*/
    private Map<String, Object> getService(String clazz) throws ClassNotFoundException {
        if (clazz == null) {
            throw new ClassNotFoundException("扫描包名为空");
        }
        Map<String,Object> services = new HashMap<>();
        //全限定名数组
        String[] clazzes = clazz.split(",");

        try {
            List<Class<?>> classes = new ArrayList<>();
            for (String cl:clazzes) {
                List<Class<?>> classList = getClasses(cl);
                classes.addAll(classList);
            }
            //通过反射循环创建示例
            for (Class<?> cla:classes) {
                Object object = cla.newInstance();
                services.put(cla.getAnnotation(Service.class).value().getName(),object);
            }
        }catch (InstantiationException | IllegalAccessException | ClassNotFoundException e ) {
            e.printStackTrace();
        }
        return services;
    }

    private  List<Class<?>> getClasses(String packageName) throws ClassNotFoundException {
        //定义返回的列表
        List<Class<?>> classes = new ArrayList<>();
        //找到指定的包目录
        File directory = null;
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        if (contextClassLoader == null) {
            throw new ClassNotFoundException("无法获取到ClassLoader");
        }
        String path = packageName.replace(".","/");
        //TODO 处理目录含中文或者空格的问题
        URL resource = contextClassLoader.getResource(path);
        if (resource == null) {
            throw new ClassNotFoundException("无法获取该资源("+packageName+")");
        }
        directory = new File(resource.getFile());

        if(directory.exists()){
            //获取包目录留下所有文件
            String[] files = directory.list();
            File[] fileList = directory.listFiles();
            for (int i = 0; fileList!=null&&i<fileList.length; i++) {
                if ( null == files[i]) {break;}
                File file = fileList[i];
                //判断是否为class文件
                if (file.isFile()&&file.getName().endsWith(".class")){
                    Class<?> clazz = Class.forName(packageName+"."+files[i].substring(0,files[i].length()-6));
                    if (clazz.getAnnotation(Service.class)!=null) {//如果有@Service注解,添加到列表
                        classes.add(clazz);
                    }
                }else if(file.isDirectory()){    //如果是目录,则递归查找
                    List<Class<?>> classList = getClasses(packageName+"."+file.getName());
                    if (classList != null && classList.size()!=0) {
                        classes.addAll(classList);
                    }
                }
            }
        }else{
            throw new ClassNotFoundException("资源不存在"+directory);
        }
        return classes;
    }
}

核心就是start方法,getService方法主要运用IO操作和反射,找到类的目录判断注解,实例化,注意异常的处理以及资源的关闭。

注意:代码没有处理URL编码问题,如果项目所在文件夹名有中文或空格会有错误。

服务端任务类:RpcServerHandler

现在看一下服务端任务类RpcServerHandler,

public class RpcServerHandler implements Runnable {
    private Socket clientSocket;
    private Map<String,Object> serviceMap;
    public RpcServerHandler(Socket client, Map<String, Object> services) {
        this.clientSocket = client;
        this.serviceMap = services;
    }

    @Override
    public void run() {
        ObjectInputStream ois = null;
        ObjectOutputStream oos = null;
        RpcResponse response = new RpcResponse();
        try{
            ois = new ObjectInputStream(clientSocket.getInputStream());
            oos = new ObjectOutputStream(clientSocket.getOutputStream());

            //反序列化
            Object object = ois.readObject();
            RpcRequest request = null;
            if (!(object instanceof RpcRequest)){
                response.setError(new Exception("请求类型错误"));
                oos.writeObject(response);
                oos.flush();
                return;
            }else {
                request = (RpcRequest) object;
            }
            //查找并执行服务
            Object service = serviceMap.get(request.getClassName());
            Class<?> clazz = service.getClass();
            Method method = clazz.getMethod(request.getMethodName(),request.getParamTypes());
            Object result = method.invoke(service,request.getParams());

            response.setResult(result);
            oos.writeObject(response);
            oos.flush();

        } catch (Exception  e) {
            if (oos != null) {
                response.setError(e);
                try {
                    oos.writeObject(response);
                    oos.flush();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        } finally {
            try {
            if (ois!=null) ois.close();
            if (oos != null) oos.close();
            if (clientSocket != null) clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

该类实现Runnable接口,作为一个任务放在线程池中执行 。收到客户端的连接后,从连接的输入流中读取信息,并反序列化为RpcRequest类。根据request中的className字段,到存放服务实例的map中去找到对应的实例,以及request中的方法有关信息,就可以通过反射调用服务实例的对应方法,最后将执行结果放到RpcResponse类中通过socket连接范送给客户端。

一次远程服务调用完成。

4、测试

定义服务接口:StudentService

定义两个方法,一个在客户端输出,一个在服务端输出。

public interface StudentService {    
    Student getInfo();    
    boolean printInfo(Student student);
}

服务实现类:StuentServiceImpl

简单写一个实现类

@Service(StudentService.class)
public class StudentServiceImpl implements StudentService {

    @Override
    public Student getInfo() {
        Student stu = new Student();
        stu.setId(1);
        stu.setName("zhangsan");
        stu.setAge(20);
        return stu;
    }

    @Override
    public boolean printInfo(Student student) {
        if (student == null) {
            return false;
        }
        System.out.println(student);
        return true;
    }
}

测试类

public class ServerTest {
    public static void main(String[] args) {
        RpcServer server= new RpcServer();
        server.start(9999,"com.youzi.test.rpc.demo");
    }
}

启动服务端,定义端口号和要扫描的包。

public class ClientTest {
    public static void main(String[] args) {
        RpcClientProxy clientProxy = new RpcClientProxy("127.0.0.1", 9999);
        StudentService proxy = clientProxy.getProxy(StudentService.class);
//        System.out.println(proxy.getInfo());
        System.out.println(proxy.printInfo(new Student(2,"lisi",18)));
    }
}

启动客户端,连接本机服务端开的9999端口,拿到服务接口类型StudentService的代理对象,调用目标方法会出发客户端的execute方法,进行远程调用。

测试结果:

在这里插入图片描述

在这里插入图片描述

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

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/95367195