请选择 进入手机版 | 继续访问电脑版

石家庄老站长

点击联系客服
客服QQ:509006671 客服微信:mengfeiseo
 找回密码
 立即注册
查看: 10|回复: 0

Dubo源学习系列(2)Dubo核心原理写作

[复制链接]

1

主题

1

帖子

-7

积分

限制会员

积分
-7
发表于 2021-5-1 23:45:50 | 显示全部楼层 |阅读模式
前言

我认为只有学习源代码才能得到与大师交流的机会。优秀的设计可以在编程思想中锻炼,更注重我的代码质量。(模板和代码)。

一、Dubbo  架构详解

在了解Dubbo之前,先制作Dubbo的结构动画是最明确有效的方法。





每个模块的责任:

注册中心:提供服务搜索和注册功能,如果服务发生变化,则通过watch机制通知服务消费者。服务消费者:服务的调用方在启动时从注册中心获取服务地址列表,并通过Map在本地缓存。服务提供者:服务提供者在启动时将服务注册到远程注册中心(例如zookeeper),并在本地注册,提供接口方法实现。显示器:在服务提供者和服务消费者调用时监视和计算部分参数数据。二、 动手写Dubbo

在写Dubbo之前,我们应该先整理一下他的实施逻辑。最好画一个流程图,或者通过顺序列出执行顺序,然后写代码。

我自己写了四个版本,每个版本都有重复,然后会继续完善。使用的Java版本是1.8。每个版本的实施步骤如下:

V1.0

服务提供者和服务消费者都使用http协议,服务提供者使用Tomcat容器处理请求,服务消费者通过HTTP客户端发送请求。

1.服务提供者

服务提供者需要做的事情可以归纳为以下两点:

1)在注册表中本地注册,并注册接口名称和实现类的类。

2)使用remotemap代替zookeeper注册中心注册接口名称和服务地址列表

3)使用http协议启动Tomcat容器以接收和处理请求。

dispatcher  servlet接收请求- http  server  handler  - response。

2.服务消费者

1)使用动态代理模式获取接口的代理对象,并使用代理对象调用目标方法

在Cglib代理模式下为目标对象生成代理对象

执行Invoker()方法。invoke()方法在调用目标方法时执行

根据接口名称,使用remotemap获取URL列表,然后选择要通过负载平衡策略调用的URL

将Url和invocation发送给服务提供商进行处理。

返回结果打印调用的结果。

V  2.0

使用Zookeeper注册中心缓存服务地址列表。

1.服务提供者

启动服务时,向zookeeper注册界面的Class  Name和服务地址列表

2.在invoke()方法中,首先通过zookeeper获取服务地址列表,然后通过负载平衡策略选择URL。

V  3.0

实现使用Netty  server处理请求的dubbo协议。

V  4.0

利用工厂设计模式优化代码结构。

原则:

1.服务消费者只调用接口,而不考虑实施细节。

2.服务提供者仅提供服务实施
ff0c;在调用的时候才去处理请求。

版本 v1.0:

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

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

项目目录结构:



项目依赖:


    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. 模拟服务提供者
      


           本地注册中心
                本地注册的用处是在启动的时候用来缓存,在处理请求的时候会用到,根据接口的全限定名获取到实现类的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[U] lists = new ArrayList();
        if (remotemap == null) {
            remotemap = new HashMap();
        }
        lists.add(url);
        remotemap.put(interfacename, lists);
        saveFile();
    }
    public static List[U] 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[U] 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. 测试服务提供者
     写好服务提供者后,首先使用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动态代理模式。

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

      



对应的代码如下:


            

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[U] 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;
    }
}



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

       服务提供者控制台:



       服务消费者控制台:




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

5. 源码地址
       https://gitee.com/bingbing-123456/dubbo-rpc.git
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|无图版|手机版|小黑屋|石家庄@IT精英团

GMT+8, 2021-5-16 05:03 , Processed in 0.026740 second(s), 19 queries .

Powered by Discuz! X3.4

© 2001-2021 Comsenz Inc.

快速回复 返回顶部 返回列表