現(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)用的邏輯。1.1 本地調(diào)用 vs 遠(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)用1.2 RPC完整流程那么如何實(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ì)解釋一下??蛻舳苏{(diào)用Service A的應(yīng)用層代碼中調(diào)用了Calculator的一個(gè)實(shí)現(xiàn)類的add方法希望執(zhí)行一個(gè)加法運(yùn)算Stub代理這個(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遠(yuǎn)程通訊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)行通訊序列化與傳輸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)服務(wù)端接收二進(jìn)制的數(shù)據(jù)傳到Service B這一邊了Service B當(dāng)然也有自己的通訊工具通過(guò)這個(gè)通訊工具接收二進(jìn)制的請(qǐng)求反序列化既然數(shù)據(jù)是二進(jìn)制的那么自然要進(jìn)行反序列化了將二進(jìn)制的數(shù)據(jù)反序列化為請(qǐng)求對(duì)象然后將這個(gè)請(qǐng)求對(duì)象交給Service B的Stub處理服務(wù)端執(zhí)行和之前的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那這里肯定用到了反射。結(jié)果返回RPC接口執(zhí)行完畢返回執(zhí)行結(jié)果現(xiàn)在輪到Service B要把數(shù)據(jù)發(fā)給Service A了怎么發(fā)一樣的道理一樣的流程只是現(xiàn)在Service B變成了ClientService A變成了Server而已Service B反序列化執(zhí)行結(jié)果-傳輸給Service A-Service A反序列化執(zhí)行結(jié)果 - 將結(jié)果返回給Application完畢。理論的講完了是時(shí)候把理論變成實(shí)踐了。二、代碼實(shí)現(xiàn)一個(gè)簡(jiǎn)單的RPC示例本文的示例代碼可到Github下載。2.1 客戶端實(shí)現(xiàn)首先是Client端的應(yīng)用層怎么發(fā)起RPCComsumerApppublic 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)了而已CalculatorRemoteImplpublic class CalculatorRemoteImpl implements Calculator { public int add(int a, int b) { ListString 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方法的前面兩行l(wèi)ookupProviders和chooseTarget可能大家會(huì)覺(jué)得不明覺(jué)厲。服務(wù)發(fā)現(xiàn)分布式應(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í)例列表的功能。負(fù)載均衡查到實(shí)例列表之后要調(diào)用哪一個(gè)實(shí)例呢只時(shí)候就需要chooseTarget了其實(shí)內(nèi)部就是一個(gè)負(fù)載均衡策略。由于我們這里只是想實(shí)現(xiàn)一個(gè)簡(jiǎn)單的RPC所以暫時(shí)不考慮服務(wù)注冊(cè)中心和負(fù)載均衡因此代碼里寫(xiě)死了返回ip地址為127.0.0.1。代碼繼續(xù)往下走我們這里用到了Socket來(lái)進(jìn)行遠(yuǎn)程通訊同時(shí)利用ObjectOutputStream的writeObject和ObjectInputStream的readObject來(lái)實(shí)現(xiàn)序列化和反序列化。2.2 服務(wù)端實(shí)現(xiàn)最后再來(lái)看看Server端的實(shí)現(xiàn)和Client端非常類似ProviderApppublic 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。2.3 實(shí)現(xiàn)總結(jié)就這樣我們實(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ī)制。三、當(dāng)前實(shí)現(xiàn)的局限性這個(gè)RPC實(shí)現(xiàn)只是為了給大家演示一下RPC的原理要是想放到生產(chǎn)環(huán)境去用那是絕對(duì)不行的。3.1 缺乏通用性我通過(guò)給Calculator接口寫(xiě)了一個(gè)CalculatorRemoteImpl來(lái)實(shí)現(xiàn)計(jì)算器的遠(yuǎn)程調(diào)用下一次要是有別的接口需要遠(yuǎn)程調(diào)用是不是又得再寫(xiě)對(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)寫(xiě)代理對(duì)象怎么做呢那自然是要求所有的遠(yuǎn)程調(diào)用都遵循一套模板把遠(yuǎn)程調(diào)用的信息放到一個(gè)RpcRequest對(duì)象里面發(fā)給Server端Server端解析之后就知道你要調(diào)用的是哪個(gè)RPC接口、以及入?yún)⑹鞘裁搭愋?、入?yún)⒌闹涤质鞘裁淳拖馜ubbo的RpcInvocationpublic class RpcInvocation implements Invocation, Serializable { private static final long serialVersionUID -4355285085441097045L; private String methodName; private Class?[] parameterTypes; private Object[] arguments; private MapString, String attachments; private transient Invoker? invoker;3.2 其他待優(yōu)化點(diǎn)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)這些策略同樣可以參考DubboDubbo - 負(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框架那么難的原因。四、總結(jié)與展望當(dāng)然我們現(xiàn)在已經(jīng)有很多很不錯(cuò)的RPC框架可以參考了我們完全可以借鑒一下前人的智慧。后面如果有dian機(jī)zan會(huì)duo的話也將和大家分享一下如何一步一步優(yōu)化現(xiàn)有的這塊RPC代碼把它做成一個(gè)小型RPC框架五、參考資料一本很棒的分布式書(shū)籍《大型網(wǎng)站系統(tǒng)與Java中間件實(shí)踐》Dubbo 使用文檔Dubbo 源碼開(kāi)發(fā)手冊(cè)