此处讲的是Java
中的RMI
,而不是通用意义上的RMI
,关于通用的RMI
能够参考分布式之RPC的协议以及错误处理 这篇文章。html
Java RMI
简介Java RMI
用于不一样虚拟机之间的通讯,这些虚拟机能够在不一样的主机上、也能够在同一个主机上;一个虚拟机中的对象调用另外一个虚拟上中的对象的方法,只不过是容许被远程调用的对象要经过一些标志加以标识。这样作的特色以下:java
在RMI
中的核心是远程对象(remote object),除了对象自己所在的虚拟机,其余虚拟机也能够调用此对象的方法,并且这些虚拟机能够不在同一个主机上。每一个远程对象都要实现一个或者多个远程接口来标识本身,声明了能够被外部系统或者应用调用的方法(固然也有一些方法是不想让人访问的)。web
RMI
的通讯模型从方法调用角度来看,RMI
要解决的问题,是让客户端对远程方法的调用能够至关于对本地方法的调用而屏蔽其中关于远程通讯的内容,即便在远程上,也和在本地上是同样的。shell
从客户端-服务器模型来看,客户端程序直接调用服务端,二者之间是经过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
这样纵向流动的。框架
咱们都知道在Java
程序中引用类型(不包括基本类型)的参数传递是按引用传递的,对于在同一个虚拟机中的传递时是没有问题的,由于的参数的引用对应的是同一个内存空间,可是对于分布式系统中,因为对象再也不存在于同一个内存空间,虚拟机A的对象引用对于虚拟机B没有任何意义,那么怎么解决这个问题呢?maven
Java
中一个对象若是可以被序列化,须要知足下面两个条件之一: Java
的基本类型;java.io.Serializable
接口(String
类即实现了该接口);RMI
中的参数传递和结果返回可使用的三种机制(取决于数据类型):分布式
Remote
接口):以远程对象的引用传递;Remote
接口):按值传递,经过序列化对象传递副本,自己不容许序列化的对象不容许传递给远程方法;在调用远程对象的方法以前须要一个远程对象的引用,如何得到这个远程对象的引用在RMI
中是一个关键的问题,若是将远程对象的发现类比于IP
地址的发现可能比较好理解一些。
在咱们平常使用网络时,基本上都是经过域名来定位一个网站,可是实际上网络是经过IP
地址来定位网站的,所以其中就须要一个映射的过程,域名系统(DNS
)就是为了这个目的出现的,在域名系统中经过域名来查找对应的IP
地址来访问对应的服务器。那么对应的,IP
地址在这里就至关于远程对象的引用,而DNS
则至关于一个注册表(Registry)。而域名在RMI中就至关于远程对象的标识符,客户端经过提供远程对象的标识符访问注册表,来获得远程对象的引用。这个标识符是相似URL
地址格式的,它要知足的规范以下:
URL
形式的,相似于http
的URL
,schema是rmi;rmi://host:port/name
,host
指明注册表运行的注解,port
代表接收调用的端口,name
是一个标识该对象的简单名称。实现RMI
所需的API
几乎都在:
java.rmi
:提供客户端须要的类、接口和异常;java.rmi.server
:提供服务端须要的类、接口和异常;java.rmi.registry
:提供注册表的建立以及查找和命名远程对象的类、接口和异常;其实在RMI
中的客户端和服务端并无绝对的界限,与Web应用中的客户端和服务器仍是有区别的。这二者实际上是平等的,客户端能够为服务端提供远程调用的方法,这时候,原来的客户端就是服务器端。
什么是远程对象?首先从名称上来看,远程对象是存在于服务端以供客户端调用。那么什么对象能够被客户端进行远程调用?这个问题从编程的角度来看,实现了java.rmi.Remote
接口的类或者继承了java.rmi.Remote
接口的全部接口都是远程对象。这些继承或者实现了该接口的类或者接口中定义了客户端能够访问的方法。这个远程对象中可能有不少个方法,可是只有在远程接口中声明的方法才能从远程调用,其余的公共方法只能在本地虚拟机中使用。
实现过程当中的注意事项:
java.rmi.RemoteException
异常,该异常是使用RMI
时可能抛出的大多数异常的父类。java.rmi.server.UnicastRemoteObject
类,该类提供了不少支持RMI
的方法,具体来讲,这些方法能够经过JRMP
协议导出一个远程对象的引用,并经过动态代理构建一个能够和远程对象交互的Stub
对象。具体的实现看以下的例子。首先远程接口以下:
public interface UserHandler extends Remote {
String getUserName(int id) throws RemoteException;
int getUserCount() throws RemoteException;
User getUserByName(String name) throws RemoteException;
}
远程接口的实现类以下:
public class UserHandlerImpl extends UnicastRemoteObject implements UserHandler {
// 该构造期必须存在,由于集继承了UnicastRemoteObject类,其构造器要抛出RemoteException
public UserHandlerImpl() throws RemoteException {
super();
}
@Override
public String getUserName(int id) throws RemoteException {
return "lmy86263";
}
@Override
public int getUserCount() throws RemoteException{
return 1;
}
@Override
public User getUserByName(String name) throws RemoteException{
return new User("lmy86263", 1);
}
}
为了测试在使用RMI
的序列化的问题,这里特别设置了一个引用类型User
:
public class User implements Serializable {
// 该字段必须存在
private static final long serialVersionUID = 42L;
// setter和getter能够没有
String name;
int id;
public User(String name, int id) {
this.name = name;
this.id = id;
}
}
在Java 1.4
及 之前的版本中须要手动创建Stub
对象,经过运行rmic
命令来生成远程对象实现类的Stub
对象,可是在Java 1.5
以后能够经过动态代理来完成,再也不须要这个过程了。
运行该远程对象的服务器代码以下:
UserHandler userHandler = null;
try {
userHandler = new UserHandlerImpl();
Naming.rebind("user", userHandler);
System.out.println(" rmi server is ready ...");
} catch (Exception e) {
e.printStackTrace();
}
这里面的核心代码为Naming.rebind("user", userHandler)
,经过一个名称映射到该远程对象的引用,客户端经过该名称获取该远程对象的引用。
在远程对象中有三个方法:getUserName(int id)
和getUserCount()
的参数和返回结果都是基本类型,所以是默认序列化的,可是对于getUserByName(String name)
方法,返回的结果是一个引用类型,所以会涉及到序列化与反序列的问题,对于User类,必须知足如下条件:
必须实现java.io.Serializable
接口;
其中必须有serialVersionUID
字段,格式以下:
private static final long serialVersionUID = 42L;
若是没有该字段,则默认该类会随机生成一个整数,且在客户端和服务器生成的整数不相同,则会抛出异常以下:
并且在服务器和客户端这个字段必须保持一致才能进行反序列化,若是两端都有该字段,可是数据不一致,则会抛出异常以下:
这个类在服务器和客户端都必须可用;
在序列化的时候,若是在字段前加入了transient
关键字,则该数据不会被序列化;
注册表其实不用写任何代码,在你的JAVA_HOME
下bin
目录下有一个rmiregistry.exe
程序,须要在你的程序的classpath
下运行该程序。
在启动服务器的时候,实际上须要运行两个服务器:
因为远程对象须要与注册表对话,因此必须首先启动注册表程序。当注册表程序没有启动的时候,若是强行启动远程对象服务器时,会抛出以下错误:
确保远程对象类能够被注册表程序发现,当远程对象类没有被注册表程序发现时,则会发现以下错误:
若是是使用maven
管理工程,则在target/classes
目录中启动该程序。
这说明注册表程序时运行在一个单独的进程中的,它做为一个第三方的组件,来协调客户端和服务器之间的通讯,可是与它们两个之间是彻底解决解耦的。
rmiregistry.exe默认状况下是监听1099端口,若是已经该端口已经被使用了,能够经过命令
rmiregistry 1020
指定其余的端口来运行。
能够经过
start rmiregistry
命令在后台运行
运行完注册表程序后,就能够运行远程对象所在的服务器,以便接受客户端的链接。
客户端的代码以下:
try {
UserHandler handler = (UserHandler) Naming.lookup("user");
int count = handler.getUserCount();
String name = handler.getUserName(1);
System.out.println("name: " + name);
System.out.println("count: " + count);
System.out.println("user: " + handler.getUserByName("lmy86263"));
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
在上边的代码中经过Naming.lookup(...)
获取该远程对象的引用。这个方法经过一个指定的名称来获取,该名称必须与远程对象服务器绑定的名称一致。能够经过Naming.list(...)
方法列出全部可用的远程对象。
在使用客户端链接服务器调用远程方法的时候,须要注意的问题以下:
UserHandler
类在客户端本地必须可用,否则没法指定要调用的方法,并且其全限定名必须与服务器上的对象彻底相同,否则抛出以下异常:
从注册表中获取的对象引用已经失去类型信息,须要强制转化为远程对象类型。这样运行客户端的时候才能得到相应的响应;
若是在方法中使用到了引用类型,好比这里的User
,那么该类型的全限定名也必须与服务器的相同,若是不相同则会抛出以下异常:
客户端的引用类型的serialVersionUID
字段要与服务器端的对象保持一致;
在客户端的User
对象以下:
public class User implements Serializable {
// 与客户端的serialVersionUID字段数据一致
private static final long serialVersionUID = 42L;
// setter和getter能够没有
String name;
int id;
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + ", id=" + id + '}';
}
}
对于实现二,和实现一的主要区别在注册表程序的运行,再也不是经过rmiregistry.exe
单独运行,而是经过编程来实现,而远程接口以及其实现类与实现一彻底相同。
这里注册表的实现是经过java.rmi.registry
包中的Registry
接口和以及其实现类LocateRegistry
来完成的。若是你详细查看JDK的源码的话,就会发现其实咱们以前使用的java.rmi.Naming
类中的方法实际上都是间接经过Registry
和LocateRegistry
实现的。
其中获取根据主机和端口获取注册表引用的源码以下:
/** * Returns a registry reference obtained from information in the URL. */
private static Registry getRegistry(ParsedNamingURL parsed) throws RemoteException {
return LocateRegistry.getRegistry(parsed.host, parsed.port);
}
并且Naming中的方法和Registry中是一一对应的。
而若是要建立一个注册表,这里要使用的是LocateRegistry
,该类中只要两类方法:
建立本地注册表而且获取该注册表的引用;
createRegistry(int port)
createRegistry(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf)
直接获取注册表引用,该注册表能够是本地运行的,也能够是远程运行的,这类方法是不可以建立注册表的,只能等注册表程序运行起来以后,和它进行通讯来获取引用,不然抛出异常以下:
其中的方法以下:
getRegistry()
getRegistry(int port)
getRegistry(String host)
getRegistry(String host, int port)
getRegistry(String host, int port, RMIClientSocketFactory csf)
因为是可能从远程主机获取注册表引用,所以可能须要指定Socket
套接字来和远程主机进行沟通,在这个过程当中也有可能由于各类缘由形成调用过程失败;
运行远程对象的服务器代码以下:
UserHandler userHandler = null;
Registry registry = null;
try {
registry = LocateRegistry.createRegistry(1099);
userHandler = new UserHandlerImpl();
registry.rebind("user", userHandler);
System.out.println(" rmi server is ready ...");
} catch (RemoteException e) {
e.printStackTrace();
}
除此以外,其余服务器端和客户端的代码与实现一彻底相同。
关于RMI
的实际使用,其中一种方式能够参考相关文章第6个,经过和ZooKeeper
结合使用RMI
。
相关文章: