日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何实现一个简单的RPC

發布時間:2025/3/15 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何实现一个简单的RPC 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在如何給老婆解釋什么是RPC中,我們討論了RPC的實現思路。
那么這一次,就讓我們通過代碼來實現一個簡單的RPC吧!

RPC的實現原理

正如上一講所說,RPC主要是為了解決的兩個問題:

  • 解決分布式系統中,服務之間的調用問題。
  • 遠程調用時,要能夠像本地調用一樣方便,讓調用者感知不到遠程調用的邏輯。

還是以計算器Calculator為例,如果實現類CalculatorImpl是放在本地的,那么直接調用即可:

?

現在系統變成分布式了,CalculatorImpl和調用方不在同一個地址空間,那么就必須要進行遠程過程調用:

?

那么如何實現遠程過程調用,也就是RPC呢,一個完整的RPC流程,可以用下面這張圖來描述:

?

其中左邊的Client,對應的就是前面的Service A,而右邊的Server,對應的則是Service B。
下面一步一步詳細解釋一下。

  • Service A的應用層代碼中,調用了Calculator的一個實現類的add方法,希望執行一個加法運算;
  • 這個Calculator實現類,內部并不是直接實現計算器的加減乘除邏輯,而是通過遠程調用Service B的RPC接口,來獲取運算結果,因此稱之為Stub
  • Stub怎么和Service B建立遠程通訊呢?這時候就要用到遠程通訊工具了,也就是圖中的Run-time Library,這個工具將幫你實現遠程通訊的功能,比如Java的Socket,就是這樣一個庫,當然,你也可以用基于Http協議的HttpClient,或者其他通訊工具類,都可以,RPC并沒有規定說你要用何種協議進行通訊
  • Stub通過調用通訊工具提供的方法,和Service B建立起了通訊,然后將請求數據發給Service B。需要注意的是,由于底層的網絡通訊是基于二進制格式的,因此這里Stub傳給通訊工具類的數據也必須是二進制,比如calculator.add(1,2),你必須把參數值1和2放到一個Request對象里頭(這個Request對象當然不只這些信息,還包括要調用哪個服務的哪個RPC接口等其他信息),然后序列化為二進制,再傳給通訊工具類,這一點也將在下面的代碼實現中體現;
  • 二進制的數據傳到Service B這一邊了,Service B當然也有自己的通訊工具,通過這個通訊工具接收二進制的請求;
  • 既然數據是二進制的,那么自然要進行反序列化了,將二進制的數據反序列化為請求對象,然后將這個請求對象交給Service B的Stub處理;
  • 和之前的Service A的Stub一樣,這里的Stub也同樣是個“假玩意”,它所負責的,只是去解析請求對象,知道調用方要調的是哪個RPC接口,傳進來的參數又是什么,然后再把這些參數傳給對應的RPC接口,也就是Calculator的實際實現類去執行。很明顯,如果是Java,那這里肯定用到了反射
  • RPC接口執行完畢,返回執行結果,現在輪到Service B要把數據發給Service A了,怎么發?一樣的道理,一樣的流程,只是現在Service B變成了Client,Service A變成了Server而已:Service B反序列化執行結果->傳輸給Service A->Service A反序列化執行結果 -> 將結果返回給Application,完畢。
  • 理論的講完了,是時候把理論變成實踐了。

    把理論變成實踐

    本文的示例代碼,可到Github下載。

    首先是Client端的應用層怎么發起RPC,ComsumerApp:

    public class ComsumerApp {public static void main(String[] args) {Calculator calculator = new CalculatorRemoteImpl();int result = calculator.add(1, 2);} }

    通過一個CalculatorRemoteImpl,我們把RPC的邏輯封裝進去了,客戶端調用時感知不到遠程調用的麻煩。下面再來看看CalculatorRemoteImpl,代碼有些多,但是其實就是把上面的2、3、4幾個步驟用代碼實現了而已,CalculatorRemoteImpl:

    public class CalculatorRemoteImpl implements Calculator {public int add(int a, int b) {List<String> addressList = lookupProviders("Calculator.add");String address = chooseTarget(addressList);try {Socket socket = new Socket(address, PORT);// 將請求序列化CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());// 將請求發給服務提供方objectOutputStream.writeObject(calculateRpcRequest);// 將響應體反序列化ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Object response = objectInputStream.readObject();if (response instanceof Integer) {return (Integer) response;} else {throw new InternalError();}} catch (Exception e) {log.error("fail", e);throw new InternalError();}} }

    add方法的前面兩行,lookupProviders和chooseTarget,可能大家會覺得不明覺厲。

    分布式應用下,一個服務可能有多個實例,比如Service B,可能有ip地址為198.168.1.11和198.168.1.13兩個實例,lookupProviders,其實就是在尋找要調用的服務的實例列表。在分布式應用下,通常會有一個服務注冊中心,來提供查詢實例列表的功能。

    查到實例列表之后要調用哪一個實例呢,只時候就需要chooseTarget了,其實內部就是一個負載均衡策略。

    由于我們這里只是想實現一個簡單的RPC,所以暫時不考慮服務注冊中心和負載均衡,因此代碼里寫死了返回ip地址為127.0.0.1。

    代碼繼續往下走,我們這里用到了Socket來進行遠程通訊,同時利用ObjectOutputStream的writeObject和ObjectInputStream的readObject,來實現序列化和反序列化。

    最后再來看看Server端的實現,和Client端非常類似,ProviderApp:

    public class ProviderApp {private Calculator calculator = new CalculatorImpl();public static void main(String[] args) throws IOException {new ProviderApp().run();}private void run() throws IOException {ServerSocket listener = new ServerSocket(9090);try {while (true) {Socket socket = listener.accept();try {// 將請求反序列化ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());Object object = objectInputStream.readObject();log.info("request is {}", object);// 調用服務int result = 0;if (object instanceof CalculateRpcRequest) {CalculateRpcRequest calculateRpcRequest = (CalculateRpcRequest) object;if ("add".equals(calculateRpcRequest.getMethod())) {result = calculator.add(calculateRpcRequest.getA(), calculateRpcRequest.getB());} else {throw new UnsupportedOperationException();}}// 返回結果ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());objectOutputStream.writeObject(new Integer(result));} catch (Exception e) {log.error("fail", e);} finally {socket.close();}}} finally {listener.close();}}}

    Server端主要是通過ServerSocket的accept方法,來接收Client端的請求,接著就是反序列化請求->執行->序列化執行結果,最后將二進制格式的執行結果返回給Client。

    就這樣我們實現了一個簡陋而又詳細的RPC。?
    說它簡陋,是因為這個實現確實比較挫,在下一小節會說它為什么挫。
    說它詳細,是因為它一步一步的演示了一個RPC的執行流程,方便大家了解RPC的內部機制。

    為什么說這個RPC實現很挫

    這個RPC實現只是為了給大家演示一下RPC的原理,要是想放到生產環境去用,那是絕對不行的。

    1、缺乏通用性
    我通過給Calculator接口寫了一個CalculatorRemoteImpl,來實現計算器的遠程調用,下一次要是有別的接口需要遠程調用,是不是又得再寫對應的遠程調用實現類?這肯定是很不方便的。

    那該如何解決呢?先來看看使用Dubbo時是如何實現RPC調用的:

    @Reference private Calculator calculator;...calculator.add(1,2);...

    Dubbo通過和Spring的集成,在Spring容器初始化的時候,如果掃描到對象加了@Reference注解,那么就給這個對象生成一個代理對象,這個代理對象會負責遠程通訊,然后將代理對象放進容器中。所以代碼運行期用到的calculator就是那個代理對象了。

    我們可以先不和Spring集成,也就是先不采用依賴注入,但是我們要做到像Dubbo一樣,無需自己手動寫代理對象,怎么做呢?那自然是要求所有的遠程調用都遵循一套模板,把遠程調用的信息放到一個RpcRequest對象里面,發給Server端,Server端解析之后就知道你要調用的是哪個RPC接口、以及入參是什么類型、入參的值又是什么,就像Dubbo的RpcInvocation:

    public class RpcInvocation implements Invocation, Serializable {private static final long serialVersionUID = -4355285085441097045L;private String methodName;private Class<?>[] parameterTypes;private Object[] arguments;private Map<String, String> attachments;private transient Invoker<?> invoker;

    2、集成Spring
    在實現了代理對象通用化之后,下一步就可以考慮集成Spring的IOC功能了,通過Spring來創建代理對象,這一點就需要對Spring的bean初始化有一定掌握了。

    3、長連接or短連接
    總不能每次要調用RPC接口時都去開啟一個Socket建立連接吧?是不是可以保持若干個長連接,然后每次有rpc請求時,把請求放到任務隊列中,然后由線程池去消費執行?只是一個思路,后續可以參考一下Dubbo是如何實現的。

    4、 服務端線程池
    我們現在的Server端,是單線程的,每次都要等一個請求處理完,才能去accept另一個socket的連接,這樣性能肯定很差,是不是可以通過一個線程池,來實現同時處理多個RPC請求?同樣只是一個思路。

    5、服務注冊中心
    正如之前提到的,要調用服務,首先你需要一個服務注冊中心,告訴你對方服務都有哪些實例。Dubbo的服務注冊中心是可以配置的,官方推薦使用Zookeeper。如果使用Zookeeper的話,要怎樣往上面注冊實例,又要怎樣獲取實例,這些都是要實現的。

    6、負載均衡
    如何從多個實例里挑選一個出來,進行調用,這就要用到負載均衡了。負載均衡的策略肯定不只一種,要怎樣把策略做成可配置的?又要如何實現這些策略?同樣可以參考Dubbo,Dubbo - 負載均衡

    7、結果緩存
    每次調用查詢接口時都要真的去Server端查詢嗎?是不是要考慮一下支持緩存?

    8、多版本控制
    服務端接口修改了,舊的接口怎么辦?

    9、異步調用
    客戶端調用完接口之后,不想等待服務端返回,想去干點別的事,可以支持不?

    10、優雅停機
    服務端要停機了,還沒處理完的請求,怎么辦?

    ......

    諸如此類的優化點還有很多,這也是為什么實現一個高性能高可用的RPC框架那么難的原因。

    當然,我們現在已經有很多很不錯的RPC框架可以參考了,我們完全可以借鑒一下前人的智慧。

    后面如果有(dian)機(zan)會(duo)的話,也將和大家分享一下如何一步一步優化現有的這塊RPC代碼,把它做成一個小型RPC框架!

    參考

    • 一本很棒的分布式書籍:《大型網站系統與Java中間件實踐》
    • Dubbo 使用文檔
    • Dubbo 源碼開發手冊

    ?

    總結

    以上是生活随笔為你收集整理的如何实现一个简单的RPC的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。