筆者之前僅看過RPC這個單詞,完全沒有了解過,不想終於還是碰上了。起因:這邊想提高並發量而去看kafka(最后折中使用了redis),其中kafka需要安裝ZooKeeper,而ZooKeeper又與分布式相關,再繼續就發現分布式的基礎是RPC,於是寫下了這篇博文
1. RPC
RPC(Remote Procedure Call)遠程過程調用,即通過網絡通信來調用遠程計算機程序上的服務,而這個調用過程就像調用本地方法一樣簡單透明,並且不需要了解底層的網絡技術協議。RPC采用C/S架構,發出請求的程序是Client,提供服務的則是Server,類似於Http請求與響應。簡單總結就是:調用的方法實際在遠程,而要像調用本地方法一樣簡單。
1)對於客戶端的我:調用本地的一個方法(存根)就能獲得服務。 這個存根是遠程服務的一個代理,其底層如何實現,對於我來說是透明的。
2)對於遠程服務器:監聽是否有連接過來,來了就調用對應的方法並返回(服務器端較易理解)
其結構圖如下:
- 用戶調用一個 “本地” 函數,該函數調用客戶句柄(遠程服務在本地的存根)
- 客戶句柄調用網絡通信來訪問遠程程序
- 遠程程序收到網絡通信及相關信息就調用服務句柄
- 服務句柄就調用服務函數,函數結束逆序返回結果完成一次遠程調用
2. 為什么需要RPC
當我們的業務量越來越龐大,垂直增加服務器的數量對提高性能的作用愈加微乎,此時難免會采用分布式的架構以便更好地提高性能。分布式架構的每個服務都是獨立的部分,當需要完成某項業務且依賴不同的服務式時,這些服務就需要互相調用,此時服務之間的調用就需要一種高效的應用程序之間的通訊手段了,這就是PRC出現的原因
3. RPC實現要求
3.1 服務提供方
提供服務:實現所提供的服務
服務暴漏:僅僅實現了服務是不夠的,還需要將提供的服務暴漏給外界,讓外界知道有何,如何使用服務
3.2 服務調用方
遠程代理對象:在調用本地方法時實際調用的是遠程的方法,那么勢必本地需要一個遠程代理對象
總結:為了實現RPC需要有:通信模型(BIO、NIO),服務定位(IP、PORT),遠程代理對象(遠程服務的本地代理),序列化(網絡傳輸轉換成二進制)
4. 簡單實現
其主要的對象有:服務端接口、服務端接口實現、服務暴漏、客戶端接口(與服務端共享同個接口)、服務的引用
4.1 服務端接口
public interface Service {
// 提供兩個服務,說hello和整數相加
public String hello();
public int sum(int a, int b);
}
4.2 服務端接口實現
public class ServiceImpl implements Service {
@Override
public String hello() {
return "Hello World";
}
@Override
public int sum(int a, int b) {
return a + b;
}
}
4.3 服務暴漏
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流獲取方法名,參數等相關信息,最后通過反射實現方法的調用並將結果響應給客戶端
4.4 客戶端接口
public interface ClientService {
// 提供兩個服務,說hello和整數相加
public String hello();
public int sum(int a, int b);
}
4.5 服務引用
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套接字連接,序列化相關請求信息發送給服務端,然后等待響應結果。其中透明調用是使用了動態代理
4.6 測試
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
5. 思考
5.1 為什么不用Http
RPC與具體協議無關,可基於Http、TCP,但因為TCP性能相對較好。Http屬於應用層協議,TCP屬於傳輸層協議,相對在底層少了一層封裝,而且為了可靠傳輸而選擇TCP不選擇UDP
5.2 常用的RPC框架
Dubbo(阿里巴巴)、SpringCloud、RMI(JDK內置)
5.3 為什么要使用動態代理
因為要像本地調用一樣,對於使用者來說是透明的。
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)
5.4 為什么參數與參數類型需要分開傳
為了方便分辨方法的重載,下面獲取方法需要方法名和參數類型
service.getClass().getMethod(methodName, parameterTypes)
6. 優化
6.1 網絡通信
上面事例中采用BIO形式,阻塞訪問而導致並發量不高,可以用NIO代替
6.2 序列化
這里用了JDK原生方法只能序列化實現了Serializable接口的類,可以使用第三方的類庫來提高性能
6.3 服務負載
服務的自動發現,客戶端能動態感知服務端的變化,從實現熱部署,可用定時輪詢的方法,eg:ZooKeeper
6.4 Cluster
集群化,這樣便可以提供負載均衡
6.5 請求與響應
請求與響應可以進行編碼封裝,而不是這樣單獨一個一個發送