以下文章來(lái)源于公眾號(hào)crossoverJie ,作者crossoverJie
前言
隨著最近關(guān)注 cim 項(xiàng)目的人越發(fā)增多剿骨,導(dǎo)致提的問(wèn)題以及 Bug 也在增加代芜,在修復(fù)問(wèn)題的過(guò)程中難免代碼潔癖又上來(lái)了。
看著一兩年前寫(xiě)的東西總是懷疑這真的是出自自己手里嘛浓利?有些地方實(shí)在忍不住了便開(kāi)始了漫漫重構(gòu)之路挤庇。
前后對(duì)比
在開(kāi)始之前先簡(jiǎn)單介紹一下 cim
這個(gè)項(xiàng)目钞速,下面是它的架構(gòu)圖:
簡(jiǎn)單來(lái)說(shuō)就是一個(gè) IM 即時(shí)通訊系統(tǒng),主要有以下部分組成:
IM-server
自然就是服務(wù)端了嫡秕,用于和客戶端保持長(zhǎng)連接渴语。IM-client
客戶端,可以簡(jiǎn)單認(rèn)為是類(lèi)似于的 QQ 這樣的客戶端工具昆咽;當(dāng)然功能肯定沒(méi)那么豐富驾凶,只提供了一些簡(jiǎn)單消息發(fā)送、接收的功能掷酗。Route
路由服務(wù)调违,主要用于客戶端鑒權(quán)、消息的轉(zhuǎn)發(fā)等汇在;提供一些 http 接口翰萨,可以用于查看系統(tǒng)狀態(tài)、在線人數(shù)等功能糕殉。
當(dāng)然服務(wù)端、路由都可以水平擴(kuò)展殖告。
這是一個(gè)消息發(fā)送的流程圖阿蝶,假設(shè)現(xiàn)在部署了兩個(gè)服務(wù)端 A、B 和一個(gè)路由服務(wù)黄绩;其中 ClientA
和 ClientB
分別和服務(wù)端 A羡洁、B 保持了長(zhǎng)連接。
當(dāng) ClientA
向 ClientB
發(fā)送一個(gè) hello world
時(shí)爽丹,整個(gè)的消息流轉(zhuǎn)如圖所示:
先通過(guò)
http
將消息發(fā)送到Route
服務(wù)筑煮。路由服務(wù)得知
ClientB
是連接在ServerB
上;于是再通過(guò)http
將消息發(fā)送給ServerB
粤蝎。最終
ServerB
將消息通過(guò)與ClientB
的長(zhǎng)連接通道push
下去真仲,至此消息發(fā)送成功。
這里我截取了 ClientA
向 Route
發(fā)起請(qǐng)求的代碼:
可以看到這就是利用 okhttp
發(fā)起了一個(gè) http
請(qǐng)求初澎,這樣雖然能實(shí)現(xiàn)功能秸应,但其實(shí)并不優(yōu)雅。
舉個(gè)例子:假設(shè)我們需要對(duì)接支付寶的接口碑宴,這里發(fā)送一個(gè) http 請(qǐng)求自然是沒(méi)問(wèn)題软啼;但對(duì)于支付寶內(nèi)部各部門(mén)直接互相調(diào)用接口時(shí)那就不應(yīng)該再使用原始的 http 請(qǐng)求了。
應(yīng)該是由服務(wù)提供方提供一個(gè) api
包延柠,服務(wù)消費(fèi)者只需要依賴(lài)這個(gè)包就可以實(shí)現(xiàn)接口調(diào)用祸挪。
當(dāng)然最終使用的是 http、還是自定義私有協(xié)議都可以贞间。
也類(lèi)似于我們?cè)谑褂?Dubbo
或者是 SpringCloud
時(shí)贿条,通常是直接依賴(lài)一個(gè) api
包雹仿,便可以像調(diào)用一個(gè)本地方法一樣調(diào)用遠(yuǎn)程服務(wù)了,并且完全屏蔽了底層細(xì)節(jié)闪唆,不管是使用的 http 還是 其他私有協(xié)議都沒(méi)關(guān)系盅粪,對(duì)于調(diào)用者來(lái)說(shuō)完全不關(guān)心。
這么一說(shuō)是不是有內(nèi)味了悄蕾,這不就是 RPC 的官方解釋嘛票顾。
對(duì)應(yīng)到這里也是同樣的道理, Client
帆调、 Route
奠骄、 Server
本質(zhì)上都是一個(gè)系統(tǒng),他們互相的接口調(diào)用也應(yīng)當(dāng)是走 RPC
才合理番刊。
所以我重構(gòu)之后的變成這樣了:
是不是代碼也簡(jiǎn)潔了許多含鳞,就和調(diào)用本地方法一樣了,而且這樣也有幾個(gè)好處:
完全屏蔽了底層細(xì)節(jié)芹务,可以更好的實(shí)現(xiàn)業(yè)務(wù)及維護(hù)代碼蝉绷。
即便是服務(wù)提供方修改了參數(shù),在編譯期間就能很快發(fā)現(xiàn)枣抱,而像之前那樣調(diào)用是完全不知情的熔吗,所以也增加了風(fēng)險(xiǎn)。
繞不開(kāi)的動(dòng)態(tài)代理
下面來(lái)聊聊具體是如何實(shí)現(xiàn)的佳晶。
要想做到對(duì)調(diào)用者無(wú)感知桅狠,就得創(chuàng)建一個(gè)接口的代理對(duì)象;在這個(gè)代理對(duì)象中實(shí)現(xiàn)編碼轿秧、調(diào)用中跌、解碼的過(guò)程。
對(duì)應(yīng)到此處其實(shí)就是創(chuàng)建一個(gè) routeApi
的代理對(duì)象菇篡,關(guān)鍵就是這段代碼:
RouteApi routeApi = new ProxyManager<>(RouteApi.class, routeUrl, okHttpClient).getInstance();
完整源碼如下:
其中的 getInstance()
函數(shù)就是返回了需要被代理的接口對(duì)象漩符;而其中的 ProxyInvocation
則是一個(gè)實(shí)現(xiàn)了 InvocationHandler
接口的類(lèi),這套代碼就是利用 JDK
實(shí)現(xiàn)動(dòng)態(tài)代理的三板斧逸贾。
查看 ProxyInvocation
的源碼會(huì)發(fā)現(xiàn)當(dāng)我們調(diào)用被代理接口的任意一個(gè)方法時(shí)陨仅,都會(huì)執(zhí)行這里的 invoke()
方法。
而 invoke()
方法自然就實(shí)現(xiàn)了上圖中提到的:編碼铝侵、遠(yuǎn)程調(diào)用灼伤、解碼的過(guò)程;相信大家很容易看明白咪鲜,由于不是本次探討的重點(diǎn)就不過(guò)多介紹了狐赡。
總結(jié)
其實(shí)理解這些就也就很容易看懂 Dubbo
這類(lèi) RPC
框架的核心源碼了,總體的思路也是類(lèi)似的疟丙,只不過(guò)使用的私有協(xié)議颖侄,所以在編解碼時(shí)會(huì)有所不同鸟雏。
所以大家要是想自己動(dòng)手實(shí)現(xiàn)一個(gè) RPC
框架,不妨參考這個(gè)思路試試览祖,當(dāng)用自己寫(xiě)的代碼跑通一個(gè) RPC
的 helloworld
時(shí)的感覺(jué)是和自己整合了一個(gè) Dubbo
孝鹊、 SpringCloud
這樣的第三方框架的感覺(jué)是完全不同的。
本文的所有源碼: