手寫Dubbo框架「建議收藏」

在使用dubbo的時候,我有些疑問:

1.爲什麼我們引入了dubbo相關jar包,配置了consumer,就可以調用接口方法實現遠程調用呢?

其實較爲簡單的思路就是:

服務端:註冊+socket通信監聽。

客戶端:jdk動態代理+代理中(匹配註冊信息+socket通信傳參)。

服務端:利用反射調用方法並返回結果。

核心就是:動態代理+socket通信+反射。

爲什麼要註冊?是爲了可維護提供者信息,而不是寫死。

爲什麼要jdk動態代理?是爲了消費者在調用方法的時候進行攔截處理(匹配註冊,socket通信傳參)。

爲什麼使用反射?是爲了提供者的socket接收到信息後根據信息通過反射機制調用方法。

有了這些後,A系統就可以調用B系統接口了。

 

 

正文:

本文章向大家介紹手寫Dubbo框架「建議收藏」,主要內容包括一句話認識Dubbo、一句話明白RPC、手寫可擴展的RPC協議、項目地址、多模塊設計、服務端、註冊中心實現、HTTP協議、消費端、測試、優化、netty實現?、基本概念、基礎應用、原理機制和需要注意的事項等,並結合實例形式分析了其使用技巧,希望通過本文能幫助到大家理解應用這部分內容。

大家好,又見面了,我是你們的朋友全棧君。

手寫Dubbo框架

  • 一句話認識Dubbo
    • 瞭解Dubbo
  • 一句話明白RPC
    • RPC是什麼?
  • 手寫可擴展的RPC協議
    • 緣起
    • 項目地址
    • 多模塊設計
    • 服務端
      • 提供API
      • 註冊服務,啓動tomcat
    • 註冊中心實現
      • 服務註冊形式
      • 兩個數據bean
      • 具體實現
    • HTTP協議
      • 內嵌tomcat啓動
      • 具體實現
    • 消費端
    • 測試
    • 優化
      • 動態代理
      • 以文本形式實現註冊中心
      • 優化後的消費端
    • netty實現?

一句話認識Dubbo

Dubbo是阿里巴巴公司開源的一個高性能優秀的服務框架,使得應用可通過高性能的 RPC 實現服務的輸出和輸入功能,可以和Spring框架無縫集成。現在已成爲Apache的開源項目。

瞭解Dubbo

詳細瞭解直接進dubbo官網看中文文檔:http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html

 

一句話明白RPC

RPC是什麼?

RPC(Remote Procedure Call)—遠程過程調用,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的計算機通信協議。該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外的爲這個交互作用編程,如果涉及的軟件採用面向對象編程(java),那麼遠程過程調用亦可稱作遠程調用遠程方法調用。只要支持網絡傳輸的協議就是RPC協議,RPC是一種框架。

手寫可擴展的RPC協議

緣起

公司的項目也在用Dubbo,近日又看一關於手寫dubbo的視頻,於是乎想着手敲一下簡單的dubbo框架

項目地址

https://github.com/ghostKang/dubbo-study

多模塊設計

按照官網架構圖,模塊內容設計如下

  • 服務端:提供API,啓動的時候要註冊服務
  • 消費端:從註冊中心獲取服務,調用子服務
  • 註冊中心:保存服務配置
  • RPC協議:基於Tomcat的HttpProtocol,基於Netty的DubboProtocol 由於模塊之間還要引用jar包,於是在手寫實現時以包的形式代表各個模塊
 

服務端

提供API

也就是接口,實現接口

public interface HelloService {

    public void sayHello(String username);
}

public class HelloServiceImpl implements HelloService {
    public void sayHello(String username) {
        System.out.println("Hello:"+username);
    }
}

註冊服務,啓動tomcat

public class Provider {
    public static void main(String[] args) {
        // 註冊服務
        URL url = new URL("localhost",8080);
        Register.regist(url, HelloService.class.getName(), HelloServiceImpl.class);

        // 啓動tomcat
        HttpServer httpServer = new HttpServer();
        httpServer.start(url.getHostname(),url.getPort());
    }
}

註冊中心實現

服務註冊形式

以接口名爲key,通過服務調用地址找到具體實現類爲。 在消費端,直接傳接口名就可以找到具體實現。

Map<interfacename,Map<URL,Class>>

兩個數據bean

Invocation .java 要實現Serializable,在服務消費端設值後序列化成對象流傳輸,然後在服務提供端轉爲對象,獲取接口名,從註冊中心獲取實現類,從而調用方法。

public class Invocation implements Serializable {
    private String interfaceName;
    private String methodName;
    private Object[] params;
    private Class[] paramTypes;
    public Invocation(String interfaceName, String methodName, Object[] params, Class[] paramTypes) {
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.params = params;
        this.paramTypes = paramTypes;
    }
    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 Object[] getParams() {
        return params;
    }
    public void setParams(Object[] params) {
        this.params = params;
    }

    public Class[] getParamTypes() {
        return paramTypes;
    }
    public void setParamTypes(Class[] paramTypes) {
        this.paramTypes = paramTypes;
    }
}

URL .java 地址接口類

public class URL {
    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;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        URL url = (URL) o;
        if (hostname != null ? !hostname.equals(url.hostname) : url.hostname != null) return false;
        return port != null ? port.equals(url.port) : url.port == null;
    }
    @Override
    public int hashCode() {
        int result = hostname != null ? hostname.hashCode() : 0;
        result = 31 * result + (port != null ? port.hashCode() : 0);
        return result;
    }
}

具體實現

Register.java

public class Register {

    private static Map<String,Map<URL,Class>> REGISTER = new HashMap<String, Map<URL, Class>>();

    /**
     * 註冊服務(暴露接口)
     * @param url
     * @param interfaceName
     * @param implClass
     */
    public static void regist(URL url,String interfaceName,Class implClass){
        Map<URL,Class> map = new HashMap<URL, Class>();
        map.put(url,implClass);
        REGISTER.put(interfaceName,map);
    }

    /**
     * 從註冊中心獲取實現類(發現服務)
     * @param url
     * @param interfaceName
     * @return
     */
    public static Class get(URL url,String interfaceName){
        return REGISTER.get(interfaceName).get(url);
    }

}

HTTP協議

內嵌tomcat啓動

引入內嵌tomcat依賴

<dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>9.0.12</version>
</dependency>

tomcat結構 server.xml

<Server port="8005" shutdown="SHUTDOWN">
	<Service name="Catalina">
		<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" 
	       URIEncoding="UTF-8"/>
		<Engine name="Catalina" defaultHost="localhost">
			<Host name="localhost"  appBase="webapps"
            	unpackWARs="true" autoDeploy="true">
    		 	<Context path="" doBase="WORKDIR" reloadable="true"/>
     		</Host>
     </Engine>
  </Service>
</Server>

是不是很熟悉,根據這個xml結構構建一個tomcat啓動類

具體實現

HttpServer.java

public class HttpServer {
    public void start(String hostname,Integer port){

        // 實例一個tomcat
        Tomcat tomcat = new Tomcat();

        // 構建server
        Server server = tomcat.getServer();

        /**
         * 在getServer的時候,就在方法內部執行了
         * Service service = new StandardService();
         * service.setName("Tomcat");
         * server.addService(service);
         */
        // 獲取service
        Service service = server.findService("Tomcat");

        // 構建Connector
        Connector connector = new Connector();
        connector.setPort(port);
        connector.setURIEncoding("UTF-8");

        // 構建Engine
        Engine engine = new StandardEngine();
        engine.setDefaultHost(hostname);

        // 構建Host
        Host host = new StandardHost();
        host.setName(hostname);

        // 構建Context
        String contextPath = "";
        Context context = new StandardContext();
        context.setPath(contextPath);
        context.addLifecycleListener(new Tomcat.FixContextListener());// 生命週期監聽器

        // 然後按照server.xml,一層層把子節點添加到父節點
        host.addChild(context);
        engine.addChild(host);
        service.setContainer(engine);
        service.addConnector(connector);
        // service在getServer時就被添加到server節點了

        // tomcat是一個servlet,設置路徑與映射
        tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet());
        context.addServletMappingDecoded("/client/*","dispatcher");

        try {
            tomcat.start();// 啓動tomcat
            tomcat.getServer().await();// 接受請求
        }catch (LifecycleException e){
            e.printStackTrace();
        }
    }
  }

HttpServerHandler.java 所有http請求交給HttpServerHandler處理,即服務消費端的遠程調用

public class DispatcherServlet extends HttpServlet{
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 方便後期在此拓展服務
        new HttpServerHandler().handler(req, resp);
    }
}

public class HttpServerHandler {
    public void handler(HttpServletRequest req, HttpServletResponse resp){
        try{
            // Http請求流轉爲對象
            InputStream is = req.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(is);
            Invocation invocation = (Invocation)ois.readObject();

            // 尋找註冊中心的實現類,通過反射執行方法
            Class implClass = Register.get(new URL("localhost",8080),invocation.getInterfaceName());
            Method method = implClass.getMethod(invocation.getMethodName(),invocation.getParamTypes());
            String result = (String) method.invoke(implClass.newInstance(),invocation.getParams());
            
            // 將結果返回
            IOUtils.write(result,resp.getOutputStream());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

注意: URL一定要重寫equals與hashCode方法,否則Register.get(new URL("localhost",8080),invocation.getInterfaceName());時爲null。

消費端

consumer .java

public class consumer {

    public static void main(String[] args) {

        // 調用哪個方法
        Invocation invocation = new Invocation(
                HelloService.class.getName(),
                "sayHello",
                new Object[]{"yukang"},
                new Class[]{String.class});
                
		// 發現服務器
        String result = new HttpClient().post("localhost",8080,invocation);
        System.out.println(result);

    }
}

HttpClient.java

public class HttpClient {

    /**
     * 遠程方法調用
     * @param hostname
     * @param port
     * @param invocation
     * @return
     */
    public String post(String hostname, Integer port, Invocation invocation){
        try {
            // 進行http連接
            URL url = new URL("http",hostname,port,"/client/");
            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);// 必填項

            // 將對象寫入輸出流
            OutputStream os = connection.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(invocation);
            oos.flush();
            oos.close();

            // 將輸入流轉爲字符串(此處可是java對象)
            InputStream is = connection.getInputStream();
            return IOUtils.toString(is);
        }catch (MalformedURLException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
        return null;
    }
}

測試

先啓動服務端

 

再啓動服務端

 

優化

dubbo是直接引入接口jar包,調用接口方法就可以獲取結果,於是使用到了動態代理返回一個代理對象。

動態代理

ProxyFactory.java

public class ProxyFactory<T> {
    public static <T> T getProxy(Class interfaceClass){
        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(),
                        args,
                        new Class[]{String.class});

                // 模擬負載均衡,隨機獲取服務器
                URL url = Register.random(interfaceClass.getName());

                // 調用
                HttpClient httpClient = new HttpClient();
                return httpClient.post(url.getHostname(),url.getPort(),invocation);
            }
        });
    }
}

以文本形式實現註冊中心

因爲消費端與服務端是兩個進程,消費端是獲取不到服務端的REGISTER的,所以需要在服務端註冊時將URL寫入文本,因然後在消費端根據interfaceName隨機調度已發佈服務的服務器地址。 Register.java

public class Register {

    private static Map<String,Map<URL,Class>> REGISTER = new HashMap<String, Map<URL, Class>>();

    /**
     * 註冊服務(暴露接口)
     * @param url
     * @param interfaceName
     * @param implClass
     */
    public static void regist(URL url,String interfaceName,Class implClass){
        Map<URL,Class> map = new HashMap<URL, Class>();
        map.put(url,implClass);
        REGISTER.put(interfaceName,map);

        // 寫入文本
        saveFile();
    }

    /**
     * 從註冊中心獲取實現類(發現服務)
     * @param url
     * @param interfaceName
     * @return
     */
    public static Class get(URL url,String interfaceName){
        return REGISTER.get(interfaceName).get(url);
    }

    /**
     * 模擬負載均衡,隨機獲取服務器
     * @param interfaceName
     * @return
     */
    public static URL random(String interfaceName){
        REGISTER = getFile();
        if(REGISTER != null){
            return REGISTER.get(interfaceName).keySet().iterator().next();
        }
        return null;
    }

    /**
     * 寫入文本
     */
    public static void saveFile(){
        try {
            FileOutputStream fos = new FileOutputStream("D://register.text");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(REGISTER);
            oos.flush();
            oos.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 獲取文本
     * @return
     */
    public static Map<String,Map<URL,Class>> getFile(){
        try {
            FileInputStream fis = new FileInputStream("D://register.text");
            ObjectInputStream ois = new ObjectInputStream(fis);
            return (Map<String,Map<URL,Class>>)ois.readObject();
        }catch (IOException | ClassNotFoundException e){
            e.printStackTrace();
        }
        return null;
    }

    public static Class getClass(URL url,String interfaceName){
        REGISTER = getFile();
        if(REGISTER != null){
            return REGISTER.get(interfaceName).get(url);
        }
        return null;
    }

}

優化後的消費端

consumer.java

public class consumer {

    public static void main(String[] args) {

		// 此處模擬spring容器
        HelloService service = ProxyFactory.getProxy(HelloService.class);
        String result = service.sayHello("yukang");
        System.out.println(result);

    }
}

netty實現?

發佈者:全棧程序員棧長,轉載請註明出處:https://javaforall.cn/128109.html原文鏈接:https://javaforall.cn

 

參考:

http://www.manongjc.com/detail/63-scdxsyseltrpppe.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章