用 java 实现一个简单的 rpc 框架

 

目录

概述

实现关键点

公共接口

序列化 consumer 端待传输的方法信息

通信方式

反射


概述

RPC(Remote Procedure Call),顾名思义,指的是远程过程(方法)调用,与之相对应的就是本地方法调用。

本地方法调用

用 java 实现一个简单的 rpc 框架_第1张图片

放到具体的代码中,可以理解为,被调用方(producer)和调用方(consumer)在同一个项目当中

远程方法调用

用 java 实现一个简单的 rpc 框架_第2张图片

放到具体的代码中,可以理解为,被调用方法(producer)和调用方(consumer)在不同的项目当中

这里有一个问题,本地调用就可以实现了想要的功能了,为什么还要使用远程调用?

远程调用主要的目的就是为了降低机器的负载,把消耗资源比较大的操作放到一个独立的机器上。比如上图中 Provider 的 print 方法就是一个比较消耗资源的操作,这时我们把它放到机器 B 上,就可以降低机器 A 的负载,从而不会影响到机器 A 中其他重要业务的正常执行。

接下来,通过 java 实现的简易 RPC 框架说明其原理

实现关键点

想要实现远程的方法调用,下面的几个关键点是必不可少的。为了方便起见,在这里我把调用方称为 consumer ,被调用方称为 producer。假设 consumer 在机器 A,producer 在机器 B,consumer 想要调用 producer 中的 print 方法,该怎么实现呢? 

简单说来就是以下流程:

  1. consumer 端获取 公共接口 中的一些属性(类名,方法 A 的名字,方法 A 的形参),并且序列化为对象 A
  2. 通过指定通信方式传输序列化对象 A 至 producer
  3. producer 端获取对象 A,反序列化后获取相关属性(类名,方法 A 的名字,方法 A 的形参)
  4. producer 端通过反射调用待执行的方法,然后把方法执行结果通过指定通信方式传回 consumer

公共接口

consumer 和 producer 中必须要有完全相同的接口(类),这里的完全相同,不仅仅是接口本身,还包括接口(类)的包名

例如 com.iflytek.yyzx.rpc.remoteMethod.HelloService.java。

package com.iflytek.yyzx.rpc.remoteMethod;

public interface HelloService {
	
	public String sayHello(String words);
	
	public String sayHello(String words, Integer time);
	
}

在 consumer 端和 producer 端都需要实现该接口,但是方法的执行逻辑不一样。

在 consumer 端的实现类 HelloServiceImpl.java

package com.iflytek.yyzx.rpc.client;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.Socket;

import com.iflytek.yyzx.rpc.remoteMethod.HelloService;
import com.iflytek.yyzx.rpc.remoteMethod.RpcRequest;

public class HelloServiceImpl implements HelloService {
	
	public String sayHello(String words) {
		try {
			return (String) remoteCall(words);
		} catch (Exception e) {
			e.printStackTrace();
			return "error";
		}
	}

	@Override
	public String sayHello(String words, Integer time) {
		try {
			return (String) remoteCall(words, time);
		} catch (Exception e) {
			e.printStackTrace();
			return "error";
		}
	}
	
	public static Object remoteCall(String words, Integer time) throws Exception {
		// 获得类名
		String className = HelloService.class.getName();
		// 获得方法
		Method method = HelloService.class.getMethod("sayHello", java.lang.String.class, java.lang.Integer.class);
		// 获得传的参数
		Object[] paramValue = { words, time };

		Socket consumer = new Socket("127.0.0.1", 8765);

		ObjectOutputStream oos = new ObjectOutputStream(consumer.getOutputStream());
		RpcRequest rpcRequest = new RpcRequest();
		rpcRequest.setClassName(className);
		rpcRequest.setMethodName(method.getName());
		rpcRequest.setParamType(method.getParameterTypes());
		rpcRequest.setParamValue(paramValue);
		System.out.println(rpcRequest.toString());
		oos.writeObject(rpcRequest);

		ObjectInputStream ois = new ObjectInputStream(consumer.getInputStream());
		Object result = ois.readObject();
		
		consumer.close();
		return result;
	}
	
	public static Object remoteCall(String words) throws Exception {

		// 获得类名
		String className = HelloService.class.getName();
		// 获得方法
		Method method = HelloService.class.getMethod("sayHello", java.lang.String.class, java.lang.Integer.class);
		// 获得传的参数
		Object[] paramValue = { "Hello! I am consumer"};

		Socket consumer = new Socket("127.0.0.1", 8765);

		ObjectOutputStream oos = new ObjectOutputStream(consumer.getOutputStream());
		RpcRequest rpcRequest = new RpcRequest();
		rpcRequest.setClassName(className);
		rpcRequest.setMethodName(method.getName());
		rpcRequest.setParamType(method.getParameterTypes());
		rpcRequest.setParamValue(paramValue);
		System.out.println(rpcRequest.toString());
		oos.writeObject(rpcRequest);

		ObjectInputStream ois = new ObjectInputStream(consumer.getInputStream());
		Object result = ois.readObject();
		
		System.out.println("consumer receive message from provider: " + result.toString());

		consumer.close();
		return result;
	}

}

在 consumer 端实现 HelloService.java 的目的是 为了让用户感知上认为就是在调用本地的方法:sayHello(String words, Integer time),而实际上该方法里面的执行逻辑是在调用 producer 端 sayHello(String words, Integer time)。调用远程方法的细节在 remoteCall(String words, Integer time) 方法里面。

在 producer 端的实现类 HelloServiceImpl.java

例如 HelloServiceImpl.java

package com.iflytek.yyzx.rpc.server;

import com.iflytek.yyzx.rpc.remoteMethod.HelloService;

public class HelloServiceImpl implements HelloService{

	public String sayHello(String words) {
		System.out.println("provider receive message from consumer: " + words);
		return "Hello! I am provider";
	}

	@Override
	public String sayHello(String words, Integer time) {
		System.out.println("provider receive message from consumer: " + words + ", time: " + time);
		return "Hello! I am provider, time: " + time;
	}

}

这样,consumer 在调用方法:sayHello(String words, Integer time) 的时候,实际上会执行 producer 中 HelloServiceImpl.java 的方法: sayHello(String words, Integer time) 的具体实现逻辑。具体原因参见后面

序列化 consumer 端待传输的方法信息

这里待传输的方法信息包括:

  • 接口名(com.iflytek.yyzx.rpc.remoteMethod.HelloService)
  • 方法名(sayHello)
  • 方法的形参类型(java.lang.String.class,java.lang.Integer.class)
  • 参数值("Hello! I am consumer", 1)

这里的形参类型和参数值是一 一 对应的,如果方法没有形参,则为空。其实,这些信息就是一个类的基本信息,如果获取到了这些信息,就可以通过 java 的反射动态加载对应的类,然后调用对应的方法。通常,在 consumer 端和 producer 端会各自申明一个完全相同的类,包含传输的方法信息,这样做使得信息的获取更加清晰,易懂。

例如 com.iflytek.yyzx.rpc.remoteMethod.RpcRequest.java

package com.iflytek.yyzx.rpc.remoteMethod;

import java.io.Serializable;

/**
 * 用于存放远程调用需要的信息,例如类名,方法名等等
 * 
 * @author hsyang
 *
 */
public class RpcRequest implements Serializable{
	
	private static final long serialVersionUID = -2865905900171967180L;
	/** 类名 */
	private String className;
	/** 方法名 */
	private String methodName;
	/** 参数类型 */
	private Class[] paramType;
	/** 参数值 */
	private Object paramValue;
	
	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[] getParamType() {
		return paramType;
	}
	
	public void setParamType(Class[] paramType) {
		this.paramType = paramType;
	}
	
	
	public Object getParamValue() {
		return paramValue;
	}

	public void setParamValue(Object paramValue) {
		this.paramValue = paramValue;
	}

	@Override
	public String toString() {
		return "RpcRequest [className=" + className + ", methodName=" + methodName + ", paramType=" + paramType + "]";
	}

}

有了这个 RpcRequest 类之后,consumer 端只需要序列化 RpcRequest,通过特定的通信方式把序列化后的 RpcRequest 字节流发送至 producer 端,然后 producer 端反序列化 RpcRequest,获取对应的参数信息。通过反射实现我们调用指定方法的目地。

通信方式

前面提到的通信方式,实际上就是:把 consumer 端的信息发送至 producer 端的一种方式。有很多方式都可以实现:使用 netty,使用 HttpClient,使用  RestTemplate 等等。在这里为了简便,直接使用了 jdk 提供 Socket 类(consumer端),ServerSocket 类(producer 端)来进行信息地传送的。

反射

producer 端在收到了 consumer 端通过特定通信方式发过来的信息 RpcRequest,反序列化获取 RpcRequest 里面的参数信息:

  • 接口名(com.iflytek.yyzx.rpc.remoteMethod.HelloService)
  • 方法名(sayHello)
  • 方法的形参类型(java.lang.String.class,java.lang.Integer.class)
  • 参数值("Hello! I am consumer", 1)

然后,通过 java 的反射技术,实现方法的调用,具体包括:

通过接口名加载对应的接口(com.iflytek.yyzx.rpc.remoteMethod.HelloService),同时获取对应接口在 producer 端提前声明好的实现类的对象

//加载接口类
Class helloService = Class.forName(className);
// 获取接口实现类的对象
Object helloServiceImpl = serviceMap.get(className);

通过方法名和形参类型从已加载的 helloService 实例中获取方法对象

Method method = helloService.getMethod(methodName, paramType);

现在类的实例(helloServiceImpl)和 方法对象(method)都已经获取到了,接着就可以通过 java 的反射调用对应的方法了

Object result = method.invoke(helloServiceImpl, paramValues);

之后,会获得一个结果对象 result。紧接着,再把 result 通过指定通信方式传回 consumer。

至此,从客户端调用远程方法开始到结束的整个过程已经完毕了。

 

 

 

你可能感兴趣的:(java,RPC原理)