Java RMI 用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上也可以在同一个主机上。一个虚拟机中的对象调用另一个虚拟上中的对象的方法,只不过是允许被远程调用的对象要通过一些标志加以标识。这样做的特点如下 :
优点 : 避免重复造轮子
缺点 : 调用过程很慢,而且该过程是不可靠的,容易发生不可预料的错误,比如网络错误等
在 RMI 中的核心是远程对象 (remote object),除了对象本身所在的虚拟机,其他虚拟机也可以调用此对象的方法,而且这些虚拟机可以不在同一个主机上。每个远程对象都要实现一个或者多个远程接口来标识自己,声明可以被外部系统或者应用调用的方法 (当然也有一些方法是不想让人访问的)
RMI 通信模型
从方法调用角度来看,RMI 要解决的问题,是让客户端对远程方法的调用可以相当于对本地方法的调用而屏蔽其中关于远程通信的内容,即使在远程上,也和在本地上是一样的。从客户端-服务器模型来看,客户端程序直接调用服务端,两者之间是通过 JRMP(Java Remote Method Protocol) 协议通信,这个协议类似于 HTTP协议,规定客户端和服务端通信要满足的规范。但是实际上,客户端只与代表远程主机中对象的 Stub对象进行通信,而不知道 Server的存在。客户端只是调用Stub对象中的本地方法,Stub对象是一个本地对象,它实现了远程对象向外暴露的接口,也就是说它的方法和远程对象暴露的方法的签名是相同的。客户端认为它是调用远程对象的方法,实际上是调用Stub对象中的方法。可以理解为Stub对象是远程对象在本地的一个代理,当客户端调用方法的时候,Stub对象会将调用通过网络传递给远程对象。在 java 1.2 之前,与 Stub对象直接对话的是 Skeleton对象,在 Stub对象将调用传递给 Skeleton的过程中,其实这个过程是通过 JRMP协议实现转化的,通过这个协议将调用从一个虚拟机转到另一个虚拟机。在 Java 1.2 之后,与 Stub对象直接对话的是 Server程序,不再是 Skeleton对象。所以从逻辑上来看,数据是在 Client 和 Server之间横向流动的,但是实际上是从Client 到 Stub,然后 从Skeleton到Server这样纵向流动的
重要的两个问题
1> 数据的传递问题
在 Java程序中引用类型 (不包括基本类型) 的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,因为的参数的引用对应的是同一个内存空间,但是对于分布式系统中,由于对象不再存在于同一个内存空间,虚拟机A 对象引用对于 虚拟机B没有任何意义,为解决这个问题有如下两种方法 :
方法1> 将引用传递更改为值传递,也就是将对象序列化为字节,然后使用该字节的副本在客户端和服务器之间传递,而且一个虚拟机中对该值的修改不会影响到其他主机中的数据;但是对象的序列化也有一个问题,就是对象的嵌套引用就会造成序列化的嵌套,这必然会导致数据量的激增,因此需要有选择的进行序列化,在 Java 中一个对象如果能够被序列化,需要满足下面两个条件之一 :
<1> 是 Java 基本类型
<2> 实现 java.io.Serializable接口 (String类源码中已经实现了该接口)
对于容器类,如果其中的对象是可以序列化的,那么该容器也是可以序列化的,可序列化的子类也是可以序列化的
方法2> 仍然使用引用传递,每当远程主机调用本地主机方法时,该调用还要通过本地主机查询该引用对应的对象,在任何一台机器上的改变都会影响原始主机上的数据,因为这个对象是共享的。RMI 中的参数传递和结果返回可以使用的三种机制 (取决于数据类型) :
<1> 简单类型 : 按值传递,直接传递数据拷贝
<2> 远程对象引用 (实现 Remote接口) : 以远程对象的引用传递
<3> 远程对象引用 (未实现 Remote接口) : 按值传递,通过序列化对象传递副本,本身不允许序列化的对象不允许传递给远程方法
2> 远程对象的发现问题
在调用远程对象的方法之前需要一个远程对象的引用,如何获得这个远程对象的引用在 RMI 中是一个关键的问题,如果将远程对象的发现类比于 IP地址的发现可能比较好理解一些。在日常使用网络时,基本上都是通过域名来定位一个网站,但是实际上网络是通过 IP地址来定位网站的,因此其中就需要一个映射的过程,域名系统 (DNS) 就是为这个目的出现的,在域名系统中通过域名来查找对应的 IP地址来访问对应的服务器。那么对应的,IP地址在这里就相当于远程对象的引用,而 DNS 则相当于一个注册表 (Registry)。而域名在 RMI 中就相当于远程对象的标识符,客户端通过提供远程对象的标识符访问注册表,来得到远程对象的引用。这个标识符是类似URL地址格式的,它要满足的规范如下 :
<1> 该名称是 URL 形式的,类似于 http 的 URL,schema 是 rmi
<2> 格式类似于 rmi://host:port/name,host指明注册表运行的注解,port表明接收调用的端口,name 是一个标识该对象的简单名称
<3> 主机和端口都是可选的,如果省略主机,则默认运行在本地;如果端口也省略,则默认端口是1099
编程实现
实现 RMI 所需的 API几乎都在 :
1> java.rmi : 提供客户端需要的类、接口和异常
2> java.rmi.server : 提供服务端需要的类、接口和异常
3> java.rmi.registry : 提供注册表的创建以及查找和命名远程对象的类、接口和异常
其实在 RMI 中的客户端和服务端并没有绝对的界限,与Web应用中的客户端和服务器还是有区别的。这两者其实是平等的,客户端可以为服务端提供远程调用的方法,这时候,原来的客户端就是服务器端
构建注册表
在启动服务器的时候,实际上需要运行两个服务器 :
1> 一个是远程对象本身
2> 一个是允许客户端下载远程对象引用的注册表
由于远程对象需要与注册表对话,所以必须首先启动注册表程序。当注册表程序没有启动的时候,如果强行启动远程对象服务器时,会抛出如下错误:
java.rmi.ConnectException: Connection refused to host: 192.168.86.58; nested exception is:
java.net.ConnectException
: Connection refused (Connection refused)
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:338)
at sun.rmi.registry.RegistryImpl_Stub.rebind(RegistryImpl_Stub.java:147)
at java.rmi.Naming.rebind(Naming.java:177)
at com.chenshun.test.server.impl.UserHandlerImpl.main(UserHandlerImpl.java:50)
Caused by: java.net.ConnectException
: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native
Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350
)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206
)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188
)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392
)
at java.net.Socket.connect(Socket.java:589
)
at java.net.Socket.connect(Socket.java:538
)
at java.net.Socket
.(Socket.java:434)
at java.net.Socket
.(Socket.java:211)
at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:148)
at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:613)
... 6 more
注册表其实不用写任何代码,在 ${JAVA_HOME}/bin 目录下有一个 rmiregistry 程序,需要在目标程序的 classpath下运行该程序
$ cd
/Users/chenshun131/Desktop/AllMyFile/Study_CodeRepository/Intellij/TestRMI/out/production/TestRMI
# 程序的输出目录根路径中
$
rmiregistry
# 启动
rmiregistry 程序
rmiregistry 程序 默认使用 1099端口,如果端口已经被使用则可以通过命令指定端口
使用 ${JAVA_HOME}/bin/rmiregistry 程序 一定要进入到程序的编译根路径中,否则将会出现类找不到异常
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: com.chenshun.test.server.UserHandler
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:393)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
at sun.rmi.registry.RegistryImpl_Stub.rebind(RegistryImpl_Stub.java:155)
at java.rmi.Naming.rebind(Naming.java:177)
at com.chenshun.test.server.impl.UserHandlerImpl.main(UserHandlerImpl.java:50)
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: com.chenshun.test.server.UserHandler
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:137)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:472)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:299)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:568)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:826)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:683)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:682)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassNotFoundException: com.chenshun.test.server.UserHandler
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.rmi.server.LoaderHandler$Loader.loadClass(LoaderHandler.java:1207)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at sun.rmi.server.LoaderHandler.loadClassForName(LoaderHandler.java:1221)
at sun.rmi.server.LoaderHandler.loadProxyInterfaces(LoaderHandler.java:731)
at sun.rmi.server.LoaderHandler.loadProxyClass(LoaderHandler.java:674)
at sun.rmi.server.LoaderHandler.loadProxyClass(LoaderHandler.java:611)
at java.rmi.server.RMIClassLoader$2.loadProxyClass(RMIClassLoader.java:646)
at java.rmi.server.RMIClassLoader.loadProxyClass(RMIClassLoader.java:311)
at sun.rmi.server.MarshalInputStream.resolveProxyClass(MarshalInputStream.java:253)
at java.io.ObjectInputStream.readProxyDesc(ObjectInputStream.java:1791)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1742)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:135)
... 14 more
也可以不使用 ${JAVA_HOME}/bin/rmiregistry 程序,直接通过 LocateRegistry类 完成。LocateRegistry类 有两类方法 :
1> 创建本地注册表并且获取该注册表的引用,方法源码如下 :
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}
public static Registry createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
return new RegistryImpl(port, csf, ssf);
}
2> 直接获取注册表引用,该注册表可以是本地运行的,也可以是远程运行的,这类方法是不能够创建注册表的,只能等注册表程序运行起来之后,和它进行通信来获取引用,相关方法源码如下 :
public static Registry getRegistry() throws RemoteException {
return getRegistry(null, Registry.REGISTRY_PORT);
}
public static Registry getRegistry(int port) throws RemoteException {
return getRegistry(null, port);
}
public static Registry getRegistry(String host) throws RemoteException {
return getRegistry(host, Registry.REGISTRY_PORT);
}
public static Registry getRegistry(String host, int port) throws RemoteException {
return getRegistry(host, port, null);
}
public static Registry getRegistry(String host, int port, RMIClientSocketFactory csf) throws RemoteException {
Registry registry = null;
if (port <= 0)
port = Registry.REGISTRY_PORT;
if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID), new TCPEndpoint(host, port, csf, null), false);
RemoteRef ref = (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}
// 启动 rmiregistry 服务,只要在启动服务端代码之前运行即可
LocateRegistry.createRegistry(1099);
构建服务器端
实现 java.rmi.Remote接口 的类或者继承 java.rmi.Remote接口的所有接口都是远程对象。这些继承或者实现了该接口的类或者接口中定义了客户端可以访问的方法。这个远程对象中可能有很多个方法,但是只有在远程接口中声明的方法才能从远程调用,其他的公共方法只能在本地虚拟机中使用
实现过程中的注意事项 :
1> 子接口的每个方法都必须声明抛出 java.rmi.RemoteException异常,该异常是使用 RMI 时可能抛出的大多数异常的父类
2> 子接口的实现类应该直接或者间接继承 java.rmi.server.UnicastRemoteObject类,该类提供了很多支持RMI的方法,具体来说,这些方法可以通过 JRMP协议导出一个远程对象的引用,并通过动态代理构建一个可以和远程对象交互的 Stub对象
注 :
在 Java 1.4及 以前的版本中需要手动建立 Stub对象,通过运行rmic命令来生成远程对象实现类的Stub对象,但是在Java 1.5之后可以通过动态代理来完成,不再需要这个过程,只需要继承 java.rmi.server.UnicastRemoteObject类 即可,具体如何使用可参考 java.rmi.server.UnicastRemoteObject源码
创建远程接口及其实现类
`UserHandler 接口
public interface UserHandler extends Remote {
String getUserName(int id) throws RemoteException;
int getUserCount() throws RemoteException;
User getUserByName(String name) throws RemoteException;
}
`UserHandlerImpl 实现类
public class UserHandlerImpl extends UnicastRemoteObject implements UserHandler {
private static final long serialVersionUID = -6916371403662229854L;
/**
* 该构造期必须存在,因为集继承了UnicastRemoteObject类,其构造器要抛出RemoteException
*
* @throws RemoteException
*/
public UserHandlerImpl() throws RemoteException {
super();
}
@Override
public String getUserName(int id) throws RemoteException {
if (id == 1) {
return "hello chenshun131";
} else if (id == 2) {
return "user chenshun131";
}
return "unknow";
}
@Override
public int getUserCount() throws RemoteException {
return 1;
}
@Override
public User getUserByName(String name) throws RemoteException {
return new User("chenshun131", 1);
}
}
创建引用类型
public class User implements Serializable {
/** 该字段必须存在 */
private static final long serialVersionUID = 4780096862366471996L;
/** setter和getter可以没有 */
private String name;
private int id;
public User(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
注 : 客户端和服务端自定义对象一定要相同且 serialVersionUID 属性值也必须相同,否则会出现序列化异常
运行远程对象服务器
// 启动 rmiregistry 服务
// LocateRegistry.createRegistry(1099);
try {
UserHandler userHandler = new UserHandlerImpl();
// Naming 类提供在对象注册表中存储和获得远程对远程对象引用的方法,每个方法都可将某个名称作为其一个参数,
// 该名称是使用以下形式的 URL格式(没有 scheme 组件) 的 java.lang.String :
// host:port/name
// host : 注册表所在的主机(远程或本地),省略则默认为本地主机
// port : 是注册表接受调用的端口号,省略则默认为1099,RMI注册表registry使用的著名端口
// name : 是未经注册表解释的简单字符串
// Naming.bind("//host:port/name", h);
Naming.bind("rmi://localhost:1099/hello", userHandler); // 通过一个名称映射到该远程对象的引用,客户端通过该名称获取该远程对象的引用
Naming.rebind("user", userHandler);
System.out.println(" rmi server is ready ...");
} catch (Exception e) {
e.printStackTrace();
}
构建客户端
客户端代码如下 :
try {
// 获取该远程对象的引用,可以通过 Naming.list(...) 方法列出所有可用的远程对象
UserHandler hello = (UserHandler) Naming.lookup("rmi://localhost:1099/hello");
System.out.println("name: " + hello.getUserName(1));
System.out.println("count: " + hello.getUserCount());
System.out.println("user: " + hello.getUserByName("lmy86263"));
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
UserHandler handler = (UserHandler) Naming.lookup("user");
System.out.println("name: " + handler.getUserName(2));
System.out.println("count: " + handler.getUserCount());
System.out.println("user: " + handler.getUserByName("lmy86263"));
} catch (NotBoundException | MalformedURLException | RemoteException e) {
e.printStackTrace();
}