文章目录
Ⅰ什么是RMI
Remote Method Invoke(远程方法调用)。RMI就是用Java实现的RPC(远程过程调用)。其目的就是:利用网络通信,客户端远程调用服务器的方法,客户端本地并不存在此方法。
在细致点来说,为了实现我们的短连接。短连接是相对于长连接而言的。CSFramework的实现手段就是长连接,即,对于服务器而言,每一个客户端连接都由一个独立线程维护其会话过程,直至会话结束,方断开连接。
短连接是:由客户端向服务器端发起“请求”,服务器连接客户端,并解析客户端传来的请求内容,并执行相关操作以得到“响应”信息,并将响应回送给该客户端,然后就立刻关闭网络连接!可以说,这是一个短平快的”请求/响应“过程。再进一步思考这个短平快的”请求/响应“过程,其本质是:客户端远程执行了一个在服务器端的方法!也就是RMI的基本意思了。
ⅡRMI框架需求分析
基于上述内容,可以更加干脆的如是思考:
- 客户端执行一个方法method(),这个方法的执行过程实质上是:向服务器发送要执行的方法名称、实参等这个方法赖以执行的信息;
- 服务器端连接客户端,并接收到要执行的方法的相关信息;
- 服务器端执行相关方法,并将执行的结果返回给客户端;
- 客户端将服务器返回的执行结果,当成method()的执行结果提交给用户!
在这种思维中,客户端实质上相当于用一个代理执行method();而这个method()在执行过程中要做的事,与method()本身的逻辑没有任何关系;
RMI客户端要做的事如下:
- 连接RMI服务器;
- 向服务器发送方法执行的所有必要信息;
- 等待服务器返回这个方法在服务器端执行的后果!
RMI服务器端要做的事如下:
- 建立RMI服务器并且accept()侦听客户端连接请求;
- 连接客户端;
- 接受客户端发送的相关方法的所有必要信息;
- 找到这个方法,并反射机制执行该方法;
- 将方法执行的结果通过网络传输给客户端
- 断开网络连接!
(一)准备工作:使用哪种代理
经过上述需求分析讨论,我们知道需要用到代理。那么问题来了,该使用JDKProxy还是CglibProxy?
我们知道两者的差别是在是否需要接口,CglibProxy不需要接口,没有接口不能告知双方各自同步的地方,提供个接口,客户端和服务器都按接口来走,是个规范和标杆。因此我们使用JDKProxy。
在RMI实际的工作过程中,客户端只有接口,而不存在也不需要接口的实现类!客户端对RMI方法的执行,实质上都是传递方法信息和方法参数,再从服务器端接受结果!因此,在客户端获取proxy的时候,能提供的仅仅是接口,没有可能提供该接口的实现类,因为它根本没有也不需要。
ⅢRMI框架的实现
(一)实现思路
其核心思想涉及到两个网络端(客户端和服务器端)。一个端可以通过调用另一个端的方法,实现相关功能。一个端“执行”一个方法,而这个方法的实际执行是在另一端进行的!
当然,如果两个端都应该有相同的类,那么自然会拥有相同的方法。但是,我们知道这在实际上是很难做到同步的,一个端升级更新,另一个端也需要尽快更新代码,这是很难达到一致的。因此,又要使用我们的接口了!接口作为我们两端共同遵守的约定,并且对于相关操作好控制,规定了可以调用什么,不可以调用什么。
一个端所谓的执行这个方法,其实是通过调用这个类的代理对象的方法,在这个方法中实际上是将执行这个方法的参数和类名称、方法名称,通过网络通讯传输给另一端;另一端根据得到的方法名称、类名称和参数,实际执行那个方法,再将方法执行结果回传给对端。
下面是我实现RMI的具体思维导图
实现思路差不多就是这个图了,但是在具体编写的时候遇到许多问题和单元测试查漏补缺。(如果感兴趣想自己实现的,具体看RMI框架一步步实现)。
(二)RMI服务器启动前准备工作
a. RMIBeanDefinition
将所要执行的方法所需要的一些东西封装成一个类,面向对象编程思想。
import java.lang.reflect.Method;
public class RMIBeanDefinition {
private Class<?> klass;//要执行的方法的所属的接口的实现类
private Object object;//要执行的方法的对象
private Method method;//要执行的方法
//一开始我以为klass和obejct可以为单例,为了节省空间只保存一份,因为方法的调用只需要一个对象啊,后来我经过点拨
//发现单例需谨慎!如果你多次扫描改变了实现类呢?以后替换就不行了,会出错,所以成员单例一定要思考在思考!
//点拨:单例就全部单例了,不同接口的实现类的对象肯定是不一样的!
// 所以,思考单例问题一定要谨慎。为了不出现更大的问题,对象重复仅仅四个字节的问题,就浪费吧。
RMIBeanDefinition() {
}
Class<?> getKlass() {
return klass;
}
void setKlass(Class<?> klass) {
this.klass = klass;
}
Object getObject() {
return object;
}
void setObject(Object object) {
this.object = object;
}
Method getMethod() {
return method;
}
void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return "RMIBeanDefinition [klass=" + klass + ", object=" + object + ", method=" + method + "]";
}
}
b. RMIBeanFactory
存放RMIBeanDefinition的工厂,因为有可能有许多实现类,所以是非单例的。
import java.util.HashMap;
import java.util.Map;
public class RMIBeanFactory {
/**
* 该类最主要就是存放接口对应某个实现类里的所有方法,键为String类型的beanId,是method.toString.hashcode
*/
private Map<String, RMIBeanDefinition> beanMap;
public RMIBeanFactory() {
beanMap = new HashMap<>();
}
public Map<String, RMIBeanDefinition> getBeanPool() {
return beanMap;
}
public void addBean(String beanId, RMIBeanDefinition rmiBeanDefinition) {
beanMap.put(beanId, rmiBeanDefinition);
}
public RMIBeanDefinition getBean(String beanId) throws Exception {
RMIBeanDefinition beanDefinition = beanMap.get(beanId);
if (beanDefinition == null) {
throw new Exception("未找到相对应的方法");
}
return beanDefinition;
}
}
c. RMIBeanFactoryBuilder
工厂模式。根据扫描到的XML文件,建立相关的RMIBeanFactory。对于其中需要的XML解析小工具,可以点击这里。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Element;
import com.mec.util.XMLParse;
/**
*
* 该类最主要就是扫描相关XML文件
*
*/
public class RMIBeanFactoryBuilder {
/**
* 键为接口某方法的接口名字,值为一个填充好的beanFactory
*/
private static final Map<String, RMIBeanFactory> factoryPool = new HashMap<>();
/*因为但凡是实现接口的实现类,其会将接口所规定的方法实现
* 因此在扫描的时候,我们需要根据扫描的XML文件把接口和实现类的方法的BeanFactory映射关系填充好
* */
public static void scanXMLConfig(String xmlPath) {
new XMLParse() {
@Override
public void dealElement(Element element, int index) {
String interfaceName = element.getAttribute("interfaceName");
String className = element.getAttribute("className");
RMIBeanFactory beanFactory = new RMIBeanFactory(); //每扫描到一个interface标签需要创建一个factory
try {
Class<?> interfaceKlass = Class.forName(interfaceName);
Class<?> implementKlass = Class.forName(className);
filterMethod(interfaceKlass, implementKlass, beanFactory);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
factoryPool.put(interfaceName, beanFactory);
}
}.parseTag(XMLParse.getDocument(xmlPath), "interface");
}
/*筛选方法
* 接口方法.getName = 实现类相关方法.getName
* 接口方法.getParameterTypes()=实现类相关方法的ParameterTypes
* 这两个条件就卡死了唯一的方法,就把映射关系完全填充好了!
* */
private static void filterMethod(Class<?> interfClass,Class<?> implementKlass ,RMIBeanFactory beanFactory) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Method[] iMethods = interfClass.getDeclaredMethods();
for (Method iMethod : iMethods) {
String methodHashCode = String.valueOf(iMethod.toString().hashCode());
String methodName = iMethod.getName();
Class<?>[] paraTypes = iMethod.getParameterTypes();
Method implementMethod = implementKlass.getDeclaredMethod(methodName, paraTypes);
RMIBeanDefinition beanDefinition = new RMIBeanDefinition();
Constructor<?> noneConstructor = implementKlass.getConstructor(new Class<?>[] {});
Object implementKlassObject = noneConstructor.newInstance(new Object[] {});
beanDefinition.setKlass(implementKlass);
beanDefinition.setObject(implementKlassObject);
beanDefinition.setMethod(implementMethod);
beanFactory.addBean(methodHashCode, beanDefinition);
}
}
public static RMIBeanFactory getBeanFactory(String interfaceName) throws Exception {
RMIBeanFactory beanFactory = factoryPool.get(interfaceName);
if (beanFactory == null) {
throw new Exception("没有找到相关接口的实现类,请检查相关XML文件");
}
return beanFactory;
}
}
(三)RMI服务器端
a. RMIServer
对外提供的主要API,包括对服务器的设置,以及侦听客户端连接,启动RMIServerWorker去处理客户端连接请求。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class RMIServer implements Runnable {
public static final int DEFAULT_PORT = 54189;
private int port;
private ServerSocket server;
private volatile boolean goon;
public RMIServer() {
this.port = DEFAULT_PORT;
this.goon = false;
}
public void setPort(int port) {
this.port = port;
}
public void startup() throws IOException {
if (this.goon == true) {
System.out.println("服务器已经启动,请勿重复启动"); //如果为了以后输出多样性这里可以使用ISpeaker和IListener模式
return;
}
this.server = new ServerSocket(this.port);
this.goon = true;
System.out.println("RMI服务器成功启动");
new Thread(this).start();
}
public void shutdown() {
if (this.goon == false) {
System.out.println("服务器已经宕机,请勿重复宕机");
return;
}
close();
}
@Override
public void run() {
System.out.println("服务器成功启动开始侦听客户端连接");
while (goon) {
try {
Socket client = server.accept();
// 收到了一个RMI客户端的连接,下面要处理接受客户端发送过来的有关方法信息
// 如果一下全写到下面,我们的服务器就不纯粹了,我们的服务器仅仅只是侦听客户端连接的作用
// 所以新写个类分离出去处理
// 同时这里会遇到大量客户端连接,所以要用线程处理
new RMIServerWorker(client);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void close() {
this.goon = false;
if (server != null && !server.isClosed()) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
server = null;
}
}
}
}
b. RMIServerWorker
RMI服务器内部处理者,不对外提供,由服务器侦听到客户端请求自动去建立,其主要作用是处理客户端请求的内部逻辑。
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Socket;
import com.mec.util.ArgumentMaker;
public class RMIServerWorker implements Runnable{
private Socket socket;
private DataOutputStream dos;
private DataInputStream dis;
RMIServerWorker(Socket socket) {
this.socket = socket;
new Thread(this).start();
}
@Override
public void run() {
try {
this.dis = new DataInputStream(socket.getInputStream());
this.dos = new DataOutputStream(socket.getOutputStream());
//开始接受来自客户端的有关将要执行的方法的众多信息
// 查找相关方法,反射机制执行该方法,并将方法执行结果返回个客户端
String headerMessage = dis.readUTF();
int colonIndex = headerMessage.indexOf(':');
String interfaceName = headerMessage.substring(0, colonIndex);
String beanId = headerMessage.substring(colonIndex + 1);
RMIBeanFactory beanFactory;
try {
beanFactory = RMIBeanFactoryBuilder.getBeanFactory(interfaceName);
RMIBeanDefinition beanDefinition = beanFactory.getBean(beanId);
Object invokeObject = beanDefinition.getObject();
Method invokeMethod = beanDefinition.getMethod();
String argsString = dis.readUTF();
Object[] paraValues = getArgsValue(argsString, invokeMethod);
Object result = invokeMethod.invoke(invokeObject, paraValues);
if (invokeMethod.getReturnType().equals(void.class)) {
dos.writeUTF("null");
} else {
dos.writeUTF(ArgumentMaker.gson.toJson(result));
}
} catch (Exception e) {
e.printStackTrace();
dos.writeUTF("ERROR");
} finally {//如果发生异常,客户端就在那边傻傻的等,就直接关闭通信信道
close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private Object[] getArgsValue(String argsString, Method method) {
int cnt = method.getParameterCount();
if (cnt <= 0) {
return new Object[] {};
}
Object[] result = new Object[cnt];
ArgumentMaker argumentMaker = new ArgumentMaker(argsString);
for (int i = 0; i < cnt; i++) {
result[i] = argumentMaker.getArgumentByName("arg" + i, method.getParameters()[i].getParameterizedType());
}
return result;
}
private void close() {
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
}
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
}
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
(四)RMI客户端
a. RMIClient
对外提供的主要API,只需要用户使用这个类得到相应的代理,便可以使用这个代理远端执行已经扫描填充好的方法。methodInvoker方法是内部的,里面有和服务器的连接,以及传递和接受对端信息。
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.Socket;
import java.net.UnknownHostException;
import com.mec.util.ArgumentMaker;
public class RMIClient {
public static final String DEFAULT_RMI_SERVER_IP = "127.0.0.1";
public static final int DEFAULT_RMI_SERVER_PORT = 54189;
private String rmiServerIp;
private int rmiServerPort;
private Socket socket;
private DataOutputStream dos;
private DataInputStream dis;
public RMIClient() {
this.rmiServerIp = DEFAULT_RMI_SERVER_IP;
this.rmiServerPort = DEFAULT_RMI_SERVER_PORT;
}
public String getRmiServerIp() {
return rmiServerIp;
}
public void setRmiServerIp(String rmiServerIp) {
this.rmiServerIp = rmiServerIp;
}
public int getRmiServerPort() {
return rmiServerPort;
}
public void setRmiServerPort(int rmiServerPort) {
this.rmiServerPort = rmiServerPort;
}
/*实际上是通过得到代理,才可以点那些接口方法,才可以使远端执行*/
@SuppressWarnings("unchecked")
public <T> T getRMIProxy(Class<?> klass) {
if (!klass.isInterface()) {
throw new RMIClientProxyMustBeInterfaceExeception("必须是接口");
//从最开始的分析我们知道,客户端这边只有接口,如果提供的不是接口,说明错误了,我们给个自定义异常
}
ClassLoader classLoader = klass.getClassLoader();
Class<?>[] interfaces = new Class<?> [] { klass };
return (T) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = methodInvoker(method, args,klass);
return result;
}
});
}
/*客户端这边所谓执行方法,就是把相关方法信息和参数信息传递给服务器端,然后等着结果*/
@SuppressWarnings("unchecked")
private <T> T methodInvoker(Method method, Object[] args,Class<?> interfacer) throws UnknownHostException, IOException {
this.socket = new Socket(rmiServerIp, rmiServerPort);
this.dis = new DataInputStream(socket.getInputStream());
this.dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(interfacer.getName() + ":" + String.valueOf(method.toString().hashCode())); //发送的内容就要改为接口名和方法的hashcode了
dos.writeUTF(getArgsString(args));
String strResult = dis.readUTF();
if (strResult.equals("null")) {
close();
return null;
} else if(strResult.equals("ERROR")) {
System.out.println("方法执行异常!");
close();
return null;
}
Type returnType = method.getGenericReturnType();
close();
return (T) ArgumentMaker.gson.fromJson(strResult, returnType);//得到的结果根据返回值类型转化为相关对象
}
private String getArgsString(Object[] args) {
ArgumentMaker argumentMaker = new ArgumentMaker();
if(args == null) {
return argumentMaker.toString();
}
for (int i = 0; i < args.length; i++) { //在反射机制的情况下,我们已经丧失了方法的参数的形参的名字
argumentMaker.add("arg" + i, args[i]);
}
return argumentMaker.toString();
}
private void close() {
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
dis = null;
}
}
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
dos = null;
}
}
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
}
Ⅳ 后记和收获
(一)RMI的诡异及其犀利之处
诡异:表面上我客户端只是调用个方法,而事实上调用的不是本地方法,而是调用服务器的方法,真实方法的执行是在服务器端执行的。这使得开发客户端只需要知道接口,通过这个接口产生相关代理,该方法执行的具体过程在遥远的服务器端执行。
犀利之处:客户端操作完完全全是“黑箱”,只知道方法执行了,但不知道具体方法执行的过程,同时本地也没有该方法,只有接口该方法的名字一具空壳。服务器这边拥有真正的相关方法的操作,因此服务器这边可以尽最大可能的提高自己的性能,升级相关代码。对于接口的修改需要双方通知。
(二)收获
- 关于将功能分离出去,保持类的功能单一。例如RMIServer只是连接客户端功能,关于连接成功后的其他逻辑操作,分离出个新类(ServerWorker)去处理。
- 其落到最底层是对代理机制的使用和认识(代理机制)。
- 其中还使用到了自己编写十分强有力的工具Argumentmaker(ArgumentMaker)。
- 对于RMIBeanDefinition的成员是否设置为单例思考,在一个JVM下,如果有第二遍赋值,单例就不行了,单例需谨慎。
- 服务器侦听客户端那里可以使用线程池,因为来来回回大量客户端就是个(短平快)短连接请求,来回创建线程限制了服务器性能,因此可以使用线程池来提高效率。
- 有了RMI后,在网络通讯过程中,有多个客户机,客户机即就可以既做RMI客户端,也可以做RMI服务器,再用一个服务器来”管理“他们,该服务器就相当于一个相关服务(方法)管理员,所有客户端来整体合作完成同一个任务,以达到”云“的目的。