Java講解RPC的基本實作
RPC遠端過程調用可以說是分布式系統的基礎,本文将通過Java示範一次普通的rpc調用到底發生了什麼。
我曾經在網上看到有人提問,為什麼RPC要叫作遠端過程調用,而不叫作RMC遠端方法調用。個人認為RPC的叫法才是合理的,遠端調用的是某個過程,不一定是一個具體的方法,你隻要看過第一個版本的代碼就能懂了。
這整個過程可以用一句話概括:機器A通過網絡與機器B建立連接配接,A發送一些參數給B,B執行某個過程,并把結果傳回給A。
先說一個前置背景,我們有一個商品類
public class Product implements Serializable {
private Integer id;
private String name;
public Product(Integer id, String name) {
this.id = id;
this.name = name;
}
//toString()
//get set 方法
}
有一個商品服務接口
public interface IProductService {
Product getProductById(Integer id);
服務端有商品服務接口的實作類
public class ProductServiceImpl implements IProductService {
@Override
public Product getProductById(Integer id) {
//實際上這裡應該去查詢資料庫獲得資料,下面簡化了
return new Product(id, "手機");
}
下面我們通過用戶端發送一個商品id到服務端,服務端獲得id後通過通過商品服務類擷取商品資訊,傳回給用戶端
public class Client {
public static void main(String[] args) throws Exception {
//建立Socket
Socket socket = new Socket("127.0.0.1", 8888);
//擷取輸出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
//把商品Id通過網絡傳到服務端
dos.writeInt(123);
socket.getOutputStream().write(baos.toByteArray());
socket.getOutputStream().flush();
//讀取服務端傳回的商品資訊
DataInputStream dis = new DataInputStream(socket.getInputStream());
Integer id = dis.readInt(); //商品id
String name = dis.readUTF(); //商品名稱
Product product = new Product(id, name);//通過服務端傳回的商品資訊生成商品
System.out.println(product);
//關閉流資源為了友善閱讀,沒有做try-catch處理
dos.close();
baos.close();
socket.close();
}
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
//建立服務端Socket
ServerSocket ss = new ServerSocket(8888);
//不斷監聽,處理用戶端請求
while (running) {
Socket socket = ss.accept();
process(socket);
socket.close();
}
ss.close();
}
private static void process(Socket socket) throws Exception {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
DataInputStream dis = new DataInputStream(is);
DataOutputStream dos = new DataOutputStream(os);
//讀取用戶端發過來的id
Integer id = dis.readInt();
//調用服務類生成商品
IProductService service = new ProductServiceImpl();
Product product = service.getProductById(id);
//把商品的資訊寫回給用戶端
dos.writeInt(id);
dos.writeUTF(product.getName());
dos.flush();
dos.close();
dis.close();
os.close();
is.close();
}
上面的是RPC遠端調用1.0版本,可以看到聯網的代碼寫死在了用戶端中,網絡的代碼和getProductById()耦合在了一起,實際的rpc架構是絕對不可能這麼做的。
在實際的使用中,我們會編寫各種各樣的遠端調用,打個比方,IProductService接口以後可能會擴充成這樣:
Product getProductById(Integer id);
Product getProductByName(String name);
Product getMostExpensiveProduct();
我們總不可能為每個方法都編寫一段網絡連接配接的代碼吧,我們得想到一種辦法為所有的方法都嵌入一段共用的網絡連接配接代碼。
那具體應該怎樣嵌入呢?這裡我們可以用到代理模式。
在Java中許多優秀的架構都用到了代理模式做代碼嵌入,比如說Mybatis。它把JDBC連接配接部分的代碼通過代理模式嵌入到sql語句的周圍,讓我們專注于寫sql。
首先,服務端的代碼要進行修改:
private static boolean running = true;
public static void main(String[] args) throws Exception {
//......
}
private static void process(Socket socket) throws Exception {
//擷取輸入流,輸出流
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(is);
ObjectOutputStream oos = new ObjectOutputStream(os);
//擷取本次遠端調用的方法名
String methodName = ois.readUTF();
//擷取本次遠端調用方法的參數類型
Class[] parameterTypes = (Class[]) ois.readObject();
//擷取具體的參數對象
Object[] args = (Object[]) ois.readObject();
//建立商品服務類執行個體
IProductService service = new ProductServiceImpl();
//根據遠端擷取的方法名和參數,調用相應的方法
Method method = service.getClass().getMethod(methodName, parameterTypes);
Product product = (Product) method.invoke(service, args);
//把結果寫回給用戶端
oos.writeObject(product);
oos.close();
ois.close();
socket.close();
}
然後在用戶端,我們建立一個新的代理類,對外提供一個getStub擷取代理類的方法。使用JDK的動态代理需要三個參數,一個是類加載器,一個是接口的class類,最後一個是InvocationHandler執行個體。
JDK動态代理背後的邏輯是這樣的:JVM會根據接口的class類動态建立一個代理類對象,這個代理對象實作了傳入的接口,也就是說它擁有了接口中所有方法的實作。方法具體的實作可以由使用者指定,也就是調用InvocationHandler的invoke方法。
在invoke方法中有三個參數,分别是proxy代理類,method調用的方法,args調用方法的參數。我們可以在invoke方法中對具體的實作方法進行增強,在本案例中就是進行網絡調用。
public class Stub {
public static IProductService getStub() {
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//和服務端建立Socket連接配接
Socket socket = new Socket("127.0.0.1", 8888);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//拿到遠端調用的方法名
String methodName = method.getName();
//拿到遠端調用方法的參數類型
Class[] parametersTypes = method.getParameterTypes();
//把方法名傳遞給服務端
oos.writeUTF(methodName);
//把方法參數類型傳遞給服務端
oos.writeObject(parametersTypes);
//把方法參數傳遞給服務端
oos.writeObject(args);
oos.flush();
//擷取遠端調用的傳回結果
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Product product = (Product) ois.readObject();
ois.close();
oos.close();
socket.close();
return product;
}
};
Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h);
return (IProductService) o;
}
這個新版本比第一個版本又美好了一些,但是其實還可以繼續優化。現在我們的代理隻能夠傳回IProductService的實作類,得想辦法讓它傳回任意類型的服務實作類。
思路和遠端調用方法相似,在遠端調用方法時,我們把方法的名稱,參數類型,參數傳遞給服務端;現在要動态建立服務類,我們可以把服務接口的名字傳給服務端。服務端拿到遠端接口的名字後,就可以從服務系統資料庫中找到對應服務實作類。
至于服務實作類如何注冊到服務系統資料庫,這裡提供一個思路:可以考慮使用Spring的注解注入。這和我們平時寫spring代碼是相似的,在建立完服務實作類後我們會加上注解@Service,這樣我們就可以在收到遠端調用後,周遊使用了@Service的Bean,找到對應的實作類。
參考:馬士兵rpc的演化過程公開課
https://www.bilibili.com/video/BV1Ug4y1875i?p=2原文位址
https://www.cnblogs.com/tanshaoshenghao/p/12767414.html