文章目錄
1. 背景
這學期上了《分佈式系統》課程,內容主要是基於Java實現分佈式計算,所以老師前幾節課主要在給我們講用Java做分佈式可能會用到的一些技術。爲了方便學習和記錄,我將老師講的內容結合書籍和資料做了一些整理,這一篇主要討論遠程過程調用。
2. 遠程過程調用的定義
遠程過程調用是一種進程間通信技術,用於基於客戶端-服務器的應用程序。它也被稱爲子例程調用或函數調用。
客戶端有一個請求消息,RPC將其轉換併發送給服務器。此請求可以是對遠程服務器的過程或函數調用。當服務器接收到請求時,它將所需的響應發送回客戶機。客戶端在服務器處理調用時被阻塞,只有在服務器完成後才恢復執行。1
遠程過程調用的順序如下:
- 客戶端存根由客戶端調用。
- 客戶端存根發出一個系統調用,將消息發送到服務器,並將參數放入消息中。
- 客戶端操作系統將消息從客戶端發送到服務器。
- 消息由服務器操作系統傳遞到服務器存根。
- 服務器存根從消息中刪除參數。
- 然後,服務器存根調用服務器執行操作。
- 執行完成後,結果原路返回到客戶端。
過程如下圖所示
3. 遠程過程調用的優點
RPC的一些優點如下:
- 遠程過程調用支持面向過程和麪向線程的模型。
- RPC的內部消息傳遞機制對用戶隱藏。重寫和重新開發代碼的工作量在遠程過程調用中是最小的。
- 遠程過程調用可以在分佈式環境中使用,也可以在本地環境中使用。
- 爲了提高性能,RPC省略了許多協議層。
4. 遠程過程調用的缺點
RPC的一些缺點如下:
- 遠程過程調用是一個可以用不同方式實現的概念。它不是一個標準。
- RPC對於硬件架構沒有靈活性。它只基於交互。
- 遠程過程調用而增加了成本。
5. 實例
Java 中可以通過使用Socket、反射機制和動態代理來實現一個RPC框架。
5.1. 客戶端
5.1.1. 客戶端實現
package service;
import rpc.DynamicProxyFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class clienTest {
static void test(int threadId) {
// 這裏實現了一個登陸的操作
// 實現細節並不重要,所以沒有給出 loginService 的代碼
// 通過動態代理與服務器建立連接
loginService loginservice =
DynamicProxyFactory.getProxy(loginService.class, "localhost", 8000);
// 執行遠程操作
String Result = loginservice.login("123", "123");
// 輸出結果
System.out.println(String.format("Client Thread %d result= %s",threadId, Result));
}
public static void main(String[] args) throws Exception {
int threadNum = 4;
// 創建多線程
ExecutorService executor = Executors.newFixedThreadPool(4);
// 多線程調用
for (int i = 0; i < 4; i++) {
int finalI = i;
executor.submit(() -> {
test(finalI + 1);
});
}
}
}
5.1.2. 動態代理類
package rpc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
// 動態代理類
public class DynamicProxyFactory {
@SuppressWarnings("unchecked")
public static <T> T getProxy(final Class<T> classType, final String host, final int port) {//泛型方法,傳入什麼類型返回什麼類型
InvocationHandler handler = (proxy, method, args) -> {
Connector connector = null;
try {
connector = new Connector(host, port);
RemoteCall call = new RemoteCall(classType.getName(), method.getName(), method.getParameterTypes(), args);
connector.send(call);
call = (RemoteCall) connector.receive();
return call.getResult();
} finally {
if (connector != null) connector.close();
}
};
System.out.println("代理開始執行");
return (T) Proxy.newProxyInstance(classType.getClassLoader(),new Class<?>[]{classType}, handler);
}
}
5.1.3. 連接器
package rpc;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Connector {
private String host;
private int port;
private Socket skt;
private InputStream is;
private ObjectInputStream ois;
private OutputStream os;
private ObjectOutputStream oos;
public Connector(String host, int port) throws Exception {
this.host = host;
this.port = port;
connect(host, port);
}
// 發送對象方法
public void send(Object obj) throws Exception {
oos.writeObject(obj);
}
// 接收對象方法
public Object receive() throws Exception {
return ois.readObject();
}
// 建立與遠程服務器的連接
public void connect() throws Exception {
connect(host, port);
}
// 建立與遠程服務器的連接
public void connect(String host, int port) throws Exception {
skt = new Socket(host, port);
os = skt.getOutputStream();
oos = new ObjectOutputStream(os);
is = skt.getInputStream();
ois = new ObjectInputStream(is);
}
// 關閉連接
public void close() {
try {
ois.close();
oos.close();
skt.close();
} catch (Exception e) {
System.out.println(" Conector.close :" + e);
}
}
}
5.2. 遠程對象
package rpc;
import java.io.Serializable;
//相當於一個javaBean
public class RemoteCall implements Serializable {
private static final long serialVersionUID = 1L;
private String className;// 表示服務類名或接口名
private String methodName; // 表示功能方法名
private Class<?>[] paramTypes;//表示方法參數類型
private Object[] params;//表示方法參數值/如果方法正常執行,則resul 爲方法返回值,如果方法拋出異常,則resul 爲該異常
private Object result;
public RemoteCall() {
}
public RemoteCall(String className, String methodName, Class<?>[] paramTypes, Object[] params) {
this.className = className;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.params = params;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class<?>[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class<?>[] paramTypes) {
this.paramTypes = paramTypes;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public String toString() {
return "className=" + className + " methodName=" + methodName;
}
}
5.3. 服務端
package rpc;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import service.loginServiceImp;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Server {
private final static String CLASS_PATH = "service.";
// 存放遠程對象的緩存
private HashMap<String, Object> remoteObjects = new HashMap<>();
//註冊服務:把一個遠程對象放到緩存中
public void register(String className, Object remoteObject) {
remoteObjects.put(className, remoteObject);
}
// 8000端口負載均衡
public void transferService(ServerSocket serverSocket, int threadId) throws Exception {
//隨機數生成器
final Random random = new Random();
// 循環運行
while (true) {
System.out.println(String.format("Thread:%d 負載均衡啓動......", threadId));
// 創建hashMap
HashMap<String, Object> map = new HashMap<>();
// 等待服務
Socket socket = serverSocket.accept();
// 獲取客戶端輸入流
ObjectInputStream clientOis = new ObjectInputStream(socket.getInputStream());
// 獲取客戶端輸出流
ObjectOutputStream clientOos = new ObjectOutputStream(socket.getOutputStream());
// 將客戶端傳輸的對象存入HashMap
map.put("clientObject", (RemoteCall) clientOis.readObject());
// 隨機獲取一個端口
int serverIndex = 8001 + random.nextInt(2);
// 打開端口
Socket targetSocket = new Socket("localhost", serverIndex);
// 輸出
System.out.println(String.format("Thread:%d 負載均衡傳輸任務到端口 %d......", threadId, serverIndex));
// 獲取輸出對象流
ObjectOutputStream oos = new ObjectOutputStream(targetSocket.getOutputStream());
// 傳輸hashMap
oos.writeObject(map);
// 獲取輸入對象流
ObjectInputStream serverOis = new ObjectInputStream(targetSocket.getInputStream());
// 接收服務器發送的Call 對象
RemoteCall remotecallobj = (RemoteCall) serverOis.readObject();
// 向客戶端發送對象
clientOos.writeObject(remotecallobj);
// 關閉資源
oos.close();
socket.close();
clientOos.close();
}
}
// 暴露服務,創建基於流的Socket,並在8001、8002 端口監聽
public void exportService(ServerSocket serverSocket, int threadId) throws Exception {
// 循環運行
while (true) {
System.out.println(String.format("Thread:%d 服務器啓動......", threadId));
Socket socket = serverSocket.accept();
// 獲取負載服務器輸入流
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
// 獲取負載服務器輸出流
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
// 獲取負載服務器傳入的hashMap
HashMap<String, Object> map = (HashMap<String, Object>) ois.readObject();
// 接收客戶發送的Call 對象
RemoteCall remotecallobj = (RemoteCall) map.get("clientObject");
// 輸出
System.out.println(remotecallobj);
// 調用相關對象的方法
System.out.println(String.format("Thread:%d calling......", threadId));
remotecallobj = invoke(remotecallobj);
// 向客戶發送包含了執行結果的remotecallobj 對象
oos.writeObject(remotecallobj);
ois.close();
oos.close();
socket.close();
}
}
public RemoteCall invoke(RemoteCall call) {
Object result = null;
try {
String className = call.getClassName();
String methodName = call.getMethodName();
Object[] params = call.getParams();
Class<?> classType = Class.forName(className);
Class<?>[] paramTypes = call.getParamTypes();
Method method = classType.getMethod(methodName, paramTypes);
// 從hashmap緩存中取出相關的遠程對象Object
Object remoteObject = remoteObjects.get(className);
if (remoteObject == null) {
throw new Exception(className + " 的遠程對象不存在");
} else {
result = method.invoke(remoteObject, params);
System.out.println("遠程調用結束:remotObject:" + remoteObject.toString() + ",params:" + params.toString());
}
} catch (Exception e) {
System.out.println("錯誤:" + e.getMessage());
}
call.setResult(result);
return call;
}
public static void main(String args[]) throws Exception {
// 線程數量
int threadNum = 6;
// 初始化
Server server = new Server();
// 創建多線程
ExecutorService executor = Executors.newFixedThreadPool(threadNum);
//把事先創建的RemoteServceImpl 對象加人到服務器的緩存中
//在服務註冊中心註冊服務
server.register(CLASS_PATH + "loginService", new loginServiceImp());
// 多線程運行
Future[] future = new Future[threadNum];
// 創建端口數組
ServerSocket[] serverSocket = new ServerSocket[threadNum];
// 創建8000端口
ServerSocket port8000 = new ServerSocket(8000);
// 創建8001端口
ServerSocket port8001 = new ServerSocket(8001);
// 創建8002端口
ServerSocket port8002 = new ServerSocket(8002);
for (int i = 0; i < threadNum; i++) {
// 第一、二個線程端口爲8000,負責調度資源,負載均衡
if (i <= 1) {
serverSocket[i] = port8000;
} else if (i <= 3) {
// 第三、四個線程端口爲8001,模擬第一臺主機
serverSocket[i] = port8001;
} else {
// 第五、六個線程端口爲8002,模擬第二臺主機
serverSocket[i] = port8002;
}
}
// 循環創建線程
for (int i = 0; i < threadNum; i++) {
int finalI = i;
future[i] = executor.submit(() -> {
try {
// 8000端口負載均衡
if (finalI <= 1) {
server.transferService(serverSocket[finalI], finalI + 1);
} else {
//打開網絡端口,接受外部請求,執行服務功能,返回結果
server.exportService(serverSocket[finalI], finalI + 1);
}
} catch (Exception e) {
e.printStackTrace();
}
return 1;
});
}
// 循環獲取線程結果,阻塞同步
for (int i = 0; i < threadNum; i++) {
future[i].get();
}
// 關閉端口
for (int i = 0; i < threadNum; i++) {
serverSocket[i].close();
}
}
}
6. 參考文獻
[1] https://www.tutorialspoint.com/remote-procedure-call-rpc
聯繫郵箱:[email protected]
Github:https://github.com/CurrenWong
歡迎轉載/Star/Fork,有問題歡迎通過郵箱交流。