RMI笔记
这是《java核心技术》 第11章 分布式对象的笔记。
RMI基本原理
我们使用远程方法调用是希望达到这样的目的: 可以像调用本地方法一样去调用一个远程方法。 实现远程调用的方式是 为客户端和服务器端各自生成一个代理对象,客户端调用远程方法其实就是调用客户端代理对象的本地方法,在代理对象的本地方法中,会将参数编组以在网络传输,然后远程访问到 服务器端的代理对象,服务器端代理将根据客户端代理的具体请求信息来定位真实处理这个请求的服务器本地方法,并把最终的执行结果或者异常信息返回给客户端代理,最终返回给客户端本地方法,这一过程在RMI中对于程序员是透明的。下图中的存根(stub)为客户端代理,接受者为服务器端代理。
RMI注册表
现在有一个问题是客户端必须首先能够获得一个存根对象才可以发起远程调用。为了解决这个问题服务器端必须把至少一个服务器对象注册到RMI注册表中,以供客户端获取。假如我们有多个远程对象,我们当然可以在注册表中注册多个,但是更好的方式是通过已注册的远程对象来获取其他远程对象,后面后有例子。
1) By default, the RMI Registry uses port 1099.
2) Client and server (stubs, remote objects) communicate over random ports. The communcation is started via a socket factory which uses 0 as starting port, which means "use any port that's available" between 0 and 65535.
关于上图,默认情况下RMI 注册表使用端口1099监听,当客户端从注册表中获得了一个stub后,将使用在0到65535之间的任意端口来与服务器端代理进行通信。
基本范例:
server:server端有三个类:
1 import java.rmi.Remote; 2 import java.rmi.RemoteException; 3 4 /* 5 * 远程对象接口必须extends Remote,并且其中定义的远程方法必须抛出RemoteException 6 */ 7 public interface Warehouse extends Remote{ 8 double getPrice(String description) throws RemoteException; 9 }
1 import java.rmi.RemoteException; 2 import java.rmi.server.UnicastRemoteObject; 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /* 7 * 有两种方式来实现远程对象,一种是 extends UnicastRemoteObject, 8 * 另一种是 调用 UnicastRemoteObject.exportObject(this, 0);就像构造器中注释掉的那样 9 * 10 */ 11 public class WarehouseImpl extends UnicastRemoteObject implements Warehouse { 12 private static final long serialVersionUID = -4941915511359795398L; 13 private Map<String, Double> map = new HashMap<>(); 14 15 public WarehouseImpl() throws RemoteException{ 16 //UnicastRemoteObject.exportObject(this, 0); 17 map.put("Apple", 11.1); 18 map.put("Orange", 22.2); 19 } 20 21 @Override 22 public double getPrice(String description) throws RemoteException { 23 Double price = map.get(description); 24 if(price == null){ 25 throw new RuntimeException("can not find this stuff in the warehouse !!!"); 26 }else{ 27 return price; 28 } 29 } 30 }
1 import java.rmi.RemoteException; 2 3 import javax.naming.Context; 4 import javax.naming.InitialContext; 5 import javax.naming.NamingException; 6 7 /* 8 * 在这里将远程对象绑定到注册表中,然后等待客户端连接 9 */ 10 public class WarehouseServer { 11 public static void main(String[] args) throws RemoteException, NamingException { 12 Warehouse warehouse = new WarehouseImpl(); 13 14 Context namingContext = new InitialContext(); 15 16 //RMI的URL以rmi:开头,后接服务器名称和端口,然后是一个远程对象的唯一名字。rmi://xx.xx.xx.xx:99/central_warehouse 17 //默认主机名是localhost,端口是1099,可以省略,也就成了 rmi:central_warehouse 18 namingContext.bind("rmi://localhost:1088/central_warehouse", warehouse); 19 20 System.out.println("rmi server is ready, waiting for clients ....."); 21 } 22 }
client端有两个文件,一个是和server中相同的Warehouse(这里没有包,如果有包的话,包名也要一样)
1 import java.rmi.RemoteException; 2 3 import javax.naming.Context; 4 import javax.naming.InitialContext; 5 import javax.naming.NamingException; 6 7 public class Client { 8 public static void main(String[] args) throws NamingException, RemoteException { 9 Context namingContext = new InitialContext(); 10 //我们这里所获得到的其实就是那个stub存根对象 11 Warehouse warehouse = (Warehouse)namingContext.lookup("rmi://localhost:1088/central_warehouse"); 12 System.out.println("stub class : " + warehouse.getClass().getName() ); 13 System.out.println("invoke remote method ......"); 14 System.out.println(warehouse.getPrice("Apple")); //远程对象可以正常调用 15 System.out.println(warehouse.getPrice("Banana"));//远程对象将抛出异常 16 } 17 }
部署:
编译后的class文件的分布:
首先我们要启动rmi注册表来等待服务器端向其中绑定远程对象,默认监听1099端口,可以通过 rmiregistry port 来手动指定。 这里需要注意的是 rmi注册表必须能够访问到Warehouse.class 这样的远程方法双方都需访问的文件,访问的方法有:
1.最简单的方式就是在拥有这些文件的目录下启动rmiregistry
2.配置CLASSPATH包含这些文件
3.通过-Djava.rmi.server.codebase参数 在启动server的时候通过这个系统参数告诉注册表,参数的值是一个URL,例如 http://192.168.4.1/export/ 或者 file:///home/admin/zhangcheng/server/ (值得注意的是url后面的分隔符是必须的), 但是如果我们希望通过这种方式让rmi加载文件的话,必须在启动的rmi注册标的时候加上 -J-Djava.rmi.server.useCodebaseOnly=false, 关于这个参数可以参考: http://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
下面将按照第三种方式启动,在rmi中启动rmiregistr, 在server中启动 服务器端,在client中发起客户端访问。
rmi注册表:
启动server端:
发起client访问:
下面我们对这个例子进行一些扩展,主要是看看RMI如何传递对象的。
1. 所有实现Remote的对象我们称之为 远程对象,在RMI中传递远程对象 其实就是传递了 由网络地址和方法唯一标识符组成的信息, 通过这些信息可以使客户端存根和服务器接受者之间建立起远程访问。(仅仅是我的理解)
2. 其他对象必须实现Serializable的对象通过序列化机制进行传输,例如上面例子中的String参数 就是一个Serializable对象。
对于warehouse例子的扩展用来展示如何传递远程对象(主要改变的代码使用红色标记出来)
server端:
1 import java.rmi.Remote; 2 import java.rmi.RemoteException; 3 4 /* 5 * 远程对象接口必须extends Remote,并且其中定义的远程方法必须抛出RemoteException 6 */ 7 public interface Warehouse extends Remote{ 8 double getPrice(String description) throws RemoteException; 9 //增加了一个访问备份仓库的远程方法,通过这个远程方法,我们可以从一个远程对象中获得另一个远程对象。 10 Warehouse getBackup() throws RemoteException; 11 }
1 import java.rmi.RemoteException; 2 import java.rmi.server.UnicastRemoteObject; 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /* 7 * 有两种方式来实现远程对象,一种是 extends UnicastRemoteObject, 8 * 另一种是 调用 UnicastRemoteObject.exportObject(this, 0);就像构造器中注释掉的那样 9 * 10 */ 11 public class WarehouseImpl extends UnicastRemoteObject implements Warehouse { 12 private static final long serialVersionUID = -4941915511359795398L; 13 private Map<String, Double> map = new HashMap<>(); 14 private Warehouse backup ; 15 16 public WarehouseImpl(Warehouse backup) throws RemoteException{ 17 //UnicastRemoteObject.exportObject(this, 0); 18 this.backup = backup; 19 } 20 21 public void add(String desc, Double price){ 22 map.put(desc, price); 23 } 24 25 @Override 26 public double getPrice(String description) throws RemoteException { 27 Double price = map.get(description); 28 if(price == null){ 29 throw new RuntimeException("can not find this stuff in the warehouse !!!"); 30 }else{ 31 return price; 32 } 33 } 34 35 @Override 36 public Warehouse getBackup() throws RemoteException { 37 return backup; 38 } 39 }
1 import java.rmi.RemoteException; 2 3 import javax.naming.Context; 4 import javax.naming.InitialContext; 5 import javax.naming.NamingException; 6 7 /* 8 * 在这里将远程对象绑定到注册表中,然后等待客户端连接 9 */ 10 public class WarehouseServer { 11 public static void main(String[] args) throws RemoteException, NamingException { 12 WarehouseImpl backup = new WarehouseImpl(null); 13 backup.add("Banana", 33.3); 14 15 //为绑定在注册表中的远程对象增加一个可获得的远程对象 16 WarehouseImpl warehouse = new WarehouseImpl(backup); 17 warehouse.add("Apple", 11.1); 18 warehouse.add("Orange", 22.2); 19 20 Context namingContext = new InitialContext(); 21 22 //RMI的URL以rmi:开头,后接服务器名称和端口,然后是一个远程对象的唯一名字。rmi://xx.xx.xx.xx:99/central_warehouse 23 //默认主机名是localhost,端口是1099,可以省略,也就成了 rmi:central_warehouse 24 namingContext.bind("rmi://localhost:1088/central_warehouse", warehouse); 25 26 System.out.println("rmi server is ready, waiting for clients ....."); 27 } 28 }
client端:
1 import java.rmi.RemoteException; 2 3 import javax.naming.Context; 4 import javax.naming.InitialContext; 5 import javax.naming.NamingException; 6 7 public class Client { 8 public static void main(String[] args) throws NamingException, RemoteException { 9 10 Context namingContext = new InitialContext(); 11 //我们这里所获得到的其实就是那个stub存根对象 12 Warehouse warehouse = (Warehouse)namingContext.lookup("rmi://localhost:1088/central_warehouse"); 13 System.out.println("stub class : " + warehouse.getClass().getName() ); 14 System.out.println("invoke remote method ......"); 15 try { 16 System.out.println(warehouse.getPrice("Banana"));//远程对象将抛出异常 17 } catch (Exception e) { 18 System.out.println("occur exception when access the main warehouse, now access the backup warehouse !!!!"); 19 //我们在客户端通过一个远程对象(warehouse)获得了另一个远程对象(backup),这个backup和warehouse一样,其实就是一个stub对象,我们可以通过它像调用本地方法一样调用远程方法。 20 Warehouse backup = warehouse.getBackup(); 21 System.out.println(backup.getPrice("Banana")); 22 } 23 } 24 }
client调用结果:
关于RMI注册表还有另外一种方式,从实现上来看稍微简单一些,其实本质上是一样的,还是理解原理最重要,可以参考
http://lavasoft.blog.51cto.com/62575/91679/
其核心就是 通过 LocateRegistry.createRegistry(8888); 这种方式在server中用代码启动rmi注册表。
另外在《java核心技术》 中还讲解了 动态加载类 延迟启动等知识,这些知识牵扯到JVM动态加载类文件等安全机制,我自己照葫芦画瓢的确实现了,但是不是太明白,就不记录了,等搞明白 JVM的安全机制再回头来看看吧!!!
使用spring可以非常容易集成RMI,但是有一点要注意: Spring的RMI server是无法与 原生的RMI client通信的,这在不是使用Spring实现的项目中会是一个问题。
每一个不能起舞的日子,都是对生命的一种辜负, 翻滚吧,少年!!!