1. 程式人生 > >使用Socket&反射&Java流操作進行方法的遠程調用(模擬RPC遠程調用)

使用Socket&反射&Java流操作進行方法的遠程調用(模擬RPC遠程調用)

Language long 模型 結果 print 框架 端口 序列 implement

寫在前面

閱讀本文首先得具備基本的Socket、反射、Java流操作的基本API使用知識;否則本文你可能看不懂。。。

服務端的端口監聽

進行遠程調用,那就必須得有客戶端和服務端。服務端負責提供服務,客戶端來對服務端進行方法調用。所以現在我們清楚了: 需要一個服務端、一個客戶端

那麽我們說幹就幹,我們先建立一個服務端:

  • 通過Socket監聽本地服務器的一個端口(8081)
  • 調用socket的accept方法等待客戶端的連接(accpet方法原理)
/**
 *  RPC服務端
 * @author wushuaiping
 * @date 2018/3/15 下午12:23
 */
public class ObjectServerSerializ {
    public static void main(String[] args) {

        try {

            // 啟動服務端,並監聽8081端口
            ServerSocket serverSocket = new ServerSocket(8081);
            // 服務端啟動後,等待客戶端建立連接
            Socket accept = serverSocket.accept();
            } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客戶端與服務端建立連接

我們服務端監聽了端口後,那麽我們需要使用客戶端去訪問目標服務端的這個端口,代碼如下:

/**
 *  RPC客戶端,這裏發起調用請求。
 *   模擬RPC框架調用過程
 * @author wushuaiping
 * @date 2018/3/15 下午12:22
 */
public class ObjectClientSerializ {
        public static void main(String[] args)  {

            try {

                // 使用Socket與指定IP的主機端口進行連接。
                Socket socket = new Socket("localhost", 8081);
            } catch (Exception e) {
                e.printStackTrace();
        }
    }
}

業務方法

與服務端建立連接後,那我們進行下一步。因為我們要模擬RPC遠程調用,那麽我們的有一個業務方法:

業務方法接口

/**
 * 業務方法接口
 */
public interface HelloService {

	String sayHello(String str);
}

業務方法實現類

遠程調用必須要實現序列化接口(Serializable)。

/**
 * 
 * @author wushuaiping
 *
 */
public class HelloServiceImpl implements Serializable, HelloService {

	/**
	 * 
	 */
	private static final long serialVersionUID = 203100359025257718L;

	/**
	 * 
	 */
	public String sayHello(String str) {
		System.out.println("執行方法體,入參=" + str);
		return str;
	}

}

數據傳輸模型對象

我們有了服務方法後,首先想到的是,我們如果將序列化後的對象傳輸到服務端以後,服務端如何知道這是哪個對象?不可能使用Object來調用方法吧,所以我們需要一個能封裝業務類方法信息的數據傳輸對象。那麽該數據傳輸對象需要具備哪些信息?服務端調用肯定得用反射來調用方法,所以我們這個數據傳輸對象就得滿足一下條件:

  • 第一,反射調用時必須知道方法名 String methodName
  • 第二,反射調用時必須知道方法參數類型 Object[] parameterTypes
  • 第三,反射調用時必須知道參數 Object[] parameters
  • 第四,反射調用時必須知道哪個對象在調用 Object invokeObject

滿足以上條件後,就可以進行反射調用方法了,但是,我們通過服務端調用後,我們需要知道服務端返回的數據信息。那麽該對象還需要一個參數:

  • 第五,需要一個返回對象 Object result

通過上述分析,我們建立了該對象:

/**
 *  數據傳輸模型
 * @author wushuaiping
 * @date 2018/3/15 下午12:25
 */
public class TransportModel implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = -6338270997494457923L;

    //返回結果
    private Object result;
    //對象
    private Object object;
    //方法名
    private String methodName;
    //參數
    private Class<?>[] parameterTypes;

    private Object[] parameters;

    public void setParameterTypes(Class<?>[] parameterTypes) {
        this.parameterTypes = parameterTypes;
    }

    public Class<?>[] getParameterTypes() {
        return parameterTypes;
    }

    public void setResult(Object result) {
        this.result = result;
    }

    public Object getResult() {
        return result;
    }

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

客戶端設置相應調用信息

有了數據傳輸模型後,我們將需要的對象信息封裝進數據傳輸模型,我們就可以真正的開始對服務端的服務進行調用了!

/**
 *  RPC客戶端,這裏發起調用請求。
 *   模擬RPC框架調用過程
 * @author wushuaiping
 * @date 2018/3/15 下午12:22
 */
public class ObjectClientSerializ {
        public static void main(String[] args)  {

            try {

                // 使用Socket與指定IP的主機端口進行連接。
                Socket socket = new Socket("localhost", 8081);

                // 創建一個業務對象,模擬客戶端發起調用。
                HelloService helloService = new HelloServiceImpl();

                // 該傳輸模型對象存儲了客戶端發起調用的業務對象的一些信息。
                TransportModel model = new TransportModel();

                // 設置客戶端的調用對象
                model.setObject(helloService);
                // 設置需要調用的方法
                model.setMethodName("sayHello");
                // 獲得業務對象的字節碼信息
                Class class1 = helloService.getClass();

                // 在業務對象的字節碼信息中獲取"sayHello"並且方法入參為String的方法
                Method method = class1.getMethod("sayHello",String.class);

                // 設置傳輸模型對象中的調用信息。
                // 設置方法參數類型
                model.setParameterTypes(method.getParameterTypes());
                // 設置方法參數
                model.setParameters(new Object[]{"The first step of RPC"});

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}

將數據傳輸模型對象發送到服務端

在設置好相關調用信息後,現在終於可以去服務端調用了,但是我們不可能直接將數據傳輸模型對象“給”服務端,在網絡中傳輸數據都是以流(比特流)的形式傳輸的, 所以我們還要將數據傳輸模型對象轉為流,傳輸給服務端。

/**
 *  RPC客戶端,這裏發起調用請求。
 *   模擬RPC框架調用過程
 * @author wushuaiping
 * @date 2018/3/15 下午12:22
 */
public class ObjectClientSerializ {
        public static void main(String[] args)  {

            try {

                // 使用Socket與指定IP的主機端口進行連接。
                Socket socket = new Socket("localhost", 8081);

                // 創建一個業務對象,模擬客戶端發起調用。
                HelloService helloService = new HelloServiceImpl();

                // 該傳輸模型對象存儲了客戶端發起調用的業務對象的一些信息。
                TransportModel model = new TransportModel();

                // 設置客戶端的調用對象
                model.setObject(helloService);
                // 設置需要調用的方法
                model.setMethodName("sayHello");
                // 獲得業務對象的字節碼信息
                Class class1 = helloService.getClass();

                // 在業務對象的字節碼信息中獲取"sayHello"並且方法入參為String的方法
                Method method = class1.getMethod("sayHello",String.class);

                // 設置傳輸模型對象中的調用信息。
                // 設置方法參數類型
                model.setParameterTypes(method.getParameterTypes());
                // 設置方法參數
                model.setParameters(new Object[]{"The first step of RPC"});

                // 把存儲了業務對象信息的數據傳輸模型對象轉為流,也就是序列化對象。方便在網絡中傳輸。
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(model);
                oos.flush();
                byte[] byteArray = bos.toByteArray();

                // 獲得一個socket的輸出流。通過該流可以將數據傳輸到服務端。
                OutputStream outputStream = socket.getOutputStream();

                // 往輸出流中寫入需要進行傳輸的序列化後的流信息
                outputStream.write(byteArray);
                outputStream.flush();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}

獲取服務端返回的信息

當我們把數據序列化後以流的方式傳輸給了服務端。肯定不是大功告成了,因為我們還得知道服務端給我們返回了什麽東西:

/**
 *  RPC客戶端,這裏發起調用請求。
 *   模擬RPC框架調用過程
 * @author wushuaiping
 * @date 2018/3/15 下午12:22
 */
public class ObjectClientSerializ {
        public static void main(String[] args)  {

            try {

                // 使用Socket與指定IP的主機端口進行連接。
                Socket socket = new Socket("localhost", 8081);

                // 創建一個業務對象,模擬客戶端發起調用。
                HelloService helloService = new HelloServiceImpl();

                // 該傳輸模型對象存儲了客戶端發起調用的業務對象的一些信息。
                TransportModel model = new TransportModel();

                // 設置客戶端的調用對象
                model.setObject(helloService);
                // 設置需要調用的方法
                model.setMethodName("sayHello");
                // 獲得業務對象的字節碼信息
                Class class1 = helloService.getClass();

                // 在業務對象的字節碼信息中獲取"sayHello"並且方法入參為String的方法
                Method method = class1.getMethod("sayHello",String.class);

                // 設置傳輸模型對象中的調用信息。
                // 設置方法參數類型
                model.setParameterTypes(method.getParameterTypes());
                // 設置方法參數
                model.setParameters(new Object[]{"The first step of RPC"});

                // 把存儲了業務對象信息的數據傳輸模型對象轉為流,也就是序列化對象。方便在網絡中傳輸。
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(model);
                oos.flush();
                byte[] byteArray = bos.toByteArray();

                // 獲得一個socket的輸出流。通過該流可以將數據傳輸到服務端。
                OutputStream outputStream = socket.getOutputStream();

                // 往輸出流中寫入需要進行傳輸的序列化後的流信息
                outputStream.write(byteArray);
                outputStream.flush();

                // 因為socket建立的是長連接,所以可以獲取到將流數據傳到服務端後,返回的信息。
                // 所以我們需要通過輸入流,來獲取服務端返回的流數據信息。
                InputStream inputStream = socket.getInputStream();
                ObjectInputStream ois = new ObjectInputStream(inputStream);

                // 將得到的流數據讀成Object對象,強轉為我們的數據傳輸模型對象。最後得到服務端返回的結果。
                TransportModel readObject = (TransportModel)ois.readObject();
                System.out.println("調用返回結果="+readObject.getResult());
                socket.close();

                System.out.println("客戶端調用結束");

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}

此時,我們客戶端的調用算是大功告成了。接下來我們應該去服務端接收客戶端發送過來的數據了。

服務端接收客戶端數據

客戶端接收到的數據是以流方式存在的,所以需要反序列化轉流為Java對象。

/**
 *  RPC服務端
 * @author wushuaiping
 * @date 2018/3/15 下午12:23
 */
public class ObjectServerSerializ {
    public static void main(String[] args) {

        try {

            // 啟動服務端,並監聽8081端口
            ServerSocket serverSocket = new ServerSocket(8081);

            // 服務端啟動後,等待客戶端建立連接
            Socket accept = serverSocket.accept();

            // 獲取客戶端的輸入流,並將流信息讀成Object對象。
            // 然後強轉為我們的數據傳輸模型對象,因為我們客戶端也是用的該對象進行傳輸,所以強轉沒有問題。
            InputStream inputStream = accept.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            TransportModel transportModel = (TransportModel) ois.readObject();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服務端通過反射調用方法

因為需要調用的對象方法等相關數據都封裝在數據傳輸模型對象裏面,所以我們只需要把裏面的參數拿出來,再通過反射去掉用服務端存在的本地方法即可。

/**
 *  RPC服務端
 * @author wushuaiping
 * @date 2018/3/15 下午12:23
 */
public class ObjectServerSerializ {
    public static void main(String[] args) {

        try {

            // 啟動服務端,並監聽8081端口
            ServerSocket serverSocket = new ServerSocket(8081);

            // 服務端啟動後,等待客戶端建立連接
            Socket accept = serverSocket.accept();

            // 獲取客戶端的輸入流,並將流信息讀成Object對象。
            // 然後強轉為我們的數據傳輸模型對象,因為我們客戶端也是用的該對象進行傳輸,所以強轉沒有問題。
            InputStream inputStream = accept.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            TransportModel transportModel = (TransportModel) ois.readObject();

            // 因為客戶端在把流信息發過來之前,已經把相關的調用信息封裝進我們的數據傳輸模型對象中了
            // 所以這裏我們可以直接拿到這些對象的信息,然後通過反射,對方法進行調用。
            Object object = transportModel.getObject();
            String methodName = transportModel.getMethodName();
            Class<?>[] parameterTypes = transportModel.getParameterTypes();
            Object[] parameters = transportModel.getParameters();

            // 通過方法名和方法參數類型,得到一個方法對象
            Method method = object.getClass().getMethod(methodName,parameterTypes);

            // 然後通過這個方法對象去掉用目標方法,返回的是這個方法執行後返回的數據
            Object res = method.invoke(object, parameters);

            System.out.println("提供服務端執行方法返回結果:"+res);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服務端將數據返回給客戶端

服務端通過反射調用完目標方法後,我們還需要將調用目標方法後得到的數據返回給客戶端。

/**
 *  RPC服務端
 * @author wushuaiping
 * @date 2018/3/15 下午12:23
 */
public class ObjectServerSerializ {
    public static void main(String[] args) {

        try {

            // 啟動服務端,並監聽8081端口
            ServerSocket serverSocket = new ServerSocket(8081);

            // 服務端啟動後,等待客戶端建立連接
            Socket accept = serverSocket.accept();

            // 獲取客戶端的輸入流,並將流信息讀成Object對象。
            // 然後強轉為我們的數據傳輸模型對象,因為我們客戶端也是用的該對象進行傳輸,所以強轉沒有問題。
            InputStream inputStream = accept.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            TransportModel transportModel = (TransportModel) ois.readObject();

            // 因為客戶端在把流信息發過來之前,已經把相關的調用信息封裝進我們的數據傳輸模型對象中了
            // 所以這裏我們可以直接拿到這些對象的信息,然後通過反射,對方法進行調用。
            Object object = transportModel.getObject();
            String methodName = transportModel.getMethodName();
            Class<?>[] parameterTypes = transportModel.getParameterTypes();
            Object[] parameters = transportModel.getParameters();

            // 通過方法名和方法參數類型,得到一個方法對象
            Method method = object.getClass().getMethod(methodName,parameterTypes);

            // 然後通過這個方法對象去掉用目標方法,返回的是這個方法執行後返回的數據
            Object res = method.invoke(object, parameters);

            System.out.println("提供服務端執行方法返回結果:"+res);

            // 獲得服務端的輸出流
            OutputStream outputStream = accept.getOutputStream();

            // 建立一個字節數組輸出流對象。把數據傳輸模型對象序列化。方便進行網絡傳輸
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);

            // 創建一個數據傳輸模型對象,將服務端的返回數據傳到客戶端。
            TransportModel transportModel1 = new TransportModel();
            transportModel1.setResult(res);
            oos.writeObject(transportModel1);

            outputStream.write(bos.toByteArray());
            outputStream.flush();
            bos.close();
            outputStream.close();
            serverSocket.close();
            System.out.println("服務端關閉");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試

先啟動服務端的main方法,在啟用客戶端的main方法。之後我們會看到如下輸出:

調用返回結果=The first step of RPC
客戶端調用結束




https://my.oschina.net/u/3152087/blog/1635614

使用Socket&反射&Java流操作進行方法的遠程調用(模擬RPC遠程調用)