一个例子理解什么是RPC?
RPC的實(shí)現(xiàn)原理
正如上一講所說,RPC主要是為了解決的兩個(gè)問題:
- 解決分布式系統(tǒng)中,服務(wù)之間的調(diào)用問題。
- 遠(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)程過程調(diào)用:
?
那么如何實(shí)現(xiàn)遠(yuǎn)程過程調(diào)用,也就是RPC呢,一個(gè)完整的RPC流程,可以用下面這張圖來描述:
?
其中左邊的Client,對應(yīng)的就是前面的Service A,而右邊的Server,對應(yīng)的則是Service B。
下面一步一步詳細(xì)解釋一下。
理論的講完了,是時(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);} }通過一個(gè)CalculatorRemoteImpl,我們把RPC的邏輯封裝進(jìn)去了,客戶端調(diào)用時(shí)感知不到遠(yuǎn)程調(diào)用的麻煩。下面再來看看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);// 將請求序列化CalculateRpcRequest calculateRpcRequest = generateRequest(a, b);ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());// 將請求發(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ì)覺得不明覺厲。
分布式應(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ù)注冊中心,來提供查詢實(shí)例列表的功能。
查到實(shí)例列表之后要調(diào)用哪一個(gè)實(shí)例呢,只時(shí)候就需要chooseTarget了,其實(shí)內(nèi)部就是一個(gè)負(fù)載均衡策略。
由于我們這里只是想實(shí)現(xiàn)一個(gè)簡單的RPC,所以暫時(shí)不考慮服務(wù)注冊中心和負(fù)載均衡,因此代碼里寫死了返回ip地址為127.0.0.1。
代碼繼續(xù)往下走,我們這里用到了Socket來進(jìn)行遠(yuǎn)程通訊,同時(shí)利用ObjectOutputStream的writeObject和ObjectInputStream的readObject,來實(shí)現(xiàn)序列化和反序列化。
最后再來看看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 {// 將請求反序列化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端主要是通過ServerSocket的accept方法,來接收Client端的請求,接著就是反序列化請求->執(zhí)行->序列化執(zhí)行結(jié)果,最后將二進(jìn)制格式的執(zhí)行結(jié)果返回給Client。
就這樣我們實(shí)現(xiàn)了一個(gè)簡陋而又詳細(xì)的RPC。
說它簡陋,是因?yàn)檫@個(gè)實(shí)現(xiàn)確實(shí)比較挫,在下一小節(jié)會(huì)說它為什么挫。
說它詳細(xì),是因?yàn)樗徊揭徊降难菔玖艘粋€(gè)RPC的執(zhí)行流程,方便大家了解RPC的內(nèi)部機(jī)制。
為什么說這個(gè)RPC實(shí)現(xiàn)很挫
這個(gè)RPC實(shí)現(xiàn)只是為了給大家演示一下RPC的原理,要是想放到生產(chǎn)環(huán)境去用,那是絕對不行的。
1、缺乏通用性
我通過給Calculator接口寫了一個(gè)CalculatorRemoteImpl,來實(shí)現(xiàn)計(jì)算器的遠(yuǎn)程調(diào)用,下一次要是有別的接口需要遠(yuǎn)程調(diào)用,是不是又得再寫對應(yīng)的遠(yuǎn)程調(diào)用實(shí)現(xiàn)類?這肯定是很不方便的。
那該如何解決呢?先來看看使用Dubbo時(shí)是如何實(shí)現(xiàn)RPC調(diào)用的:
@Reference private Calculator calculator;...calculator.add(1,2);...Dubbo通過和Spring的集成,在Spring容器初始化的時(shí)候,如果掃描到對象加了@Reference注解,那么就給這個(gè)對象生成一個(gè)代理對象,這個(gè)代理對象會(huì)負(fù)責(zé)遠(yuǎn)程通訊,然后將代理對象放進(jìn)容器中。所以代碼運(yùn)行期用到的calculator就是那個(gè)代理對象了。
我們可以先不和Spring集成,也就是先不采用依賴注入,但是我們要做到像Dubbo一樣,無需自己手動(dòng)寫代理對象,怎么做呢?那自然是要求所有的遠(yuǎn)程調(diào)用都遵循一套模板,把遠(yuǎn)程調(diào)用的信息放到一個(gè)RpcRequest對象里面,發(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)了代理對象通用化之后,下一步就可以考慮集成Spring的IOC功能了,通過Spring來創(chuàng)建代理對象,這一點(diǎn)就需要對Spring的bean初始化有一定掌握了。
3、長連接or短連接
總不能每次要調(diào)用RPC接口時(shí)都去開啟一個(gè)Socket建立連接吧?是不是可以保持若干個(gè)長連接,然后每次有rpc請求時(shí),把請求放到任務(wù)隊(duì)列中,然后由線程池去消費(fèi)執(zhí)行?只是一個(gè)思路,后續(xù)可以參考一下Dubbo是如何實(shí)現(xiàn)的。
4、 服務(wù)端線程池
我們現(xiàn)在的Server端,是單線程的,每次都要等一個(gè)請求處理完,才能去accept另一個(gè)socket的連接,這樣性能肯定很差,是不是可以通過一個(gè)線程池,來實(shí)現(xiàn)同時(shí)處理多個(gè)RPC請求?同樣只是一個(gè)思路。
5、服務(wù)注冊中心
正如之前提到的,要調(diào)用服務(wù),首先你需要一個(gè)服務(wù)注冊中心,告訴你對方服務(wù)都有哪些實(shí)例。Dubbo的服務(wù)注冊中心是可以配置的,官方推薦使用Zookeeper。如果使用Zookeeper的話,要怎樣往上面注冊實(shí)例,又要怎樣獲取實(shí)例,這些都是要實(shí)現(xiàn)的。
6、負(fù)載均衡
如何從多個(gè)實(shí)例里挑選一個(gè)出來,進(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ī)了,還沒處理完的請求,怎么辦?
......
諸如此類的優(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框架!
參考
- 一本很棒的分布式書籍:《大型網(wǎng)站系統(tǒng)與Java中間件實(shí)踐》
- Dubbo 使用文檔
- Dubbo 源碼開發(fā)手冊
總結(jié)
以上是生活随笔為你收集整理的一个例子理解什么是RPC?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM:堆与栈的比较
- 下一篇: JVM: G1和CMS的区别