自己動手寫RPC框架到dubbo的服務動態註冊,服務路由,負載均衡功能實現

来源:http://www.cnblogs.com/intsmaze/archive/2016/11/12/6056763.html
-Advertisement-
Play Games

RPC即遠程過程調用,它的實現方式有很多,比如webservice等。框架調多了,煩了,沒激情了,我們就該問自己,這些框架的作用到底是什麼,來找回當初的激情。 一般來說,我們寫的系統就是一個單機系統,一個web伺服器一個資料庫服務,但是當這單台伺服器的處理能力受硬體成本的限制,是不能無限的提升處理性 ...


  RPC即遠程過程調用,它的實現方式有很多,比如webservice等。框架調多了,煩了,沒激情了,我們就該問自己,這些框架的作用到底是什麼,來找回當初的激情。
  一般來說,我們寫的系統就是一個單機系統,一個web伺服器一個資料庫服務,但是當這單台伺服器的處理能力受硬體成本的限制,是不能無限的提升處理性能的。這個時候我們使用RPC將原來的本地調用轉變為調用遠端的伺服器上的方法,給系統的處理能力和吞吐量帶來了提升。
  RPC的實現包括客戶端和服務端,即服務的調用方和服務的提供方。服務調用方發送rpc請求到服務提供方,服務提供方根據調用方提供的參數執行請求方法,將執行的結果返回給調用方,一次rpc調用完成。

   先讓我們利用socket簡單的實現RPC,來看看他是什麼鬼樣子。

原文和作者一起討論:http://www.cnblogs.com/intsmaze/p/6056763.html

可接網站開發,java開發。

新浪微博:intsmaze劉洋洋哥

微信:intsmaze

     服務端代碼如下 

服務端的提供服務的方法

package cn.intsmaze.tcp.two.service;
public class SayHelloServiceImpl  {
    public String sayHello(String helloArg) {
        if(helloArg.equals("intsmaze"))
        {
            return "intsmaze";
        }
        else
        {
            return "bye bye";
        }
    }
}

  服務端啟動接收外部方法請求的埠類,它接收到來自客戶端的請求數據後,利用反射知識,創建指定類的對象,並調用對應方法,然後把執行的結果返回給客戶端即可。

package cn.intsmaze.tcp.two.service;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
public class Provider {

    public static void main(String[] args) throws Exception {

        ServerSocket server=new ServerSocket(1234);
        while(true)
        {
            Socket socket=server.accept();
            ObjectInputStream input=new ObjectInputStream(socket.getInputStream());
            
            String classname=input.readUTF();//獲得服務端要調用的類名
            String methodName=input.readUTF();//獲得服務端要調用的方法名稱        
            Class<?>[] parameterTypes=(Class<?>[]) input.readObject();//獲得服務端要調用方法的參數類型
            Object[] arguments=(Object[]) input.readObject();//獲得服務端要調用方法的每一個參數的值        
            
            Class serviceclass=Class.forName(classname);//創建類
            Object object = serviceclass.newInstance();//創建對象
            Method method=serviceclass.getMethod(methodName, parameterTypes);//獲得該類的對應的方法
            
            Object result=method.invoke(object, arguments);//該對象調用指定方法
            
            ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream());
            output.writeObject(result);
            socket.close();
        }
    }
} 

  

服務調用者代碼

  調用服務的方法,主要就是客戶端啟動一個socket,然後向提供服務的服務端發送數據,其中的數據就是告訴服務端去調用哪一個類的哪一個方法,已經調用該方法的參數是多少,然後結束服務端返回的數據即可。

調用服務

package cn.intsmaze.tcp.two.client;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
public class consumer {
    
    @SuppressWarnings({ "unused", "rawtypes" })
    public static void main(String[] arg) throws Exception
    {
        //我們要想調用遠程提供的服務,必須告訴遠程我們要調用你的哪一個類,這裡我們可以在本地創建一個interface來獲取類的名稱,但是這樣我們必須
        //保證該interface和遠程的interface的所在包名一致。這種方式不好。所以我們還是通過硬編碼的方式吧。
     //雖然webservice就是這樣的,我個人覺得不是多好。     
// String interfacename=SayHelloService.class.getName(); String classname="cn.intsmaze.tcp.two.service.SayHelloServiceImpl"; String method="sayHello"; Class[] argumentsType={String.class}; Object[] arguments={"intsmaze"}; Socket socket=new Socket("127.0.0.1",1234); ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(classname); output.writeUTF(method); output.writeObject(argumentsType); output.writeObject(arguments); ObjectInputStream input=new ObjectInputStream(socket.getInputStream()); Object result=input.readObject(); System.out.println(result); socket.close(); } }

   當然實際中出於性能考慮,往往採用非阻塞式I/O,避免無限的等待,帶來系統性能的消耗。

  上面的只是一個簡單的過程,當系統之間的調用變的複雜之後,該方式有如下不足:服務調用者代碼以硬編碼的方式指明所調用服務的信息(類名,方法名),當服務提供方改動所提供的服務的代碼後,服務調用者必須修改代碼進行調整,不然會導致服務調用者無法成功進行遠程方法調用導致系統異常,並且當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,導致異常。

  一個系統中,服務提供者往往不是一個,而是多個,那麼服務消費者如何從眾多的服務者找到對應的服務進行RPC就是一個問題了,因為這個時候我們不能在在服務調用者代碼中硬編碼指出調用哪一個服務的地址等信息,因為我們可以想象,沒有一個統一的地方管理所有服務,那麼我們在錯綜複雜的系統之間無法理清有哪些服務,已經服務的調用關係,這簡直就是災難。

    這個時候就要進行服務的註冊,通過一個第三方的存儲介質,當服務的提供者上線時,通過代碼將所提供的服務的相關信息寫入到存儲介質中,寫入的主要信息以key-value方式:服務的名稱:(類名,方法名,參數類型,參數,IP地址,埠)。服務的調用者向遠程調用服務時,會先到第三方存儲介質中根據所要調用的服務名得到(類名,方法名,參數類型,參數,IP地址,埠)等參數,然後再向服務端發出調用請求。通過這種方式,代碼就變得靈活多變,不會再因為一個局部的變得引發全局架構的變動。因為一般的改動是不會變得服務的名稱的。這種方式其實就是soa架構,服務消費者通過服務名稱,從眾多服務中找到要調用的服務的相關信息,稱為服務的路由。

  下麵通過一個靜態MAP對象來模擬第三方存儲的介質。

package cn.intsmaze.tcp.three;
import net.sf.json.JSONObject;
public class ClassWays {
    
    String classname;//類名
    
    String method;//方法
    
    Class[] argumentsType;//參數類型
     
    String ip;//服務的ip地址
    
    int port;//服務的埠
    
    get,set......
 }

  第三方存儲介質,這裡固定了服務提供者的相關信息,理想的模擬是,當服務啟動後,自動向該類的map集合添加信息。但是因為服務端和客戶端啟動時,是兩個不同的jvm進程,客戶端時無法訪問到服務端寫到靜態map集合的數據的。

package cn.intsmaze.tcp.three;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
public class ServiceRoute {
    
    public static Map<String,String> NAME=new HashMap<String, String>();
    
    public ServiceRoute()
    {
        ClassWays classWays=new ClassWays();
        Class[] argumentsType={String.class};
        classWays.setArgumentsType(argumentsType);
        classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl");
        classWays.setMethod("sayHello");
        classWays.setIp("127.0.0.1");
        classWays.setPort(1234);
        JSONObject js=JSONObject.fromObject(classWays);
        NAME.put("SayHello", js.toString());
    } 
}

  接下來看服務端代碼的美麗面孔吧。

package cn.intsmaze.tcp.three.service;public class Provider {

    //服務啟動的時候,組裝相關信息,然後寫入第三方存儲機制,供服務的調用者去獲取
    public void reallyUse() {
        
        ClassWays classWays = new ClassWays();
        Class[] argumentsType = { String.class };
        classWays.setArgumentsType(argumentsType);
        classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl");
        classWays.setMethod("sayHello");
        classWays.setIp("127.0.0.1");
        classWays.setPort(1234);
        
        JSONObject js=JSONObject.fromObject(classWays);
        
        //模擬第三方存儲介質,實際中應該是redis,mysql,zookeeper等。
        ServiceRoute.NAME.put("SayHello", js.toString());
    }

    public static void main(String[] args) throws Exception {

        ServerSocket server = new ServerSocket(1234);
        //實際中,這個地方應該調用如下方法,但是因為簡單的模擬服務的註冊,將註冊的信息硬編碼在ServiceRoute類中,這個類的構造方法裡面會自動註冊服務的相關信息。
        //server.reallyUse();
        while (true) {
            Socket socket = server.accept();
            ObjectInputStream input = new ObjectInputStream(socket.getInputStream());

            String classname = input.readUTF();
            String methodName = input.readUTF();
            Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
            Object[] arguments = (Object[]) input.readObject();

            Class serviceclass = Class.forName(classname);

            Object object = serviceclass.newInstance();

            Method method = serviceclass.getMethod(methodName, parameterTypes);

            Object result = method.invoke(object, arguments);

            ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
            output.writeObject(result);
            socket.close();
        }
    }
}

  服務的調用者代碼:



package cn.intsmaze.tcp.three.client;public class Consumer {
    
    public Object reallyUse(String provideName,Object[] arguments) throws Exception
    {
        //模擬從第三方存儲介質拿去數據
        ServiceRoute serviceRoute=new ServiceRoute();
        String js=serviceRoute.NAME.get(provideName);
        JSONObject obj = new JSONObject().fromObject(js);
        ClassWays classWays = (ClassWays)JSONObject.toBean(obj,ClassWays.class);
        
        String classname=classWays.getClassname();
        String method=classWays.getMethod();
        Class[] argumentsType=classWays.getArgumentsType();
        Socket socket=new Socket(classWays.getIp(),classWays.getPort());
        
        ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream());
        
        output.writeUTF(classname);
        output.writeUTF(method);
        output.writeObject(argumentsType);
        output.writeObject(arguments);
        
        ObjectInputStream input=new ObjectInputStream(socket.getInputStream());
        Object result=input.readObject();
        socket.close();
        return result;
    }
    @SuppressWarnings({ "unused", "rawtypes" })
    public static void main(String[] arg) throws Exception
    {
        Consumer consumer=new Consumer();
        Object[] arguments={"intsmaze"};
        Object result=consumer.reallyUse("SayHello",arguments);
        System.out.println(result);
    }
}

  回到開始的問題現在我們保證了服務調用者對服務的調用的相關參數以動態的方式進行控制,通過封裝,服務調用者只需要指定每一次調用時的參數的值即可。但是當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,導致異常。這個時候我們該如何考慮解決了?

  剩下的我就不寫代碼示例了,代碼只是思想的表現形式,就像開發語言一直變化,但是思想是不變的。

  服務下線我們應該把該服務從第三方存儲刪除,在服務提供方寫代碼進行刪除控制,也就是服務下線前訪問第三方刪除自己提供的服務。這樣當然行不通的,因為服務宕機時,才不會說,我要宕機了,服務提供者你快去第三方存儲介質刪掉該服務信息。所以這個時候我們就要在第三方存儲介質上做手腳,比如服務提供方並不是直接把服務信息寫入第三方存儲介質,而是與一個第三方系統進行交互,第三方系統把接收到來自服務提供者的服務信息寫入第三方存儲介質中,然後在服務提供者和第三方系統間建立一個心跳檢測,當第三方系統檢測到服務提供者宕機後,就會自動到第三方介質中刪除對應服務信息。

  這個時候我們就可以選擇zookeeper作為第三方存儲介質,服務啟動會到zookeeper上面創建一個臨時目錄,該目錄存儲該服務的相關信息,當服務端宕機了,zookeeper會自動刪除該文件夾,這個時候就實現了服務的動態上下線了。

  這個地方其實就是dubbo的一大特色功能:服務配置中心——動態註冊和獲取服務信息,來統一管理服務名稱和其對於的伺服器的信息。服務提供者在啟動時,將其提供的服務名稱,伺服器地址註冊到服務配置中心,服務消費者通過配置中心來獲得需要調用服務的機器。當伺服器宕機或下線,相應的機器需要動態地從服務配置中心移除,並通知相應的服務消費者。這個過程中,服務消費者只在第一次調用服務時需要查詢服務配置中心,然後將查詢到的信息緩存到本地,後面的調用直接使用本地緩存的服務地址信息,而不需要重新發起請求到服務配置中心去獲取相應的服務地址,直到服務的地址列表有變更(機器上線或者下線)。

  zookeeper如何知道的?zookeeper其實就是會和客戶端直接有一個心跳檢測來判斷的,zookeeper功能很簡單的,可以自己去看對應的書籍即可。

   隨著業務的發展,服務調用者的規模發展到一定的階段,對服務提供方也帶來了巨大的壓力,這個時候服務提供方就不在是一臺機器了,而是一個服務集群了。這個時候服務調用者如何知道調用服務集群的哪一臺機器?

  多台伺服器組成的集群,在請求到來時,需要有一個負載均衡程式從服務的地址列表中選取一臺伺服器進行訪問(服務的負載均衡)。這個時候我們可能會這樣做,在第三方介質中存儲每一個服務集群的代理地址,這樣服務消費者獲取對應服務集群的代理地址,向代理髮送請求,然後再由代理負責轉發請求到對應的服務機器。比如nginx。(實現原理,每一個服務啟動,向第三方存儲介質存儲該服務的代理地址即可,具體多個相同服務產生代理地址重覆可以通過代碼進行控制,這裡就不扯了,都是有經驗的開發人員,服務下線的檢測,第三方存儲介質進行控制。)  
  我們來看看這種有什麼問題,使用nginx進行負載均衡,一旦nginx宕機,依賴他的服務均將失效。所以我們會對動態註冊和獲取服務信息這個功能進行改進,同一個服務每上線一臺新的機器,在zookeeper中的每一個服務名下麵創建一個臨時目錄,一個臨時目錄對應該服務的一個集群,進而實現了服務的集群功能。然後考慮服務的調用者,因為內部系統的調用不像對外的服務,他的訪問數量是可控的且有限的,所以就沒有必要將負載均衡演算法在zookeeper那一端進行實現,就是服務調用者每一次調用都到zookeeper上通過負載均衡來選擇對應服務下的某一個機器目錄,只需要在服務的調用端編寫負載均衡演算法,具體就不需要講太多,條條大路通羅馬。

  如果有沒有講明白的可以留言,我進行更正。基本上一個RPC就是這樣,剩下的一些基於RPC的框架無非就是實現了多些協議,以及一些多種語言環境的考慮和效率的提升。

   覺得不錯點個推薦吧,看在我花了一天時間把自己的知識整理分析,謝謝嘍。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 學習一種知識,我喜歡看看源碼是怎麼進行它們類之間的關係以及方法的調用,是怎麼實現的。這樣我才感覺踏實。 既然現在談到HandlerMapping,我們先知道HandlerMapping的作用:HandlerMapping的作用就是解析請求鏈接,然後根據請求鏈接找到執行這個請求的類(HandlerMa ...
  • 這是繼上次svn 客戶端與伺服器安裝後的如何在Eclipse 環境下線上安裝 SVN插件,我的Eclipse版本是4.50 SVN的線上安裝 下麵為大家提供SVN 的線上安裝教程。下麵是安裝的 詳細過程: 1.打開Eclipse的help的Install New Software 其中http:// ...
  • ...
  • 1、簡介 EventBus是一個Android端優化的publish/subscribe消息匯流排,簡化了應用程式內各組件間、組件與後臺線程間的通信。比如請求網路,等網路返回時通過Handler或Broadcast通知UI,兩個Fragment之間需要通過Listener通信,這些需求都可以通過Eve ...
  • 1、簡介 ButterKnife是註解中相對簡單易懂的很不錯的開源框架 1.強大的View綁定和Click事件處理功能,簡化代碼,提升開發效率 2.方便的處理Adapter里的ViewHolder綁定問題 3.運行時不會影響APP效率,使用配置方便 4.代碼清晰,可讀性強 2、下載地址 https: ...
  • 有不足之處,請大家指出 一、 基礎知識 1、SDK的最新下載 搜索oracle,進入網站,à Downloads –> JavaSEI à 選第一個下載(其實java 8u111和8u112的區別就是在8u111的基礎上優化了一下,升了下級,實際使用沒什麼區別的) 其次註意一下選32位還是64位,是 ...
  • 無限級分類是一種設計技巧,在開發中經常使用,例如:網站目錄、部門結構、文章分類。筆者覺得它在對於設計表的層級結構上面發揮很大的作用,比如大家在一些平臺上面, 填寫邀請人,它就是一種上下級的關係,上級會有多個下級,下級又會有自己的分支,大多數都是利用遞歸的思想去實現。話不多說,首先來溫故一下遞歸的實現 ...
  • 如果你還沒有搭建gtest框架,可以參考我之前的博客:http://www.cnblogs.com/jycboy/p/6001153.html。。 1.The first sample: sample1 你把github上的項目導來之後,github地址:https://github.com/goo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...