正如上一講所說,RPC主要是爲了解決的兩個問題:html
仍是以計算器Calculator爲例,若是實現類CalculatorImpl是放在本地的,那麼直接調用便可:
java
如今系統變成分佈式了,CalculatorImpl和調用方不在同一個地址空間,那麼就必需要進行遠程過程調用:
git
那麼如何實現遠程過程調用,也就是RPC呢,一個完整的RPC流程,能夠用下面這張圖來描述:
github
其中左邊的Client,對應的就是前面的Service A,而右邊的Server,對應的則是Service B。
下面一步一步詳細解釋一下。apache
理論的講完了,是時候把理論變成實踐了。緩存
本文的示例代碼,可到Github下載。網絡
首先是Client端的應用層怎麼發起RPC,ComsumerApp:負載均衡
public class ComsumerApp { public static void main(String[] args) { Calculator calculator = new CalculatorRemoteImpl(); int result = calculator.add(1, 2); } }
經過一個CalculatorRemoteImpl,咱們把RPC的邏輯封裝進去了,客戶端調用時感知不到遠程調用的麻煩。下面再來看看CalculatorRemoteImpl,代碼有些多,可是其實就是把上面的二、三、4幾個步驟用代碼實現了而已,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()); // 將請求發給服務提供方 objectOutputStream.writeObject(calculateRpcRequest); // 將響應體反序列化 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,可能你們會以爲不明覺厲。異步
分佈式應用下,一個服務可能有多個實例,好比Service B,可能有ip地址爲198.168.1.11和198.168.1.13兩個實例,lookupProviders,其實就是在尋找要調用的服務的實例列表。在分佈式應用下,一般會有一個服務註冊中心,來提供查詢實例列表的功能。
查到實例列表以後要調用哪個實例呢,只時候就須要chooseTarget了,其實內部就是一個負載均衡策略。
因爲咱們這裏只是想實現一個簡單的RPC,因此暫時不考慮服務註冊中心和負載均衡,所以代碼裏寫死了返回ip地址爲127.0.0.1。
代碼繼續往下走,咱們這裏用到了Socket來進行遠程通信,同時利用ObjectOutputStream的writeObject和ObjectInputStream的readObject,來實現序列化和反序列化。
最後再來看看Server端的實現,和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); // 調用服務 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(); } } // 返回結果 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端的請求,接着就是反序列化請求->執行->序列化執行結果,最後將二進制格式的執行結果返回給Client。
就這樣咱們實現了一個簡陋而又詳細的RPC。
說它簡陋,是由於這個實現確實比較挫,在下一小節會說它爲何挫。
說它詳細,是由於它一步一步的演示了一個RPC的執行流程,方便你們瞭解RPC的內部機制。
這個RPC實現只是爲了給你們演示一下RPC的原理,要是想放到生產環境去用,那是絕對不行的。
一、缺少通用性
我經過給Calculator接口寫了一個CalculatorRemoteImpl,來實現計算器的遠程調用,下一次要是有別的接口須要遠程調用,是否是又得再寫對應的遠程調用實現類?這確定是很不方便的。
那該如何解決呢?先來看看使用Dubbo時是如何實現RPC調用的:
@Reference private Calculator calculator; ... calculator.add(1,2); ...
Dubbo經過和Spring的集成,在Spring容器初始化的時候,若是掃描到對象加了@Reference註解,那麼就給這個對象生成一個代理對象,這個代理對象會負責遠程通信,而後將代理對象放進容器中。因此代碼運行期用到的calculator就是那個代理對象了。
咱們能夠先不和Spring集成,也就是先不採用依賴注入,可是咱們要作到像Dubbo同樣,無需本身手動寫代理對象,怎麼作呢?那天然是要求全部的遠程調用都遵循一套模板,把遠程調用的信息放到一個RpcRequest對象裏面,發給Server端,Server端解析以後就知道你要調用的是哪一個RPC接口、以及入參是什麼類型、入參的值又是什麼,就像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;
二、集成Spring
在實現了代理對象通用化以後,下一步就能夠考慮集成Spring的IOC功能了,經過Spring來建立代理對象,這一點就須要對Spring的bean初始化有必定掌握了。
三、長鏈接or短鏈接
總不能每次要調用RPC接口時都去開啓一個Socket創建鏈接吧?是否是能夠保持若干個長鏈接,而後每次有rpc請求時,把請求放到任務隊列中,而後由線程池去消費執行?只是一個思路,後續能夠參考一下Dubbo是如何實現的。
四、 服務端線程池
咱們如今的Server端,是單線程的,每次都要等一個請求處理完,才能去accept另外一個socket的鏈接,這樣性能確定不好,是否是能夠經過一個線程池,來實現同時處理多個RPC請求?一樣只是一個思路。
五、服務註冊中心
正如以前提到的,要調用服務,首先你須要一個服務註冊中心,告訴你對方服務都有哪些實例。Dubbo的服務註冊中心是能夠配置的,官方推薦使用Zookeeper。若是使用Zookeeper的話,要怎樣往上面註冊實例,又要怎樣獲取實例,這些都是要實現的。
六、負載均衡
如何從多個實例裏挑選一個出來,進行調用,這就要用到負載均衡了。負載均衡的策略確定不僅一種,要怎樣把策略作成可配置的?又要如何實現這些策略?一樣能夠參考Dubbo,Dubbo - 負載均衡
七、結果緩存
每次調用查詢接口時都要真的去Server端查詢嗎?是否是要考慮一下支持緩存?
八、多版本控制
服務端接口修改了,舊的接口怎麼辦?
九、異步調用
客戶端調用完接口以後,不想等待服務端返回,想去幹點別的事,能夠支持不?
十、優雅停機
服務端要停機了,還沒處理完的請求,怎麼辦?
......
諸如此類的優化點還有不少,這也是爲何實現一個高性能高可用的RPC框架那麼難的緣由。
固然,咱們如今已經有不少很不錯的RPC框架能夠參考了,咱們徹底能夠借鑑一下前人的智慧。
後面若是有(dian)機(zan)會(duo)的話,也將和你們分享一下如何一步一步優化現有的這塊RPC代碼,把它作成一個小型RPC框架!