分布式基础(RMI和RPC)

一、RMI介绍

1.1 什么是RMI?

RMI(Remote Method Invocation),翻译过来是远程方法调用的意思。所谓远程方法调用,实际上就是在不同系统之间的方法调用。比如说,A系统调用B系统的方法。RMI技术使得Java能够实现基于RPC的应用程序。
在这里插入图片描述

1.2 RMI的一般流程

  1. 客户对象调用客户端辅助对象中的方法;
  2. 客户端辅助对象对调用信息(如方法名、参数类型等)进行打包,然后通过网络发送给服务端的辅助对象;
  3. 服务端辅助对象将客户端辅助对象发送过来的信息进行解包,然后在服务端找出真正被调用的方法以及该方法所在对象;
  4. 服务端调用真正对象的方法,并将结果返回;
  5. 服务辅助对象将结果打包,并发送给客户端辅助对象;
  6. 客户端辅助对象将结果进行解包,并返回给客户对象;
  7. 客户对象获得返回值后进行相应处理;

但是,对于客户对象来说,第2~6步都是透明的。也就意味着,开发人员无须关心任何网络传输的细节问题。

1.3 使用Java实现远程方法调用

(1)定义服务端

// 定义业务接口
public interface IBiz extends Remote {

	String sayHello(String name) throws RemoteException;
	
}

业务接口需要继承Remote父接口,而且接口中的方法需要抛出RemoteException异常。

// 业务实现类
public class BizImpl extends UnicastRemoteObject implements IBiz {

	public BizImpl() throws RemoteException {
		super();
	}

	@Override
	public String sayHello(String name) throws RemoteException {
		System.out.println("executing sayHello...");
		return "hello " + name;
	}

}

接口实现类需要继承UnicastRemoteObject父类,并且提供一个无参构造函数。

定义完成后,接下来新建一个服务测试类。

public class Test {

	public static void main(String[] args) {
		try {
			IBiz biz = new BizImpl();
			// 注册对外服务的端口号
			LocateRegistry.createRegistry(8080);
			// 绑定服务的识别名
			Naming.bind("rmi://127.0.0.1:8080/hello", biz);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

上面代码首先新建一个业务对象,然后注册对外服务的端口号,最后把业务对象与服务的识别名进行绑定。

(2)定义客户端
首先,把上面业务接口类复制到客户端工程中。

public interface IBiz extends Remote {

	String sayHello(String name) throws RemoteException;
	
}

注意,为了与服务端的业务接口保持一致,尽量不要任意修改业务接口方法的信息,否则可能会调用失败。

准备完成后,我们定义一个客户端测试类。

public class Test {

	public static void main(String[] args) {
		try {
			// 连接远程服务,如果远程服务没有开启,就会抛出ConnectionException异常
			IBiz biz = (IBiz) Naming.lookup("rmi://127.0.0.1:8080/hello");
			// 调用服务对象方法
			String info = biz.sayHello("aa");
			System.out.println(info);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

该测试类的逻辑也很简单,首先调用java.rmi.Naming类提供的lookup方法连接远程服务。如果连接失败,giant方法会抛出ConnectionException异常。如果连接成功,我们就可以想调用本项目对象方法一样去调用远程服务对象的方法。

二、分布式RPC实现

RMI技术存在不足的地方:

  1. 在业务代码侵入较多的RMI特定的API。比如说每一个业务方法都需要抛出RemoteException异常,每个业务实现类都要继承UnicastRemoteObject等等。
  2. RMI使用了通用统一的网络通讯方式,客户端参数传递即服务器端计算完成后的返回值传递使用了通用的序列化方式,这对于一些需要在完成分布式调度时对数据进行自定义的安全加密流程的项目来说,无疑是一个不好的消息;
  3. RMI采用了标准的Socket通信,对于那些使用了其他通信协议的系统而言也是一个非常大的障碍;
  4. 由于数据序列化部分是专门为Java对象定制的,所以对于其他语言的系统支持不足;

基于上述原因分析,我们有理由自己构建出一套无需太多开发规范限制,能够自由扩展加密方式、底层网络协议实现方式的RPC调度模块。如果借鉴Java原生的RMI技术实现自己的RPC组件,主要涉及到的技术点:

  • 网络编程
  • 反射
  • 动态代理技术

网络编程和反射方面的内容这里不去研究,我们主要了解一下动态代理技术。

2.1 动态代理

代理模式是一种常见的设计模式,其目的是为了对其他对象提供一个代理以控制某个真实对象的方法。代理模式又分为静态代理和动态代理两种。其中动态代理是我们重点要讲解的部分。

Java动态代理可以动态创建并动态处理所代理对象的方法调用。在代理对象上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的策略。
在这里插入图片描述
动态代理类是目标类的代理,它继承了目标类。使用动态代理后,客户端不再直接调用目标类方法,而是通过目标类的代理Proxy间接地调用目标类的方法。每次调用调用代理类方法的时候,都会自动执行代理处理器的invoke方法。我们可以根据业务需求在调用目标类的前后加入一些处理。例如:事务控制、记录日志等。

下面是Java实现动态代理的代码:

第一步:新建业务接口。

public interface IBiz extends Remote {

	String sayHello(String name) throws RemoteException;
	
}

第二步:新建一个动态代理处理器类,该类实现了InvocationHandler接口,并实现它的invoke方法。

/*
	动态代理处理器
*/
public class DynamicProxyHandler implements InvocationHandler {

	/**
	 * 方法描述:调用代理对象时候自动触发该方法
	 * @param proxy 代理对象
	 * @param method 调用方法
	 * @param args 参数列表
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		System.out.println("执行代理对象的invoke方法...");
		System.out.println("调用方法名:" + method.getName());
		if (args != null && args.length > 0) {
			System.out.println("传入参数:" + Arrays.toString(args));
		}
		return null;
	}

}

第三步:定义完成后,我们新建一个测试类。

public class DynamicProxyTest {

	public static void main(String[] args) {
		try {
			// 创建IBiz的代理对象
			IBiz biz = (IBiz) Proxy.newProxyInstance(IBiz.class.getClassLoader()
					, new Class[]{IBiz.class}
					, new DynamicProxyHandler());
			// 调用代理对象方法,并获取返回结果
			String info = biz.sayHello("jacky");
			// 输出返回结果
			System.out.println(info); 
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

上面代码中,Proxy是JDK提供的一个动态代理类。它提供了一个newProxyInstance的静态方法创建代理对象。该方法第一个参数代表代理类的类加载器,第二个参数代表要动态代理的业务接口,第三个参数代表一个动态代理的处理器。只有当调用代理对象方法的时候,处理器的invoke方法才会自动被触发。

2.2 自定义RPC组件的实现流程

在这里插入图片描述

2.3 自定义RPC组件

2.3.1 RPC服务端

RPC服务端的目录结构如下图所示:
在这里插入图片描述
ConnectThread:线程类,用于监听客户端连接
Service:服务工具类,提供一些访问业务对象的方法;
ServiceFlag:服务标识类,在该类中定义了一些常量,用来标识不同的指令操作;
ServiceNotFoundException:异常类
ServiceThread:线程类,处理客户端发送过来的请求;
IBiz:业务接口类;
BizImpl:业务接口的实现类;
Server:测试类

具体代码实现:

(1)定义业务接口和实现类

public interface IBiz {

	String sayHello(String name);
	
}

public class BizImpl implements IBiz {

	@Override
	public String sayHello(String name) {
		return "hello " + name;
	}

}

(2)服务工具类

public class Service {
	// 服务端Socket
	public static ServerSocket server;
	// 服务端Socket默认监听的端口号
	private static int servicePort = 1689;

	// 如果server为空,该方法用于指定服务端Socket的端口号
	public static void setPort(int port) {
		if (server == null) {
			servicePort = port;
		}
	}
	
	// 该map保存了服务名和业务对象的对应关系
	private static Hashtable<String, Object> bindservice = new Hashtable<String, Object>();

	// 把bindName和bindObject组成的键值对添加到bindservice中
	public static void bind(String bindName, Object bindObject) {
		// 如果server为空,启动一个线程去监听客户端连接
		// 如果有客户端连接进来,就读取客户端发送过来的数据,并返回结果
		if (server == null) {
			try {
				server = new ServerSocket(servicePort);
				ConnectThread ct = new ConnectThread(server);
				ct.start();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		bindservice.put(bindName, bindObject);
	}

	/**
	 * 根据服务名获取绑定接口的名字
	 * @param bindName 服务名
	 * @return 返回服务名对应业务对象实现接口的名称
	 */
	public static String getBindObjectInterface(String bindName) {
		// 定义一个变量,保存业务对象的接口
		Class<?> interfaceClass = null;
		try {
			// 根据服务名获取对应的业务对象
			Class<?> nowClass = bindservice.get(bindName).getClass();
			// 获取业务对象实现的接口,如果没有,就从它的父类获取父类实现接口,以此类推,直到获取到实现接口为止。
			while (nowClass.getSuperclass() != null && interfaceClass == null) {
				if (nowClass.getInterfaces().length > 0) {
					interfaceClass = nowClass.getInterfaces()[0];
					break;
				}
				nowClass = nowClass.getSuperclass();
			}
			// 返回接口的名称
			return interfaceClass.getCanonicalName();
		} catch (Exception ex) {
			return "";
		}
	}

	/**
	 * 获取方法的返回值
	 * @param bindName 服务名
	 * @param methodName 方法名
	 * @param argtypes 参数类型列表
	 * @param args 实参列表
	 * @return 方法返回值
	 */
	public static Object getInvokeReturn(String bindName, String methodName,
			Class<?>[] argtypes, Object[] args) {
		try {
			// 根据服务名获取业务对象
			Object obj = bindservice.get(bindName);
			// 根据方法名和参数类型获取该方法的Method对象
			Method method = obj.getClass().getMethod(methodName, argtypes);
			// 调用方法,并获取方法返回值
			Object returnObj = method.invoke(obj, args);
			// 返回结果
			return returnObj;
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
	}
}

(3)监听客户端的线程类

public class ConnectThread extends Thread {
	// 定义服务端Socket
	ServerSocket server;
	// 定义一个线程池
	Executor exec = new ScheduledThreadPoolExecutor(50);
	
	public ConnectThread(ServerSocket server) {
		// 初始化服务端Socket
		this.server = server;
	}

	public void run() {
		while (true) {
			try {
				// 监听客户端连接
				Socket client = server.accept();
				// 在线程池中启动一条线程处理客户端请求
				ServiceThread st = new ServiceThread(client);
				exec.execute(st);
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
	}
}

(4)处理客户端请求的线程类

public class ServiceThread implements Runnable {
	// 客户端Socket
	Socket client;

	public ServiceThread(Socket client) {
		// 初始化客户端Socket
		this.client = client;
	}

	public void run() {
		// 服务名
		String bindName = "";
		try {
			// 创建字节输入流对象,用于读取客户端发送过来的数据
			DataInputStream dis = new DataInputStream(client.getInputStream());
			// 创建字节输出流对象,用于向客户端返回结果
			DataOutputStream dos = new DataOutputStream(client.getOutputStream());
			// 读取命令,1代表调用接口方法, 2代表调用带返回值接口方法
			int command = dis.readInt();
			// 判断
			switch (command) {
			// 如果command等于1,那么就读取服务名,然后把接口名发送给客户端
			case ServiceFlag.SERVICE_GETINTERFACE:
				// 读取服务名
				bindName = dis.readUTF();
				// 获取接口名,然后发送给客户端
				dos.writeUTF(Service.getBindObjectInterface(bindName));
				break;
			// 如果command等于2,那么就读取服务名、方法名、参数类型和参数名称,然后把序列化后的返回值发送给客户端
			case ServiceFlag.SERVICE_GETINVOKERETURN:
				// 读取服务名
				bindName = dis.readUTF();
				// 读取方法名
				String methodName = dis.readUTF();
				// 读取参数类型
				ObjectInputStream ois = new ObjectInputStream(
						client.getInputStream());
				Class<?>[] argtypes = (Class[]) ois.readObject();
				// 读取实参
				Object[] args = (Object[]) ois.readObject();
				// 调用方法,并获取返回值
				Object obj = Service.getInvokeReturn(bindName, methodName,
						argtypes, args);
				// 对返回值进行序列化操作
				ObjectOutputStream oos = new ObjectOutputStream(
						client.getOutputStream());
				// 把序列化后的结果发送给客户端
				oos.writeObject(obj);
				break;
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}

(5)服务标识类

public interface ServiceFlag {
	int SERVICE_GETINTERFACE = 1; // 调用接口方法
	int SERVICE_GETINVOKERETURN = 2; // 调用带返回值接口方法
}

(6)自定义异常类

public class ServiceNotFoundException extends RuntimeException {
	private static final long serialVersionUID = 6482029944969529522L;

	public ServiceNotFoundException() {
		super("没有该服务!");
	}
}

(7)服务端测试类

public class Server {

	public static void main(String[] args) {
	    // 创建业务对象
		IBiz biz = new BizImpl();
		// 把业务对象和服务名进行绑定,并启动线程监听客户端连接
		Service.bind("hello", biz);
	}

}

2.3.2 RPC客户端

在这里插入图片描述
ConnectThread:线程类,用于监听客户端连接
Service:服务工具类,提供一些访问业务对象的方法;
ServiceFlag:服务标识类,在该类中定义了一些常量,用来标识不同的指令操作;
ServiceNotFoundException:异常类
ServiceThread:线程类,处理客户端发送过来的请求;
IBiz:业务接口类;
BizImpl:业务接口的实现类;
Server:测试类

具体代码实现:

(1)业务接口

public interface IBiz {

	String sayHello(String name);
	
}

该业务接口可以从服务端拷贝过来。

(2)服务工具类

public class Service {
	// 服务端Socket默认监听的端口号
	private static int servicePort = 1689;

	/**
	 * 连接远程服务
	 * @param address IP地址
	 * @param bindName 服务名
	 * @return 返回代理对象
	 * @throws ServiceNotFoundException
	 */
	public static Object lookup(String address, String bindName) throws ServiceNotFoundException {
		try (
			// 创建客户端Socket对象
			Socket client = new Socket(InetAddress.getByName(address), servicePort);
			// 创建字节输出流对象
			DataOutputStream dos = new DataOutputStream(client.getOutputStream());
			// 创建字节输入流对象
			DataInputStream dis = new DataInputStream(client.getInputStream());
		) {
			
			// 发送操作指令
			dos.writeInt(ServiceFlag.SERVICE_GETINTERFACE);
			// 发送服务名
			dos.writeUTF(bindName);
			// 读取服务端发送过来的接口名称
			String interfaceName = dis.readUTF();
			// 根据接口名获取Class对象
			Class<?> interfaceClass = Class.forName(interfaceName);
			// 创建动态代理处理器
			ServiceHandler handler = new ServiceHandler(servicePort, bindName, address);
			// 创建接口的代理对象
			Object proxyObj = Proxy.newProxyInstance(
					interfaceClass.getClassLoader(),
					new Class[] { interfaceClass }, 
					handler);
			// 返回代理对象
			return proxyObj;
		} catch (Exception ex) {
			ex.printStackTrace();
			throw new ServiceNotFoundException();
		} 
	}
}

(3)动态代理处理器

public class ServiceHandler implements InvocationHandler {
	// 端口号
	int port;
	// 服务名
	String bindName;
	// 服务IP地址
	String address;

	public ServiceHandler(int port, String bindName, String address) {
		this.port = port;
		this.bindName = bindName;
		this.address = address;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) {
		Socket client = null;
		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try {
			// 创建客户端Socket对象
			client = new Socket(InetAddress.getByName(address), port);
			// 创建字节输出流对象
			DataOutputStream dos = new DataOutputStream(client.getOutputStream());
			// 发送指令
			dos.writeInt(ServiceFlag.SERVICE_GETINVOKERETURN);
			// 发送服务名
			dos.writeUTF(bindName);
			// 发送方法名
			dos.writeUTF(method.getName());
			// 创建对象输出流对象,用于序列化操作
			oos = new ObjectOutputStream(client.getOutputStream());
			// 发送方法类型
			oos.writeObject(method.getParameterTypes());
			// 发送实参
			oos.writeObject(args);
			// 创建对象输入流对象,用于反序列化操作	
			ois = new ObjectInputStream(client.getInputStream());
			// 读取服务发送过来的数据内容
			Object obj = ois.readObject();
			// 返回内容
			return obj;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} finally {
			if (ois != null) {
				try {
					ois.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (oos != null) {
				try {
					oos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (client != null) {
				try {
					client.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

}

(4)服务标识类

public interface ServiceFlag {
	int SERVICE_GETINTERFACE = 1; // 调用接口方法
	int SERVICE_GETINVOKERETURN = 2; // 调用带返回值接口方法
}

(5)自定义异常类

public class ServiceNotFoundException extends RuntimeException {
	private static final long serialVersionUID = 6482029944969529522L;

	public ServiceNotFoundException() {
		super("没有该服务!");
	}
}

(6)客户端测试类

public class Client {

	public static void main(String[] args) throws ServiceNotFoundException {
		IBiz biz = (IBiz) Service.lookup("127.0.0.1", "hello");
		String info = biz.sayHello("aa");
		System.out.println(info);
	}

}

注意:由于RPC通信过程中有大量数据需要在客户端和服务端之间进行来回传输,所以需要依赖到序列化和反序列化的相关技术。因此,在RPC里面所有能够被远程调用的方法参数和返回值都要求必须能够被序列化和反序列化的。如果在实际开发中某些数据无法进行序列化和反序列化操作,那么它们就无法应用到RPC场景中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值