如果要我们手动去实现这些步骤,将遇到一个很大的编码难题:必须设计一种传输格式,还必须为数据与该传输之间的转换而编码,幸运的是在Web应用中W3C设计出了HTTP协议,浏览器厂商遵循HTTP协议能解析HTTP响应,Web服务器厂商也遵循HTTP协议能够解析客户端提交的请求并作出响应返还给浏览器。而程序员只要遵循HTTP协议与相应Web服务器技术规范就能实现浏览器与Web服务器之间的通信,而底层的交互都由遵循HTTP协议Web服务器与浏览器完成了。
我们需要的也是一种类似的机制,客户端程序员以常规的方式进行方法调用,而无而操心数据在网络上传输或者解析响应之类的问题,但问题是,提供服务的对象可能不在同一个虚拟机内,甚至,它可以不是Java语言实现的对象。其解决办法是,在客户端为服务对象安装一个代理(proxy),客户端调用此代理,而代理对象负责与服务器联系。同理,编写服务器对象的程序员也不想因与客户端之间的通信而被绊住,解决办法是在服务端安装第二个代理对象,该服务器代理与客户端代理进行通信,并且它将以常规方式调用服务器对象上的方法,如下图:
代理之间的通信方法通常有三种:RMI、CORBA、SOAP。而RMI就是Java提出的远程方法调用技术,支持Java的分布式对象之间的方法调用。CORBA与SOAP都是完全独立于语言的,而RMI只支持Java的分布式对象。
Java远程方法调用中,客户代码要在远程对象上调用一个远程方法时,实现上调用的是代理对象上的一个普通方法,这个代理对象被称为存根(sub),存根位于客户端机器上,存根将远程方法所需的参数打包成一组字节。这个打包的过程使用与硬件无关的编码方式来编码第一个参数,在RMI协议中数字总是以大尾数法字节顺序发送,而对象则使用Java的序列化机制进行编码的。对参数的编码过程称为参数编组(paramerer marshalling),参数编组的目的是将参数转换成适合在虚拟机之间进行传递的格式。
一次远程方法调用的流程为:客户端的存根方法构造了一个信息块,它由以下几部分组成:
a.被使用的远程对象的标识符;
b.被调用的方法的描述;
c.编组后的参数。
然后,存根将此信息发送给服务器,在服务器端,一个接收对象为每个远程方法调用执行以下动作:
a.反编组参数
b.定位要调用的对象
c.捕获返回值或该调用产生的异常,并对它编组;
d.将返回值编组,打包送回给客户端存根。
如图:
下面是远程方法调用的一个例子:
在Java中一个对象要能够用于远程调用,有一定限制,远程对象所在类必须实现接口,并且该接口一定要继承java.rmi.Remote;接口而该类通常都会继承自java.rmi.server.RemoteServer类,而该类是一个抽象类,它只定义了服务器对象与远程存根通信的基本机制,而java.rmi.server.UnicastRemoteObject 继承自RemoteServer类,UnicastRemoteObject不是抽象类故而可以直接使用它,所以我们真正继承的是java.rmi.server.UnicastRemoteObject类,下面就是一个远程服务器类:
public interface Product extends Remote { String getDescription() throws RemoteException; } public class ProductImpl extends UnicastRemoteObject implements Product { private static final long serialVersionUID = -2489180038994927428L; private String desc; public ProductImpl(String desc) throws RemoteException { this.desc = desc; } public String getDescription() throws RemoteException { return "I am a " + desc + ". Buy me!"; } }
下面启动RMI服务端:
public class ProductServer { public static void main(String[] args) throws Exception { LocateRegistry.createRegistry(1099); System.out.println("Constructing server implementations..."); ProductImpl p1 = new ProductImpl("Blackwell Toaster"); ProductImpl p2 = new ProductImpl("ZapXpress Microwave Oven"); System.out.println("Binding server implementations to registry..."); Context namingContext = new InitialContext(); namingContext.bind("rmi:toaster", p1); namingContext.bind("rmi:microwave", p2); System.out.println("Waiting for invocations from clients..."); } }
LocateRegistry.createRegistry(1099);必须为RMI注册端口号,RMI默认的端口号就为1099,否则服务器会拒绝连接。如果要修改默认的调用器可以显示调用UnicastRemoteObject的protected UnicastRemoteObject(int port) throws RemoteException重载构造方法。
如果你不用代码进行注册也可以在命令行输入命名rmiregistry 1099,其效果与代码注册是一样的。
namingContext.bind("rmi:toaster", p1);为对远程对象进行绑定,只有进行了绑定客户而才能通过相应的标识符进行查找到远程对象存根。绑定也可以调用Naming.bind()方法,在JDK1.3以后RMI命名服务被整合到了JNDI中。
运行main方法就可以启动服务端。
下面编写客户端:
public class ProductClient { public static void main(String[] args) throws Exception { System.setProperty("java.security.policy", "client.policy");//指定安全策略文件 System.setSecurityManager(new RMISecurityManager());//注册安全管理器 String url = "rmi://127.0.0.1:1099/"; Context namingContext = new InitialContext(); Product p1 = (Product) namingContext.lookup(url+"toaster");//查找远程对象,其实得到的是一个存根对象,该存根实现了相应的接口 System.out.println(p1.getClass()); Product p2 = (Product) namingContext.lookup(url+"microwave"); System.out.println(p1.getDescription()); System.out.println(p2.getDescription()); } }
使用RMI的客户端程序要安装一个安全管理器,用以控制动态加载存根的行为,RMISecurityManager就是这样的一安全管理器,安装代理很简单System.setSecurityManager(new RMISecurityManager());(Java程序默认是没有安全管理器的)默认情况下RMISecurityManager将对程序中建立网络连接的所有代码进行限制,然而,程序需要建立网络连接以达到RMI注册表与联系服务器对象,所以我们要通过指一定个策略文件来允许客户端连接RMI注册表以及服务器对象。策略文件如下:
grant {
permission java.net.SocketPermission "*:1024-65535", "connect";
};
文件名可以任意,在程序中我们指定的文件名为client.policy,如果你是用eclipse运行客户端请将该策略文件放置在项目根路径下,如果你是以命令行运行请将策略文件放置在类根路径下,否则会找不到策略文件而访问遭拒绝。
除了可以设置系统属性指定策略文件外也可以在运行java使命的时候通过设置运行参数指定,如:java -Djava.security.policy=client.policy xxx.xxx.ProductClient这时的路径是相对于命令窗口所在目录的。
如果使用的是JDK1.4或更低的版本,客户端存根类是要通过rmic命令进行生成的,如:rmic xxx.xxx.ProductImpl,注意编译的是类这样会生成一个名为xxx.xxx.ProductImpl_Sub的类,然后将这个类放置在客户端类路径上,但在JDK1.5开始,该存根类可以通过动态代理生成,就不需要执行rmic命令了。当然如果这时你就是要运行rmic命令也是可以的,这样客户端还是会使用该命令生成的存根类而不使用动态代理。
运行客户端main方法应该就可以看到相应的输出了。
如果你把这个例子把服务端搬到另一台机器上可能会报java.rmi.ConnectException: Connection refused to host: 127.0.0.1异常,至于原因,请参看:异常原因