RPC調(diào)用是面向服務(wù)架構(gòu)場景下進行服務(wù)間調(diào)用的常用組件销钝,一個完整的RPC調(diào)用的流程如圖1所示:
為了方便RPC調(diào)用者和服務(wù)者的開發(fā),開發(fā)者們開發(fā)了很多RPC框架。比較有名的RPC框架有Google的gRPC边苹、Facebook的Thrift 和 阿里的 Dubbo 等狱窘。這些框架在具體實現(xiàn)上雖然各不相同杜顺,但其工作原理基本上是一致的。
一個設(shè)計良好的RPC框架的愿景是簡化服務(wù)提供者和調(diào)用者的開發(fā)蘸炸,將圖1中 2~8 步驟的所有操作全部隱藏躬络,讓開發(fā)者可以像開發(fā)和調(diào)用本地方法一樣開發(fā)和調(diào)用遠程方法。為了隱藏這些實現(xiàn)細節(jié)搭儒,RPC框架需要關(guān)注如下幾點:
- 方法調(diào)用:方法調(diào)用的過程如何轉(zhuǎn)換為可以傳輸?shù)南?nèi)容穷当;
- 服務(wù)發(fā)現(xiàn):如何發(fā)布服務(wù)并告知調(diào)用者服務(wù)地址;
- 數(shù)據(jù)傳輸:服務(wù)調(diào)用者和提供者之間如何進行數(shù)據(jù)傳輸淹禾。
下面我們以Java語言為例馁菜,就以上幾點分別做出分析。
1. 方法調(diào)用
1.1 “協(xié)議”設(shè)定
要完成一個完整的方法調(diào)用铃岔,需要在調(diào)用者和被調(diào)用者之間設(shè)計一套“協(xié)議”對被調(diào)接口汪疮、被調(diào)方法、參數(shù)類型、參數(shù)值智嚷、返回類型卖丸、返回值等進行定義,如下是一段比較合理的通信“協(xié)議”:
public class Request implements Serializable {
private long sid;
private String serviceName;
private String methodName;
private Class<?>[] paramTypes;
private Object[] params;
}
public class Response implements Serializable {
private long sid;
private Class<?> resultType;
private Object result;
}
Request里面盏道,需要指定具體的服務(wù)稍浆、方法名,以及調(diào)用方法時需要傳入的參數(shù)類型和參數(shù)值摇天;Response里面直接指定返回值及其類型(因為返回值最多只有一個)粹湃。特別地,在Request 和 Response里面分別有一個 sid 字段泉坐,是為了在異步處理時能正確地對應(yīng)請求和返回的關(guān)系为鳄。
1.2 請求、返回與“協(xié)議”對象的轉(zhuǎn)化
為了簡化服務(wù)方和調(diào)用方的實現(xiàn)腕让,RPC框架里面需要完成方法請求與request對象之間的轉(zhuǎn)換 和 處理結(jié)果與reponse對象之間的轉(zhuǎn)換孤钦。對于Java,可以采用JDK的動態(tài)代理來實現(xiàn)這個轉(zhuǎn)換纯丸;如果采用Spring框架偏形,還可以通過Spring的動態(tài)代理類 -- FactoryBean進行實現(xiàn)。下面一段代碼是通過JDK的動態(tài)代理實現(xiàn)本地調(diào)用轉(zhuǎn)化為消息的一個demo:
// 動態(tài)代理類
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class RpcProxyInvoker implements InvocationHandler {
private Class<?> clazz;
public RpcProxyInvoker(Class<?> clazz) {
this.clazz = clazz;
}
public static Object getProxy(Class<?> clazz) {
RpcProxyInvoker invoker = new RpcProxyInvoker(clazz);
return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, invoker);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Class<?>[] paramTypes = new Class[] {};
for (int idx = 0; idx < args.length; ++idx) {
paramTypes[idx] = args[idx].getClass();
}
Request req = new Request();
req.setServiceName(clazz.getClass().getName());
req.setMethodName(method.getName());
req.setParameterTypes(paramTypes);
req.setParameters(args);
/// ... 后續(xù)操作:序列化觉鼻、網(wǎng)絡(luò)傳輸?shù)? return null;
}
}
// 暴露給調(diào)用方的接口
public interface UserService {
UserDTO get(int userId);
}
// 調(diào)用方調(diào)用簡單示例
public class Main {
public static void main(String[] args) {
UserService userService = (UserService) RpcProxyInvoker.getProxy(UserService.class);
userService.get(1);
}
}
為了方便傳輸俊扭,需要對生成的Request對象進行序列化。最簡單的序列化方法是采用Java的ObjectInput/OutputStream實現(xiàn)坠陈,當然也可以通過JSON等格式進行對象的序列化萨惑。
序列化結(jié)束后,交給網(wǎng)絡(luò)模塊進行網(wǎng)絡(luò)傳輸并等待返回結(jié)果進行后續(xù)處理仇矾。
2. 服務(wù)發(fā)現(xiàn)
當RPC框架應(yīng)用于大型分布式系統(tǒng)時庸蔼,不可能將服務(wù)的地址人為地告知到每個調(diào)用方,應(yīng)該自動化地完成這個“告知”工作贮匕。這個工作的流程如圖2所示:
注冊中心可以采用redis或者zookeeper等方式進行實現(xiàn)姐仅。
從服務(wù)提供者的角度看:當提供者服務(wù)啟動時,需要自動向注冊中心注冊服務(wù)刻盐;當提供者服務(wù)停止時掏膏,需要向注冊中心注銷服務(wù);提供者需要定時向注冊中心發(fā)送心跳敦锌,一段時間未收到來自提供者的心跳后壤追,認為提供者已經(jīng)停止服務(wù),從注冊中心上摘取掉對應(yīng)的服務(wù)供屉。從調(diào)用者的角度看:調(diào)用者啟動時訂閱注冊中心的消息并從注冊中心獲取提供者的地址行冰;當有提供者上線或者下線時溺蕉,注冊中心會告知到調(diào)用者;調(diào)用者下線時悼做,取消訂閱疯特。
3. 數(shù)據(jù)傳輸
服務(wù)提供者和調(diào)用者之間進行數(shù)據(jù)傳輸,可以借助基于端口的協(xié)議和基于HTTP的協(xié)議進行肛走,如使用TCP協(xié)議通信 或 使用HTTP協(xié)議通信漓雅。通信方式上,可以使用傳統(tǒng)的BIO(Blocked I/O) 或 NIO(New I/O)方式進行朽色,在此不再一一贅述邻吞。
雖然RPC框架的原理比較簡單,但要實現(xiàn)一個功能齊全葫男、適應(yīng)性強抱冷、對代碼無侵入的RPC框架并非易事,需要在網(wǎng)絡(luò)傳輸梢褐、序列化旺遮、Stub和Skeleton功能等方面下功夫。當然盈咳,在了解原理的情況下耿眉,使用一個RPC框架,對業(yè)務(wù)代碼的編寫和優(yōu)化也有一定的促進作用鱼响。