RPC框架原理簡述:從實現一個簡易RPCFramework說起

摘要:

本文闡述了RPC框架與遠程調用的產生背景,介紹了RPC的基本概念和使用背景,之後手動實現了簡易的RPC框架並佐以實例進行演示,以便讓各位看官對RPC有一個感性、清晰和完整的認識,最後討論了RPC框架幾個較為重要問題。總之,RPC框架的精髓在於動態代理和反射,通過它們使得遠程調用“本地化”,對用戶透明且友好。

一. 引子

上學時我們寫得應用大都比較簡單,基本上都屬於單體應用,服務調用也侷限於本地,如下所示:

// 服務接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}// 服務本地實現
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) { return "Hello " + name;
} @Override
public String hi(String msg) { return "Hi, " + msg;
}
}// 服務本地調用
public class Main {
public static void main(String[] args) {
HelloService helloService = new HelloServiceImpl();
helloServiceProxy.hello("Panda");
helloServiceProxy.hi("Panda");
}/** Output
hello : Hello rico
hi : Hi, panda
**/ }

我們寫這樣的單體應用來學習、做實驗正常且合理,但是在生產環境中,單體應用在各方面的性能上和可維護性方面就遠遠不能滿足需求了。應用內各項業務互相糾纏、耦合性太大,不利於後期的維護和升級,主要表現在以下兩點上:

  • 可用性低。所有雞蛋都放在同一個籃子裡,一旦有問題導致單體應用掛掉,所有業務都不能訪問,穩定性要求難以滿足;
  • 不利於各業務團隊進行合作,開發效率低。單體應用各業務耦合度太高,不同業務團隊開發進度和實現細節不盡相同,難以高效協作。

將不同的業務拆分到多個應用中,讓不同的應用分別承擔不同的功能是解決這些問題的必殺技。將不同業務分拆到不同的應用後,不但可以大幅度提升系統的穩定性還有助於豐富技術選型,進一步保證系統的性能。總的來說,從單體應用到分佈式多體應用是系統升級必經之路。

當一個單體應用演化成多體應用後,遠程調用就粉墨登場了。在一個應用時,相互通信直接通過本地調用就可完成,而變為多體應用時,相互通信就得依賴遠程調用了,這時一個高效穩定的RPC框架就顯得非常必要了。可能有的同學會覺得,沒必要非得用RPC框架啊,簡單的HTTP調用不是也可以實現遠程通信嗎?確實,簡單的HTTP調用確實也可以實現遠程通信,但是它不是那麼的合適,原因有二:

  • RPC遠程調用像本地調用一樣乾淨簡潔,但其他方式對代碼的侵入性就比較強;
  • 一般使用RPC框架實現遠程通信效率比其他方式效率要高一些。

當我們踏入公司尤其是大型互聯網公司就會發現,公司的系統都由成千上萬大大小小的服務組成,各服務部署在不同的機器上,由不同的團隊負責。這時就會有兩個很關鍵的問題:

  • 要搭建一個新服務,免不了需要依賴已有的服務,而現在已有的服務都在遠端,怎麼調用?
  • 其它團隊想使用我們的新服務,我們的服務該怎麼發佈以便他人調用?

下文將對RPC框架的基本原理進行介紹,並對這兩個問題展開探討,同時參考前輩的博文《RPC框架幾行代碼就夠了》手寫一個簡易RPC框架以加深對PRC原理的理解。

二. RPC 框架介紹

對於多體應用,由於各服務部署在不同機器,服務間的調用免不了網絡通信過程,服務消費方每調用一個服務都要寫一坨網絡通信相關的代碼,不僅複雜而且極易出錯。如果有一種方式能讓我們像調用本地服務一樣調用遠程服務,而讓調用者對網絡通信這些細節透明,那麼將大大解放程序員的雙手,大幅度提高生產力。比如,服務消費方在執行helloService.hi(“Panda”)時,實質上調用的是遠端的服務。這種方式其實就是RPC(Remote Procedure Call Protocol),在各大互聯網公司中被廣泛使用,如阿里巴巴的HSF、Dubbo(開源)、Facebook的Thrift(開源)、Google GRPC(開源)、Twitter的Finagle(開源)等。

RPC的主要功能目標是讓構建分佈式計算(應用)更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。為實現該目標,RPC框架需提供一種透明調用機制讓使用者不必顯式的區分本地調用和遠程調用。要讓網絡通信細節對使用者透明,我們需要對通信細節進行封裝,下面是一個RPC的經典調用的流程,並且反映了所涉及到的一些通信細節:

RPC框架原理簡述:從實現一個簡易RPCFramework說起

(1). 服務消費方(client)以本地調用方式調用服務;

(2). client stub接收到調用後負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;

(3). client stub找到服務地址,並將消息發送到服務端;

(4). server stub收到消息後進行解碼;

(5). server stub根據解碼結果 反射調用 本地的服務;

(6). 本地服務執行並將結果返回給server stub;

(7). server stub將返回結果打包成消息併發送至消費方;

(8). client stub接收到消息,並進行解碼;

(9). 服務消費方得到最終結果。

RPC框架就是要將2~8這些步驟封裝起來,讓用戶對這些細節透明,使得遠程方法調用看起來像調用本地方法一樣。

三. RPC框架簡易實現及其實例分析

(1).服務端

服務端提供客戶端所期待的服務,一般包括三個部分:服務接口,服務實現以及服務的註冊暴露三部分,如下:

  • 服務接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}

  • 服務實現
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) { return "Hello " + name;
} @Override
public String hi(String msg) { return "Hi, " + msg;
}
}

  • 服務暴露:只有把服務暴露出來,才能讓客戶端進行調用,這是RPC框架功能之一。
public class RpcProvider {
public static void main(String[] args) throws Exception {
HelloService service = new HelloServiceImpl(); // RPC框架將服務暴露出來,供客戶端消費
RpcFramework.export(service, 1234);
}
}

(2).客戶端

客戶端消費服務端所提供的服務,一般包括兩個部分:服務接口和服務引用兩個部分,如下:

  • 服務接口:與服務端共享同一個服務接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}

  • 服務引用:消費端通過RPC框架進行遠程調用,這也是RPC框架功能之一
public class RpcConsumer {
public static void main(String[] args) throws Exception { // 由RpcFramework生成的HelloService的代理
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
String hello = service.hello("World");
System.out.println("客戶端收到遠程調用的結果 : " + hello);
}
}

(3).RPC框架原型實現

RPC框架主要包括兩大功能:一個用於服務端暴露服務,一個用於客戶端引用服務。

  • 服務端暴露服務
 /**
* 暴露服務
*
* @param service 服務實現
* @param port 服務端口

* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port); // 建立Socket服務端
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try { // 監聽Socket請求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try { /* 獲取請求流,Server解析並獲取請求*/
// 構建對象輸入流,從源中讀取對象到程序中
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
System.out.println("\nServer解析請求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName); // 泛型與數組是不兼容的,除了通配符作泛型參數以外
Class>[] parameterTypes = (Class>[])input.readObject();
System.out.println( "parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 處理請求,進行響應*/
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try { // service類型為Object的(可以發佈任何服務),故只能通過反射調用處理請求
// 反射調用,處理請求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);
System.out.println("\nServer 處理並生成響應 :");
System.out.println("result : " + result);
output.writeObject(result);

} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

從該RPC框架的簡易實現來看,RPC服務端邏輯是:首先創建ServerSocket負責監聽特定端口並接收客戶連接請求,然後使用Java原生的序列化/反序列化機制來解析得到請求,包括所調用方法的名稱、參數列表和實參,最後反射調用服務端對服務接口的具體實現並將得到的結果回傳至客戶端。至此,一次簡單PRC調用的服務端流程執行完畢。


  • 客戶端引用服務
 /**
* 引用服務
*
* @param 接口泛型

* @param interfaceClass 接口類型
* @param host 服務器主機名
* @param port 服務器端口
* @return 遠程服務,返回代理對象
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static T refer(final Class interfaceClass, final String host, final int port) throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
// JDK 動態代理的約束,只能實現對接口的代理
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
// JDK 動態代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class>[] {interfaceClass}, new InvocationHandler() {
// invoke方法本意是對目標方法的增強,在這裡用於發送RPC請求和接收響應
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
// 創建Socket客戶端,並與服務端建立鏈接
Socket socket = new Socket(host, port);
try { /* 客戶端像服務端進行請求,並將請求參數寫入流中*/
// 將對象寫入到對象輸出流,並將其發送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try { // 發送請求
System.out.println("\nClient發送請求 : ");

output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客戶端讀取並返回服務端的響應*/
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable)result;
}
System.out.println("\nClient收到響應 : ");
System.out.println("result : " + result);
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
}); return proxy;
}

從該RPC框架的簡易實現來看,RPC客戶端邏輯是:首先創建Socket客戶端並與服務端建立鏈接,然後使用Java原生的序列化/反序列化機制將調用請求發送給客戶端,包括所調用方法的名稱、參數列表將服務端的響應返回給用戶即可。至此,一次簡單PRC調用的客戶端流程執行完畢。特別地,從代碼實現來看,實現透明的PRC調用的關鍵就是 動態代理,這是RPC框架實現的靈魂所在。


  • RPC原型實現
public class RpcFramework {
/**
* 暴露服務
*
* @param service 服務實現
* @param port 服務端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception { if (service == null) { throw new IllegalArgumentException("service instance == null");
} if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port); // 建立Socket服務端
ServerSocket server = new ServerSocket(port); for (; ; ) { try { // 監聽Socket請求
final Socket socket = server.accept(); new Thread(new Runnable() { @Override
public void run() { try { try { /* 獲取請求流,Server解析並獲取請求*/
// 構建對象輸入流,從源中讀取對象到程序中
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream()); try {
System.out.println("\nServer解析請求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName); // 泛型與數組是不兼容的,除了通配符作泛型參數以外
Class>[] parameterTypes = (Class>[])input.readObject();
System.out.println( "parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 處理請求,進行響應*/
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream()); try { // service類型為Object的(可以發佈任何服務),故只能通過反射調用處理請求
// 反射調用,處理請求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);

System.out.println("\nServer 處理並生成響應 :");
System.out.println("result : " + result);
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
} /**
* 引用服務
*
* @param 接口泛型
* @param interfaceClass 接口類型
* @param host 服務器主機名
* @param port 服務器端口
* @return 遠程服務,返回代理對象
* @throws Exception
*/
@SuppressWarnings("unchecked") public static T refer(final Class interfaceClass, final String host, final int port) throws Exception { if (interfaceClass == null) { throw new IllegalArgumentException("Interface class == null");
} // JDK 動態代理的約束,只能實現對接口的代理
if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( "The " + interfaceClass.getName() + " must be interface class!");
} if (host == null || host.length() == 0) { throw new IllegalArgumentException("Host == null!");
} if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println( "Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port); // JDK 動態代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class>[] {interfaceClass}, new InvocationHandler() { // invoke方法本意是對目標方法的增強,在這裡用於發送RPC請求和接收響應

@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable { // 創建Socket客戶端,並與服務端建立鏈接
Socket socket = new Socket(host, port); try { /* 客戶端像服務端進行請求,並將請求參數寫入流中*/
// 將對象寫入到對象輸出流,並將其發送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream()); try { // 發送請求
System.out.println("\nClient發送請求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客戶端讀取並返回服務端的響應*/
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream()); try {
Object result = input.readObject(); if (result instanceof Throwable) { throw (Throwable)result;
}
System.out.println("\nClient收到響應 : ");
System.out.println("result : " + result); return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
}); return proxy;
}
}

以上是簡易RPC框架實現的簡易完整代碼。

四. 關於RPC框架的若干問題說明

(1).RPC框架如何做到透明化遠程服務調用?

如何封裝通信細節才能讓用戶像以本地調用方式調用遠程服務呢?就Java而言,動態代理恰是解決之道。Java動態代理有JDK動態代理和CGLIB動態代理兩種方式。儘管字節碼生成方式實現的代理更為強大和高效,但代碼維護不易,因此RPC框架的大部分實現還是選擇JDK動態代理的方式。在上面的例子中,RPCFramework實現中的invoke方法封裝了與遠端服務通信的細節,消費方首先從RPCFramework獲得服務提供方的接口,當執行helloService.hi(“Panda”)方法時就會調用invoke方法。


(2).如何發佈自己的服務?

如何讓別人使用我們的服務呢?難道就像我們上面的代碼一樣直接寫死服務的IP以及端口就可以了嗎?事實上,在實際生產實現中,使用人肉告知的方式是不現實的,因為實際生產中服務機器上/下線太頻繁了。如果你發現一臺機器提供服務不夠,要再添加一臺,這個時候就要告訴調用者我現在有兩個IP了,你們要輪詢調用來實現負載均衡;調用者咬咬牙改了,結果某天一臺機器掛了,調用者發現服務有一半不可用,他又只能手動修改代碼來刪除掛掉那臺機器的ip。這必然是相當痛苦的!

有沒有一種方法能實現自動告知,即機器的上線/下線對調用方透明,調用者不再需要寫死服務提供方地址?當然可以,生產中的RPC框架都採用的是自動告知的方式,比如,阿里內部使用的RPC框架HSF是通過ConfigServer來完成這項任務的。此外,Zookeeper也被廣泛用於實現服務自動註冊與發現功能。不管具體採用何種技術,他們大都採用的都是 發佈/訂閱模式。


(3).序列化與反序列化

我們知道,Java對象是無法直接在網絡中進行傳輸的。那麼,我們的RPC請求如何發給服務端,客戶端又如何接收來自服務端的響應呢?答案是,在傳輸Java對象時,首先對其進行序列化,然後在相應的終端進行反序列化還原對象以便進行處理。事實上,序列化/反序列化技術也有很多種,比如Java的原生序列化方式、JSON、阿里的Hessian和ProtoBuff序列化等,它們在效率上存在差異,但又有各自的特點。


除上面提到的三個問題外,生產中使用的RPC框架要考慮的東西還有很多,在此就不作探討了。本文的目的就是為了讓各位看官對RPC框架有一個感性的、較為深入的瞭解,如果達到了這一目的,筆者的目的基本就算達到了。

五. 總結

本文闡述了遠程調用的產生背景,然後介紹了RPC的基本概念和要解決的問題,之後手動實現了簡易得RPC框架並佐以實例進行演示,使看官們對RPC有一個感性完整的認識,最後討論了RPC框架的幾個重要問題。總之,RPC框架的精髓在於動態代理和反射,通過它們使得遠程調用“本地化”,對用戶透明且友好。

引用

RPC原理及RPC實例分析

http://www.importnew.com/22003.html

RPC框架幾行代碼就夠了

http://javatar.iteye.com/blog/1123915


分享到:


相關文章: