筆者以前僅看過RPC這個單詞,徹底沒有了解過,不想終於仍是碰上了。原由:這邊想提升併發量而去看kafka(最後折中使用了redis),其中kafka須要安裝ZooKeeper,而ZooKeeper又與分佈式相關,再繼續就發現分佈式的基礎是RPC,因而寫下了這篇博文java
RPC(Remote Procedure Call)遠程過程調用,即經過網絡通訊來調用遠程計算機程序上的服務,而這個調用過程就像調用本地方法同樣簡單透明,而且不須要了解底層的網絡技術協議。RPC採用C/S架構,發出請求的程序是Client,提供服務的則是Server,相似於Http請求與響應。簡單總結就是:調用的方法實際在遠程,而要像調用本地方法同樣簡單。redis
1)對於客戶端的我:調用本地的一個方法(存根)就能得到服務。 這個存根是遠程服務的一個代理,其底層如何實現,對於我來講是透明的。服務器
2)對於遠程服務器:監聽是否有鏈接過來,來了就調用對應的方法並返回(服務器端較易理解)網絡
其結構圖以下:架構
當咱們的業務量愈來愈龐大,垂直增長服務器的數量對提升性能的做用越發微乎,此時不免會採用分佈式的架構以便更好地提升性能。分佈式架構的每一個服務都是獨立的部分,當須要完成某項業務且依賴不一樣的服務式時,這些服務就須要互相調用,此時服務之間的調用就須要一種高效的應用程序之間的通信手段了,這就是PRC出現的緣由併發
提供服務:實現所提供的服務負載均衡
服務暴漏:僅僅實現了服務是不夠的,還須要將提供的服務暴漏給外界,讓外界知道有何,如何使用服務框架
遠程代理對象:在調用本地方法時實際調用的是遠程的方法,那麼勢必本地須要一個遠程代理對象socket
總結:爲了實現RPC須要有:通訊模型(BIO、NIO),服務定位(IP、PORT),遠程代理對象(遠程服務的本地代理),序列化(網絡傳輸轉換成二進制)分佈式
其主要的對象有:服務端接口、服務端接口實現、服務暴漏、客戶端接口(與服務端共享同個接口)、服務的引用
public interface Service { // 提供兩個服務,說hello和整數相加 public String hello(); public int sum(int a, int b); }
public class ServiceImpl implements Service { @Override public String hello() { return "Hello World"; } @Override public int sum(int a, int b) { return a + b; } }
public static void export(Object service, int port) { if (service == null || port <= 0 || port > 65535) { throw new RuntimeException("Arguments error"); } System.out.println(service.getClass().getName() + ": " + port + "服務暴露"); new Thread( () -> { try (ServerSocket server = new ServerSocket(port);) { while(true){ try ( Socket socket = server.accept(); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ) { // 讀取方法名 String methodName = in.readUTF(); // 讀取參數類型 Class<?>[] parameterTypes = (Class<?>[])in.readObject(); // 讀取參數值 Object[] arguments = (Object[])in.readObject(); // 獲取方法 Method method = service.getClass().getMethod(methodName, parameterTypes); // 處理結果 Object result = method.invoke(service, arguments); // 寫入結果 out.writeObject(result); } catch (Exception e) { e.printStackTrace(); } } } catch (IOException e1) { e1.printStackTrace(); } }).start(); }
這個暴露的邏輯是服務端監聽特定端口,等客戶端發起請求後鏈接,而後經過Java的IO流獲取方法名,參數等相關信息,最後經過反射實現方法的調用並將結果響應給客戶端
public interface ClientService { // 提供兩個服務,說hello和整數相加 public String hello(); public int sum(int a, int b); }
public static <T>T refer(Class<T> interfaceClass, String host, int port){ if(interfaceClass == null || !interfaceClass.isInterface() || host == null || port <= 0 || port > 65535){ throw new RuntimeException("Arguments error"); } System.out.println("正在調用遠程服務"); @SuppressWarnings("unchecked") T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try ( Socket socket = new Socket(host, port); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); ) { out.writeUTF(method.getName()); out.writeObject(method.getParameterTypes()); out.writeObject(args); result = in.readObject(); } catch (Exception e) { e.printStackTrace(); } return result; } }); return proxy; }
而引用服務的邏輯是:建立Socket套接字鏈接,序列化相關請求信息發送給服務端,而後等待響應結果。其中透明調用是使用了動態代理
public class Test { public static void main(String[] args) { // 暴露服務 ServiceImpl service = new ServiceImpl(); RPCFramework.export(service, 8080); // 調用服務 Client client = RPCFramework.refer(Client.class, "127.0.0.1", 8080); int sum = client.sum(1, 2); String rs = client.hello(); System.out.println("遠程響應:" + sum); System.out.println("遠程響應:" + rs); } }
RPC.ServiceImpl:8080----- 服務暴露 正在調用遠程服務 遠程響應:3 遠程響應:Hello World
RPC與具體協議無關,可基於Http、TCP,但由於TCP性能相對較好。Http屬於應用層協議,TCP屬於傳輸層協議,相對在底層少了一層封裝,並且爲了可靠傳輸而選擇TCP不選擇UDP
Dubbo(阿里巴巴)、SpringCloud、RMI(JDK內置)
由於要像本地調用同樣,對於使用者來講是透明的。
Object result = XXX(String method, String host, int port)
上面這樣其實也行,但並不能感受到是調用本地方法同樣,並且若是一個接口有多個方法的話,每調用一次方法就須要發送一次host / port
// 動態代理能夠這樣使用 ProxyObject.方法1 ProxyObject.方法2 // 沒有使用動態代理則不人性化 XXX(String method1, String host, int port) XXX(String method2, String host, int port)
爲了方便分辨方法的重載,下面獲取方法須要方法名和參數類型
service.getClass().getMethod(methodName, parameterTypes)
上面事例中採用BIO形式,阻塞訪問而致使併發量不高,能夠用NIO代替
這裏用了JDK原生方法只能序列化實現了Serializable接口的類,可使用第三方的類庫來提升性能
服務的自動發現,客戶端能動態感知服務端的變化,從實現熱部署,可用定時輪詢的方法,eg:ZooKeeper
集羣化,這樣即可以提供負載均衡
請求與響應能夠進行編碼封裝,而不是這樣單獨一個一個發送