RMI基础
一 简介
1.RMI,Remote Method Invocation,即远程方法调用,是Java远程访问的重要规范之一,允许java程序直接调用远程java方法,就像调用该本地java方法一样。Java RMI不是什么新技术,但却是非常重要的底层技术。大名鼎鼎的EJB都是建立在RMI基础之上的,现在还有一些开源的远程调用组件,其底层技术也是RMI。
2.远程访问:网络中的一台计算机,可以使用另一台计算机的服务、对象或方法,效果就像使用本地的服务、对象和方法一样。通过远程访问,程序可以在物理位置不同的机器上运行,从而实现分布式计算
3.RMI是纯JAVA实现的,无需其他额外的支持,实现RMI调用的客户端程序和被调用的方法,都是纯JAVA代码,因此实现起来很简单。在JDK1.5发布以前,读者需要手动使用rmic命名(在JDK安装路径的bin路径下)来编译远程服务类,从而为远程服务类生成静态的Stub和Skeleton类。在JDK1.5及以后的版本中,采用了动态方式来自动生成Stub和Skeleton类,简化了RMI编程。
4.RMI可以说是Java最早实现的远程访问技术,而且性能十分优秀,一直是其他Java EE规范的实现基础。掌握RMI的理论和编程方法,对于开发者更好的理解EJB等分布式组件的底层实现具有重要意义。
RMI其实是对Java Socket网络通信的高度封装,如果应用程序提供基于Socket的网络通信功能,都可以考虑使用RMI实现。
当使用原生Socket进行网络编程时,有两个重要的问题:
网络信息的交换问题:需要采用合适的IO流进行网络读写,如果需要进行复杂的网络信息交换,往往还要封装自己的通信协议
多线程问题:服务器和客户端的网络程序,两端都要采用多线程功能。编写多线程就不可避免的要处理线程同步、线程安全、死锁等棘手问题。
而使用RMI来开发,以上的问题就会迎刃而解,可以简化开发难度,提高开发效率。
二 基本原理
JDK1.5以后动态生成Stub对象来作为客户端调用远程方法的代理,由于Stub是根据远程服务类来动态生成的,因此当远程服务端的服务类改变时,系统会动态的改变所生成的Stub对象。
由图中可以看出,当客户端面向远程服务接口调用远程方法后,会有以下几个步骤:
(1)本地客户端调用远程服务对象的方法,实际上是调用Stub对象的方法。
(2)Stub对象实际上是远程服务对象在客户端的代理。Stub对象会对请求进行编码,保证远程调用请求可以在网络上传输,所以要求调用远程方法的所有参数都是可序列化的。
(3)通过底层网络传输将请求传递到Skeleton
(4)Skeleton对请求进行解码,将请求转换为满足远程服务对象要求的请求。
(5)Skeleton将解码后的请求发送到远程服务对象,让远程服务对象来负责处理调用请求。
(6)Skeleton收到远程服务对象的执行结果,再次对执行结果进行编码,这就要求RMI中的方法返回值都是可序列化的。
(7)通过底层网络传输将结果送到Stub。
(8)Stub解码处理结果
(9)Stub将解码后的符合本地客户端要求的结果送到本地客户端
(10)本地客户端收到执行结果
三 动手
开发RMI服务器
Step1:RMI需要通过远程接口暴露服务,因此所有向北客户端调用的方法都必须在Remote接口里声明,否则无法调用。
远程接口如下:
public interface Server extends Remote{
String helloWorld(String name) throws RemoteException;
Person getPerson(String name,int age) throws RemoteException;
}
注意:远程接口必须集成java.rmi.Remote接口。所有在Remote接口里声明的方法都应该抛出RemoteException异常。
这是因为远程接口中的方法会通过网络传输,而网络传输又是不可靠的,因此都要抛出一个异常。
Step2:定义一个简单的Person类
public class Person implements Serializable{
private String name;
private int age;
...//省略getter/setter等方法
}
Step3:远程服务实现类
public class ServerImpl extends UnicastRemoteObject implements Server{
//由于默认构造器必须声明抛出RemoteException
//因此此处必须显示定义该构造器
public ServerImpl() throws RemoteException{}
@Override
public String helloWorld(String name) throws RemoteException {
return name+", hello";
}
@Override
public Person getPerson(String name, int age) throws RemoteException {
return new Person(name,age);
}
//下面是服务类的本地方法,不会暴露为远程服务
public void info(){
System.out.println("这是本地方法");
}
//提供程序入口,将远程类实例绑定为本机的服务
public static void main(String[] args) throws Exception{
try {
Server imp=new ServerImpl();
LocateRegistry.createRegistry(8888);
Naming.rebind("rmi://localhost:8888//myTest",imp);
}catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}
注意:远程服务类必须继承UnicastRemoteObject,并实现Remote接口,所有继承java.rmi.server.UnicastRemoteObject的对象可以暴露远程服务。
开发RMI客户端
public class RMIClient {
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
Server ser=(Server) Naming.lookup("rmi://127.0.0.1:8888/Server");
System.out.println(ser.helloWorld("lyy"));
System.out.println(ser.getPerson("lyy",20));
}
}
运行时注意,先运行服务器端,在运行客户端。
结果:
lyy, hello
Person [name=lyy, age=20]
四 同时作为客户端和服务器的RMI程序
很多程序,客户端需要调用服务器的远程方法,服务器端也要反过来调用客户端的远程方法,这样才能形成一个完整的网络通信程序。但是服务器端和客户端存在很多区别,因此服务器端不大可能直接调用远程客户端的方法,只能先由客户端调用服务器端的远程方法,这样服务器端才可以回过来调用远程客户端的方法,这种调用方法也被成为回调(callback)。
举例:聊天窗口
客户端
建包client
Step1:由于客户端也要被远程服务器回调,因此客户端程序也要提供远程服务。
public interface Client extends Remote {
void showDialog(String msg) throws java.rmi.RemoteException;
}
Step2:服务实现
public class RMIClient implements Client{
public static void main(String[] args) throws Exception{
Client client = new RMIClient();
UnicastRemoteObject.exportObject(client);
Server stub=(Server)Naming.lookup("rmi://127.0.0.1:8888/Server");
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
String line=null;
while((line=br.readLine())!=null){
//调用远程方法时,将自身作为参数
stub.hello(client,line);
}
}
@Override
public void showDialog(String msg) throws RemoteException {
System.out.println(msg);
}
}
服务器端程序
建包server
Step1:接口
public interface Server extends Remote{
//定义允许远程调用的方法
void hello(Client client,String saying) throws Exception;
}
Step2:实现
public class ServerImpl extends UnicastRemoteObject implements Server{
public ServerImpl() throws RemoteException{}
//定义一个List保存所有连接进来的客户
static List<Client> users=new ArrayList<Client>();
//提供程序入口,将远程类实例绑定为本机的服务
public static void main(String[] args) throws Exception{
try {
Server imp=new ServerImpl();
LocateRegistry.createRegistry(8888);
Naming.rebind("rmi://127.0.0.1:8888/Server",imp);
}catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
@Override
public void hello(Client client, String saying) throws Exception {
if(!users.contains(client)){
users.add(client);
}
try{
java.util.Date now=new java.util.Date();
String msg=now+saying;
for(Client c:users){
//回调远程客户端方法
c.showDialog(msg);
}
}catch(RemoteException ex){
users.remove(client);
}
}
}