手写模拟Dubbo底层原理

RPC : 远程方法调用 (Remote Procedure Call),是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一个地址空间的子程序,就想调用本地程序一样,无需额外的为这个交互作用编程(无需关注细节),是一种服务器-客户端(Client/Server)模式,经典实现是通过发送请求-接受回应进行信息交互的系统。是指两个进程内的方法相互调用。后续会用RPC基于Http协议来传输数据的方式手写实现。

Dubbo: 一开始的定位是RPC,专注于两个服务之间的调用,但随着微服务的盛行,除开服务调用外,Dubbo也在逐步的涉猎服务治理、服务监控、服务网关等,所以现在的Dubbo目标已经不止是RPC框架了,而是一个微服务的的服务框架

简图:

手写模拟Dubbo底层原理_第1张图片

 1.定义好服务提供的接口及实现类及方法

public interface HelloService {
    String sayHello(String userName);
}
public class HelloServerImpl implements HelloService{
    @Override
    public String sayHello(String userName) {
        return "hello:"+userName;
    }
}

正常情况下 本地可直接 new 一个实例来调用方法,或者注入这个Bean来调用方法;这里先定义好,便于请求方调用

2.作为服务端,需要启动一个能接收Http网络请求的服务,例如Jetty/Tomcat/Netty这里用Tomcat来实现,写一个Httpserver类,用于启动Tomcat

public class Provider {
    public static void main(String[] args) {
   //1.接口 实现类  2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
      

        //启动http服务 才能接收http请求 自己的启动方法 定义好服务的ip 端口
        Httpserver httpserver = new Httpserver();
        httpserver.start("localhost",8080);
        //3.需要双方协商好 交互的 内容  请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
        //这里不考虑多实现类多版本问题
        //请求内容可封装为一个对象 双方统一序列化方式都用JDK
        //请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来


    }
}
public class Httpserver {
//启动服务
        public void start(String hostname ,Integer port){
            //tomcat 启动 用内嵌的tomcat 本身就是java写的
            Tomcat tomcat = new Tomcat();

            Server server= tomcat.getServer();
            Service service =server.findService("Tomcat");

            Connector connector = new Connector();
            connector.setPort(port);

            Engine engine = new StandardEngine();
            engine.setDefaultHost(hostname);

            Host host = new StandardHost();
            host.setName(hostname);

            String contextPath = "";
            Context context = new StandardContext();
            context.setPath(contextPath);
            context.addLifecycleListener(new Tomcat.FixContextListener());

            host.addChild(context);
            engine.addChild(host);
            service.setContainer(engine);
            service.addConnector(connector);
            //tomcat只是一个servlet容器  没法处理,还需要添加servlet进行处理 交给servlet处理
            //相当于所有tomcat收到的请求都会到 DispatcherServletMyself里面去
            tomcat.addServlet(contextPath,"dispatcher",new DispatcherServletMyself());
            context.addServletMappingDecoded("/*","dispatcher");
            
            try {
                tomcat.start();
                tomcat.getServer().await();
            } catch (LifecycleException e) {
                throw new RuntimeException(e);
            }
        }
}

3.但实际上Tomcat只是一个容器,并不能直接处理,需要引入servlet来处理,所有tomcat收到的请求都会到DispatcherServletMyself类里面去 我自定义处理

public class DispatcherServletMyself extends HttpServlet {


    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //请求都到这里来具体进行处理
//        super.service(req, resp);
        //自定义处理方式 可以用多个handler 来处理  有的用于处理心跳 有的处理鉴权等等~~更灵活便于扩展
        new HttpServerHandler().hanler(req,resp);
    }
}

3.2在继承了HttpServlet以后,又建了一个Handler类来专门处理,以后可以多个Handler来处理不同请求不同的数据,便于扩展,

public class HttpServerHandler {
    public void hanler(HttpServletRequest req, HttpServletResponse resp) {
        //处理请求的核心逻辑 req  需要知道 req里面有什么 将我们自定义的invocation对象拿出来
        try {
            Invocation invocation = (Invocation) new ObjectInputStream(req.getInputStream()).readObject();
            //执行client想要执行的方法 拿到接口名
            String interfaceName = invocation.getInterfaceName();
                //将注册的具体实现类 获取到  如果有多个实现类 需要由客户端传个版本号过来服务端才能识别
            Class classImpl = LoclRegister.get(interfaceName);
//将方法名字,方法参数类拿到 获取到具体的方法对象
            Method method = classImpl.getMethod(invocation.getMethodName(), invocation.getParamType());

            String result = (String)method.invoke(classImpl.newInstance(), invocation.getParams());
            //结果返回出去 这个工具 这里只支持写string  写到响应,返回结果出去
            IOUtils.write(result,resp.getOutputStream());

        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        }
    }
}

3.3 上面代码的Invocation 对象,就是客户端和服务端约定好的 交互对象,如果要处理请求并返回结果,双方就必须约定好相互传输的数据内容格式这些东西,否则也无法解析(可以理解为一种协议),这里用JDK的序列化即可实现

public class Invocation implements Serializable {
    //接口名字 全限定名
    private String interfaceName;
    //方法名字
    private String methodName;
    //参数类型列表
    private Class[] paramType;
    //参数值列表
    private Object[] params;


    public Invocation(String interfaceName, String methodName, Class[] paramType, Object[] params) {
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.paramType = paramType;
        this.params = params;
    }

    public String getInterfaceName() {
        return interfaceName;
    }

    public void setInterfaceName(String interfaceName) {
        this.interfaceName = interfaceName;
    }

    public String getMethodName() {
        return methodName;
    }

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

    public Class[] getParamType() {
        return paramType;
    }

    public void setParamType(Class[] paramType) {
        this.paramType = paramType;
    }

    public Object[] getParams() {
        return params;
    }

    public void setParams(Object[] params) {
        this.params = params;
    }
}

3.4 Client想要实现RPC像本地调用一样的得到结果,就必须要传递数据给服务端,服务端才知道客户端具体调用的哪个接口的哪个方法,入参等等。这里最基本的就是客户端调用的时候,要告诉服务端,调用的 1.接口名字(这里只有一个实现类如果服务端多实现类就需要加上版本version识别) 2.方法名字(一个接口可能有多个方法的) 3.由于服务端可能存在重载,还需要加上参数类型列表  4.参数值列表  5.(可能需要version)这里暂不考虑多实现类的情况

4.服务端可以再加上本地服务注册,具体哪些接口和实现类是暴露出去可以被调用的

/**
 * Author:Eric
 * DATE:2023/8/2-21:58
 * Decription: 本地注册 服务端用于识别具体接口对应的实现类
 */
public class LoclRegister {
    // key 是接口的名字  value 是具体对应的实现类
    private static Map map=new HashMap<>();
    //注册接口及其实现类
    public static void regist(String interfaceName,Class implClass){
        map.put(interfaceName,implClass);
    }
    //获得接口的实现类
    public static Class get(String interfaceName){
        return map.get(interfaceName);
    }
}
public class Provider {
    public static void main(String[] args) {
   //1.接口 实现类  2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
        //本地注册 暴露接口出去
        LoclRegister.regist(HelloService.class.getName(), HelloServerImpl.class);
        //可进行操作,如果不符合的接口请求 返回对应提示信息
        //启动http服务 才能接收http请求
        Httpserver httpserver = new Httpserver();
        httpserver.start("localhost",8080);
        //3.需要双方协商好 交互的 内容  请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
        //这里不考虑多实现类多版本问题
        //请求内容可封装为一个对象 双方统一序列化方式都用JDK
        //请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来
        
    }
}

5.消费端 正常发送Http请求来访问,不关心具体的请求方式是post还是get

/**
 * Author:Eric
 * DATE:2023/8/2-23:06
 * Decription: 发送数据出去
 */
public class HttpClient {
    public String send(String hostname, Integer port, Invocation invocation){
        //配置

        try {
            URL url =new URL("http",hostname,port,"/");
            HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
            httpURLConnection.setRequestMethod("POST");
            httpURLConnection.setDoOutput(true);

            OutputStream outputStream = httpURLConnection.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(outputStream);

            oos.writeObject(invocation);
            oos.flush();
            oos.close();

            InputStream inputStream = httpURLConnection.getInputStream();
            String res = IOUtils.toString(inputStream);
            return res;

        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (ProtocolException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
}

6.消费端启动 发请求过去

public class Consumer {
    public static void main(String[] args) {
        Invocation invocation = new Invocation(HelloService.class.getName(),"sayHello",
                new Class[]{String.class}, new String[]{"lily65465"});
        String result = new HttpClient().send("localhost",8080,invocation);
        System.out.println("获取返回结果:"+result);

       }
}

7.先启动服务端,再启动消费端发送请求

手写模拟Dubbo底层原理_第2张图片

 手写模拟Dubbo底层原理_第3张图片

 8.消费端每次这样调用也很麻烦,这里可以引入代理此处用jdk的动态代理,来代理实现具体的接口,传入需要访问调用的接口类,即可对其进行代理的访问。在代理中 对localhost:8080的服务端进行访问

代理类:

public class ProxyFactory {
    public static T getProxy(final Class interfaceClass){
        //读取用户的配置,看用户用什么,这里用jdk
        //得到代理对象
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class[]{interfaceClass},
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
                        method.getParameterTypes(), args);
                String result = new HttpClient().send("localhost",8080,invocation);
               return result;
            }
        });

    }

}

改造后的Client:因为如果要真的像本地调用一样的方式去用,不用代理,就需要将服务端打成jar包放进去了,就是单体应用了。生成代理对象后,执行代理对象的方法,就会进入invoke方法,invoke方法负责 发送http请求给服务端

public class Consumer {
    public static void main(String[] args) {
        //Invocation invocation = new Invocation(HelloService.class.getName(),"sayHello",
        //        new Class[]{String.class}, new String[]{"lily65465"});
      //  String result = new HttpClient().send("localhost",8080,invocation);
      //  System.out.println("获取返回结果:"+result);

        //##########################################
        //获取返回结果:hello:lily
//        HelloService helloService 如果直接得到这个对象 就可以直接调用方法  如果要得到就需要将provider整体打jar包放进去,就成了
        //单体应用了 所以可以用代理对象 得到helloservice的代理对象
        HelloService helloService = ProxyFactory.getProxy(HelloService.class);
        //执行的时候会进入invoke方法中去
        String res2 = helloService.sayHello("lalala");
        System.out.println("res2用了代理:"+res2);


    }
}

结果:

9.进一步优化 实现注册中心获取数据,通过注册中心发现调用的接口,同时针对集群服务,需要负载均衡获取对应的url

**
 * Author:Eric
 * DATE:2023/8/4-20:38
 * Decription: 将ip及端口具象化出来
 */
public class URL implements Serializable {
    private String hostname;
    private Integer port;

    public URL(String hostname, Integer port) {
        this.hostname = hostname;
        this.port = port;
    }

    public String getHostname() {
        return hostname;
    }

    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }
}

模拟注册中心需要共享数据zk,redis,nacos

**
 * Author:Eric
 * DATE:2023/8/2-21:58
 * Decription: 模拟注册中心
 */
public class RemoteRegister {
    // key 是接口的名字  value 是具体对应的具体访问的url列表
    private static Map> REGISTER=new HashMap<>();
    //注册接口及其实现类
    public static void regist(String interfaceName,URL url){
        List urls = REGISTER.get(interfaceName);
        if(urls==null){
            urls=new ArrayList<>();
        }
        urls.add(url);
        REGISTER.put(interfaceName,urls);
  //解决目前两个进程无法共享 注册的Map数据,这里将注册的数据保存到本机的 文件中
        saveFile();
    }

    private static void saveFile() {
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("/temp.txt");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(REGISTER);
        } catch (IOException e) {
           e.printStackTrace();
        }


    }

    //获得接口的实现类
    public static List get(String interfaceName){
        //读取数据的时候先从本机读
        REGISTER=getFile();
        List list = REGISTER.get(interfaceName);
        return list;
    }
    private static  REGISTER getFile() {

        try {
            FileInputStream fileIutputStream = new FileInputStream("/temp.txt");
            ObjectInputStream objectinputStream = new ObjectInputStream(fileIutputStream);
            REGISTER o = (REGISTER)objectinputStream.readObject();
            return o;

        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }
}

将接口名字,url存入map中,可能有集群服务,会有多个url,但每次只能访问一个,引入模拟的负载均衡

/**
 * Author:Eric
 * DATE:2023/8/4-20:47
 * Decription: 负载均衡
 */
public class LoadBalance {
    public static URL random(List list){
        Random random = new Random();
        int n = random.nextInt(list.size());
        return list.get(n);

    }
}

代理类拿数据的时候从注册中心拿,需要优化

public class ProxyFactory {
    public static T getProxy(final Class interfaceClass){
        //读取用户的配置,看用户用什么,这里用jdk
        //得到代理对象
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class[]{interfaceClass},
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),
                        method.getParameterTypes(), args);
                //根据interfaceClass.getName 来判断该访问哪个ip 端口  可能是集群有多个ip
                List list = RemoteRegister.get(interfaceClass.getName());
                //拿到多个URL 选择其中一个 需要负载均衡  读取配置 第一次从注册中心拿 拿了后可以进行本地注册放到本地
                //zk的监听机制 redis的发布订阅 可以共享数据的同时保证本地的数据实时更新
                // 如果就这样代码 会拿到空,因为服务端和消费端是两个进程
                URL url = LoadBalance.random(list);

                String result = new HttpClient().send(url.getHostname(), url.getPort(), invocation);
               return result;
            }
        });

    }

}

同时注册及获取对应的数据这里将数据存入了文件,读取的时候也是读取的文件获取的配置的url信息,服务端可动态修改ip 端口

public class Provider {
    public static void main(String[] args) {
   //1.接口 实现类  2.通过网络来访问我这个服务 需要有服务来接收网络请求 例如:jetty/tomcat/netty 接收http 这里服务端需要有相应的服务
        //本地注册 暴露接口出去
        LoclRegister.regist(HelloService.class.getName(), HelloServerImpl.class);
        //正常需要在注册中心注册 这样client根据interface类既可以找到对应的 ip 端口  Map<接口名,List>
    //注册中心注册
        URL url = new URL("localhost",8081);
        RemoteRegister.regist(HelloService.class.getName(),url);

        //可进行操作,如果不符合的接口请求 返回对应提示信息
        //启动http服务 才能接收http请求
        Httpserver httpserver = new Httpserver();
        httpserver.start(url.getHostname(),url.getPort());
        //3.需要双方协商好 交互的 内容  请求服务需要给 ->接口名字、方法名、方法参数类型列表(可能重载)、参数值列表、version 可加版本实现多版本功能
        //这里不考虑多实现类多版本问题
        //请求内容可封装为一个对象 双方统一序列化方式都用JDK
        //请求过来后 会传接口名过来,那么到底对应的是哪个具体实现类,只有provider知道 provider需要有一个本地注册,将自己的实现类记录下来

    }
}

消费端直接调用

public class Consumer {
    public static void main(String[] args) {

        //获取返回结果:hello:lily
//        HelloService helloService 如果直接得到这个对象 就可以直接调用方法  如果要得到就需要将provider整体打jar包放进去,就成了
        //单体应用了 所以可以用代理对象 得到helloservice的代理对象
        HelloService helloService = ProxyFactory.getProxy(HelloService.class);
        //执行的时候会进入invoke方法中去
        String res2 = helloService.sayHello("lalala8081");
        System.out.println("res2用了代理:"+res2);


    }
}

结果

手写模拟Dubbo底层原理_第4张图片

 可以在代理类里面捕获异常,如果连接失败服务失败,返回对应数据

手写模拟Dubbo底层原理_第5张图片

 


1.如何将服务方的接口暴露给客户端?可单独将服务端的接口及实现类 都放到一个maven项目中,打jar包,让客户端引入

2.引入注册中心,将接口和url存入

你可能感兴趣的:(dubbo)