如何手写一个RPC(面试要知道)


面试官:
1 Dubbo的底层是什么?
2 你能不能设计一个Dubbo ?
3 注册中心的优化方案是什么?
4 网络的IO 怎么优化?
5 重试机制?
6 线程池问题?

一、RPC 到底是什么?

RPC 就是一个远程调用
什么叫远程调用?
有个问题,我不会,别人会,我让别人帮我做。
例子:大学考试时,你不会,但是室友会!让室友帮我做?

二、场景的模拟

以下代码都是IDEA写,可能有出入
2.0 父pom
在这里插入图片描述
修改父项目的打包方式
在这里插入图片描述
2.1 我
在这里插入图片描述

2.2 室友
在这里插入图片描述
在这里插入图片描述

2.3 rpc-self的场景和启动类

在这里插入图片描述
2.4 室友的场景和启动类
在这里插入图片描述
2.5 解决思路
在这里插入图片描述
2.5.1 我发送题目并且接受答案

package com.zy.rpc;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;

public class SelfApp {
    public static void main(String[] args) throws  Exception{
        System.out.println("我在考试");
        System.out.println("有一个题目1+1=?不会,请教室友");
        System.out.println("怎么请教室友?");
        Socket client=new Socket("localhost",8888);
        String question="1+1=?";
        OutputStream outputStream=client.getOutputStream();
        ObjectOutputStream  objectOutputStream=new ObjectOutputStream(outputStream);
        /**
         * 把题目写过去
         */
        objectOutputStream.writeObject(question);
        /**
         * 接受室友的答案
         */
        InputStream inputStream=client.getInputStream();
        ObjectInputStream objectInputStream=new ObjectInputStream(inputStream);
        String answer = (String) objectInputStream.readObject();
        System.out.println("室友的答案为:"+answer);
    }
}

2.5.2 室友接受题目并且计算以及把答案发给我

package com.zy.rpc;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ClassmateApp {
    public static void main(String[] args)  throws  Exception {
        System.out.println("今天菜逼去考试了,他说让我把手机一直开机,他有题目。。。");
        System.out.println("手机监听菜逼的题目中。。");
        int port=8888;
        ServerSocket serverSocket=new ServerSocket(port);
        System.out.println("开始监听");
        Socket client = serverSocket.accept();
        InputStream inputStream = client.getInputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        String question = (String) objectInputStream.readObject();
        System.out.println("菜逼的题目是:"+question);
        /**
         * 开始帮菜逼计算
         */
        String answer = "2" ;
        /**
         * 把答案发给菜逼
         */
        OutputStream outputStream = client.getOutputStream();
        ObjectOutputStream  objectOutputStream=new ObjectOutputStream(outputStream);

        /**
         * 把答案写个菜逼
         */
        objectOutputStream.writeObject(answer);
    }
}

2.5.3 测试
1 启动室友
在这里插入图片描述
2 启动自己
在这里插入图片描述
参考代码:
http://120.26.80.180/liangtiandong/0812project/tree/master/rpc-demo

三、rpc-core项目

3.1 新建项目
在这里插入图片描述

在这里插入图片描述

3.2 core的作用

完成公共类和模型的封装和提取!

3.2.1 关闭资源的操作
在这里插入图片描述

public final class ResourceUtil {
    /**
     * 关闭资源
     */
    public static void close(Closeable ...res){
        for (Closeable re : res) {
            if(null!=re){
                try {
                    re.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    re = null ; // jvm 可以回收
                }
            }
        }
    }
}

3.2.2 模型类
题目究竟代表啥?
1 + 1 = ?
在这里插入图片描述
在这里插入图片描述

3.3 将自己和室友的代码改造

3.3.1 新建一个接口(在rpc-core)
在这里插入图片描述
3.3.2 对自己(rpc-self)的改造
在rpc-self 里面依赖rpc-core

在这里插入图片描述
对1 + 1 的改造:
在这里插入图片描述
发送题目的改造
在这里插入图片描述
3.3.3 对室友的改造
添加依赖
在这里插入图片描述
接受的题目是Request 不是字符串
在这里插入图片描述

计算
在这里插入图片描述

四、谁应该实现接口?

我们调用室友,室友肯定会这个方法,而我肯定不会!
在这里插入图片描述

五、室友室友这个类来计算答案

/**
     * 室友通过菜逼的题目来做一个计算
     * @param rquest
     * @return
     */
    private static Object invoker(Request rquest) {
        /**
         * 调用的接口
         */
        String interfaceName = rquest.getInterfaceName();
        /**
         * 方法的名称
         */
        String methodName = rquest.getMethodName();
        /**
         * 调用的参数
         */
        Object[] agrs = rquest.getAgrs();
        System.out.println(interfaceName+":"+methodName+":"+agrs);
        // 室友怎么实现计算
//        object.invoke(method,agrs) ;
        // 接口的名称:com.sxt.rpc.service.AddService
        // 实现类的名称:com.sxt.rpc.service.impl.AddServiceImpl
        try {
            Class<?> clazz = Class.forName("com.sxt.rpc.service.impl.AddServiceImpl");
            Class<?> []paramTypes = null ;
            if(agrs!=null){
                paramTypes = new Class<?>[agrs.length];
                for (int i = 0; i < agrs.length; i++) {
                    paramTypes[i] = agrs[i].getClass();
                }
            }
            // 反射得到要调用的方法
            Method method = clazz.getMethod(methodName, paramTypes);
            // 实例化对象
            Object realObject = clazz.newInstance();
            // 完成方法的调用
            Object result = method.invoke(realObject, agrs);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null ;
    }

六、对自己的改造(关键的一步)

6.1 形式
在这里插入图片描述
6.2 使用动态代理的改造

public class SelfApp {

    public static void main(String[] args) {
        System.out.println("我考试了");
        System.out.println("题目不会");
        AddService addService = getProxyObject(AddService.class) ; // 接口,接口能不能被调用?1 实现类(没有) 2 代理对象
        System.out.println(addService); //其实底层调用toString
        Integer result = addService.add(10, 100);
        System.out.println("计算答案为:"+result);
    }

    /**
     * 通过一个接口来创建代理对象 接口:JDK 没有接口:Cglib
     * @param interfacee
     * @param <T>
     * @return
     */
    private static <T> T getProxyObject(Class<T> interfacee) {

        return (T)Proxy.newProxyInstance(SelfApp.class.getClassLoader(), new Class<?>[]{interfacee}, new InvocationHandler() {
            /**
             * 代理对象调用任何方法,都会进入下面的方法里面
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String name = method.getName();
               switch (name){ // 这些都是普通的方法,不需要远程调用
                   case "toString":
                       return interfacee.getName()+"$Proxy"; // 是代理对象调用该方法
                   case "hashCode":
                       return -1;
                   case "equals":
                       return false;
                   default:
                       // add 方法,它需要远程调用
                       Request request = new Request();
                       request.setInterfaceName(interfacee.getName());
                       request.setMethodName(name);
                       request.setAgrs(args);
                       return rpcInvoke(request);
               }
            }
        });

    }

    private static Object rpcInvoke(Request request) {
        Socket client = null ;
        OutputStream outputStream = null ;
        ObjectOutputStream objectOutputStream = null ;
        InputStream inputStream = null ;
        ObjectInputStream objectInputStream = null ;
        try {
             client = new Socket("localhost", 8888);
             outputStream = client.getOutputStream();
             objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(request); // 把题目发个室友
             inputStream = client.getInputStream();
             objectInputStream = new ObjectInputStream(inputStream);
            Object result = objectInputStream.readObject();
            return  result ;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            ResourceUtil.close(objectInputStream,inputStream,objectOutputStream,outputStream,client);
        }
        return  null ;
    }

    public static void main1(String[] args) throws Exception {
        System.out.println("---------我在考试---------");
//        System.out.println("有个题目1 + 1 = ?不会,求救室友");
        System.out.println("怎么求救室友?");
        ////////////////////使用Request 来表示题目//////////////////////
//        String question = "1 + 1 = ?" ;
        Request request = new Request();
        request.setAgrs(new Object[]{10,20});
        request.setMethodName("add");
        request.setInterfaceName("com.sxt.rpc.service.AddService");
        System.out.println("本次的题目为:"+request);
        ////////////////////改造完成////////////////////////
        /**
         * 需要把题目发送给室友
         */
        Socket client = new Socket("localhost", 8888); // 去连接室友

        OutputStream outputStream = client.getOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        /**
         * 把题目写过去
         */
        objectOutputStream.writeObject(request);

        /**
         * 接受室友的答案
         */
        InputStream inputStream = client.getInputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        Integer answer = (Integer)objectInputStream.readObject();
        System.out.println("室友的答案为:"+answer);



    }
}

6.3 把代理对象的部分提取出来
      在rpc-core 里新建工具类

/**
 * 动态代理的工具类
 */
public final class ProxyUtil {

    /**
     * 通过一个接口来创建代理对象 接口:JDK 没有接口:Cglib
     * @param interfacee
     * @param <T>
     * @return
     */
    public static <T> T getProxyObject(Class<T> interfacee) {

        return (T) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(), new Class<?>[]{interfacee}, new InvocationHandler() {
            /**
             * 代理对象调用任何方法,都会进入下面的方法里面
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String name = method.getName();
                switch (name){ // 这些都是普通的方法,不需要远程调用
                    case "toString":
                        return interfacee.getName()+"$Proxy"; // 是代理对象调用该方法
                    case "hashCode":
                        return -1;
                    case "equals":
                        return false;
                    default:
                        // add 方法,它需要远程调用
                        Request request = new Request();
                        request.setInterfaceName(interfacee.getName());
                        request.setMethodName(name);
                        request.setAgrs(args);
                        return rpcInvoke(request);
                }
            }
        });

    }

    private static Object rpcInvoke(Request request) {
        Socket client = null ;
        OutputStream outputStream = null ;
        ObjectOutputStream objectOutputStream = null ;
        InputStream inputStream = null ;
        ObjectInputStream objectInputStream = null ;
        try {
            client = new Socket("localhost", 8888);
            outputStream = client.getOutputStream();
            objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(request); // 把题目发个室友
            inputStream = client.getInputStream();
            objectInputStream = new ObjectInputStream(inputStream);
            Object result = objectInputStream.readObject();
            return  result ;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            ResourceUtil.close(objectInputStream,inputStream,objectOutputStream,outputStream,client);
        }
        return  null ;
    }
}

6.4 简化调用的流程
在这里插入图片描述

七、使用注册中心来解决室友端口变化的问题

7.1 修改室友的监听端口
在这里插入图片描述
7.2 菜逼的调用
在这里插入图片描述
7.3 设计注册中心的数据结构
在dubbo 里面?
/服务的名称
/服务的名称/服务的地址
在这里插入图片描述
7.3.1 添加zk的依赖
在父项目里面添加依赖管理
在这里插入图片描述
在rpc-core 里面引入该依赖

<dependencies>
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
    </dependency>
</dependencies>

7.3.2 使用docker 安装一个zookeeper(ecs)

docker run --name zk -p 2181:2181 -d zookeeper

测试:
在这里插入图片描述
在这里插入图片描述

Ecs 必须放行2181 端口

7.3.4 节点的创建和测试

public static void createNode(){
    /**
     * 节点的类型:4 种
     *  a:顺序的节点(节点后面带有数字的标识)  无序的节点
     *  b:持久的 (创建后一直存在)临时的(创建它的客户端断开连接后,它自动删除)
     *
     */
    zkClient.createPersistent("/0812");
    zkClient.createEphemeral("/rpc");
    zkClient.createPersistentSequential("/0812","xxx");
    zkClient.createEphemeralSequential("/rpc","xxxx");
    System.out.println("创建完成!");
    try {
        System.in.read();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述

7.3.5 zk 的复习

/**
 * zk 的工具类
 */
public class ZKUtil {

    private static final String ZK_SERVER_URL = "120.26.80.180:2181" ; // 大家不要连接我的zk
    private static ZkClient zkClient = null  ;

    static{
        zkClient = new ZkClient(ZK_SERVER_URL);
        System.out.println("zk已经连接成功");
    }

    public static void main(String[] args) {
//        createNode();
//        deleteNode();
//        getSubNode();
        subscribe();
    }

    /**
     * 1 节点的创建
     */

    public static void createNode(){
        /**
         * 节点的类型:4 种
         *  a:顺序的节点(节点后面带有数字的标识)  无序的节点
         *  b:持久的 (创建后一直存在)临时的(创建它的客户端断开连接后,它自动删除)
         *
         */
        zkClient.createPersistent("/0812");
        zkClient.createEphemeral("/rpc");
        zkClient.createPersistentSequential("/0812","xxx");
        zkClient.createEphemeralSequential("/rpc","xxxx");
        System.out.println("创建完成!");
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 2 节点的删除
     */
    public static void deleteNode(){
        boolean delete = zkClient.delete("/08120000000002");
        if (delete){
            System.out.println("删除成功");
        }
    }
    /**
     * 3 获取子节点
     */
    public  static void getSubNode(){
//        zkClient.createPersistent("/com.sxt.service.AddService");
        zkClient.createEphemeral("/com.sxt.service.AddService/localhost:8888");
        zkClient.createEphemeral("/com.sxt.service.AddService/localhost:9999");
        zkClient.createEphemeral("/com.sxt.service.AddService/localhost:7777");
        System.out.println("节点创建完毕");
        List<String> childrens = zkClient.getChildren("/com.sxt.service.AddService");
        if(childrens!=null && !childrens.isEmpty()){
            childrens.forEach((k)-> System.out.println(k));
        }
    }
    /**
     * 4 节点的订阅
     */
    public static void  subscribe(){
        // 订阅子节点的改变?
        /**
         * 当父节点里面有子节点删除了,或者新增了,都会触发监听器
         */
        zkClient.subscribeChildChanges("/com.sxt.service.AddService", new IZkChildListener() {

            @Override
            public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
                System.out.println("父节点:/com.sxt.service.AddServic,的子节点有改变");
                System.out.println("当前最新的父节点为:"+currentChilds);
            }
        }) ;
        System.out.println("开始监听");
        try {
            System.in.read() ;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7.4 注册中心的功能?

7.4.1 服务的注册

7.4.2 服务的发现

/**
 * 注册中心的功能设计和完善
 */
public class RegisterCenter {

    private static ZkClient zkClient = null ;
    private static String SERVER_URL = "" ;
    static {
        int connectionTime = 30 *1000  ; // 尝试来连接zk的时间
        int sessionTime = 5*1000; // zk服务端和客户保持连接的时间,若在该时间内,客户端都没有向服务器响应,则服务器认为客户端死了
        zkClient  = new ZkClient(SERVER_URL,connectionTime,sessionTime);
    }
    /**
     * 服务的注册
     * 需要提供:
     *      服务的名称
     *      服务的地址
     */
    public static void register(String serviceName ,String sericeAddress){
        if(!zkClient.exists("/"+serviceName)){
            zkClient.createPersistent("/"+serviceName); // 这句话有异常,当你创建的节点存在时,它会有异常
        }
        if(!zkClient.exists("/"+serviceName+"/"+sericeAddress)){
            zkClient.createEphemeral("/"+serviceName+"/"+sericeAddress);
            System.out.println("服务serviceName,在:" + sericeAddress+ "注册成功");
        }

    }
    /**
     * 服务的发现
     * 使用服务的名称得到服务的地址
     *
     */
    public static List<String> discovery(String serviceName){
        if(zkClient.exists("/"+serviceName)){
            List<String> childrens = zkClient.getChildren("/" + serviceName);
            if(childrens!=null){
                return childrens ;
            }
        }
        return  Collections.emptyList() ;
    }

}

7.5 使用服务的注册和发现

7.5.1 服务提供者启动时,服务要注册

室友启动时,注册自己
在这里插入图片描述
7.5.2 服务消费者调用时,服务做发现
在这里插入图片描述

八、完善负载均衡的过程

在这里插入图片描述
在这里插入图片描述

九、使用zk 有个性能问题?

我们每次调用时,都需要来一次服务的发现:
在这里插入图片描述
在这里插入图片描述
9.1 缓存的思想

可以让它第一次去中心拉取,拉取到后,放入缓存里面,以后就可以不用去zk 里面拉取,直接从缓存里面获取
在这里插入图片描述

9.2 缓存有个问题
缓存的脏读的问题怎么解决?
1 删除缓存
2 更新缓存

我们在我们使用场景里面,无法知道缓存有脏读的现象!
Add对象
删除对象
更新对象

在这里插入图片描述

十、死循环监听

public class ClassmateApp {

    public static void main(String[] args) throws Exception {
        System.out.println("今天菜逼去考试了,他说让我把手机一直开机,他有题目要发给我");
        int port = 8888 ;
        listener(port) ;

    }
    public static void listener(int port){
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("开始监听菜逼的题目.....");
        } catch (IOException e) {
            e.printStackTrace();
        }
        /**
         * 室友提供的是:AddService 服务
         */
        RegisterCenter.register(AddService.class.getName(),"localhost:"+port);

        while(true){
            Socket client = null ;
            InputStream inputStream = null ;
            ObjectInputStream objectInputStream = null ;
            OutputStream outputStream = null ;
            ObjectOutputStream objectOutputStream = null ;
            try {
                 client = serverSocket.accept();// 监听菜逼的链接,如果菜逼不连接,将一直阻塞
                 inputStream = client.getInputStream();
                 objectInputStream = new ObjectInputStream(inputStream);
                /**
                 * 得到菜逼的题目
                 */
                Request rquest = (Request) objectInputStream.readObject();

                System.out.println("菜逼的题目是"+rquest);
                /**
                 * 开始帮菜逼计算
                 */
//        String answer = "2" ;
                Object answer = invoker(rquest);
                /**
                 * 把答案发给菜逼
                 */
                 outputStream = client.getOutputStream();
                 objectOutputStream = new ObjectOutputStream(outputStream);
                /**
                 * 把答案写个菜逼
                 */
                objectOutputStream.writeObject(answer);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                ResourceUtil.close(objectOutputStream,outputStream,objectInputStream,inputStream,client);
            }
        }

    }

    /**
     * 室友通过菜逼的题目来做一个计算
     * @param rquest
     * @return
     */
    private static Object invoker(Request rquest) {
        /**
         * 调用的接口
         */
        String interfaceName = rquest.getInterfaceName();
        /**
         * 方法的名称
         */
        String methodName = rquest.getMethodName();
        /**
         * 调用的参数
         */
        Object[] agrs = rquest.getAgrs();
        System.out.println(interfaceName+":"+methodName+":"+agrs);
        // 室友怎么实现计算
//        object.invoke(method,agrs) ;
        // 接口的名称:com.sxt.rpc.service.AddService
        // 实现类的名称:com.sxt.rpc.service.impl.AddServiceImpl
        try {
            Class<?> clazz = Class.forName("com.sxt.rpc.service.impl.AddServiceImpl");
            Class<?> []paramTypes = null ;
            if(agrs!=null){
                paramTypes = new Class<?>[agrs.length];
                for (int i = 0; i < agrs.length; i++) {
                    paramTypes[i] = agrs[i].getClass();
                }
            }
            // 反射得到要调用的方法
            Method method = clazz.getMethod(methodName, paramTypes);
            // 实例化对象
            Object realObject = clazz.newInstance();
            // 完成方法的调用
            Object result = method.invoke(realObject, agrs);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null ;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_43623447/article/details/104173255