一實習小護士給我掛針,拿着針在我胳膊上紮了好幾針也沒找到血管html
但這位小姑娘真鎮定啊,表情嚴肅認真,勢有不扎到血管不罷休的意思java
十幾針以後,我忍着劇痛,帶着敬畏的表情問小護士:你這針法跟容嬤嬤學的麼?git
單機應用中的方法調用很簡單,直接調用就行,像這樣設計模式
由於調用方與被調用方在一個進程內網絡
隨着業務的發展,單機應用會愈來愈力不從心,勢必會引入分佈式來解決單機的問題,那麼調用方如何調用另外一臺機器上的方法呢 ?負載均衡
這就涉及到分佈式通訊方式,從單機走向分佈式,產生了不少通訊方式框架
而 RPC 就是實現遠程方法調用的方式之一;說 RPC 不是協議,可能不少小夥伴難以置信,覺得我在騙大家socket
看着大家這一身腱子肉,我哪敢騙大家;只要大家把下面的看完,騙沒騙大家,大家本身說了算
分佈式
先說明一下,下文中的示例雖然是 Java 代碼實現的,但原理是通用的,重點是理解其中的原理ide
兩臺機器之間進行交互,那麼確定離不開網絡通訊協議,TCP / IP 也就成了繞不開的點,因此先輩們最初想到的方法就是經過 TCP / IP 來實現遠程方法的調用
而操做系統是沒有直接暴露 TCP / IP 接口的,而是經過 Socket 抽象了 TCP / IP 接口,因此咱們能夠經過 Socket 來實現最第一版的遠程方法調用
完整示例代碼:rpc-01,核心代碼以下
Server:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import com.qsl.rpc.server.UserServiceImpl; import com.qsl.rpc.service.IUserService; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Server { private static boolean is_running = true; public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8888); while (is_running) { System.out.println("等待 client 鏈接"); Socket client = serverSocket.accept(); System.out.println("獲取到 client..."); handle(client); client.close(); } serverSocket.close(); } private static void handle(Socket client) throws Exception { InputStream in = client.getInputStream(); OutputStream out = client.getOutputStream(); DataInputStream dis = new DataInputStream(in); DataOutputStream dos = new DataOutputStream(out); // 從 socket 讀取參數 int id = dis.readInt(); System.out.println("id = " + id); // 查詢本地數據 IUserService userService = new UserServiceImpl(); User user = userService.getUserById(id); // 往 socket 寫響應值 dos.writeInt(user.getId()); dos.writeUTF(user.getName()); dos.flush(); dis.close(); dos.close(); } }
Client:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import java.io.DataInputStream; import java.io.DataOutputStream; import java.net.Socket; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Client { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1", 8888); // 網絡傳輸數據 // 往 socket 寫請求參數 DataOutputStream dos = new DataOutputStream(s.getOutputStream()); dos.writeInt(18); // 從 socket 讀響應值 DataInputStream dis = new DataInputStream(s.getInputStream()); int id = dis.readInt(); String name = dis.readUTF(); // 將響應值封裝成 User 對象 User user = new User(id, name); dos.close(); dis.close(); s.close(); // 進行業務處理 System.out.println(user); } }
代碼很簡單,就是一個簡單的 Socket 通訊;若是看不懂,那就須要去補充下 Socket 和 IO 的知識
測試結果以下
能夠看到 Client 與 Server 之間是能夠進行通訊的;可是,這種方式很是麻煩,有太多缺點,最明顯的一個就是
Client 端業務代碼 與 網絡傳輸代碼 混合在一塊兒,沒有明確的模塊劃分
若是有多個開發者同時進行 Client 開發,那麼他們都須要知道 Socket、IO
針對初版的缺點,演進出了這一版,引進 Stub (早期的叫法,不用深究,理解成代理就行)實現 Client 端網絡傳輸代碼的封裝
完整示例代碼:rpc-02,改動部分以下
Stub:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import java.io.DataInputStream; import java.io.DataOutputStream; import java.net.Socket; /** * 至關於一個靜態代理,封裝了網絡數據傳輸 * @author 青石路 * @date 2021/1/17 9:38 */ public class Stub { public User getUserById(Integer id) throws Exception { Socket s = new Socket("127.0.0.1", 8888); // 網絡傳輸數據 // 往 socket 寫請求參數 DataOutputStream dos = new DataOutputStream(s.getOutputStream()); dos.writeInt(id); // 從 socket 讀響應值 DataInputStream dis = new DataInputStream(s.getInputStream()); int userId = dis.readInt(); String name = dis.readUTF(); // 將響應值封裝成 User 對象 User user = new User(userId, name); dos.close(); dis.close(); s.close(); return user; } }
Client:
package com.qsl.rpc; import com.qsl.rpc.entity.User; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Client { public static void main(String[] args) throws Exception { // 再也不關注網絡傳輸 Stub stub = new Stub(); User user = stub.getUserById(18); // 進行業務處理 System.out.println(user); } }
Client 再也不關注網絡數據傳輸,一心關注業務代碼就好
有小夥伴可能就槓上了:這不就是把網絡傳輸代碼移了個位置嘛,這也算改進?
迭代開發是一個逐步完善的過程,而這也算是一個改進哦
但這一版仍是有不少缺點,最明顯的一個就是
Stub 只能代理 IUserService 的一個方法 getUserById ,侷限性太大,不夠通用
若是想在 IUserService 新增一個方法: getUserByName ,那麼須要在 Stub 中新增對應的方法,Server 端也須要作對應的修改來支持
第二版中的 Stub 代理功能太弱了,那有沒有什麼方式能夠加強 Stub 的代理功能了?
前面的 Stub 至關因而一個靜態代理,因此功能有限,那靜態代理的加強版是什麼了,沒錯,就是:動態代理
不熟悉動態代理的小夥伴,必定要先弄懂動態代理:設計模式之代理,手動實現動態代理,揭祕原理實現
JDK 有動態代理的 API,咱們就用它來實現
完整示例代碼:rpc-03,相較於第二版,改動比較大,你們須要仔細看
Server:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import com.qsl.rpc.server.UserServiceImpl; import com.qsl.rpc.service.IUserService; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Server { private static boolean is_running = true; public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8888); while (is_running) { System.out.println("等待 client 鏈接"); Socket client = serverSocket.accept(); System.out.println("獲取到 client..."); handle(client); client.close(); } serverSocket.close(); } private static void handle(Socket client) throws Exception { InputStream in = client.getInputStream(); OutputStream out = client.getOutputStream(); ObjectInputStream ois = new ObjectInputStream(in); ObjectOutputStream oos = new ObjectOutputStream(out); // 獲取方法名、方法的參數類型、方法的參數值 String methodName = ois.readUTF(); Class[] parameterTypes = (Class[]) ois.readObject(); Object[] args = (Object[]) ois.readObject(); IUserService userService = new UserServiceImpl(); Method method = userService.getClass().getMethod(methodName, parameterTypes); User user = (User) method.invoke(userService, args); // 往 socket 寫響應值;直接寫可序列化對象(實現 Serializable 接口) oos.writeObject(user); oos.flush(); ois.close(); oos.close(); } }
Stub:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import com.qsl.rpc.service.IUserService; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.Socket; /** * 動態代理,封裝了網絡數據傳輸 * @author 青石路 * @date 2021/1/17 9:38 */ public class Stub { public static IUserService getStub() { Object obj = Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, new NetInvocationHandler()); return (IUserService)obj; } static class NetInvocationHandler implements InvocationHandler { /** * * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Socket s = new Socket("127.0.0.1", 8888); // 網絡傳輸數據 ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream()); // 傳輸方法名、方法參數類型、方法參數值;可能會有方法重載,因此要傳參數列表 oos.writeUTF(method.getName()); Class[] parameterTypes = method.getParameterTypes(); oos.writeObject(parameterTypes); oos.writeObject(args); // 從 socket 讀響應值 ObjectInputStream ois = new ObjectInputStream(s.getInputStream()); User user = (User) ois.readObject(); oos.close(); ois.close(); s.close(); return user; } } }
Client:
package com.qsl.rpc; import com.qsl.rpc.entity.User; import com.qsl.rpc.service.IUserService; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Client { public static void main(String[] args) throws Exception { IUserService userService = Stub.getStub(); //User user = userService.getUserById(23); User user = userService.getUserByName("李小龍"); // 進行業務處理 System.out.println(user); } }
咱們來看下效果
此時, IUserService 接口的方法都能被代理了,即便它新增接口, Stub 不用作任何修改也能代理上
另外, Server 端的響應值改爲了對象,而不是單個屬性逐一返回,那麼不管 User 是新增屬性,仍是刪減屬性,Client 和 Server 都不受影響了
這一版的改進是很是大的進步;但仍是存在比較明顯的缺點
只支持 IUserService ,通用性仍是不夠完美
若是新引進了一個 IPersonService ,那怎麼辦 ?
第三版至關於 Client 與 Server 端約定好了,只進行 User 服務的交互,因此 User 以外的服務,兩邊是通訊不上的
若是還須要進行其餘服務的交互,那麼 Client 就須要將請求的服務名做爲參數傳遞給 Server,告訴 Server 我須要和哪一個服務進行交互
因此,Client 和 Server 都須要進行改造
完整示例代碼:rpc-04,相較於第三版,改動比較小,相信你們都能看懂
Server:
package com.qsl.rpc; import com.qsl.rpc.server.PersonServiceImpl; import com.qsl.rpc.server.UserServiceImpl; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Server { private static boolean is_running = true; private static final HashMap<String, Object> REGISTRY_MAP = new HashMap(); public static void main(String[] args) throws Exception { // 向註冊中心註冊服務 REGISTRY_MAP.put("com.qsl.rpc.service.IUserService", new UserServiceImpl()); REGISTRY_MAP.put("com.qsl.rpc.service.IPersonService", new PersonServiceImpl()); ServerSocket serverSocket = new ServerSocket(8888); while (is_running) { System.out.println("等待 client 鏈接"); Socket client = serverSocket.accept(); System.out.println("獲取到 client..."); handle(client); client.close(); } serverSocket.close(); } private static void handle(Socket client) throws Exception { InputStream in = client.getInputStream(); OutputStream out = client.getOutputStream(); ObjectInputStream ois = new ObjectInputStream(in); ObjectOutputStream oos = new ObjectOutputStream(out); // 獲取服務名 String serviceName = ois.readUTF(); System.out.println("serviceName = " + serviceName); // 獲取方法名、方法的參數類型、方法的參數值 String methodName = ois.readUTF(); Class[] parameterTypes = (Class[]) ois.readObject(); Object[] args = (Object[]) ois.readObject(); // 獲取服務;從服務註冊中心獲取服務 Object serverObject = REGISTRY_MAP.get(serviceName); // 經過反射調用服務的方法 Method method = serverObject.getClass().getMethod(methodName, parameterTypes); Object resp = method.invoke(serverObject, args); // 往 socket 寫響應值;直接寫可序列化對象(實現 Serializable 接口) oos.writeObject(resp); oos.flush(); ois.close(); oos.close(); } }
Stub:
package com.qsl.rpc; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.Socket; /** * 動態代理,封裝了網絡數據傳輸 * @author 青石路 * @date 2021/1/17 9:38 */ public class Stub { public static Object getStub(Class clazz) { Object obj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new NetInvocationHandler(clazz)); return obj; } static class NetInvocationHandler implements InvocationHandler { private Class clazz; NetInvocationHandler(Class clazz){ this.clazz = clazz; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Socket s = new Socket("127.0.0.1", 8888); // 網絡傳輸數據 ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream()); // 傳輸接口名,告訴服務端,我要哪一個服務 oos.writeUTF(clazz.getName()); // 傳輸方法名、方法參數類型、方法參數值;可能會有方法重載,因此要傳參數列表 oos.writeUTF(method.getName()); Class[] parameterTypes = method.getParameterTypes(); oos.writeObject(parameterTypes); oos.writeObject(args); // 從 socket 讀響應值 ObjectInputStream ois = new ObjectInputStream(s.getInputStream()); Object resp = ois.readObject(); oos.close(); ois.close(); s.close(); return resp; } } }
Client:
package com.qsl.rpc; import com.qsl.rpc.entity.Person; import com.qsl.rpc.entity.User; import com.qsl.rpc.service.IPersonService; import com.qsl.rpc.service.IUserService; /** * @author 青石路 * @date 2021/1/16 19:49 */ public class Client { public static void main(String[] args) throws Exception { /*IUserService userService = (IUserService)Stub.getStub(IUserService.class); User user = userService.getUserByName("青石路"); System.out.println(user);*/ IPersonService personService = (IPersonService)Stub.getStub(IPersonService.class); Person person = personService.getPersonByPhoneNumber("123"); System.out.println(person); } }
此版本抽象的比較好了,屏蔽了底層細節,支持任何服務的任意方法,算是一個比較完美的版本了
至此,一個最基礎的 RPC 就已經實現了
可是,仍是有大量的細節能夠改善,序列化與反序列化就是其中之一
網絡中數據的傳輸都是二進制,因此請求參數須要序列化成二進制,響應參數須要反序列化成對象
而 JDK 自帶的序列化與反序列化,具備語言侷限性、效率慢、序列化後的長度太長等缺點
序列化與反序列化協議很是多,常見的有
這些協議孰好孰壞,本文不作過多闡述,這裏提出來只是想告訴你們:序列化與反序列化協議是 RPC 中的重要一環
一、RPC 的演進過程
二、RPC 的組成要素
三要素:動態代理、序列化與反序列化協議、網絡通訊協議
網絡通訊協議能夠是 TCP、UDP,也能夠是 HTTP 1.x、HTTP 2,甚至有能力能夠是自定義協議
三、RPC 框架
RPC 不等同於 RPC 框架,RPC 是一個概念,是一個分佈式通訊方式
基於 RPC 產生了不少 RPC 框架:Dubbo、Netty、gRPC、BRPC、Thrift、JSON-RPC 等等
RPC 框架對 RPC 進行了功能豐富,包括:服務註冊、服務發現、服務治理、服務監控、服務負載均衡等功能
如今回到標題:RPC 是通訊協議嗎 ? 歡迎評論區留言