天天看點

Dubbo 基本介紹與手寫模拟 Dubbo

什麼是 RPC

RPC(Remote Procedure Call)—遠端過程調用,它是一種通過網絡從遠端計算機程式上請求服務,而不需要了解底層網絡技術的協定。也就是說兩台伺服器A,B,一個應用部署在A伺服器上,想要調用B伺服器上應用提供的方法,由于不在一個記憶體空間,不能直接調用,需要通過網絡來表達調用的語義和傳達調用的資料。

簡單來說,RPC 就是遠端方法調用,遠端方法調用和本地方法調用是相對的兩個概念,本地方法調用指的是程序内部的方法調用,而遠端方法調用指的是兩個程序内的方法互相調用。實作遠端方法調用,基本的就是通過網絡,通過傳輸資料來進行調用。

RPC 可以基于 HTTP 或者TCP 來傳輸資料:

  • 1.RPC over Http:基于Http協定來傳輸資料。
  • 2.PRC over Tcp:基于Tcp協定(socket)來傳輸資料。

對于所傳輸的資料,可以交由RPC的雙方來協商定義,但基本都會包括:

  • 1.調用的是哪個類或接口。
  • 2.調用的是哪個方法,方法名和方法參數類型(考慮方法重載)。
  • 3.調用方法的入參。

是以,我們其實可以看到 RPC 的自定義性是很高的,各個公司内部都可以實作自己的一套 RPC 架構,而 Dubbo 就是阿裡所開源出來的一套 RPC 架構。

RPC 工作原理

RPC的設計由Client,Client stub,Network ,Server stub,Server構成。其中Client就是用來調用服務的,Cient stub是用來把調用的方法和參數序列化的(因為要在網絡中傳輸,必須要把對象轉變成位元組),Network用來傳輸這些資訊到Server stub, Server stub用來把這些資訊反序列化的,Server就是服務的提供者,最終調用的就是Server提供的方法。

Dubbo 基本介紹與手寫模拟 Dubbo
  • 1.Client像調用本地服務似的調用遠端服務。
  • 2.Client stub接收到調用後,将方法、參數序列化。
  • 3.用戶端通過網絡(socket,http等)将消息發送到服務端。
  • 4.Server stub 收到消息後進行解碼(将消息對象反序列化)。
  • 5.Server stub 根據解碼結果調用本地的服務。
  • 6.本地服務執行(對于服務端來說是本地執行)并将結果傳回給Server stub。
  • 7.Server stub将傳回結果打包成消息(将結果消息對象序列化)。
  • 8.服務端通過sockets将消息發送到用戶端。
  • 9.Client stub接收到結果消息,并進行解碼(将結果消息反序列化)。
  • 10.用戶端得到最終結果。
Dubbo 基本介紹與手寫模拟 Dubbo

目前,官網上是這麼介紹的:Apache Dubbo 是一款高性能、輕量級的開源 Java 服務架構。在幾個月前,官網的介紹是:Apache Dubbo 是一款高性能、輕量級的開源 Java RPC架構。

為什麼會将 RPC 改為服務?

Dubbo 一開始的定位就是 RPC 架構,專注于兩個服務之間的調用。但随着微服務的盛行,除開服務調用之外,Dubbo 也在逐漸的涉獵服務治理、服務監控、服務網關等等,是以現在的 Dubbo 目标已經不止是 RPC 架構了,而是想成為和Spring Cloud 類似的一個服務架構。

上面所說的 Dubbo 指的是 Dubbo 架構,另外 Dubbo 還有 Dubbo 協定的含義,Dubbo 架構提供了許許多多的協定實作,例如:dubbo(預設,基于 Netty),rmi, webservice,http,redis 等。

Dubbo 基本介紹與手寫模拟 Dubbo

Provider需要完成以下内容:

  • 1.提供服務的接口。
  • 2.提供服務實作類。
  • 3.将服務注冊到注冊中心(根據接口可以擷取到服務實作類和服務位址)。
  • 4.暴露服務:HTTP協定(基于Tomcat),Dubbo協定(基于Netty)。

Consumer調用服務的時需要完成以下内容:

  • 1.去注冊中心擷取服務資訊。
  • 2.調用方法時需要提供以下資訊,我們把這四個必要的東西建構成一個對象(Invocation對象):
    • 1.接口名
    • 2.方法名
    • 3.方法參數類型清單
    • 4.方法值參數值清單

github 位址:

https://github.com/cr7258/dubbo-lab/tree/master/dubbo-simulate

HelloService 服務

添加一個 HelloService 服務的接口,Consumer 在依賴中隻需要引用 HelloService 接口:

package com.chengzw.provider.api;
/**
 * 服務的接口
 * @author 程治玮
 * @since 2021/3/30 10:10 下午
 */
public interface HelloService {
    public String sayHello(String name);
}      

Provider 需要實作該接口:

package com.chengzw.provider.impl;
import com.chengzw.provider.api.HelloService;
/**
 * 服務實作類
 * @author 程治玮
 * @since 2021/3/30 10:11 下午
 */
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "hello " + name;
    }
}      

RPC 遠端調用子產品

HTTP 和 Dubbo 兩個遠端調用協定都實作了 Protocol接口:

package com.chengzw.framework;
/**
 * HTTP和Dubbo協定都實作該接口
 * 政策模式
 * @author 程治玮
 * @since 2021/3/31 11:17 下午
 */
public interface Protocol {
    void start(URL url);
    String send(URL url,Invocation invocation);
}      

Provider 和 Consumer 可以通過配置檔案來指定使用哪個協定來完成遠端調用(主要就是 Invocation 對象的序列号反序列化和方法的處理),而不需要将調用哪個協定寫死在代碼中。

package com.chengzw.framework;
import com.chengzw.framework.protocol.dubbo.DubboProtocol;
import com.chengzw.framework.protocol.http.HttpProtocol;
import org.springframework.beans.factory.annotation.Value;
/**
 * 讀取配置檔案決定服務端和用戶端使用http還是dubbo協定
 * 工廠模式,解決協定切換的問題
 * @author 程治玮
 * @since 2021/3/31 11:23 下午
 */
public class ProtocolFactory {
    @Value("${protocol}")
    private static String name;
    public static Protocol getProtocol() {
        if (name == null || name.equals("")) name = "http";
        switch (name) {
            case "http":
                return new HttpProtocol();
            case "dubbo":
                return new DubboProtocol();
            default:
                break;
        }
        return new HttpProtocol();
    }
}      

服務注冊

使用 Zookeeper 來做服務的注冊中心,Provider 将服務(接口名)和 位址端口(URL)注冊到注冊中心中,Consumer 從注冊中心擷取服務的資訊:

package com.chengzw.framework.register;
import com.chengzw.framework.URL;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import java.util.HashMap;
import java.util.Map;
/**
 * Zookeeper注冊中心寫入讀取服務端資訊
 * @author 程治玮
 * @since 2021/3/31 11:39 下午
 */
public class ZookeeperRegister {
    static CuratorFramework client;
    static Map<String, String> UrlCache = new HashMap<>();
    static {
        client = CuratorFrameworkFactory
                .newClient("localhost:2181", new RetryNTimes(3, 1000));
        client.start();
    }
    private static Map<String, String> REGISTER = new HashMap<>();
    //Provider注冊服務
    public static void regist(String interfaceName, String implClass, String url) {
        try {
            Stat stat = client.checkExists().forPath(String.format("/dubbo/service/%s", interfaceName));
            if(stat != null){
                client.delete().forPath(String.format("/dubbo/service/%s", interfaceName));
            }
            String result = client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(String.format("/dubbo/service/%s", interfaceName),(implClass + "::" + url).getBytes());
            System.out.println("Provier服務注冊: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //擷取Provider URL
    public static URL getURL(String interfaceName) {
        URL url = null;
        String urlString = null;
        //先查詢緩存
        if (UrlCache.containsKey(interfaceName)) {
            urlString = UrlCache.get(interfaceName);
        } else {
            try {
                byte[] bytes = client.getData().forPath(String.format("/dubbo/service/%s", interfaceName));
                urlString = new String(bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        String host = urlString.split("::")[1].split(":")[0];
        String port = urlString.split("::")[1].split(":")[1];
        return new URL(host,Integer.parseInt(port));
    }
    //擷取Provider實作類
    public static Class getImplClass(String interfaceName) throws Exception {
        byte[] bytes = client.getData().forPath(String.format("/dubbo/service/%s", interfaceName));
        String urlString = new String(bytes);
        return Class.forName(urlString.split("::")[0]);
    }
}      

參考連結