Dubbo 源码学习系列(二) 动手写dubbo核心原理

 

前言

       我觉得只有学习源码,才能获取到与大师交流的机会,优秀的设计能让我在编程思想上得到锻炼提升,也会让我去更注重自己的代码质量 !

一、 Dubbo 架构详解 

      理解Dubbo前,最好先手动画一下dubbo的架构图,画图理解架构是最清晰有效地方式。

Dubbo 源码学习系列(二) 动手写dubbo核心原理_第1张图片

各模块的职责:

  • 注册中心:  提供服务发现与注册功能, 如果服务发生变动通过watch机制通知服务消费方。
  • 服务消费者:  服务的调用方,在启动的时候会从注册中心拉取到服务地址列表通过Map缓存到本地。
  • 服务提供者:  服务的提供方,在启动的时候会将服务注册到远程注册中心,例如zookeeper, 同时也本地注册,提供接口方法的实现。
  • 监视器:  监视并统计服务提供者和服务消费者在调用时的一些参数数据。

二、  动手写Dubbo

       在写dubbo前,我们一定要先理清他的实现逻辑,最好画一个流程图,或者是通过顺序罗列出执行顺序,然后再去写代码。     

       我自己写了四个版本, 每个版本都有迭代,后续会继续完善,使用的Java版本是 1.8, 每个版本的实现步骤描述如下: 

v1.0 
服务提供者和服务消费者都使用http协议, 服务提供者使用tomcat容器处理请求,服务消费者通过httpClient将请求发送出去。
1. 服务提供者
     服务提供者要做的事情可以总结为如下两点: 
    1)  本地注册到registry, 注册接口名和实现类的Class。
    2)  使用remotemap 替代zookeeper 注册中心,注册接口名和服务地址列表
    3)  使用http 协议启动tomcat容器接收请求,处理请求。
         DispatcherServlet 接收请求-----> HttpServerHandler-----> response。


2. 服务消费者
    1)  使用动态代理模式获取接口的代理对象, 使用代理对象调用目标方法
         通过cglib代理模式生成目标对象的代理对象
            执行invoker()方法,invoke()方法在调用目标方法时执行
                   根据接口名从使用remotemap拿到url列表,然后通过负载均衡策略选一个url进行调用
                     将Url和invocation发送到服务提供者处理。
                         返回result打印调用结果。



v 2.0
   使用zookeeper注册中心缓存服务地址列表。
   1. 服务提供者
       在服务启动时将接口的Class Name和服务地址列表注册到zookeeper
       
   2. 在invoke()方法里先通过zookeeper 获取到服务地址列表,然后通过负载均衡策略选一个url。

v 3.0 
   实现dubbo协议,使用netty server 来处理请求。
   
v 4.0 
   利用工厂设计模式优化代码结构。

 

原则:

       1. 服务消费者只管接口调用,不管实现细节。

       2. 服务提供者只提供服务实现,在调用的时候才去处理请求。

 版本 v1.0: 

        1. 服务提供者能提供服务,服务消费者能够消费服务。

        2. 使用本地文件替代zookeeper注册中心。

项目目录结构:

Dubbo 源码学习系列(二) 动手写dubbo核心原理_第2张图片

 项目依赖:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.4.5
         
    
    com.example.dubbo
    dubbo-project
    0.0.1-SNAPSHOT
    dubbo-project
    project for Spring Boot
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.apache.tomcat.embed
            tomcat-embed-core
            9.0.45
        

        
            com.alibaba
            fastjson
            1.2.58
        
        
        
            cn.hutool
            hutool-all
            4.5.7
        
        
        
            org.apache.commons
            commons-io
            1.3.2
        
        
            com.openhtmltopdf
            openhtmltopdf-core
            0.0.1-RC9
        
        
            org.apache.httpcomponents
            httpclient
            4.5.13
        

    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    


 

 1. 模拟服务提供者

      Dubbo 源码学习系列(二) 动手写dubbo核心原理_第3张图片

           本地注册中心

                本地注册的用处是在启动的时候用来缓存,在处理请求的时候会用到,根据接口的全限定名获取到实现类的Class,  当服务消费方把请求的目标接口的方法名和方法参数列表发过来后就可以获取到method对象, 然后通过method对象在服务提供方执行目标方法。

package com.example.dubbo.provider;

import com.example.dubbo.framework.URL;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author bingbing
 * @date 2021/4/29 0029 10:53
 * 本地注册中心
 */
public class LocalRegistry {


    private static Map clazzMap = new HashMap<>();


    public static void regist(String interfacename, Class implClazz) {
        clazzMap.put(interfacename, implClazz);
    }

    public static Class getClass(String interfacename) {
        return clazzMap.get(interfacename);
    }

}

         本地模拟zookeeper注册中心

             理解了dubbo的服务消费模块后,我们就知道服务提供者在启动应用时会将本地的接口信息注册到zookeeper注册中心。

             本地可以使用map去替代zookeeper缓存接口的全限定名和服务地址列表,为什么缓存这两个值?  是因为在调用时如果有多实例的情况下,服务消费端调用接口达到负载均衡的效果。

package com.example.dubbo.registry;

import com.example.dubbo.framework.URL;
import com.sun.org.apache.regexp.internal.RE;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author bingbing
 * @date 2021/4/29 0029 10:53
 * 暂时替代远程注册中心 zookeeper
 * 使用map缓存地址列表
 */
public class RemoteRegistry {


    private static Map> remotemap = new HashMap<>();

    private static final String filePath = "/temp.text";

    public static void registry(String interfacename, URL url) {
        List lists = new ArrayList<>();
        if (remotemap == null) {
            remotemap = new HashMap<>();
        }
        lists.add(url);
        remotemap.put(interfacename, lists);
        saveFile();
    }

    public static List get(String interfacename) {
        Map> map = getFile();
        return map.get(interfacename);
    }


    public static void saveFile() {
        File file = new File(filePath);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 将对象序列化后保存到文件里
        try {
            FileOutputStream fos = new FileOutputStream(filePath);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(remotemap);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 从文件中读取
    public static Map> getFile() {
        try {
            FileInputStream fis = new FileInputStream(filePath);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object obj = ois.readObject();
            return (Map>) obj;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

}

     注:   此处有坑!

         坑一: 

               我在invoke()方法里获取到服务地址列表时, List lists = RemoteRegistry.get(interfaceclass.getName());  从RemoteRegistry获取到的lists 值竟然为null,我明明在服务提供者写了启动时自动将服务地址列表注册到 RemoteRegistry呀! 然后我找到原因,因为两个不同的应用是不能直接通信的,相当于两个不同的环境,每个应用的RemoteRegistry是独立的,所以拿不到服务提供者的map。

       解决方法: 

               在注册时,将map写入到文件里,服务消费者在调用的时, 每次get前去拿getFile(), 这样数据就能达到共享的目的了!

       坑二:

               将map对象在写入到文件时报错: URL不能被序列化。

      解决方法:

               继承Serializable 接口并添加唯一标识。

            Dubbo 源码学习系列(二) 动手写dubbo核心原理_第4张图片

              

 

    基于Http协议设置tomcat启动相关参数

           设置一个tomcat容器,作为请求处理的载体,另外需要一个DispatcherServlet接收和分发请求,写好后执行一下main方法,看能不能正常启动, 可以在启动完成后,访问localhost:8080。

package com.example.dubbo.protocol.http;

import com.example.dubbo.framework.URL;
import org.apache.catalina.*;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;

/**
 * @author bingbing
 * @date 2021/4/29 0029 11:00
 */
public class HttpServer {


    public void start(URL url) {
        Tomcat tomcat = new Tomcat();
        Server server = tomcat.getServer();
        Service service = server.findService("Tomcat");

        Connector connector = new Connector();
        connector.setPort(url.getPort());


        Engine engine = new StandardEngine();
        engine.setDefaultHost(url.getHost());


        Host host = new StandardHost();
        host.setName(url.getHost());
        String contextpah = "";


        Context context = new StandardContext();

        context.setPath(contextpah);
        context.addLifecycleListener(new Tomcat.FixContextListener());

        host.addChild(context);
        engine.addChild(host);

        service.setContainer(engine);
        service.addConnector(connector);

        //配置dispatcherServlet 来处理请求
        tomcat.addServlet(contextpah, "dispatcher", new DispatcherServlet());
        // 配置mapping
        context.addServletMappingDecoded("/*", "dispatcher");
        try {
            tomcat.start();
            tomcat.getServer().await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        URL url = new URL("localhost", 8080);
        HttpServer server = new HttpServer();
        server.start(url);
    }
}

      DispatcherServlet

         分发并处理请求

package com.example.dubbo.protocol.http;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author bingbing
 * @date 2021/4/29 0029 11:00
 */
public class DispatcherServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        new HttpServerHandler().handler(req, resp);
    }
}

       HttpServerHandler,  万事具备,只欠东风! 在此方法里method对象通过反射执行目标接口方法。

package com.example.dubbo.protocol.http;

import com.alibaba.fastjson.JSONObject;
import com.example.dubbo.framework.Invocation;
import com.example.dubbo.provider.LocalRegistry;
import com.example.dubbo.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author bingbing
 * @date 2021/4/29 0029 11:00
 */
public class HttpServerHandler {

    Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);

    /**
     * 处理请求,解析invocation对象
     *
     * @param req
     * @param resp
     */
    public void handler(HttpServletRequest req, HttpServletResponse resp) {
        try {
            logger.debug("收到一个请求,开始处理....");
            // 1. 反序列化,解析对象
            Invocation invocation = JSONObject.parseObject(req.getInputStream(), Invocation.class);
//            invocation.setParamTypes(new Class[]{String.class});
            logger.debug("接收到invocation{}", invocation.toString());
            // 2. 根据接口名获取到接口实现类的Class
            Class impl = LocalRegistry.getClass(invocation.getInterfaceName());
            // 3. 获取反射调用的method对象, 通过方法名和参数列表类型获取方法method对象
            Method method = impl.getMethod(invocation.getMethodName(), invocation.getParamTypes());
            // 4. 执行方法调用
            Object obj = method.invoke(impl.newInstance(), invocation.getParams());
            logger.debug("返回结果:{}", obj);
            resp.setCharacterEncoding("utf-8");
            resp.setContentType("*/*;charset=UTF-8");
            IOUtils.write(obj, resp.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}

 

  2. 测试服务提供者

     写好服务提供者后,首先使用postman测一下看能不能获取到数据,如果不能就先检查一下代码是否写的有问题。

          postman测试技巧: 

            1)  将参数指定为invocation对象对应的属性值。

            2)  接口名为接口所在的全限定名,即所在包名路径.接口名

            3)  参数类型的Class 用java.lang.String 字符串。

{
    "interfaceName": "com.example.dubbo.provider.api.UserInterface",
    "methodName": "sayHello",
    "paramTypes": [
        "java.lang.String"
    ],
    "params": [
        "bingbing"
    ]
}

 

      Dubbo 源码学习系列(二) 动手写dubbo核心原理_第5张图片

              

    服务提供者可以调通后, 就可以开始写服务消费者了! 

3. 模拟服务消费者

           消费方不管实现,只知道接口即可,屏蔽实现细节,因此在调用的接口的时候应该达到尽可能地精简, 通过代理工厂,达到只需要传接口的Class即可获取到代理对象,然后调用目标方法传递参数即可!

package com.example.dubbo.consumer;

import com.example.dubbo.framework.ProxyFactory;
import com.example.dubbo.provider.api.UserInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author bingbing
 * @date 2021/4/29 0029 10:17
 */
public class ConsumerApplication {

    public static Logger logger = LoggerFactory.getLogger(ConsumerApplication.class);

    public static void main(String[] args) {
        // 消费方不管实现,只知道接口即可,屏蔽实现细节
        UserInterface userInterface = ProxyFactory.getProxy(UserInterface.class);
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String result = userInterface.sayHello("bingbing");
            System.out.println("执行成功,返回结果:{}" + result);
        }

    }
}

          如果你还不了解动态代理,那么可以先补一下cglib动态代理和jdk动态代理模式。

          画个图分析一下执行流程:

       

Dubbo 源码学习系列(二) 动手写dubbo核心原理_第6张图片

对应的代码如下: 

           

package com.example.dubbo.framework;

import com.example.dubbo.protocol.http.HttpClient;
import com.example.dubbo.provider.LocalRegistry;
import com.example.dubbo.registry.RemoteRegistry;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Random;

/**
 * @author bingbing
 * @date 2021/4/29 0029 10:46
 */
public class ProxyFactory {


    public static  T getProxy(final Class interfaceclass) {

        return (T) Proxy.newProxyInstance(interfaceclass.getClassLoader(), new Class[]{interfaceclass}, new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                //  可以自己定义mock, 通过Mock实现服务降级

                // 2. 获取服务地址列表
                List lists = RemoteRegistry.get(interfaceclass.getName());
                // 3. 负载均衡策略选择一个url进行使用。
                URL url = LoadBalance.random(lists);
                // 3. 发送http 请求
                HttpClient client = new HttpClient();
                Invocation invocation = new Invocation(interfaceclass.getName(), method.getName(), objects, method.getParameterTypes());
                Object obj = client.send(url, invocation);
                return obj;
            }
        });
    }

}

  最终实现http请求调用的地方是Object obj = client.send(url, invocation);

package com.example.dubbo.protocol.http;

import com.alibaba.fastjson.JSONObject;
import com.example.dubbo.framework.Invocation;
import com.example.dubbo.framework.URL;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

/**
 * @author bingbing
 * @date 2021/4/29 0029 11:00
 */
public class HttpClient {


    public Object send(URL url, Invocation invocation) {

        // 1.将invocation对象序列化转换成json对象
        // 2. 通过post方式发起http请求。
        String jsonInvocation = JSONObject.toJSONString(invocation);

        // 3. 获取响应结果并返回。
        String urlDetail = url.getHost() + ":" + url.getPort();
        try {
            return doPostData(urlDetail, jsonInvocation);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static Object doPostData(String url, String json) throws Exception {
        DefaultHttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost("http://" + url);
        String result = "";
        HttpResponse res = null;
        try {
            StringEntity s = new StringEntity(json, "UTF-8");
            s.setContentType("application/json");
            post.setHeader("Accept", "application/json");
            post.setHeader("Content-type", "application/json; charset=utf-8");
            post.setEntity(s);
            res = client.execute(post);
            if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                result = EntityUtils.toString(res.getEntity());
                return result;
            }
        } catch (Exception e) {
            if (res == null) {
                return "HttpResponse 为 null!";
            }
            throw new RuntimeException(e);
        }
        if (res == null || res.getStatusLine() == null) {
            return "无响应";
        }
        return result;
    }

}

 

    先启动服务提供者,再启动服务消费者

       服务提供者控制台:

Dubbo 源码学习系列(二) 动手写dubbo核心原理_第7张图片

       服务消费者控制台: 

 Dubbo 源码学习系列(二) 动手写dubbo核心原理_第8张图片

4. 总结 

        v1.0版本实现了服务消费者能够调用服务提供者的目标接口方法,通过文件实现共享服务地址列表。

5. 源码地址

       https://gitee.com/bingbing-123456/dubbo-rpc.git

你可能感兴趣的:(源码学习,dubbo源码学习,分布式架构dubbo,手写dubbo核心原理)