简单实现RPC/RMI框架

概述

RMI

RMI:(Remote Method Invocation) 远程方法调用。能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端Java虚拟机中的对象上的方法。

RPC

RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用某种服务。它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

区别

RPC不支持传输对象,是网络服务协议,与操作系统和语言无关。RMI只用于Java,支持传输对象

框架原理

核心技术

  • JDK动态代理
  • 反射机制
  • 序列化与反序列
  • 选择短连接

核心思想

客户端可以调用服务器端的方法,来实现相关功能。表面上客户端在调用一个方法,实质上该方法的实现都在服务器端完成,并通过网络传输将执行结果发送给客户端。

核心步骤

  • 注册

    • 服务器端需要知道客户端发送来的接口信息及对应的方法来找到相应的实现类执行该方法
    • 可以通过扫描带有指定接口注解的类、再通过遍历所有的方法找到相应需要执行的方法
    • 需要将接口中的方法和接口实现类中的方法相互映射,注册到一个容器map中
  • 客户端

    • 使用jdk动态代理传递接口,完成远程操作

    • 需要连接服务器(可以采用负载均衡的方式选择服务器),将方法名称以及参数序列化发送给服务器

    • 接收服务器传来的执行结果并反序列化

  • 服务器端

    • 可以开启一个线程池来处理客户端的方法调用
    • 与客户端建立连接,接收客户端发来的信息并反序列化
    • 从注册仓库中找到接收得到的方法id,并执行
    • 将执行结果序列化,发送给客户端

在这里插入图片描述
在这里插入图片描述

具体实现

注册

MethodDefinition

package stu.crayue.rpc.register;

import java.lang.reflect.Method;

/**
* @author Crayue
*保存方法信息
* @version 2020年7月16日 下午1:06:32
*/
public class MethodDefinition {
	private Class<?> klass;
	private Object object;
	private Method method;

	public MethodDefinition() {
	}
	
	public MethodDefinition(Class<?> klass, Object object, Method method) {
		this.klass = klass;
		this.object = object;
		this.method = method;
	}
	
	public Class<?> getKlass() {
		return klass;
	}

	public void setKlass(Class<?> klass) {
		this.klass = klass;
	}

	public Object getObject() {
		return object;
	}

	public void setObject(Object object) {
		this.object = object;
	}

	public Method getMethod() {
		return method;
	}

	public void setMethod(Method method) {
		this.method = method;
	}

}

MethodFactory

package stu.crayue.rpc.register;

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

import com.mec.util.PackageScanner;

import stu.crayue.rpc.test.AppFunctionAnnotation;

/**
 * @author Crayue
 * @version 2020年7月16日 下午1:06:42
 */
public class MethodFactory {
	private static final Map<String, MethodDefinition> methodPool;

	public MethodFactory() {
	}

	static {
		methodPool = new HashMap<>();
	}

	/**
	 * 实现包扫描,找到有注解的类
	 */

	public static void scanActionPackage(String pakageNmae) {

		new PackageScanner() {

			@Override
			public void dealClass(Class<?> klass) {
				if (klass.isPrimitive()// 八大基本类型
						|| klass.isInterface()// 接口
						|| klass.isAnnotation()// 注解
						|| klass.isEnum()// 枚举
						|| klass.isArray()// 数组
						|| !klass.isAnnotationPresent(AppFunctionAnnotation.class))// 不包含AppFunctionAnnotation注解的类
				{
					return;
				}

				AppFunctionAnnotation action = klass.getAnnotation(AppFunctionAnnotation.class);
				Class<?>[] interfaces = action.interfaces();
				for (Class<?> klazz : interfaces) {
					RegistryMethod(klazz, klass);
				}
			}
		}.scannerPackage(pakageNmae);
		;
	}

	/**
	 * 遍历接口中的方法,注册到map中
	 */
	private static void RegistryMethod(Class<?> Interface, Class<?> klass) {
		Method[] methods = Interface.getDeclaredMethods();// 接口方法
		Object object = null;
		for (Method method : methods) {
			try {
				String methodid = String.valueOf(method.toGenericString().hashCode());
				Class<?>[] paraTypes = method.getParameterTypes();
				// 通过接口方法名以及参数类型找到实现该接口的实现类里面对应的方法
				Method classMethod = klass.getDeclaredMethod(method.getName(), paraTypes);
				object = klass.newInstance();
				MethodDefinition md = new MethodDefinition(klass, object, classMethod);
				// 以接口完整方法名的hashCode作为键,RMIMethodDefinition作为值形成键值对保存在Map容器里面中,以供服务端反射执行调用
				methodPool.put(methodid, md);
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
			} catch (SecurityException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * @param methodid
	 *            接口方法的hashCode
	 * @return 根据id找到该接口的实现类对应的MethodDefinition
	 */
	public MethodDefinition GetByMethodId(String methodid) {
		return methodPool.get(methodid);
	}
}

AppFunctionAnnotation注解

package stu.crayue.rpc.test;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;


/**
* @author Crayue
* @version 2020年7月16日 下午1:59:21
* 对接口的实现类加上该注解,可以准确的找到实现类
*/
@Retention(RUNTIME)
@Target(TYPE)
public @interface AppFunctionAnnotation {
	Class<?>[] interfaces();
}

客户端

ClientProxy

package stu.crayue.rpc.client;

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

/**
* @author Crayue
* @version 2020年7月16日 下午1:05:55
*/
public class ClientProxy {
	private RpcClient client;
	public ClientProxy() {

	}
	
	public void setClient(RpcClient client) {
		this.client = client;
	}
	
	/**
	 * 通过jdk方式代理传递进来的接口,在invoke()中进行远程操作;
	 * @param interfaces 被代理的目标接口
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T> T jdkProxy(Class<?> interfaces) {
		ClassLoader classLoader = interfaces.getClassLoader();
		return (T) Proxy.newProxyInstance(classLoader, new Class<?>[]{interfaces}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				return client.remoteProcedureCall(method, args);
			}
		});
	}
}

RpcClient

package stu.crayue.rpc.client;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;

import com.mec.util.ArgumentMaker;

/**
* @author Crayue
* @version 2020年7月16日 下午1:05:44
*/
public class RpcClient {
	public static final String DEFAULT_IP = "127.0.0.1";
	public static final int DEFAULT_PORT = 54188;
	
	private Socket client;
	private DataInputStream dis;
	private DataOutputStream dos;
	private int port;
	private String ip;
	
	public RpcClient() {
		this.port=DEFAULT_PORT;
		this.ip=DEFAULT_IP;
	}

	public int getPort() {
		return port;
	}

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

	public String getIp() {
		return ip;
	}

	public void setIp(String ip) {
		this.ip = ip;
	}
	
	/**
	 * 关闭通信
	 */
	private void close() {
		try {
			if (!client.isClosed() && client != null) {
				client.close();		
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			client = null;
		}
		try {
			if (dis != null) {
				dis.close();		
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			dis = null;
		}
		try {
			if (dos != null) {
				dos.close();	
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			dos = null;
		}
	}
	
	/**
	 * 连接服务器
	 */
	void connectToServer() {
		try {
			client=new Socket(ip, port);
			dis=new DataInputStream(client.getInputStream());
			dos=new DataOutputStream(client.getOutputStream());
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 
	 * 1、连接服务端;
	 * 2、得到调用方法的哈希值并传递给服务端;
	 * 3、将参数数组转化为指定的字符串传递给服务端;
	 * 4、接收来自服务端方法的执行结果;
	 * 5、将接收的结果转化为对象并返回;
	 * 6、关闭通信信道
	 * @param method 调用的方法
	 * @param args 参数对象数组
	 * @return
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public <T> T remoteProcedureCall(Method method, Object[] args) throws UnknownHostException, IOException {
		connectToServer();//连接服务器
		
		ArgumentMaker maker = new ArgumentMaker();
		T result =null;
		try {
			dos.writeUTF(String.valueOf(method.toGenericString().hashCode()));
			if (args == null) {
				dos.writeUTF("");
			} else {
				int index = 0;
				for (Object object : args) {
					maker.addArg("arg" + index++, object);
				}
				dos.writeUTF(maker.toString());
			}
			
			String str = dis.readUTF();
			
			 result= ArgumentMaker.gson.fromJson(str, method.getGenericReturnType());
			close();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}
}

服务器端

RpcServer

package stu.crayue.rpc.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author Crayue
 * @version 2020年7月16日 下午1:05:12
 * 使用线程池来处理RPC客户端的方法调用
 */
public class RpcServer implements Runnable {
	private ServerSocket server;
	private int port;
	private volatile boolean goon;
	private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 100, 
			5000, TimeUnit.MILLISECONDS, 
			new LinkedBlockingDeque<>());

	public RpcServer() {
	}

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

	public void startup() {
		if (goon) {
			return;
		}

		try {
			server = new ServerSocket(port);
			this.goon = true;
			new Thread(this, "RMI Server").start();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	public void shutdown() {
		if (goon == false) {
			return;
		}

		close();
	}

	private void close() {
		this.goon = false;
		try {
			if (server != null && !server.isClosed()) {
				server.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			server = null;
		}
	}

	@Override
	public void run() {
		while (goon) {
			try {
				Socket socket = server.accept();
				ServerService executor = new ServerService();
				executor.setClient(socket);
				threadPool.execute(executor);
			} catch (IOException e) {
				if (goon) {
					goon = false;
					System.out.println("服务器异常宕机");
				}
				close();
			}
		}
	}
}

ServerService

package stu.crayue.rpc.server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.Socket;

import com.mec.util.ArgumentMaker;

import stu.crayue.rpc.register.MethodDefinition;
import stu.crayue.rpc.register.MethodFactory;

/**
* @author Crayue
* @version 2020年7月16日 下午1:05:24
*/
public class ServerService implements Runnable{
	private DataInputStream dis;
	private DataOutputStream dos;
	
	public ServerService() {
	}

	public void setClient(Socket client) {
		try {
			dis = new DataInputStream(client.getInputStream());
			dos = new DataOutputStream(client.getOutputStream());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 关闭通信信道
	 */
	private void close() {
		try {
			if (dis != null) {
				dis.close();		
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			dis = null;
		}
		try {
			if (dos != null) {
				dos.close();	
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			dos = null;
		}
	}

	/**
	 * 1、接收客户端发来的两次信息
	 * 2、通过接收到的methodid,在factory中找到对应的方法并执行
	 * 3、将执行的结果返回给客户端
	 */
	@Override
	public void run() {
		try {
			String method = dis.readUTF();
			String argsStr = dis.readUTF();
			
			MethodFactory factory=new MethodFactory();
			MethodDefinition methodDefinition = factory.GetByMethodId(method);
			
			
			if (methodDefinition == null) {
				dos.writeUTF("null");
			} else {
				
				ArgumentMaker maker = new ArgumentMaker(argsStr);
				Object result = invokeMethod(methodDefinition, maker);
				String resultStr = ArgumentMaker.gson.toJson(result);
				dos.writeUTF(resultStr);
			}
			close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 该方法用来执行客户端传来的方法
	 * 1、通过methodDefinition得到对应的方法和对象;<br>
	 * 2、通过gson将传送过来的字符串转换为指定的对象;
	 * 
	 * @param methodDefinition
	 * @param maker
	 * @return
	 */
	private Object invokeMethod(MethodDefinition methodDefinition, ArgumentMaker maker) {
		Object object = methodDefinition.getObject();
		Method method = methodDefinition.getMethod();
		Parameter[] parameters = method.getParameters();
		
		int size = parameters.length;
		Object[] args = new Object[size];
		
		for (int index = 0; index < size; index++) {
			args[index] = maker.getValue(
						"arg" + index, 
						parameters[index].getParameterizedType());
		}
		try {
			
			return method.invoke(object, args);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}		
}

辅助类

IAppinterface

public interface IAppinterface {
	String getUser(String id, String password);
}

Appfunction

@AppFunctionAnnotation(interfaces= {IAppinterface.class})
public class Appfunction implements IAppinterface {
		
	@Override
	public String getUser(String id, String password) {
		UserInfo user= new UserInfo(id,"bigbang",password);
		return user.getNick();
	}
}

UserInfo

package stu.crayue.rpc.test;
/**
* @author Crayue
* @version 2020年7月16日 下午1:17:21
*/
public class UserInfo {
	private String id;
	private String nick;
	private String password;
	
	public UserInfo() {
	}
	

	public UserInfo(String id, String password) {
		this.id = id;
		this.password = password;
	}


	public UserInfo(String id, String nick, String password) {
		this.id = id;
		this.nick = nick;
		this.password = password;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getNick() {
		return nick;
	}

	public void setNick(String nick) {
		this.nick = nick;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	@Override
	public String toString() {
		return "UserInfo [id=" + id + ", nick=" + nick + ", password=" + password + "]";
	}
	
}


ServerTest

public class ServerTest {
	public static void main(String[] args) {
		MethodFactory.scanActionPackage("stu.crayue.rpc");
		RpcServer server=new RpcServer();
		server.setPort(54188);
		server.startup();
	}
}

ClientTest

public class ClientTest {

	public static void main(String[] args) {
		ClientProxy proxy=new ClientProxy();
		RpcClient client =new RpcClient();
		proxy.setClient(client);
		
		IAppinterface app=proxy.jdkProxy(IAppinterface.class);
		String user = app.getUser("20060819", "forever");
		System.out.println(user);
	}
}

总结

这只是一个简单的实现,还有很多功能没有完善。比如为了防止用户多次调用框架造成不必要的重复连接,可以在可以在调用远程方法后开启一个模态框,阻止用户操作,并在执行结果返回后关闭模态框。也可以让客户端采用负载均衡策略选择连接哪个服务器,在哪里选择呢?就引入到了我们之后要实现的内容:带有注册中心的RPC框架,实现服务暴露及服务发现。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值