仿照dubbo手寫一個RPC框架

AlanWang0o0發表於2020-11-11

dubbo介紹:

dubbo: Dubbo是一款高效能、輕量級的開源 Java RPC框架,它提供了三大核心能力:面向介面的遠端方法呼叫,智慧容錯和負載均衡,以及服務自動註冊和發現。

目的: 實現呼叫遠端服務像呼叫本地服務一樣,將呼叫過程進行封裝。在消費者端只需要一個要呼叫服務的介面,不需要實現,dubbo對該介面進行動態代理。並且支援多種呼叫協議/伺服器。

框架實現:

  • 動態代理介面(核心):動態代理要呼叫服務的介面,在這裡完成了對方法的遠端呼叫,傳遞一個inovocation。
public static <T> T getProxy(final 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 {
                String mock = System.getProperty("mock");
                if(mock != null && mock.startsWith("return:")){
                    return mock.replace("return","");    // 測試用,不呼叫遠端服務
                }

                Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(),method.getParameterTypes(), args);

                List<URL> urlList = RemoteMapRegister.get(interfaceClass.getName());    // 從註冊中心獲取到對應的URLs(可能有多臺伺服器提供服務)

                /*負載均衡*/
                URL url = LoadBalance.random(urlList);    
                Protocol protocol = ProtocolFactory.getProtocol();
                return protocol.send(url,invocation);

            }
        });
    }
  • invocation類(通訊的資訊載體):包含要呼叫的介面名,方法名,引數型別,引數。
public class Invocation implements Serializable {

    /*呼叫介面名*/
    private String interfaceName;
    /*呼叫方法名*/
    private String methodName;
    /*引數型別*/
    private Class[] paramType;
    /*引數*/
    private Object[] params;
  • 伺服器與通訊協議介面(通訊的方式):包含兩個方法,啟動伺服器與傳送訊息
/**
 * 將多個協議抽象為一個Protocol
 */
public interface Protocol {
    /*啟動伺服器*/
    void start(URL url);

    /**
     * 傳送資料
     * @param url 傳送的url
     * @param invocation 傳送的資料,包裝為要給invocation
     * @return
     */
    String send(URL url, Invocation invocation);
}

  • 使用工廠模式來獲取需要的Protocol:
public class ProtocolFactory {
    public static Protocol getProtocol() throws ProtocolNotFoundException {

        /*通過命令列引數來決定通訊使用dubbo還是http*/
        String name = System.getProperty("protocolName");

        /*預設為http請求*/
        if(name == null || name.equals("")) name = "http";

        switch(name) {
            case "http":
                return new HttpProtocol();
            case "dubbo":
                return new DubboProtocol();
            default:
                throw new ProtocolNotFoundException("此協議暫不支援");
        }
    }
}
  • 註冊中心:

    記錄:<介面名,對應提供者的URI>

    1. 使用一個本地檔案模擬

      提供者註冊時寫入檔案,消費者使用時讀取檔案即可

    2. 使用zookeeper儲存

      • 使用curator操作zookeeper:

        client = CuratorFrameworkFactory.newClient("localhost:2181", new RetryNTimes(3, 1000));
        client.start();
        
      • 註冊方法:

        String result = client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(String.format("/dubbo/service/%s/%s", interfaceName, JSONObject.toJSONString(url)), null);
                    System.out.println(result);
        
      • 獲取方法:

        public static List<URL> get(String interfaceName) {
                List<URL> urlList = new ArrayList<>();
        
                try {
                    List<String> result = client.getChildren().forPath(String.format("/dubbo/service/%s", interfaceName));
                    for (String urlstr : result) {
                        urlList.add(JSONObject.parseObject(urlstr, URL.class));
                    }
        
                    REGISTER.put(interfaceName, urlList);
                } catch (Exception e) {
                    e.printStackTrace();
                }
        
                return urlList;
            }
        

執行流程:

提供者:

  1. 暴露服務

    URL url = new URL("localhost",8080);  // 這個url是自己封裝的一個物件,只包括ip和埠
    
    /* 將<對應介面,url>註冊到遠端(redis/zookeeper)*/
    RemoteMapRegister.regist(IHelloService.class.getName(),url);
    
    /* 伺服器本地註冊:<介面,對應實現類> */
    LocalRegister.regist(IHelloService.class.getName(), HelloServiceImpl.class);
    
  2. 開啟伺服器,接受請求

    // 開啟伺服器,使用工廠獲得伺服器
    Protocol protocol = ProtocolFactory.getProtocol();
    protocol.start(url);    // 開啟伺服器,並且將伺服器阻塞接受請求
    
  3. 處理接受到的請求( 展示使用 tomcat & Http協議

    // java 13:獲取req中內容,將InputStream轉換為Invocation物件
    // Invocation invocation = JSONObject.parseObject(req.getInputStream(),Invocation.class);
    
    // java8:使用URL傳送請求,獲得InputStream,轉換為ObjectInputStream,讀取物件
    InputStream inStream = req.getInputStream();
    ObjectInputStream objectInputStream = new ObjectInputStream(inStream);
    Invocation invocation = (Invocation) objectInputStream.readObject();
    
    
    String interfaceName = invocation.getInterfaceName();
    /*通過介面獲得對應實現類*/
    Class implClass = LocalRegister.get(interfaceName);
    /*通過方法名和引數型別獲得唯一的方法*/
    Method method = implClass.getMethod(invocation.getMethodName(),invocation.getParamType());
    /*反射機制method.invoke進行方法呼叫*/
    String result = (String) method.invoke(implClass.newInstance(),invocation.getParams());
    
    // 列印結果,並且將結果寫入到輸出流
    System.out.println("tomcat:" + result);
    IOUtils.write(result,resp.getOutputStream());
    

消費者:

  1. 獲取代理物件,使用代理物件直接呼叫方法

    public static void main(String[] args) {
        IHelloService helloService = ProxyFactory.getProxy(IHelloService.class);    // 獲取要呼叫介面的代理物件
    
        String alan = helloService.sayHello("alan");     // 直接呼叫方法,底層進行方法呼叫
        System.out.println(alan);
    }
    
  2. 代理物件開啟伺服器,並且傳送請求( 展示使用 tomcat & Http 協議 ):

    // 使用JDK13的Java.net傳送請求
    // var request = HttpRequest.newBuilder()
    //    .uri(new URI("http", null, hostname, port, "/", null, null))
    //    .POST(HttpRequest.BodyPublishers.ofString(JSONObject.toJSONString(invocation)))
    //    .build();
    // var client = java.net.http.HttpClient.newHttpClient();
    //
    // HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    //
    // String result = response.body();
    
    // 使用 JDK1.8的url進行傳送網路請求
    /*建立一個URL物件*/
    URL url = new URL("http", hostname, port, "/");
    /*建立連線*/
    HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
    /*設定連線引數*/
    httpURLConnection.setRequestMethod("POST");
    httpURLConnection.setDoOutput(true);
    /*輸出流*/
    OutputStream outputStream = httpURLConnection.getOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(outputStream);
    oos.writeObject(invocation);
    oos.flush();
    oos.close();
    
    /*傳送請求,獲得結果*/
    InputStream inputStream = httpURLConnection.getInputStream();
    String result = IOUtils.toString(inputStream);
    
    return result;
    

執行驗證:

首先啟動服務提供者,然後啟動服務消費者:

  • 提供者列印:
    在這裡插入圖片描述

  • 消費者列印:

在這裡插入圖片描述

這裡測試的只是一個消費者與一個提供者。

當使用到提供者叢集時,也是可以的,本框架消費端提供了負載均衡功能(最簡單的 隨機負載均衡 )

一個建議的dubbo框架就這樣完成了,雖然簡單,但是五臟俱全!

相關文章