Java中的遠程過程調用(RPC)

1. 背景

這學期上了《分佈式系統》課程,內容主要是基於Java實現分佈式計算,所以老師前幾節課主要在給我們講用Java做分佈式可能會用到的一些技術。爲了方便學習和記錄,我將老師講的內容結合書籍和資料做了一些整理,這一篇主要討論遠程過程調用。

2. 遠程過程調用的定義

遠程過程調用是一種進程間通信技術,用於基於客戶端-服務器的應用程序。它也被稱爲子例程調用或函數調用。

客戶端有一個請求消息,RPC將其轉換併發送給服務器。此請求可以是對遠程服務器的過程或函數調用。當服務器接收到請求時,它將所需的響應發送回客戶機。客戶端在服務器處理調用時被阻塞,只有在服務器完成後才恢復執行。1

遠程過程調用的順序如下:

  • 客戶端存根由客戶端調用。
  • 客戶端存根發出一個系統調用,將消息發送到服務器,並將參數放入消息中。
  • 客戶端操作系統將消息從客戶端發送到服務器。
  • 消息由服務器操作系統傳遞到服務器存根。
  • 服務器存根從消息中刪除參數。
  • 然後,服務器存根調用服務器執行操作。
  • 執行完成後,結果原路返回到客戶端。

過程如下圖所示

RPC

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,有問題歡迎通過郵箱交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章