(一)模拟实现简单 RMI 框架(详解)

本文介绍了RMI(RemoteMethodInvocation)的基本思想和实现,探讨了RMI框架的技术难点,如动态代理、参数传递和恢复,并提出了使用JSON字符串替代二进制传输以提高效率。文章详细阐述了RMI服务器和客户端的功能,以及如何通过代理模式实现远程方法调用。此外,还涵盖了RMI框架的需求分析,包括客户端的连接、方法执行和结果接收,以及服务器端的请求处理和结果回传。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

《RMI》框架的实现是本人的一个编程训练项目,为了提升本人的编程能力、JAVA 编程思想,基于框架的角度出发,实现对远程过程调用(RPC)协议,对《RMI》的个人理解和底层实现方法进行阐述。

此《RMI》框架“不重复造方轮子”,只是自我的编程训练的项目。

RMI

RMI 概述

RMI(Remote Method Invocation)是一种 Java 编程语言中的远程过程调用(RPC)协议,用于在不同的Java虚拟机(JVM)之间进行通信和交互。它允许远程计算机上的Java对象像本地对象一样进行访问和操作,从而使分布式应用程序的开发变得更加容易和方便。

一般市面上的 RMI 框架是建立在 Java 远程调用 JRMP(Java Remote Method Protocol)之上的。

这里博主对 RMI 进行过思考之后,感觉将所要执行的方法、参数等信息用二进制字节的传递方式效率太过低下,同时还得考虑相关类的序列号,而且传递的大量二进制信息降低了内部效率,所以本人想出了将对象、方法以及参数转换成 JSON 字符串的形式进行保存和传递。

RMI 框架技术难点

  • 动态代理模式的实现
  • 实现参数进行传递和恢复的工具(Gson)
  • XML文件的解析工具
  • 将所要执行方法的对象和方法进行封装
  • RMI 工厂模式(方法池)的建立

RMI 基本思想

RMI 基本思想是远程方法调用,即客户端调用某个方法,其本质是将这个方法的调用请求,发送给服务器,由服务器代为执行,且,
服务器将执行结果回送客户端。

  • 对于客户端而言,RMI 只要求客户端针对方法本身,产生一种错觉:方法是在本地被调用的;
  • 对于服务器而言,RMI 相当于要处理一个来自客户端的“请求”;这个请求针对某个方法。

RMI 框架思考

RMI 相较于其他服务器客户短模式(B/S,NIO)来看,它是属于一种短连接(由客户端向服务器端发起“请求”,服务器连接客户端,并解析客户端传来的请求内容,并执行相关操作以得到“响应”信息,并将响应回送给该客户端,然后就立刻切断网络连接,这是一个短而快的”请求/响应“过程。)

其本质是也就是远程方法调用:Java 应用程序调用在远程 Java 虚拟机上运行的对象上的方法,就像调用本地对象的方法一样。

RMI 需求分析

客户端功能:

  • 连接 RMI 服务器;
  • 向服务器发送要执行的方法名称、实参等;
  • 等待服务器返回这个方法在服务器端执行的结果

RMI服务器端功能:

  • 建立 RMI 服务器;
  • 侦听客户端连接请求;
  • 连接 RMI 客户端;
  • 接受客户端发送过来的要执行的方法名称、实参等信息;
  • 找到这个需要代理执行方法,并反射机制执行该方法,并将方法执行的结果回传给客户端,断开与客户端的连接。

RMI CS模式相关思考

由于 RMI 框架建立涉及CS模式,所以需要要建立,并且进行连接,实现传输信息等等。

对于客户端而言,上述功能要能实现,就必须依赖“代理”技术。即,在客户端的代理执行相关方法时,实现:

  • 1、连接服务器;
  • 2、传递方法及实参数据;
  • 3、等待并接收服务器回传的计算结果。

RMI 代理模式相关思考

首先 JDKProxy 和 CglibProxy 两者的差别是在是否提供接口

  • CglibProxy 不提供接口,没有接口则不能让双方知道各自同步的地方。
  • JDKProxy 提供了接口,客户端和服务器都按接口来走,规范了双方的类。

因此我们使用 JDKProxy。

在 RMI 实际的工作过程中,客户端只有接口,而不存在也不需要接口的实现类,客户端对 RMI 方法的执行,实质上都是传递方法信息和方法参数,再从服务器端接受执行的结果。因此,在客户端获取 proxy 的时候,能提供的仅仅是接口,没有可能提供该接口的实现类,因为它根本没有也不需要。

最终决定使用 JDKProxy(即,基于接口)。

基于 JDKProxy 代理模式下的客户端和服务器:首先客户端和服务器端相互建立连接,然后客户端将执行方法所需要的参数和方法传递给服务器端,服务器接收了相关数据后,通过反射机制执行相关方法,再将执行的结果传递给客户端。

RMI 框架实现

实现思路分析

RMI 框架的核心思想是客户端调用某个方法,其本质是将这个方法的调用请求,发送给服务器,由服务器代为执行实现相关功能,并且服务器将执行结果回送客户端。

一端“执行”一个方法,而这个方法的实际执行是在对端进行的。要实现这种模式,前提是服务器与客户端拥有相同的类,同时也会拥有相同的方法,想要实现这种客户端与服务器端达到同步的模式,我们就需要面向接口编程,接口作为服务器端与客户端共同遵守的约定,并且对于相关操作好控制,规范了双方的方法调用。

诚然客户端执行的方法,其实是通过调用这个类的代理对象的方法,在这个方法中实际上是将执行这个方法的参数和类名称、方法名称,通过网络通讯传输给服务器端;服务器端根据接收到的方法名称、类名称和参数,实际执行那个方法,再将方法执行结果回传给对端。

RmiProxy 的具体实现

这里客户端获取 JDK 代理,用代理执行所要执行的方法,并且将所执行的结果返回。

package com.mec.rmi.core;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class RmiProxy {
	private RmiClient rmiClient;

	public RmiProxy() {
		this.rmiClient = new RmiClient();
	}
	
	public RmiClient getClient() {
		return this.rmiClient;
	}
	
	@SuppressWarnings("unchecked")
	public <T> T getProxy(Class<?> interfaze) {
		ClassLoader classLoader = interfaze.getClassLoader();
		Class<?>[] interfaces = new Class<?>[] { interfaze };
		InvocationHandler h = new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				rmiClient.doRemoteInvoker(method, args);
				return rmiClient.getResult();
			}
		};
		
		return (T) Proxy.newProxyInstance(classLoader, interfaces, h);
	}
}

RMI 服务器基础工厂准备

  • MethodBeanDefinition

将所要执行的方法所需要的:要执行方法的对象 object 和方法 method 封装成一个类 MethodBeanDefinition

public class MethodBeanDefinition {
	private Object object;
	private Method method;
	
	MethodBeanDefinition() {
	}
    …………
}

  • RmiImplClassFactory

建立生成 bean 对象的工厂模式。定义单例的存放 bean 对象的方法池,根据扫描到的XML文件,获得所要执行的方法名称、实参等信息,放入到 map 中;

package com.mec.rmi.core;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

import org.w3c.dom.Element;

import com.mec.util.XMLParser;

public class RmiImplClassFactory {
	private static final Map<Integer, MethodBeanDefinition> methodPool;
	static {
		methodPool = new HashMap<Integer, MethodBeanDefinition>();
	}

	public RmiImplClassFactory() {
	}
	
	public static void initRmi(String interfaceConfigXml) throws Exception {
		new XMLParser() {
			@Override
			public void dealElement(Element element, int index) throws Exception {
				String interfaceName = element.getAttribute("name");
				String className = element.getAttribute("class");
				
				Class<?> interfaze = Class.forName(interfaceName);
				Class<?> klass = Class.forName(className);
				
				Object object = klass.newInstance();
				
				Method[] methodsInInterface = interfaze.getDeclaredMethods();
				for (Method methodInInterface : methodsInInterface) {
					int methodKey = methodInInterface.toString().hashCode();
					
					String methodName = methodInInterface.getName();
					Parameter[] parameters = methodInInterface.getParameters();
					Class<?>[] parameterTypes = new Class<?>[parameters.length];
					int i = 0;
					for (Parameter parameter : parameters) {
						Class<?> parameterType = parameter.getType();
						parameterTypes[i++] = parameterType;
					}
					
					Method method = klass.getDeclaredMethod(methodName, parameterTypes);
					MethodBeanDefinition bean = new MethodBeanDefinition();
					bean.setObject(object);
					bean.setMethod(method);
					
					methodPool.put(methodKey, bean);
				}
			}
		}.parse(XMLParser.getDocument(interfaceConfigXml), "interface");
	}
	
	static MethodBeanDefinition getMethodBean(int methodKey) {
		return methodPool.get(methodKey);
	}
}

这里涉及到对 XML 文件的解析,具体的工具实现附在这里:HB 个人博客:https://blog.csdn.net/SwaggerHB/article/details/129969784

RMI 服务器端的具体实现

对外提供的主要API,包括对服务器的设置,以及侦听客户端连接,启动 RequestDealer 去处理客户端连接请求。

package com.mec.rmi.core;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import com.mec.util.IListener;
import com.mec.util.ISpeaker;

public class RmiServer implements Runnable, ISpeaker {
	private int port;
	private ServerSocket server;
	private volatile boolean goon;
	
	private Object lock;
	private List<IListener> listenerList;
	
	public RmiServer() {
		this.listenerList = new ArrayList<IListener>();
		this.lock = new Object();
		this.port = IAddressManager.DEFAULT_PORT;
		this.goon = false;
	}
	
	public boolean isStartup() {
		return this.goon;
	}
	
	public void startup() throws IOException {
		if (this.goon) {
			publish("RMI服务器已启动!");
			return;
		}
		
		publish("开始启动RMI服务器……");
		this.server = new ServerSocket(this.port);
		publish("RMI服务器启动成功!");
		synchronized (this.lock) {
			this.goon = true;
			new Thread(this).start();
			try {
				this.lock.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void shutdown() {
		if (!this.goon) {
			publish("RMI服务器尚未启动!");
			return;
		}
		
		closeServer();
		publish("RMI服务器已宕机!");
	}
	
	private void closeServer() {
		this.goon = false;
		
		try {
			if (this.server != null && !this.server.isClosed()) {
				this.server.close();
			}
		} catch (IOException e) {
		} finally {
			this.server = null;
		}
	}

	@Override
	public void run() {
		synchronized (this.lock) {
			this.lock.notify();
		}
		
		publish("开始侦听客户端连接请求……");
		
		while (this.goon) {
			try {
				Socket client = server.accept();
				new RequestDealer(client);
			} catch (IOException e) {
				this.goon = false;
			}
		}
		
		closeServer();
	}	

	public void setPort(int port) {
		this.port = port;
	}

	@Override
	public void addListener(IListener listener) {
		if (!this.listenerList.contains(listener)) {
			this.listenerList.add(listener);
		}
	}

	@Override
	public void removeListener(IListener listener) {
		if (this.listenerList.contains(listener)) {
			this.listenerList.remove(listener);
		}
	}

	@Override
	public void publish(String message) {
		for (IListener listener : this.listenerList) {
			listener.acceptMessage(message);
		}
	}
}

RMI 客户端的具体实现

RMI 客户端对外提供的主要 API,只需要用户使用这个类得到相应的代理,便可以使用这个代理远端执行已经扫描填充好的方法。doRemoteInvoker 方法是内部的,里面有和服务器的连接,以及传递和接受对端信息。

package com.mec.rmi.core;

import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.Socket;

import javax.swing.JFrame;

import com.mec.util.ArgumentMaker;
import com.mec.util.io.NodeAddress;

public class RmiClient {
	private JFrame parentView;
	private String tooltip;
	private Object result;
	private IAddressManager nodeAddressManager;
	
	public RmiClient() {
		this.nodeAddressManager = new ServerAddress();
	}

	public void setServerAddress(String ip, int port) {
		this.nodeAddressManager.setAddress(new NodeAddress(ip, port));
	}
	
	public void setModalDialog(JFrame parentView, String tooltip) {
		this.parentView = parentView;
		this.tooltip = tooltip;
	}
	
	public void setAddressManager(IAddressManager nodeAddressManager) {
		this.nodeAddressManager = nodeAddressManager;
	}

	Object getResult() {
		return this.result;
	}
	
	void doRemoteInvoker(Method method, Object[] args) throws Throwable {
		if (this.parentView != null) {
			ModalDialog modalDialog = new ModalDialog(this.parentView, this.tooltip);
			modalDialog.addFocusListener(new FocusAdapter() {
				@Override
				public void focusGained(FocusEvent e) {
					try {
						result = doMethod(method, args);
						modalDialog.closeDialog();
					} catch (Throwable e1) {
					}
				}
			});
			modalDialog.showDialog();
		} else {
			this.result = doMethod(method, args);
		}
	}
	
	private Object doMethod(Method method, Object[] args) throws Throwable {
		NodeAddress serverAddress = this.nodeAddressManager.getAddress();
		String ip = serverAddress.getIp();
		int port = serverAddress.getPort();
		
		Socket socket = new Socket(ip, port);
		DataInputStream dis = new DataInputStream(socket.getInputStream());
		DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
		
		dos.writeUTF(String.valueOf(method.toString().hashCode()));
		dos.writeUTF(argsToString(args));
		
		try {
			if (method.getReturnType().equals(void.class)) {
				return null;
			}
			
			String resultString = dis.readUTF();
			RmiResult rmiResult = ArgumentMaker.gson.fromJson(resultString, RmiResult.class);
			if (rmiResult.isOk()) {
				String result = rmiResult.getResString();
				Type returnType = method.getGenericReturnType();
				Object res = ArgumentMaker.gson.fromJson(result, returnType);
				
				return res;
			} else {
				throw new Exception(rmiResult.getResString());
			}
		} catch (Exception e) {
			throw e;
		} finally {
			dis.close();
			dos.close();
			socket.close();
		}
	}
	
	private String argsToString(Object[] args) {
		int argCount = args.length;
		if (argCount <= 0) {
			return "";
		}
		
		ArgumentMaker maker = new ArgumentMaker();
		
		for (int index = 0; index < argCount; index++) {
			maker.add("arg" + index, args[index]);
		}
		
		return maker.toString();
	}
}

这里涉及到参数进行传递和恢复的工具 ArugumentMaker 的实现,具体的工具实现过程以及代码附在这里:HB 个人博客:基于 Gson 的 ArgumentMaker 工具实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HB0o0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值