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

Posted Dream_it_possible!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dubbo 源码学习系列 动手写dubbo核心原理相关的知识,希望对你有一定的参考价值。

前言

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

一、 Dubbo 架构详解 

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

各模块的职责:

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

二、  动手写Dubbo

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

       dubbo源码执行流程分析: Dubbo 源码学习系列(一) 浅析远程调用的核心流程_Dream_it_possible!的博客-CSDN博客_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(不是实现类的Class)和服务地址列表注册到zookeeper
       
   2. 在invoke()方法里先通过zookeeper 获取到服务地址列表,然后通过负载均衡策略选一个url。

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

实现原则:

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

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

 版本 v1.0: 

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

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

项目目录结构:

 项目依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.dubbo</groupId>
    <artifactId>dubbo-project</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>dubbo-project</name>
    <description>project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.45</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
        <!-- java 工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.7</version>
        </dependency>
        <!-- apache common工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.openhtmltopdf</groupId>
            <artifactId>openhtmltopdf-core</artifactId>
            <version>0.0.1-RC9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

 1. 模拟服务提供者

        本地注册中心

        本地注册的用处是在启动的时候用来缓存,在处理请求的时候会用到,根据接口的全限定名获取到实现类的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<String, Class> 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<String, List<URL>> remotemap = new HashMap<>();

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

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

    public static List<URL> get(String interfacename) 
        Map<String, List<URL>> 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<String, List<URL>> getFile() 
        try 
            FileInputStream fis = new FileInputStream(filePath);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object obj = ois.readObject();
            return (Map<String, List<URL>>) obj;
         catch (FileNotFoundException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
         catch (ClassNotFoundException e) 
            e.printStackTrace();
        
        return null;
    


     注:   此处有坑!

         坑一: 

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

       解决方法: 

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

       坑二:

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

      解决方法:

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

            

              

    基于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. 测试服务提供者

      为什么要测试服务提供者?

    1.   因为在服务消费端最终会把invocation对象发送给服务提供者,服务提供者会根据相应的协议去接收并处理请求。

     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"
    ]

      

              

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

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动态代理模式。

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

       

newInvocationHandler的invoke()方法执行消费方完整的流程, 代码如下: 

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<T> 


    public static <T> 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<URL> 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;
            
        );
    


最终实现将请求发送给服务提供者的地方是Object obj = client.send(url, invocation);

   借用 apache的工具包下的DefaultHttpClient发送post请求,因为dubbo调用的方式只有post请求,因此我们也只需要用post请求方式即可。

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;
    


写好后

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

       服务提供者控制台:

       服务消费者控制台: 

4. 总结 

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

5. 源码地址

       dubbo-rpc: 手写dubbo仓库源码

以上是关于Dubbo 源码学习系列 动手写dubbo核心原理的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo 源码分析系列之三 —— 架构原理

Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

Dubbo源码学习系列 整合Netty

解密Dubbo:自己动手编写一个较为完善的RPC框架(两万字干货)

DUBBO源码-自适应扩展机制

dubbo 源码 v2.7 分析:核心机制