RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用某种服务。它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。 应用:Dubbo。
RMI(Remote Method Invocation)远程方法调用。能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端java 虚拟机中的对象上的方法。应用:EJB。
它们之间的区别在于,RPC不支持传输对象,是网络服务协议,与操作系统和语言无关。RMI只用于Java,支持传输对象。 RMI是面向对象的,Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型。而RPC的结果统一由外部数据表示(External Data Representation,XDR)语言表示,这种语言抽象了字节序类和数据类型结构之间的差异。只有由XDR定义的数据类型才能被传递,可以说RMI是面向对象方式的Java RPC。
更多详细区别可以参见RMI与RPC的区别
一种用于实现远程过程调用(RPC)(Remote procedure call)的Java API,这是一种基于网络的技术。本地机执行一个方法,而这个方法实质上是在服务器端的。表面上是客户端在调用一个方法,但,本质上是服务器在执行这个方法,并通过网络传输返回方法执行结果。
从简介中我们可以确定几个基本问题:
1、建立服务器;
2、客户端连接服务器;
3、在客户端执行一个方法,而该方法应该在服务器上执行;
4、为了保证其工具性质,可以通过接口完成方法的确定。
进一步分析:
客户端在执行一个方法的时候,需要在执行过程中,连接服务器,并在连接成功后,将这个方法的方法名以及参数列表发送给给服务器;然后等待服务器返回调用方法的返回值。
服务器与客户端连接建立好后,接收客户端传送过来的方法名以及参数列表,并反射调用该方法;最后,将这个方法的执行结果返回给客户端。
本篇通过代理机制来实现RMI框架;只提供给客户端调用方法的接口,而不保存接口实现类,通过代理机制得到接口的实现类对象,在方法拦截中连接远程服务器、发送方法及参数列表、接收执行结果(接收完后关闭与服务器的连接)。在服务器端保存接口和接口实现类,并通过注解将实现类对象和方法注册到容器里,当客户端调用方法时,从容器中取出对应方法反射执行,并且将结果发送回客户端。
要想实现实现RMI,服务端就必须得通过客户端发送的调用的接口里面定义的方法找到对应服务端该接口实现类的方法执行。而服务端通过客户端发送来的方法的方法名、参数个数、参数类型等来找到保存在服务端的接口实现类的方法,这个过程是繁琐且耗时的;所以我先通过包扫描将接口中的方法和接口实现类中的方法相互映射,并注册到一个容器里面,这样客户端只用将接口方法发送过来,就可以该方法直接找到对应实现的方法,为远程调用节省时间。
为了准确地描述接口中的方法和接口实现类中的方法的映射关系,注册的方式可以是注解或通过文件配置如(XML文件等),这里我使用注解的方式。
如下是RmiAction注解类。
package com.rmi.annotation;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 1.该类是一个注解类,里面有一个数组类型的成员RmiInterfaces
* 2.使用方法是,对实现了接口的实现类加上该注解,保证准确地找到实现类
* 3.为后续的包扫描以及注册接口方法与实现类方法之间的对应关系提供了前提条件。
* @author dingxiang
*
*/
@Retention(RUNTIME)
@Target(TYPE)
public @interface RmiAction {
Class<?>[] RmiInterfaces();
}
服务端开启前要先通过包扫描找到该注解标记的类,注解中是接口类型数组,将数组中每个接口含有的方法依次进行扫描并找到实现类对应的方法,形成一个MethodDefinition,再以接口完整方法名的hashCode作为键,RMIMethodDefinition作为值形成键值对保存在Map容器里面中,以供服务端反射执行调用。
如下是RMIMethodDefinitionFactory类,专门处理接口方法与实现类对应方法的映射关系的注册。
package com.rmi.core;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import com.mec.util.PackageScanner;
import com.rmi.annotation.RmiAction;
/**rmi方法扫描注册类
* 1.该类失RMI框架核心类,扫描传入的路径,找到带有RmiAction注解的类
* 2.对类中的注解理的接口数组进行遍历
* 3.以接口方法的hashCode为键,实现类中对应的RMIMethodDefinition为值
* 4.提供注册这种键值对应关系的方法
* 5.提供根据接口方法的hascode
*
* @author dingxiang
*
*/
public class RMIMethodDefinitionFactory {
private static final Map<String, RMIMethodDefinition> methodpool;
static {
methodpool=new HashMap<String, RMIMethodDefinition>();
}
public RMIMethodDefinitionFactory() {
}
public static Map<String, RMIMethodDefinition> getMethodPool() {
return methodpool;
}
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(RmiAction.class))//不包含RmiAction注解的类
{
return;
}
RmiAction action=klass.getAnnotation(RmiAction.class);
Class<?>[] interfaces=action.RmiInterfaces();
for(Class<?> klazz:interfaces) {
RegistryMethod(klazz,klass);
}
}
}.scannerPackage(pakageNmae);;
}
/**
* 注册接口方法与实现类中的方法的映射关系
* @param Interface 接口类
* @param klass 实现类
* */
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();
RMIMethodDefinition md=new RMIMethodDefinition(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 实现该接口的实现类对应的RMIMethodDefinition
* */
public RMIMethodDefinition GetByMethodId(String methodid) {
return methodpool.get(methodid);
}
}
如下是RMIMethodDefinition类,用来描述实现类中实现注解里面接口数组里面的接口的方法。
package com.rmi.core;
import java.lang.reflect.Method;
/**
* 保存实现类实现的接口方法信息的类
* 将扫描到的实现类方法和对应的实现类对象保存;
*
* @author dingxiang
*
*/
public class RMIMethodDefinition {
private Object object;
private Method method;
public RMIMethodDefinition() {
}
public RMIMethodDefinition(Object object, Method method) {
this.object = object;
this.method = method;
}
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;
}
}
服务端的端口可以通过配置文件(如properties)文件来配置,可以参见properties文件解析。
package com.rmi.core;
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;
import com.mec.util.PropertiesParser;
/**
* 1.该类是服务端类
* 2.提供了开启和关闭的方法
* 3.可以设置服务端端口(rmiPort)
* 4.对于侦听到的每一个客户端的方法调用请求
* 这里采用了线程池开启一个RmiAction来处理
*
* @author dingxiang
*
*/
public class RMIServer implements Runnable{
private static final int DEFAULT_PORT=54199;
private int rmiPort;
private ServerSocket server;
private ThreadPoolExecutor threadpool;
private volatile boolean goon;
public RMIServer() {
this.rmiPort=DEFAULT_PORT;
}
public void loadRMIServerConfig(String RMIServerConfigPath) {
PropertiesParser.loadProperties(RMIServerConfigPath);
String configRMIServerPortStr=PropertiesParser.value("configRMIServerPort");
if (configRMIServerPortStr!=null&&configRMIServerPortStr.length()>0) {
int rmiServerPort=Integer.valueOf(configRMIServerPortStr);
if (rmiServerPort>0&&rmiServerPort<65536) {
this.rmiPort=rmiServerPort;
}
}
}
public ThreadPoolExecutor getThreadpool() {
return threadpool;
}
public RMIServer(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void startRmiServer() {
if (goon==true) {
return;
}
threadpool = new ThreadPoolExecutor(10, 50, 5000, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>());
try {
server=new ServerSocket(rmiPort);
} catch (IOException e) {
e.printStackTrace();
}
goon=true;
new Thread(this, "RMIServer").start();
}
@Override
public void run() {
while (goon) {
try {
Socket socket=server.accept();
threadpool.execute(new RMIActioner(socket));
} catch (IOException e) {
}
}
}
public void stopRmiServer() {
if (goon==false) {
return;
}
goon=false;
if (threadpool.isShutdown()) {
threadpool.shutdownNow();
}else {
threadpool.shutdown();
}
if (server!=null&&!server.isClosed()) {
try {
server.close();
} catch (IOException e) {
}finally {
server=null;
}
}
}
}
package com.rmi.core;
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.Type;
import java.net.Socket;
import com.mec.util.ArgumentMaker;
/**1.该类专门处理客户端的请求
* @author dingxiang
*
*/
public class RMIActioner implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
//与客户端建立通信信道,传输方法名及参数列表和执行结果
public RMIActioner(Socket socket) {
this.socket = socket;
try {
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
/**客户端发两次,服务端接收两次
*服务端第一次接收的是调用的方法id,第二次接收的是参数列表
*通过方法id,找到对应的RMIMethodDefinition,并执行
*将执行结果发送给客户端
*/
@Override
public void run() {
try {
String methodid=dis.readUTF();
String argString=dis.readUTF();
RMIMethodDefinitionFactory methodfactory=new RMIMethodDefinitionFactory();
RMIMethodDefinition methoddefinition=methodfactory.GetByMethodId(methodid);
Object res=null;
if (methoddefinition==null) {
res=new RMIErrorMethod(methodid);
}
else {
Method method=methoddefinition.getMethod();
Object excuter=methoddefinition.getObject();
Object[] paras=getParas(method, argString);
res=method.invoke(excuter, paras);
}
dos.writeUTF(ArgumentMaker.gson.toJson(res));//将执行结果返回给请求的客户端
Close();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* 1、通过method得到方法的参数类型列表
* 2、通过gson将传送过来的字符串转换为指定的对象;
*
* @param method
* @param argString
* @return Object[]
*/
public Object[] getParas(Method method,String argString) {
ArgumentMaker maker=new ArgumentMaker(argString);
Object[] paras=null;
Type[] paratypes=method.getParameterTypes();
int paraCount=method.getParameterCount();
if (paraCount<=0) {
paras=new Object[] {};
}
paras=new Object[paraCount];
for (int i = 0; i < paraCount; i++) {
paras[i]=maker.getValue("arg"+i, paratypes[i]);
}
return paras;
}
/*
* 关闭通信信道
* */
public 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;
}
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
最开始是通过json来传输参数列表的,我把参数加入到一个Map里面,这样虽然可以保存参数名与参数对象的映射关系,但会打乱参数的顺序,而反射机制得不到准确参数顺序,会导致反射执行方法失败;所以需要通过协议来规范各个参数与之对应对象的关系。
一开始,客户端将参数对象转化为String类型,服务端再把String类型的参数按照其具体类型转化为具体类型的值。
具体的应用详见RmiActioner的getParas(Method method,String argString)与RMIClient的invokeMethod()方法。
package com.mec.util;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
public class ArgumentMaker {
public static final Gson gson = new GsonBuilder().create();
private static final Type type = new TypeToken<Map<String, String>>() {}.getType();
private Map<String, String> paraMap;
public ArgumentMaker() {
paraMap = new HashMap<String, String>();
}
//构造方法可以将Json化的参数Map转化为Map
public ArgumentMaker(String str) {
paraMap = gson.fromJson(str, type);
}
//按照参数顺序将参数加入到参数Map里面
public ArgumentMaker addArg(String name, Object value) {
paraMap.put(name, gson.toJson(value));
return this;
}
@SuppressWarnings("unchecked")
public <T> T getValue(String name, Type type) {
String valueString = paraMap.get(name);
if (valueString == null) {
return null;
}
return (T) gson.fromJson(valueString, type);
}
//根据参数顺序及其类型,取出String类型的参数值,并将其按照其实际类型还原成该类型的具体值
@SuppressWarnings("unchecked")
public <T> T getValue(String name, Class<?> type) {
String valueString = paraMap.get(name);
if (valueString == null) {
return null;
}
return (T) gson.fromJson(valueString, type);
}
//将参数Map转化为json
@Override
public String toString() {
return gson.toJson(paraMap);
}
}
RMI框架提供给调用方调用的是接口,必须得为接口构造一个假的实现。显然,要使用动态代理。这样,调用方的调用就被动态代理接收到了,同时,为了避免重复多次RMI调用(如用户多次点击等),这里引入了模态框。
package com.rmi.core;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.swing.JFrame;
import com.rmi.annotation.MecDialog;
import com.rmi.dialog.core.MecModelDialog;
/**
* 代理类
* 1、通过接口得到代理对象来调用方法;
* 2、在方法中进行拦截,拦截时连接服务器进行远程方法调用;
* 3、支持模态框的开启;
* @author dingxiang
*
*/
public class RMIClientProxy {
private RMIClient rmiClient;
public RMIClientProxy(RMIClient rmiClient) {
this.rmiClient = rmiClient;
}
public RMIClientProxy() {
}
public void setRmiClient(RMIClient rmiClient) {
this.rmiClient = rmiClient;
}
public RMIClient getRmiClient() {
return rmiClient;
}
/**根据调用的方法检查返回结果
* @param method
* @param result
*/
public void checkMethodResult(Method method,Object result) {
if (result instanceof RMIErrorMethod) {
throw new RuntimeException("没有找到"+method.getName()+"方法");
}
}
/**通过JDK代理的方式来调用RMI服务器的方法
* @param interfaces 被代理的目标接口
* @return
*/
public Object getProxy(Class<?> interfaces) {
return Proxy.newProxyInstance(interfaces.getClassLoader(), new Class<?>[] {interfaces}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
rmiClient.connectToServer();
rmiClient.setArgs(args);
rmiClient.setMethod(method);
Object result=rmiClient.invokeMethod();
checkMethodResult(method,result);
return result;
}
});
}
/**
* 模态框式RMI连接(防止用户多次点击)
* 1、对于通过JDK代理的方式传递进来的接口,在invoke()中检查调用的方法是否有模态框注解,有则开启模态框;
* 2.在得到RMI服务器返回的结果后,自动关闭模态框
* @param interfaces 被代理的目标接口
* @param parent 模态框依赖显示的父窗口
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<?> interfaces, JFrame parent) {
return (T) Proxy.newProxyInstance(
interfaces.getClassLoader(),
new Class<?>[] {interfaces},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
rmiClient.connectToServer();
rmiClient.setMethod(method);
rmiClient.setArgs(args);
if (method.isAnnotationPresent(MecDialog.class)) {
MecDialog modelDialog = method.getAnnotation(MecDialog.class);
String caption = modelDialog.caption();
MecModelDialog dialog = new MecModelDialog(parent, true);
dialog.setRmiClient(rmiClient);
dialog.setCaption(caption);
dialog.showDialog();
result = dialog.getResult();
} else {
result = rmiClient.invokeMethod();
}
checkMethodResult(method,result);
return result;
}
});
}
}
package com.rmi.annotation;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(METHOD)
public @interface ModelDialog {
String caption();
}
用法示例(要调用的接口方法前加上注解,caption表示要显示的内容)。
package com.rmi.action;
import java.util.List;
import com.rmi.annotation.ModelDialog;
import com.rmi.model.NetNode;
import com.rmi.model.Student;
public interface IRmiAction {
@ModelDialog(caption = "****Time is coming....****")
String addHello(String message);
@ModelDialog(caption = "<<<<获取学生信息中,请耐心等待!...>>>>")
Student GetStudent();
@ModelDialog(caption="获取服务节点信息")
List<NetNode> GetServerNodes();
}
package com.rmi.dialog.core;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GraphicsConfiguration;
import java.awt.Window;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import javax.swing.BorderFactory;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import com.rmi.core.RMIClient;
public class RMIModelDialog extends JDialog {
private static final long serialVersionUID = -6816161311513319945L;
private Container container;
private JLabel jlblMessage;
private JPanel jpnlMessage;
private RMIClient rmiClient;
private Object result;
public RMIModelDialog setCaption(String message) {
Font font = new Font("宋体", Font.BOLD, 16);
Container parent = getParent();
int parentLeft = parent.getX();
int parentTop = parent.getY();
int parentWidth = parent.getWidth();
int parentHeight = parent.getHeight();
int width = (message.length() + 4) * font.getSize();
int height = 5 * font.getSize();
setSize(width, height);
setLocation(parentLeft + (parentWidth - width) / 2,
parentTop + (parentHeight - height) / 2);
container = getContentPane();
container.setLayout(null);
setUndecorated(true);
jpnlMessage = new JPanel();
jpnlMessage.setSize(width, height);
jpnlMessage.setLayout(new BorderLayout());
jpnlMessage.setBackground(Color.lightGray);
jpnlMessage.setBorder(BorderFactory.createLineBorder(Color.gray, 2));
container.add(jpnlMessage);
jlblMessage = new JLabel(message, JLabel.CENTER);
jlblMessage.setFont(font);
jlblMessage.setSize(width, height);
jlblMessage.setForeground(Color.blue);
jlblMessage.setHorizontalTextPosition(JLabel.CENTER);
jpnlMessage.add(jlblMessage, BorderLayout.CENTER);
dealAction();
return this;
}
public void setRmiClient(RMIClient rmiClient) {
this.rmiClient = rmiClient;
}
private void dealAction() {
addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
result = rmiClient.invokeMethod();
dispose();
}
});
}
public Object getResult() {
return result;
}
public void showDialog() {
this.setVisible(true);
}
public void exitDialog() {
this.dispose();
}
public RMIModelDialog() {
super();
}
public RMIModelDialog(Dialog owner, boolean modal) {
super(owner, modal);
}
public RMIModelDialog(Dialog owner, String title, boolean modal, GraphicsConfiguration gc) {
super(owner, title, modal, gc);
}
public RMIModelDialog(Dialog owner, String title, boolean modal) {
super(owner, title, modal);
}
public RMIModelDialog(Dialog owner, String title) {
super(owner, title);
}
public RMIModelDialog(Dialog owner) {
super(owner);
}
public RMIModelDialog(Frame owner, boolean modal) {
super(owner, modal);
}
public RMIModelDialog(Frame owner, String title, boolean modal, GraphicsConfiguration gc) {
super(owner, title, modal, gc);
}
public RMIModelDialog(Frame owner, String title, boolean modal) {
super(owner, title, modal);
}
public RMIModelDialog(Frame owner, String title) {
super(owner, title);
}
public RMIModelDialog(Frame owner) {
super(owner);
}
public RMIModelDialog(Window owner, ModalityType modalityType) {
super(owner, modalityType);
}
public RMIModelDialog(Window owner, String title, ModalityType modalityType, GraphicsConfiguration gc) {
super(owner, title, modalityType, gc);
}
public RMIModelDialog(Window owner, String title, ModalityType modalityType) {
super(owner, title, modalityType);
}
public RMIModelDialog(Window owner, String title) {
super(owner, title);
}
public RMIModelDialog(Window owner) {
super(owner);
}
}
package com.rmi.core;
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;
import com.mec.util.PropertiesParser;
/**RMI客户端
* 1.支持RMI服务器Ip和Port的配置
* 2.提供调用方法的方法id以及参数列表和方法执行结果的收发
* @author dingxiang
*
*/
public class RMIClient {
private static int DEFAULT_PORT=54199;
private static String DEFAULT_IP="192.168.137.1";
private int rmiPort;
private String rmiIp;
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Method method;
private Object[] args;
private LoadBalance loadBalance;
public RMIClient() {
this(DEFAULT_PORT, DEFAULT_IP);
}
public RMIClient(int rmiPort,String rmiIp) {
this.rmiIp=rmiIp;
this.rmiPort=rmiPort;
}
/**
* @param loadBalance
* 设置负载均衡算法
*/
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
/**
* @param RMIClientConfigPath
* 根据路径加载客户端配置文件
*/
public void loadRMIClientConfig(String RMIClientConfigPath) {
PropertiesParser.loadProperties(RMIClientConfigPath);
String RMIIp=PropertiesParser.value("configRMIIp");
if (rmiIp!=null&&rmiIp.length()>0) {
this.rmiIp=RMIIp;
}
String configRMIPortStr=PropertiesParser.value("configRMIPort");
if (configRMIPortStr!=null&&configRMIPortStr.length()>0) {
int rmiServerPort=Integer.valueOf(configRMIPortStr);
if (rmiServerPort>0&&rmiServerPort<65536) {
this.rmiPort=rmiServerPort;
}
}
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiIP(String rmiIp) {
this.rmiIp=rmiIp;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
/**
* 连接服务器,如果设置了负载均衡算法
* ,则选择负载均衡后的服务节点进行连接
*/
void connectToServer() {
try {
if (loadBalance!=null) {
ServerNode serverNode=loadBalance.getServerNode(String serviceMethod);
this.rmiIp=serverNode.getIp();
this.rmiPort=serverNode.getPort();
}
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public Object invokeMethod() {
ArgumentMaker maker=new ArgumentMaker();
if (args==null) {
args=new Object[] {};
}
int argsCount=args.length;
for(int i=0;i<argsCount;i++) {
maker.addArg("arg"+i, args[i]);
}
String argString=maker.toString();
Object result=null;
try {
dos.writeUTF(String.valueOf(method.toGenericString().hashCode()));
dos.writeUTF(argString);
String res=dis.readUTF();
close();
if (res.contains("rMIError")) {
result=ArgumentMaker.gson.fromJson(res, RMIErrorMethod.class);
}else {
if (method.getGenericReturnType().equals(void.class)) {
result= null;
}
else {
result=ArgumentMaker.gson.fromJson(res, method.getGenericReturnType());
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
void close() {
if (dis!=null) {
try {
dis.close();
} catch (IOException e) {
}
finally {
dis=null;
}
}
if (dos!=null) {
try {
dos.close();
} catch (IOException e) {
}finally {
dos=null;
}
}
if (socket!=null&&!socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
}finally {
socket=null;
}
}
}
}
测试代码以及结果截图如下
ServerDemo
package com.rmi.test;
import com.rmi.core.RMIMethodDefinitionFactory;
import com.rmi.core.RMIServer;
public class ServerDemo {
public static void main(String[] args) {
RMIMethodDefinitionFactory.scanActionPackage("com.rmi");
RMIServer server=new RMIServer(54199);
server.startRmiServer();
}
}
ClientDemo4
package com.rmi.test;
import com.rmi.core.RMIClient;
import com.rmi.core.RMIClientProxy;
import com.rmi.view.ParentView;
public class ClientDemo4 {
public static void main(String[] args) {
RMIClientProxy proxy=new RMIClientProxy();
RMIClient client=new RMIClient();
proxy.setRmiClient(client);
ParentView parentView=new ParentView();
parentView.setProxy(proxy);
parentView.showView();
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// parentView.closeView();
}
}
RMI框架使用者只需根据提供的服务的接口来调用服务,而不必知道服务实现的详细细节;这里调用的服务包括(无参,单参,多参以及无返回值的服务)并且采用短连接的方式,减小了服务器的压力,服务器也不必维护与客户端的长时间连接而耗费资源与时间。
代码以上传至[https://github.com/dx99606707/depository/blob/master/[email protected]]
在ArgumentMaker中使用了json来序列化数据(包括参数以及返回结果),用writeUTF()来进行传输,然而writeUTF(value:String) :将 UTF-8 字符串写入字节流。先写入以两个字节表示的 UTF-8 字符串长度(以字节为单位),然后写入表示字符串字符的字节。因为先把字符长度写入二进制,16位能保存的字节长度为65535。对于保存参数列表的Map,在某些情况下转化为json以后太长,会报出String Too Long Exception。因此为了解决这个问题,要么把传输的数据大小限制在65535,但这样的解决方式,着实落了下乘;这里使用writeObject()和readObject()来进行进行参数列表以及调用方法执行结果的传输,而writeObject和readObject()的辅助信息较大,因此对于数据长度不是很大的方法的hashCode,仍然用writeUTF()和readUTF()来进行传输。
通过这样的方式,则不必在需要ArgumentMaker使用json来序列化以及反序列化传输的数据,传输的参数列表就是有序的参数列表,服务器根据传过来的方法的hashCode找到对应RMIMethodDefinition以后,直接反射执行即可。修改过后的RMIClient以及RMiActioner如下
package com.rmi.core;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;
import com.mec.util.PropertiesParser;
/**RMI客户端
* 1.支持RMI服务器Ip和Port的配置
* 2.提供调用方法的方法id以及参数列表和方法执行结果的收发
* @author dingxiang
*/
public class RMIClient {
private static int DEFAULT_PORT=54199;
private static String DEFAULT_IP="192.168.137.1";
private int rmiPort;
private String rmiIp;
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Method method;
private Object[] args;
private LoadBalance loadBalance;
public RMIClient() {
this(DEFAULT_PORT, DEFAULT_IP);
}
public RMIClient(int rmiPort,String rmiIp) {
this.rmiIp=rmiIp;
this.rmiPort=rmiPort;
}
public void loadRMIClientConfig(String RMIClientConfigPath) {
PropertiesParser.loadProperties(RMIClientConfigPath);
String RMIIp=PropertiesParser.value("configRMIIp");
if (rmiIp!=null&&rmiIp.length()>0) {
this.rmiIp=RMIIp;
}
String configRMIPortStr=PropertiesParser.value("configRMIPort");
if (configRMIPortStr!=null&&configRMIPortStr.length()>0) {
int rmiServerPort=Integer.valueOf(configRMIPortStr);
if (rmiServerPort>0&&rmiServerPort<65536) {
this.rmiPort=rmiServerPort;
}
}
}
/**
* @param loadBalance
* 设置负载均衡算法
*/
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiIP(String rmiIp) {
this.rmiIp=rmiIp;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
/**
* 连接服务器,如果设置了负载均衡算法
* ,则选择负载均衡后的服务节点进行连接
*/
void connectToServer() {
try {
if (loadBalance!=null) {
ServerNode serverNode=loadBalance.getServerNode();
this.rmiIp=serverNode.getIp();
this.rmiPort=serverNode.getPort();
}
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public Object invokeMethod() {
if (args==null) {
args=new Object[] {};
}
Object result=null;
try {
ObjectOutputStream out=new ObjectOutputStream(dos);
dos.writeUTF(String.valueOf(method.toGenericString().hashCode()));
out.writeObject(args);
ObjectInputStream ois=new ObjectInputStream(dis);
result=ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return result;
}
void close() {
if (dis!=null) {
try {
dis.close();
} catch (IOException e) {
}
finally {
dis=null;
}
}
if (dos!=null) {
try {
dos.close();
} catch (IOException e) {
}finally {
dos=null;
}
}
if (socket!=null&&!socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
}finally {
socket=null;
}
}
}
}
package com.rmi.core;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
/**1.该类专门处理客户端的请求
* @author dingxiang
*/
public class RMIActioner implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
//与客户端建立通信信道,传输方法名及参数列表和执行结果
public RMIActioner(Socket socket) {
this.socket = socket;
try {
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
/**客户端发两次,服务端接收两次
*服务端第一次接收的是调用的方法id,第二次接收的是参数列表
*通过方法id,找到对应的RMIMethodDefinition,并执行
*将执行结果发送给客户端
*/
@Override
public void run() {
try {
ObjectInputStream ois=new ObjectInputStream(dis);
String methodid= dis.readUTF();
Object[] args=(Object[]) ois.readObject();
RMIMethodDefinitionFactory methodfactory=new RMIMethodDefinitionFactory();
RMIMethodDefinition methoddefinition=methodfactory.GetByMethodId(methodid);
Object res=null;
if (methoddefinition==null) {
res=new RMIErrorMethod(methodid);
}
else {
Method method=methoddefinition.getMethod();
Object excuter=methoddefinition.getObject();
res=method.invoke(excuter, args);
}
ObjectOutputStream out=new ObjectOutputStream(dos);
out.writeObject(res);
Close();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/*
* 关闭通信信道
* */
public 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;
}
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
至此,RMI框架完成。也许你会问既然有HTTP请求,为什么还要用RPC调用?(前面我们说过可以说RMI是面向对象方式的Java RPC。)对于HTTP,以其中的Restful规范为代表,它可读性好,且可以得到防火墙的支持、甚至跨语言的支持;但是HTTP也有其缺点,这是与其优点相对应的。首先是有用信息占比少,毕竟HTTP工作在第七层,包含了大量的HTTP头等信息。其次是效率低,还是因为第七层的缘故。还有,其可读性似乎没有必要,因为我们可以引入网关增加可读性,此外,使用HTTP协议调用远程方法比较复杂,要封装各种参数名和参数值。。
在分布式系统中,因为每个服务的边界都很小,很有可能调用别的服务提供的方法。这就出现了服务A调用服务B中方法的需求,但如前所述,基于Restful的远程过程调用有着明显的缺点,主要是效率低、封装调用复杂。当存在大量的服务间调用时,这些缺点变得更为突出。
服务A调用服务B的过程是应用间的内部过程。而牺牲可读性提升效率、易用性是可取的。由此产生了RPC。通常,RPC要求在调用方中放置被调用的方法的接口。调用方只要调用了这些接口,就相当于调用了被调用方的实际方法,十分易用。于是,调用方可以像调用内部接口一样调用远程的方法,而不用封装参数名和参数值等操作。调用方调用内部的一个方法,但是被RPC框架偷梁换柱为远程的一个方法。之间的通信数据可读性不需要好,只需要RPC框架能读懂即可,因此效率可以更高。
代码已上传至:[https://github.com/dx99606707/depository/blob/master/[email protected]]
然而,如果因为writeUTF/readUTF有长度限制选择writeObject方法的话,看似解决了长度限制问题,但是又引入了新的问题,writeObject/readObject的开销大,效率也不是很高,并且传输的的对象必须实现Serializable接口,显得十分繁琐(最好约定一个类用来传输,这个类应保存调用的方法标识,参数数组,参数类型等)。由此,我们要么选择一种把字符串和字节数组能相互转化的方法,要么选择一种更高效的对象序列化与反序列化方法。这里,我使用Base64来实现字符串与字节数组的转化,使用fastjson来序列化对象。再次修改过后的RMIClient以及RMiActioner如下:
package com.rmi.core;
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 java.util.Base64;
import java.util.Base64.Encoder;
import com.alibaba.fastjson.JSON;
import com.mec.util.ArgumentMaker;
public class RMIClient {
private static int DEFAULT_PORT=54199;
private static String DEFAULT_IP="192.168.137.1";
private int rmiPort;
private String rmiIp;
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Encoder encoder;
private Method method;
private Object[] args;
public RMIClient() {
this(DEFAULT_PORT, DEFAULT_IP);
}
public RMIClient(int rmiPort,String rmiIp) {
this.rmiIp=rmiIp;
this.rmiPort=rmiPort;
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiIP(String rmiIp) {
this.rmiIp=rmiIp;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
void connectToServer() {
try {
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
encoder=Base64.getEncoder();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public Object invokeMethod() {
ArgumentMaker maker=new ArgumentMaker();
if (args==null) {
args=new Object[] {};
}
int argsCount=args.length;
for(int i=0;i<argsCount;i++) {
maker.addArg("arg"+i, args[i]);
}
String argString=maker.toString();
Object result=null;
try {
//方法名
dos.writeUTF(String.valueOf(method.toGenericString().hashCode()));
//参数串
byte[] org=encoder.encode(argString.getBytes("UTF-8"));
dos.writeInt(org.length);
dos.write(org);
//服务器返回结果
result=JSON.parseObject(dis, method.getGenericReturnType());
close();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
void close() {
if (dis!=null) {
try {
dis.close();
} catch (IOException e) {
}
finally {
dis=null;
}
}
if (dos!=null) {
try {
dos.close();
} catch (IOException e) {
}finally {
dos=null;
}
}
if (socket!=null&&!socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
}finally {
socket=null;
}
}
}
}
package com.rmi.core;
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.Type;
import java.net.Socket;
import java.util.Base64;
import java.util.Base64.Decoder;
import com.alibaba.fastjson.JSON;
import com.mec.util.ArgumentMaker;
public class RMIActioner implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Decoder decoder;
public RMIActioner(Socket socket) {
this.socket = socket;
try {
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
decoder=Base64.getDecoder();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
//方法id
String methodid=dis.readUTF();
//字符串
int len=dis.readInt();
byte[] value=new byte[len];
dis.read(value,0,len);
String argString=new String(decoder.decode(value),"UTF-8");
RMIMethodDefinitionFactory methodfactory=new RMIMethodDefinitionFactory();
RMIMethodDefinition methoddefinition=methodfactory.GetByMethodId(methodid);
Object res=null;
if (methoddefinition==null) {
res=new RMIErrorMethod(methodid);
}
else {
Method method=methoddefinition.getMethod();
Object excuter=methoddefinition.getObject();
Object[] paras=getParas(method, argString);
res=method.invoke(excuter, paras);
}
//返回结果
JSON.writeJSONString(dos, res);
Close();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public Object[] getParas(Method method,String argString) {
ArgumentMaker maker=new ArgumentMaker(argString);
Object[] paras=null;
Type[] paratypes=method.getParameterTypes();
int paraCount=method.getParameterCount();
if (paraCount<=0) {
paras=new Object[] {};
}
paras=new Object[paraCount];
for (int i = 0; i < paraCount; i++) {
paras[i]=maker.getValue("arg"+i, paratypes[i]);
}
return paras;
}
public 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;
}
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
代码已上传至:https://gitee.com/dx99606707/MyRepository/blob/master/[email protected]
在前面讲的示例中,方法标识以及参数数组(或者字符串)都是分开传输的,这样会多增加一次通信的开销,因此,我在ArgumentMaker类里将方法标识也加进去,减少一次通信开销。
修改RMIClient如下。
package com.rmi.core;
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.alibaba.fastjson.JSON;
public class RMIClient {
private static int DEFAULT_PORT=54199;
private static String DEFAULT_IP="127.0.0.1";
private int rmiPort;
private String rmiIp;
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Method method;
private Object[] args;
public RMIClient() {
this(DEFAULT_PORT, DEFAULT_IP);
}
public RMIClient(int rmiPort,String rmiIp) {
this.rmiIp=rmiIp;
this.rmiPort=rmiPort;
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiIP(String rmiIp) {
this.rmiIp=rmiIp;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
void connectToServer() {
try {
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public Object invokeMethod() {
ArgumentMaker maker=new ArgumentMaker();
if (args==null) {
args=new Object[] {};
}
int argsCount=args.length;
for(int i=0;i<argsCount;i++) {
maker.addArg("arg"+i, args[i]);
}
Object result=null;
try {
maker.addArg("methodId", String.valueOf(method.toGenericString().hashCode()));
byte[] org=JSON.toJSONBytes(maker);
dos.writeInt(org.length);
dos.write(org);
// //服务器返回结果
result=JSON.parseObject(dis, method.getGenericReturnType());
close();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
void close() {
if (dis!=null) {
try {
dis.close();
} catch (IOException e) {
}
finally {
dis=null;
}
}
if (dos!=null) {
try {
dos.close();
} catch (IOException e) {
}finally {
dos=null;
}
}
if (socket!=null&&!socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
}finally {
socket=null;
}
}
}
}
修改RMIActioner如下。
package com.rmi.core;
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.Type;
import java.net.Socket;
import java.util.Base64;
import java.util.Base64.Decoder;
import com.alibaba.fastjson.JSON;
public class RMIActioner implements Runnable {
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
public RMIActioner(Socket socket) {
this.socket = socket;
try {
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
int len=dis.readInt();
byte[] org=new byte[len];
dis.read(org);
ArgumentMaker maker=JSON.parseObject(org,ArgumentMaker.class);
String methodId=maker.getValue("methodId", String.class);
RMIMethodDefinitionFactory methodfactory=new RMIMethodDefinitionFactory();
RMIMethodDefinition methoddefinition=methodfactory.GetByMethodId(methodId);
Object res=null;
if (methoddefinition==null) {
res=new RMIErrorMethod(methodId);
}
else {
Method method=methoddefinition.getMethod();
Object excuter=methoddefinition.getObject();
Object[] paras=getParas(method, maker);
res=method.invoke(excuter, paras);
}
//返回结果
JSON.writeJSONString(dos, res);
Close();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private Object[] getParas(Method method,ArgumentMaker maker) {
Object[] paras=null;
Type[] paratypes=method.getParameterTypes();
int paraCount=method.getParameterCount();
if (paraCount<=0) {
paras=new Object[] {};
}
paras=new Object[paraCount];
for (int i = 0; i < paraCount; i++) {
paras[i]=maker.getValue("arg"+i, paratypes[i]);
}
return paras;
}
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;
}
}
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket = null;
}
}
}
一个完善的RPC和RMI框架应该还包含注册中心,负载均衡、以及超时设置等。
如果某种服务的服务提供者不止一个,也为了使得在某一种服务的服务提供者不能正常提供服务时可以切换到另一个服务提供者继续享受服务,这里引入注册中心,用来注册服务。具体实现部分请参见微服务框架之服务发现。该框架实现了服务提供者注册、注销服务;服务消费者可根据想要消费的服务获取服务提供者列表,再根据自身配置的选择算法,选取合适的服务提供者享受服务;并对注册中心异常宕机做出了处理。
在不设置负载均衡算法的时候,需要指定具体的服务提供者享受服务。如果有多个服务提供者,出于一些服务消费者的特殊目的(比如,选取响应最快的服务提供者),就需要设置负载均衡算法,这里给出了接口和默认的实现,用户可根据自身的需求具体实现该接口,并配置。
package com.rmi.core;
import com.rmi.model.NetNode;
/**
* @author dingxiang
*提供负载均衡的接口,具体的负载均衡算法由客户端实现。
*/
public interface LoadBalance {
NetNode getServerNode(String serviceMethod);
}
默认的负载均衡算法(随机选择)
package com.rmi.core;
import java.util.List;
import com.rmi.fwfx.center.IRegistryCenterService;
import com.rmi.model.NetNode;
/**
* 随机选择算法
* @author dingxiang
*
*/
public class RandomBalance implements LoadBalance{
private String centerIp;
private int centerPort;
public String getCenterIp() {
return centerIp;
}
public void setCenterIp(String centerIp) {
this.centerIp = centerIp;
}
public int getCenterPort() {
return centerPort;
}
public void setCenterPort(int centerPort) {
this.centerPort = centerPort;
}
@Override
public NetNode getServerNode(String serviceMethod) {
RMIClient rmiClient=new RMIClient(centerPort, centerIp);
RMIClientProxy clientProxy=new RMIClientProxy();
clientProxy.setRmiClient(rmiClient);
IRegistryCenterService registryCenterService=(IRegistryCenterService) clientProxy.getProxy(IRegistryCenterService.class);
List<NetNode> serverNodes=registryCenterService.getNodesByServiceName(serviceMethod);
if (serverNodes!=null&&serverNodes.size()>0) {
int index=(int)(Math.random()*10000)%serverNodes.size();
return serverNodes.get(index);
}
return null;
}
}
/**
* 连接服务器,如果设置了负载均衡算法
* ,则选择负载均衡后的服务节点进行连接
* @throws Exception
*/
void connectToServer() throws Exception {
try {
if (loadBalance!=null) {
NetNode serverNode=loadBalance.getServerNode(method.getName());
System.out.println("选择服务节点为:"+serverNode);
if (serverNode!=null) {
this.rmiIp=serverNode.getIp();
this.rmiPort=serverNode.getPort();
}else {
System.out.println("注册中心没有该服务的服务节点!");
}
}
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
关于超时的设置,可以在服务端以及客户端都配置超时时间,缺省的超时设置可以由服务端进行设置,这样一方面所有的消费端(服务调用方)都无需设置,可以通过注册中心传递给消费端,另一方面服务端相比消费端更清楚自己的接口性能,所以交给服务端进行设置也是合理的。
服务端的超时设置(RMIActioner),支持配置文件配置。
/**
*处理请求线程的的run方法
*/
@Override
public void run() {
//执行方法调用,并统计耗时
long start=System.currentTimeMillis();
Method method=invokeMethod();
long elapsed=System.currentTimeMillis()-start;
//判断是否超时
if (elapsed>RMIServer.timeOut) {
//打印日志
System.out.println("方法:"+method.getName()+" invoke time out...");
}else {
System.out.println("方法:"+method.getName()+" invoked ...");
}
}
服务端的超时设置并不会影响实际的调用过程,就算超时也会执行完整个处理逻辑。
客户端的超时设置(RMIClient),支持配置文件配置。
package com.rmi.core;
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 java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JFrame;
import com.alibaba.fastjson.JSON;
import com.mec.util.PropertiesParser;
import com.mec.util.ViewTool;
import com.rmi.model.NetNode;
public class RMIClient {
private static int DEFAULT_RMI_PORT=54188;
private static String DEFAULT_RMI_IP="127.0.0.1";
private static int DEFAULT_TIME_OUT=5000;
private int rmiPort;
private String rmiIp;
private Socket socket;
private DataInputStream dis;
private DataOutputStream dos;
private Method method;
private Object[] args;
private LoadBalance loadBalance;
private ReentrantLock lock;
private volatile boolean isDealed;
private long timeOut;
public RMIClient() {
this(DEFAULT_RMI_PORT, DEFAULT_RMI_IP);
}
public RMIClient(int rmiPort,String rmiIp) {
this.rmiIp=rmiIp;
this.rmiPort=rmiPort;
this.timeOut=DEFAULT_TIME_OUT;
this.lock=new ReentrantLock();
}
public boolean isDone() {
return isDealed;
}
public void setRmiPort(int rmiPort) {
this.rmiPort=rmiPort;
}
public void setRmiIP(String rmiIp) {
this.rmiIp=rmiIp;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
/**
* @param RMIClientConfigPath
* 根据路径加载客户端配置文件
*/
public void loadRMIClientConfig(String RMIClientConfigPath) {
PropertiesParser.loadProperties(RMIClientConfigPath);
String RMIIp=PropertiesParser.value("configRMIIp");
if (rmiIp!=null&&rmiIp.length()>0) {
this.rmiIp=RMIIp;
}
String configRMIPortStr=PropertiesParser.value("configRMIPort");
if (configRMIPortStr!=null&&configRMIPortStr.length()>0) {
int rmiServerPort=Integer.valueOf(configRMIPortStr);
if (rmiServerPort>0&&rmiServerPort<65536) {
this.rmiPort=rmiServerPort;
}
}
String timeOutStr=PropertiesParser.value("timeOut");
if (timeOutStr!=null&&timeOutStr.length()>0) {
long outTime=Long.valueOf(timeOutStr);
if (outTime>0&&outTime<Long.MAX_VALUE) {
this.timeOut=outTime;
}
}
}
/**
* 连接服务器,如果设置了负载均衡算法
* ,则选择负载均衡后的服务节点进行连接
* @throws Exception
*/
void connectToServer() throws Exception {
try {
if (loadBalance!=null) {
NetNode serverNode=loadBalance.getServerNode(method.getName());
System.out.println("选择服务节点为:"+serverNode);
if (serverNode!=null) {
this.rmiIp=serverNode.getIp();
this.rmiPort=serverNode.getPort();
}else {
System.out.println("注册中心没有该服务的服务节点!");
}
}
socket=new Socket(rmiIp, rmiPort);
dis=new DataInputStream(socket.getInputStream());
dos=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端调用方法
* @return
*/
public Object invokeMethod() {
Object locker=new Object();
Object result=null;
Invoker invoker=new Invoker();
new Thread(invoker).start();
try {
if (!isDone()) {
long start=System.currentTimeMillis();
this.lock.lock();
try {
while (!isDone()) {
synchronized (locker) {
locker.wait(timeOut);
}
//判断是否已经返回结果或者已经超时
long elapsed=System.currentTimeMillis()-start;
if (isDone()||elapsed>=this.timeOut) {
System.out.println("超时间隔为:"+elapsed);
break;
}
}
} catch (Exception e) {
throw new RuntimeException("服务调用异常!");
}finally {
this.lock.unlock();
}
}
if (!isDone()) {
//超时未返回结果
throw new TimeoutException("调用服务超时,请稍后重试!");
}
result=invoker.getResult();
} catch (Exception e1) {
if (e1 instanceof TimeoutException||e1 instanceof RuntimeException) {
ViewTool.showError(new JFrame(), e1.getMessage());
e1.printStackTrace();
}
}finally {
isDealed=false;
close();
}
return result;
}
void close() {
if (dis!=null) {
try {
dis.close();
} catch (IOException e) {
}
finally {
dis=null;
}
}
if (dos!=null) {
try {
dos.close();
} catch (IOException e) {
}finally {
dos=null;
}
}
if (socket!=null&&!socket.isClosed()) {
try {
socket.close();
} catch (IOException e) {
}finally {
socket=null;
}
}
}
/**
* 具体发送请求和接收结果的内部类
* @author dingxiang
*
*/
class Invoker implements Runnable{
private Object result;
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
@Override
public void run() {
try {
ArgumentMaker maker=new ArgumentMaker();
if (args==null) {
args=new Object[] {};
}
int argsCount=args.length;
for(int i=0;i<argsCount;i++) {
maker.addArg("arg"+i, args[i]);
}
maker.addArg("methodId", String.valueOf(method.toGenericString().hashCode()));
System.out.println("200"+method.toGenericString());
byte[] org=JSON.toJSONBytes(maker);
System.out.println("org.length:"+org.length);
dos.writeInt(org.length);
dos.write(org);
//服务器返回结果
result=JSON.parseObject(dis, method.getGenericReturnType());
isDealed=true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这里的客户端实现的超时设置,在客户端调用方法时,如果在超时间间隔内没有返回结果,就会报出超时异常,并弹出提示框显示超时信息。并在控制台打印异常信息(也可以打印到日志)。但这样的做法有些缺陷,比如,执行线程从发起请求到接受结果只花了3秒钟,但是客户端设置的全局超时时间是5秒钟,这样的情况下,5秒后才会去检查请求是否处理完,时间上就会有延迟。
如果客户端不设置负载均衡算法,也可以采用集群容错的缺省方式,先根据要调用的服务,获取服务提供者列表,这样,当调用失败时,如果是业务异常,则停止重试,否则一直重试直到达到重试次数,比如网络异常、响应超时等情况就会一直重试直到达到重试次数。
这样的做法不是很灵活,客户端和服务端的超时时间都是全局的,这里有提出两种解决办法。一种是多粒度超时设置。通过注解的方式给调用的方法所属类以及方法加注解,再调用方法的时候在通过反射获取注解里面的超时时间,动态设置超时时间。另一种是定时超时检测设置。在执行调用的时候,先根据设置的超时时间开启一个定时器,再开启一个执行线程。如果执行线程在超时时间间隔内处理完请求调用,则返回结果,关闭定时器。否则,定时器定时时间一到,则设置结果为null,并关闭定时器。主线程一直检测定时器状态,直到定时器状态为关闭,则立刻获取执行线程的结果。这里给出下一种的实现方式。
RMIClient
/**
* 调用方法
* @return
*/
public Object invokeMethod() {
lock.lock();
Invoker invoker=new Invoker();
this.dida.setAction(invoker);
dida.start();//开始定时
Thread thread=new Thread(invoker);
thread.start();//开始执行
Object result=null;
while (true) {
if (dida.isGoon()) {
}else {
break;
}
}
result=invoker.getResult();
System.out.println("执行线程状态!"+thread.isAlive());
lock.unlock();
close();
return result;
}
/**
* 内部执行线程
* @author dingxiang
*
*/
class Invoker implements Runnable,IDidaUserAction{
private Object result;
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
@Override
public void run() {
try {
boolean flag=false;
while (dida.isGoon()&&!flag) {
ArgumentMaker maker=new ArgumentMaker();
if (args==null) {
args=new Object[] {};
}
int argsCount=args.length;
for(int i=0;i<argsCount;i++) {
maker.addArg("arg"+i, args[i]);
}
maker.addArg("methodId", String.valueOf(method.toGenericString().hashCode()));
System.out.println("200"+method.toGenericString());
byte[] org=JSON.toJSONBytes(maker);
dos.writeInt(org.length);
dos.write(org);
//服务器返回结果
result=JSON.parseObject(dis, method.getGenericReturnType());
flag=true;
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (dida.isGoon()) {
dida.stop();
}else {
return;
}
}
}
@Override
public void doUserAction() {
if (!dida.isGoon()) {
return;
}else {
result=null;
dida.stop();
ViewTool.showError(new JFrame(), "请求超时,请稍后重试!");
}
}
}
DidaTimer
package com.rmi.timer;
public class DidaTimer implements Runnable {
public static final long DEFAULT_DELAY_TIME = 1000;
private long delayTime;
private Object lock;
private volatile boolean goon;
public long getDelayTime() {
return delayTime;
}
private IDidaUserAction action;
public DidaTimer() {
this(DEFAULT_DELAY_TIME);
}
public DidaTimer(long delayTime) {
this(delayTime, null);
}
public DidaTimer(long delayTime, IDidaUserAction action) {
this.lock = new Object();
this.delayTime = delayTime;
this.action = action;
}
public void setAction(IDidaUserAction action) {
this.action = action;
}
public void setDelayTime(long delayTime) {
this.delayTime = delayTime;
}
public void start() {
if (action == null) {
System.out.println("无事可做!");
return;
}
if (goon) {
return;
}
goon = true;
new Thread(this, "didadida Timer").start();
}
public void stop() {
if (action == null) {
System.out.println("没做任何事!");
return;
}
if (goon == false) {
return;
}
goon = false;
System.out.println("关闭定时器!");
}
public boolean isGoon() {
return goon;
}
@Override
public void run() {
synchronized (lock) {
try {
lock.wait(delayTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
action.doUserAction();
goon=false;
}
}
RPC框架的超时重试机制到底是为了解决什么问题呢?从微服务架构这个宏观角度来说,它是为了确保服务链路的稳定性,提供了一种框架级的容错能力。从微观上来讲,举几个例子来说明。
consumer调用provider,如果不设置超时时间,则consumer的响应时间肯定会大于provider的响应时间。当provider性能变差时,consumer的性能也会受到影响,因为它必须无限期地等待provider的返回。假如整个调用链路经过了A、B、C多个服务,只要C的性能变差,就会自下而上影响到A、B,最终导致整个服务链路超时甚至瘫痪,因此设置超时时间是非常有必要的。
假设consumer是核心的商品服务,provider是非核心的评论服务,当评价服务出现性能问题时,商品服务可以接受不返回评价信息,从而保证能继续对外提供服务。这样情况下,就必须设置一个超时时间,当评价服务超过这个阈值时,商品服务不用继续等待。
provider很有可能是因为某个瞬间的网络抖动或者机器高负载引起的超时,如果consumer超时后直接放弃,某些场景会造成业务损失(比如库存接口超时会导致下单失败)。因此,对于这种临时性的服务抖动,如果在超时后重试一下是可以挽救的,所以有必要通过重试机制来解决。
但引入超时重试机制也会带来副作用,这些是开发RPC接口必须要考虑,同时也是最容易忽视的问题:
重复请求:有可能provider执行完了,但是因为网络抖动consumer认为超时了,这种情况下重试机制就会导致重复请求,从而带来脏数据问题,因此服务端必须考虑接口的幂等性(读接口是天然幂等的)。
降低consumer的负载能力:如果provider并不是临时性的抖动,而是确实存在性能问题,这样重试多次也是没法成功的,反而会使得consumer的平均响应时间变长。比如正常情况下provider的平均响应时间是2s,consumer将超时时间设置成3s,重试次数设置为2次,这样单次请求将耗时6s,consumer的整体负载就降低下来,如果consumer是一个高QPS的服务,还有可能引起连锁反应造成雪崩。
重试风暴:假如一条调用链路经过了3个服务,最底层的服务C出现超时,这样上游服务都将发起重试,假设重试次数都设置的4次,那么B将面临正常情况下4倍的负载量,C是16倍,整个服务集群可能因此雪崩。
那么,应该如何去合理的设置超时时间呢?
先设计性能测试用例(可以用jmeter测试),测试出依赖服务的TP99响应时间是多少(如果依赖服务性能波动大,也可以选择TP95),调用方的超时时间可以在此基础上加50%。(TP99时延即满足99%的网络请求的最长耗时)。
支持多粒度的超时设置,则:全局超时时间应该要略大于接口级别最长的耗时时间,每个接口的超时时间应该要略大于方法级别最长的耗时时间,每个方法的超时时间应该要略大于实际的方法执行时间。(前面讲过的多粒度超时设置就可以按照这样设置)。
区分服务是否可重试,如果接口没实现幂等则不允许设置重试次数。读接口是天然幂等的,写接口则可以使用业务单据ID(比如订单)或者在调用方生成唯一ID传递给服务端,通过此ID进行防重避免引入脏数据。
从业务角度来看,有些服务可用性要求不用那么高(比如偏内部的应用系统),则可以不用设置超时重试次数,直接人工重试即可,这样能减少接口实现的复杂度,反而更利于后期维护。(本文实现的RPC与RMI框架就是人工重试的。
如果调用方是高QPS服务,则必须考虑服务方超时情况下的降级和熔断策略。(比如超过10%的请求出错,则停止重试机制直接熔断,改成调用其他服务、或者使用调用方的缓存数据等等)。
RPC接口的超时设置,要从一个全局的角度来看,涉及到的技术问题有(接口幂、服务降级和熔断、限流、性能测试以及优化等等),有时还需要从业务角度来决定如何设置超时设置。
本文所述RPC与RMI框架从开始设计,一直到后面的不断改进以及完善花费了大量时间,一个RPC框架关键的地方,包括序列化协议,负载均衡,注册中心,超时设置等,博主都自行实现了一遍,但由于博主所知有限,在有些地方肯定还存在一些不足,请大家多多指教。
测试代码:
注册中心。
RegistryCenterDemo
服务提供者。(服务器端)
ServerDemo
服务消费者者。(客户端)
ClientDemo
代码已上传至码云:
20200719@RMI整合注册中心@lock实现.rar
20200720@RMI整合注册中心@定时器实现.rar
引用文档。
RPC的超时设置,一不小心就是线上事故
如何给女朋友解释什么是熔断?
Dubbo 的心跳设计,值得学习!
如何给女朋友解释什么是RPC