如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的RPC

如何給老婆解釋什么是RPC中鱼鸠,我們討論了RPC的實(shí)現(xiàn)思路。
那么這一次桩皿,就讓我們通過(guò)代碼來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的RPC吧初橘!

RPC的實(shí)現(xiàn)原理

正如上一講所說(shuō)验游,RPC主要是為了解決的兩個(gè)問(wèn)題:

  • 解決分布式系統(tǒng)中,服務(wù)之間的調(diào)用問(wèn)題保檐。
  • 遠(yuǎn)程調(diào)用時(shí)批狱,要能夠像本地調(diào)用一樣方便,讓調(diào)用者感知不到遠(yuǎn)程調(diào)用的邏輯展东。

還是以計(jì)算器Calculator為例赔硫,如果實(shí)現(xiàn)類CalculatorImpl是放在本地的,那么直接調(diào)用即可:

現(xiàn)在系統(tǒng)變成分布式了盐肃,CalculatorImpl和調(diào)用方不在同一個(gè)地址空間爪膊,那么就必須要進(jìn)行遠(yuǎn)程過(guò)程調(diào)用:

那么如何實(shí)現(xiàn)遠(yuǎn)程過(guò)程調(diào)用,也就是RPC呢砸王,一個(gè)完整的RPC流程推盛,可以用下面這張圖來(lái)描述:

其中左邊的Client,對(duì)應(yīng)的就是前面的Service A谦铃,而右邊的Server耘成,對(duì)應(yīng)的則是Service B。
下面一步一步詳細(xì)解釋一下驹闰。

  1. Service A的應(yīng)用層代碼中瘪菌,調(diào)用了Calculator的一個(gè)實(shí)現(xiàn)類的add方法,希望執(zhí)行一個(gè)加法運(yùn)算嘹朗;
  2. 這個(gè)Calculator實(shí)現(xiàn)類师妙,內(nèi)部并不是直接實(shí)現(xiàn)計(jì)算器的加減乘除邏輯,而是通過(guò)遠(yuǎn)程調(diào)用Service B的RPC接口屹培,來(lái)獲取運(yùn)算結(jié)果默穴,因此稱之為Stub怔檩;
  3. Stub怎么和Service B建立遠(yuǎn)程通訊呢?這時(shí)候就要用到遠(yuǎn)程通訊工具了蓄诽,也就是圖中的Run-time Library薛训,這個(gè)工具將幫你實(shí)現(xiàn)遠(yuǎn)程通訊的功能,比如Java的Socket仑氛,就是這樣一個(gè)庫(kù)乙埃,當(dāng)然,你也可以用基于Http協(xié)議的HttpClient调衰,或者其他通訊工具類膊爪,都可以自阱,RPC并沒(méi)有規(guī)定說(shuō)你要用何種協(xié)議進(jìn)行通訊嚎莉;
  4. Stub通過(guò)調(diào)用通訊工具提供的方法,和Service B建立起了通訊沛豌,然后將請(qǐng)求數(shù)據(jù)發(fā)給Service B趋箩。需要注意的是,由于底層的網(wǎng)絡(luò)通訊是基于二進(jìn)制格式的加派,因此這里Stub傳給通訊工具類的數(shù)據(jù)也必須是二進(jìn)制叫确,比如calculator.add(1,2),你必須把參數(shù)值1和2放到一個(gè)Request對(duì)象里頭(這個(gè)Request對(duì)象當(dāng)然不只這些信息芍锦,還包括要調(diào)用哪個(gè)服務(wù)的哪個(gè)RPC接口等其他信息)竹勉,然后序列化為二進(jìn)制,再傳給通訊工具類娄琉,這一點(diǎn)也將在下面的代碼實(shí)現(xiàn)中體現(xiàn)次乓;
  5. 二進(jìn)制的數(shù)據(jù)傳到Service B這一邊了,Service B當(dāng)然也有自己的通訊工具孽水,通過(guò)這個(gè)通訊工具接收二進(jìn)制的請(qǐng)求票腰;
  6. 既然數(shù)據(jù)是二進(jìn)制的,那么自然要進(jìn)行反序列化了女气,將二進(jìn)制的數(shù)據(jù)反序列化為請(qǐng)求對(duì)象杏慰,然后將這個(gè)請(qǐng)求對(duì)象交給Service B的Stub處理;
  7. 和之前的Service A的Stub一樣炼鞠,這里的Stub也同樣是個(gè)“假玩意”缘滥,它所負(fù)責(zé)的,只是去解析請(qǐng)求對(duì)象谒主,知道調(diào)用方要調(diào)的是哪個(gè)RPC接口完域,傳進(jìn)來(lái)的參數(shù)又是什么,然后再把這些參數(shù)傳給對(duì)應(yīng)的RPC接口瘩将,也就是Calculator的實(shí)際實(shí)現(xiàn)類去執(zhí)行吟税。很明顯凹耙,如果是Java,那這里肯定用到了反射肠仪。
  8. RPC接口執(zhí)行完畢肖抱,返回執(zhí)行結(jié)果,現(xiàn)在輪到Service B要把數(shù)據(jù)發(fā)給Service A了异旧,怎么發(fā)意述?一樣的道理,一樣的流程吮蛹,只是現(xiàn)在Service B變成了Client荤崇,Service A變成了Server而已:Service B反序列化執(zhí)行結(jié)果->傳輸給Service A->Service A反序列化執(zhí)行結(jié)果 -> 將結(jié)果返回給Application,完畢潮针。

理論的講完了术荤,是時(shí)候把理論變成實(shí)踐了。

把理論變成實(shí)踐

本文的示例代碼每篷,可到Github下載瓣戚。

首先是Client端的應(yīng)用層怎么發(fā)起RPC,ComsumerApp:

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

通過(guò)一個(gè)CalculatorRemoteImpl焦读,我們把RPC的邏輯封裝進(jìn)去了子库,客戶端調(diào)用時(shí)感知不到遠(yuǎn)程調(diào)用的麻煩。下面再來(lái)看看CalculatorRemoteImpl矗晃,代碼有些多仑嗅,但是其實(shí)就是把上面的2、3张症、4幾個(gè)步驟用代碼實(shí)現(xiàn)了而已仓技,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);

            // 將請(qǐng)求序列化
            CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

            // 將請(qǐng)求發(fā)給服務(wù)提供方
            objectOutputStream.writeObject(calculateRpcRequest);

            // 將響應(yīng)體反序列化
            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吠冤,可能大家會(huì)覺(jué)得不明覺(jué)厲浑彰。

分布式應(yīng)用下,一個(gè)服務(wù)可能有多個(gè)實(shí)例拯辙,比如Service B郭变,可能有ip地址為198.168.1.11和198.168.1.13兩個(gè)實(shí)例,lookupProviders涯保,其實(shí)就是在尋找要調(diào)用的服務(wù)的實(shí)例列表诉濒。在分布式應(yīng)用下,通常會(huì)有一個(gè)服務(wù)注冊(cè)中心夕春,來(lái)提供查詢實(shí)例列表的功能未荒。

查到實(shí)例列表之后要調(diào)用哪一個(gè)實(shí)例呢,只時(shí)候就需要chooseTarget了及志,其實(shí)內(nèi)部就是一個(gè)負(fù)載均衡策略片排。

由于我們這里只是想實(shí)現(xiàn)一個(gè)簡(jiǎn)單的RPC寨腔,所以暫時(shí)不考慮服務(wù)注冊(cè)中心和負(fù)載均衡仇奶,因此代碼里寫死了返回ip地址為127.0.0.1胁黑。

代碼繼續(xù)往下走,我們這里用到了Socket來(lái)進(jìn)行遠(yuǎn)程通訊邓梅,同時(shí)利用ObjectOutputStream的writeObject和ObjectInputStream的readObject冶共,來(lái)實(shí)現(xiàn)序列化和反序列化乾蛤。

最后再來(lái)看看Server端的實(shí)現(xiàn),和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 {
                    // 將請(qǐng)求反序列化
                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                    Object object = objectInputStream.readObject();

                    log.info("request is {}", object);

                    // 調(diào)用服務(wù)
                    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();
                        }
                    }

                    // 返回結(jié)果
                    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端主要是通過(guò)ServerSocket的accept方法家卖,來(lái)接收Client端的請(qǐng)求,接著就是反序列化請(qǐng)求->執(zhí)行->序列化執(zhí)行結(jié)果庙楚,最后將二進(jìn)制格式的執(zhí)行結(jié)果返回給Client上荡。

就這樣我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)陋而又詳細(xì)的RPC。
說(shuō)它簡(jiǎn)陋醋奠,是因?yàn)檫@個(gè)實(shí)現(xiàn)確實(shí)比較挫榛臼,在下一小節(jié)會(huì)說(shuō)它為什么挫伊佃。
說(shuō)它詳細(xì)窜司,是因?yàn)樗徊揭徊降难菔玖艘粋€(gè)RPC的執(zhí)行流程,方便大家了解RPC的內(nèi)部機(jī)制航揉。

為什么說(shuō)這個(gè)RPC實(shí)現(xiàn)很挫

這個(gè)RPC實(shí)現(xiàn)只是為了給大家演示一下RPC的原理塞祈,要是想放到生產(chǎn)環(huán)境去用,那是絕對(duì)不行的帅涂。

1议薪、缺乏通用性
我通過(guò)給Calculator接口寫了一個(gè)CalculatorRemoteImpl,來(lái)實(shí)現(xiàn)計(jì)算器的遠(yuǎn)程調(diào)用媳友,下一次要是有別的接口需要遠(yuǎn)程調(diào)用斯议,是不是又得再寫對(duì)應(yīng)的遠(yuǎn)程調(diào)用實(shí)現(xiàn)類?這肯定是很不方便的醇锚。

那該如何解決呢哼御?先來(lái)看看使用Dubbo時(shí)是如何實(shí)現(xiàn)RPC調(diào)用的:

@Reference
private Calculator calculator;

...

calculator.add(1,2);

...

Dubbo通過(guò)和Spring的集成,在Spring容器初始化的時(shí)候焊唬,如果掃描到對(duì)象加了@Reference注解恋昼,那么就給這個(gè)對(duì)象生成一個(gè)代理對(duì)象,這個(gè)代理對(duì)象會(huì)負(fù)責(zé)遠(yuǎn)程通訊赶促,然后將代理對(duì)象放進(jìn)容器中液肌。所以代碼運(yùn)行期用到的calculator就是那個(gè)代理對(duì)象了。

我們可以先不和Spring集成鸥滨,也就是先不采用依賴注入嗦哆,但是我們要做到像Dubbo一樣谤祖,無(wú)需自己手動(dòng)寫代理對(duì)象,怎么做呢老速?那自然是要求所有的遠(yuǎn)程調(diào)用都遵循一套模板泊脐,把遠(yuǎn)程調(diào)用的信息放到一個(gè)RpcRequest對(duì)象里面,發(fā)給Server端烁峭,Server端解析之后就知道你要調(diào)用的是哪個(gè)RPC接口容客、以及入?yún)⑹鞘裁搭愋汀⑷雲(yún)⒌闹涤质鞘裁?/strong>约郁,就像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
在實(shí)現(xiàn)了代理對(duì)象通用化之后,下一步就可以考慮集成Spring的IOC功能了鬓梅,通過(guò)Spring來(lái)創(chuàng)建代理對(duì)象供置,這一點(diǎn)就需要對(duì)Spring的bean初始化有一定掌握了。

3绽快、長(zhǎng)連接or短連接
總不能每次要調(diào)用RPC接口時(shí)都去開(kāi)啟一個(gè)Socket建立連接吧芥丧?是不是可以保持若干個(gè)長(zhǎng)連接,然后每次有rpc請(qǐng)求時(shí)坊罢,把請(qǐng)求放到任務(wù)隊(duì)列中续担,然后由線程池去消費(fèi)執(zhí)行?只是一個(gè)思路活孩,后續(xù)可以參考一下Dubbo是如何實(shí)現(xiàn)的物遇。

4、 服務(wù)端線程池
我們現(xiàn)在的Server端憾儒,是單線程的询兴,每次都要等一個(gè)請(qǐng)求處理完,才能去accept另一個(gè)socket的連接起趾,這樣性能肯定很差诗舰,是不是可以通過(guò)一個(gè)線程池,來(lái)實(shí)現(xiàn)同時(shí)處理多個(gè)RPC請(qǐng)求训裆?同樣只是一個(gè)思路眶根。

5、服務(wù)注冊(cè)中心
正如之前提到的缭保,要調(diào)用服務(wù)汛闸,首先你需要一個(gè)服務(wù)注冊(cè)中心,告訴你對(duì)方服務(wù)都有哪些實(shí)例艺骂。Dubbo的服務(wù)注冊(cè)中心是可以配置的诸老,官方推薦使用Zookeeper。如果使用Zookeeper的話,要怎樣往上面注冊(cè)實(shí)例别伏,又要怎樣獲取實(shí)例蹄衷,這些都是要實(shí)現(xiàn)的。

6厘肮、負(fù)載均衡
如何從多個(gè)實(shí)例里挑選一個(gè)出來(lái)愧口,進(jìn)行調(diào)用,這就要用到負(fù)載均衡了类茂。負(fù)載均衡的策略肯定不只一種耍属,要怎樣把策略做成可配置的?又要如何實(shí)現(xiàn)這些策略巩检?同樣可以參考Dubbo厚骗,Dubbo - 負(fù)載均衡

7、結(jié)果緩存
每次調(diào)用查詢接口時(shí)都要真的去Server端查詢嗎兢哭?是不是要考慮一下支持緩存领舰?

8、多版本控制
服務(wù)端接口修改了迟螺,舊的接口怎么辦冲秽?

9、異步調(diào)用
客戶端調(diào)用完接口之后矩父,不想等待服務(wù)端返回锉桑,想去干點(diǎn)別的事,可以支持不浙垫?

10刨仑、優(yōu)雅停機(jī)
服務(wù)端要停機(jī)了郑诺,還沒(méi)處理完的請(qǐng)求夹姥,怎么辦?

......

諸如此類的優(yōu)化點(diǎn)還有很多辙诞,這也是為什么實(shí)現(xiàn)一個(gè)高性能高可用的RPC框架那么難的原因辙售。

當(dāng)然,我們現(xiàn)在已經(jīng)有很多很不錯(cuò)的RPC框架可以參考了飞涂,我們完全可以借鑒一下前人的智慧旦部。

后面如果有(dian)機(jī)(zan)會(huì)(duo)的話,也將和大家分享一下如何一步一步優(yōu)化現(xiàn)有的這塊RPC代碼较店,把它做成一個(gè)小型RPC框架士八!

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市梁呈,隨后出現(xiàn)的幾起案子婚度,更是在濱河造成了極大的恐慌,老刑警劉巖官卡,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝗茁,死亡現(xiàn)場(chǎng)離奇詭異醋虏,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)哮翘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門颈嚼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人饭寺,你說(shuō)我怎么就攤上這事阻课。” “怎么了艰匙?”我有些...
    開(kāi)封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵柑肴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我旬薯,道長(zhǎng)晰骑,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任绊序,我火速辦了婚禮硕舆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骤公。我一直安慰自己抚官,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布阶捆。 她就那樣靜靜地躺著凌节,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洒试。 梳的紋絲不亂的頭發(fā)上倍奢,一...
    開(kāi)封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音垒棋,去河邊找鬼卒煞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛叼架,可吹牛的內(nèi)容都是我干的畔裕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼乖订,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扮饶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起乍构,我...
    開(kāi)封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤甜无,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毫蚓,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡占键,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了元潘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畔乙。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖翩概,靈堂內(nèi)的尸體忽然破棺而出牲距,到底是詐尸還是另有隱情,我是刑警寧澤钥庇,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布牍鞠,位于F島的核電站,受9級(jí)特大地震影響评姨,放射性物質(zhì)發(fā)生泄漏难述。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一吐句、第九天 我趴在偏房一處隱蔽的房頂上張望胁后。 院中可真熱鬧,春花似錦嗦枢、人聲如沸攀芯。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)侣诺。三九已至,卻和暖如春氧秘,著一層夾襖步出監(jiān)牢的瞬間年鸳,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工敏储, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阻星,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓已添,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親滥酥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子更舞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容