使用Socket&反射&Java流操作进行方法的远程调用(模拟RPC远程调用)

Spring Wu 200 2021-02-03

写在前面

阅读本文首先得具备基本的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
客户端调用结束

写在最后

至此,方法的远程调用已经完成~~
这篇文章写得有点仓促,明天还有面试。今天就先这样了~ 晚安~


# RPC