1. 程式人生 > 其它 >趁熱打鐵,我們今天來手寫一個RPC框架……

趁熱打鐵,我們今天來手寫一個RPC框架……

前言

昨天大概理了下思路,覺得目前最合適的事,就是手寫一個rpc框架,因為只有創造、創作,才讓我覺得生活更有激情,而且做具體的事,也會讓生活更充實、更真實,會讓你不那麼迷茫。

如果有同樣感受的小夥伴,可以試著找一些具體的事來做一做,這樣你也就不再那麼浮躁了。

好了,廢話少說,我們直接正文。

手寫RPC

在開始正文前,我們先看下什麼是rpc

關於RPC

rpc的英文全稱是Remote Procedure Call,翻譯過來就是遠端呼叫,顧名思義,rpc就是一套介面的呼叫方式,一種呼叫協議。

關於rpc的呼叫原理,這裡放上百度百科的一張圖片,供大家參考:

正如上圖所示,因為是遠端呼叫,所以介面的呼叫方和被呼叫方是通過網路實現呼叫和通訊的,今天我們是基於socket

來實現遠端呼叫的。

rpc廣義的定義角度來說,restful也屬於遠端呼叫,只是遵循的通訊協議不一樣。

好了,基礎知識就到這裡,下面我們看下具體如何實現。

具體實現

開始之前,我想先說下思路。在此之前,我們已經手寫過springboot的一個簡易框架,通過那個框架,我們瞭解了伺服器的工作流程,和呼叫過程,當然更重要的是也為我們今天的實現提供思路。簡單來說,今天的實現是這樣的:

分別建立socket服務端和客戶端,他們分別代表服務提供者和服務消費者,服務提供者先啟動(服務端),消費者向服務端傳送訪問請求(包括類名、方法名、引數等),服務提供者收到介面訪問請求時,解析請求引數(拿出類名、方法名、引數列表),通過反射呼叫方法,並將執行結果返回,然後呼叫完成。

這裡是簡單的流程圖:

下面我們看下具體實現過程。

服務提供者

就是一個socket服務端,這裡最核心的問題是,你需要提前確定消費者和提供者要傳輸哪些資料,因為客戶端也是我們自己寫,所以具體資料傳輸就比較靈活。

這裡我通過objectInputStreamobjectOutputStream進行資料操作,因為我們操作的都是javaobject,所以用這個就比較方便,當然網上絕大多示例也都是這麼實現的。

你直接用InputStream/OutputStream也是可以的,重要的是要理解原理。

這裡需要注意的是,服務端讀取的順序,必須和客戶端傳送的一致,否則在反序列化的時候會報錯,會強轉失敗:

  1. 讀取類名
  2. 讀取方法名
  3. 讀取引數
  4. 讀取引數型別
public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8888);
            while (true) {
                Socket accept = serverSocket.accept();
                ObjectInputStream objectInputStream = new ObjectInputStream(accept.getInputStream());
                // 讀取類名
                String classFullName = objectInputStream.readUTF();
                // 讀取方法名
                String methodName = objectInputStream.readUTF();
                // 讀取方法呼叫入參
                Object[] parameters = (Object[])objectInputStream.readObject();
                // 讀取方法入參型別
                Class<?>[] parameterTypes = (Class<?>[])objectInputStream.readObject();
                System.out.println(String.format("收到消費者遠端呼叫請求:類名 = {%s},方法名 = {%s},呼叫入參 = %s,方法入參列表 = %s",
                        classFullName, methodName, Arrays.toString(parameters), Arrays.toString(parameterTypes)));
                Class<?> aClass = Class.forName(classFullName);
                Method method = aClass.getMethod(methodName, parameterTypes);
                Object invoke = method.invoke(aClass.newInstance(), parameters);
                // 回寫返回值
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(accept.getOutputStream());
                System.out.println("方法呼叫結果:" + invoke);
                objectOutputStream.writeObject(invoke);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
介面實現

這裡的介面實現就是我們提供給消費者的服務。

public class HelloSercieImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "hello, " + name;
    }
}
服務消費者

前面說了,服務消費者傳送訊息的順序必須和服務提供者讀取的順序一致,所以消費者傳送的順序也必須如下:

  1. 寫類名
  2. 寫方法名
  3. 寫引數
  4. 寫引數型別
public static void main(String[] args) {
        try {
            String classFullName = "io.github.syske.rpc.service.HelloSercieImpl";
            String methodName = "sayHello";
            String[] parameters = {"syske"};
            Class<?>[] parameterTypes = {String.class};
            Socket socket = new Socket("127.0.0.1", 8888);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeUTF(classFullName);
            objectOutputStream.writeUTF(methodName);
            objectOutputStream.writeObject(parameters);
            objectOutputStream.writeObject(parameterTypes);
            // 讀取返回值
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object o = objectInputStream.readObject();
            System.out.println("消費者遠端呼叫返回結果:" + o);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

這裡的方法名和引數列表主要是為了確認方法的唯一性,便於獲取方法,後期如果引入facade層,方法名和引數可以直接從介面獲取。

測試

我們來測試一下,先啟動服務提供者,然後啟動服務消費者,這時候會看到服務提供者控制檯輸出如下資訊:

同時,消費者控制檯也返回了遠端呼叫結果:

到這裡,我們簡易的rpc服務框架就基本完成了。怎麼樣,是不是很簡單呢?

總結

不知道大家發現沒,雖然我們的rpc框架實現了,但是在呼叫具體方法的時候很不靈活,不僅需要執行實現類,要指定方法名、入參、引數列表,同時還需要配置服務地址和埠,很繁瑣,而且如果我們的遠端介面很多,我們就需要配置很多不同的介面資訊,介面管理起來就很不方便。

所以這時候我們就應該明白了,為什麼rpc框架需要註冊中心,以及註冊中心的作用?是的,它就是用來註冊管理我們的服務的,讓我們可以更方便地找到服務的提供者以及介面的資訊。

我現在對很多框架了元件有了更深刻的瞭解和認知,任何元件的出現都是為了解決具體的問題,比如zookeeperredis、配置中心等,但這些元件並非必須的,所以我們在使用具體元件的時候,已經要有知其然,知其所以然的思維,不能僅僅停留在會用的層面,而應該去思考為什麼用,為什麼不用,只有在這樣的思維指導下,你才可能在這個行業走的更遠,至少作為一名技術人員,我覺得是這樣的。

技術本身只是器,只是工具,脫離技術的思維才是yyds,當然行動也是很重要的,我們也需要不斷地實踐和探索。

web伺服器一樣,我以前對rpc的認知也比較淺顯,總覺得這些東西很牛逼,但是當我真正理解了原理,然後自己動手做的時候,我覺得也沒有那麼難,還是那句話,知易行難,還是要自己動手,才能真正瞭解其中意。

好了,今天就到這裡吧!

完整專案開源地址如下,感興趣的小夥伴可以去看下,後續我們會繼續實現相關功能,比如整合註冊中心、實現動態代理等待:

https://github.com/Syske/syske-rpc-server