所谓RMI(Remote Method Invocation)即远程方法调用。用以实现系统(JVM)间对象的互操作。Java从JDK1.1开始就提供了关于它的API,从而大大增强了Java开发分布式应用的能力。RMI是Java的,就是说它是用Java语言实现的,可能连他的名字也是SUN给的吧:)。不管那么多,来开一下如何使用RMI。
从上面的示图可以看到,与其他类似的服务体系一样,在RMI体系中,提供方法调用服务的类可以理解为Server,而使用这些服务的类可以叫做Client。即前者是被调用者,后者是调用者。
一个正常工作的RMI系统由下面几个部分组成:
·远程服务的接口定义
·远程服务接口的具体实现
·桩(Stub)和框架(Skeleton)文件
·一个运行远程服务的服务器
·一个RMI命名服务,它允许客户端去发现这个远程服务
·类文件的提供者(一个HTTP或者FTP服务器)
·一个需要这个远程服务的客户端程序
RMI是面向接口编程的。比如说我们要把Server类中的方法提供给其他对象调用,那我们首先要定义一个远程服务的接口,不妨记作ServerI,在这个接口中进行服务方法的签名。这样,客户端就根据服务接口里的签名方法操作远程的对象。既然我们要通过RMI来实现方法调用,因此服务的接口类和实现类都要继承RMI的API,因此ServerI和Server的完整代码写出来就是下面这样:
有几点需要说明:
1、服务的接口类要继承java.rmi.Remote接口,这仅是一个标记接口,标识一个RMI远程接口,不实现任何方法。
2、服务的实现类在实现接口类的同时还要继承java.rmi.server.UnicastRemoteObject类,这使得实现类可以通过远程被操作。
3、接口类和实现类中用于提供RMI服务的方法都要throws RemoteException,因为网络是不稳定的,这些远程调用的方法随时可能因为网络的问题而产生不可预知的异常。
4、服务的实现类必须有一个无参的构造方法,并且该构造方法也要throws RemoteException,因此我们必须写一个显式的无参构造方法,即使它什么都不做。
5、如果远程方法需要参数或返回值,参数和返回值必须是实现了Serializable接口的对象。因为远程的方法的参数、返回值都必须在网络上传输,网络只能传输字节流,因此,要求参数、返回值都可以转换成字节流——即实现序列化。实际上,就连服务实现类本身也通过继承UnicastRemoteObject实现了Serializable接口。
6、RMI使用的协议是Java远程方法协议(Java Remote Method Protocol)。这需要RMI注册服务的支持。启动RMI注册服务方法是在命令行输入:start rmiregistry。RMI服务默认的端口号是1099。如果要使用其他端口,只要在命令后面跟一个端口号。比如,我们这个例子要使用的端口是2008,我们会输入:start rmiregistry 2008。RMI注册服务启动的后,我们就可以把所提供的服务对象注册(或者说绑定)在服务器上,在正如我们所写的代码:
Server sv = new Server();
Naming.bind("rmi://192.168.0.242:2008/myServer", sv);
这里我们首先生成一个服务对象sv,然后调用了java.rmi.Naming类的静态方法static void bind(String name, Remote obj)来绑定服务对象。其中“rmi:”表示RMP协议,这个可以省略不写。“sv”是我们绑定的服务对象,绑定的名称是“myServer”。“192.168.0.242”是服务器IP地址,这必须是一个客户机能够看到的IP地址,就是说如果你的服务器有多个IP,请确保这里注册的IP地址是客户端能够找到的。你也可以写机器的名字,同样也要保证客户机认识这个名字。“2008”是这个例子中RMI服务的端口。举个例子,假设你有一个服务对象urServer,服务器名称是ABC,服务器IP是10.38.159.24,端口采用默认的1099,服务对象的注册名称是theServer,那么你可以写Naming.bind("//ABC/theServer", urServer);或者Naming.bind("//10.38.159.24/theServer", urServer);。
7、在服务器的RMI注册服务中,绑定的对象必须有唯一的注册名称。这里sv对象注册的名称是“myServer”,因此其他服务对象就不能再使用这个名称,否则会产生异常。如果你不能保证你要用的注册名称没有在此之前被使用,你可以使用Naming.rebind(String name, Remote obj)方法,这样将覆盖之前被绑定的对象。当然你也可以先用Naming.lookup(String name)查找,然后决定使用什么注册名。但是这样客户端如何知道你使用了什么名称呢,这由你来决定吧,呵呵。
接下来看一下客户端如何调用远程的服务,客户端可以是一个普通的类,像这样:
同样要说明一下:
1、192.168.0.242是我那台服务器的ip地址,这里同样可以写机器名。RMI服务的端口是2008,服务对象的名字是那个“myServer”。
2、可以看到,客户端得到的只是一个服务接口的引用句柄sv,他并不关心服务是如何实现的,只要按照服务接口里的签名方法调用服务就OK了。
现在我们来编译这写代码:
编译上面的三个文件分别产生以下class文件:
ServerI.class、Server.class、Client.class。
然后运行rmic编译器,产生桩(Stub)和框架(Skeleton)文件。stub文件用于与客户端交流,建立Socket请求连接;skeleton文件用于与服务器端交流,建立ServerSocket监听请求。
执行:rmic Server
产生:Server_Stub.class、Server_Skel.class
由于我们在程序中引入了java的安全管理机制(沙箱),所以需要提供安全策略。这里我们编写两个策略文件,一个用于服务端,另一个用于客户端。
服务端srv.policy:
客户端clt.policy:
如果我们不打算引入安全策略,只要把Server.java和Client.java里的这句话注释掉:System.setSecurityManager(new RMISecurityManager());,这样也就不需要提供策略文件。俗话说“安全第一”,所以我还是引入了安全策略。
接下来是程序的部署:
服务器端:ServerI.class、Server.class、Server_Skel.class、srv.policy
客户端:Client.class、ServerI.class、Server_Stub.class、clt.policy
这里我选择了两台机器进行测试,服务器是192.168.0.242,客户机是192.168.0.64。
最后我们可以运行程序了:
服务器端:
1、打开一个命令行窗口,输入:
start rmiregistry 2008
这将启动RMI注册服务,此时会弹出另一个命令行窗口,请不要关闭它。
2、还是在原来的命令行窗口,输入:
java -Djava.security.policy=srv.policy Server,启动服务端程序,可以看到命令行打出的消息:Ready to serve
客户端:
1、打开一个命令行窗口,输入:
java -Djava.security.policy=clt.policy Client
可以看到命令行打出的消息:
PerfectTime:1208953133390
I am the server, I received your message:~Hello,i am the client !
服务器端:
命令行窗口也打出消息:
Message received from client:~Hello,i am the client !
最后的说明:
1. 如果你在同一台机器上运行这个例子,只要把所有的IP地址都改成你机器的IP就可以。
2. 这个例子中,我们是手动在控制台启动了RMI注册服务。如果你不想这样,你也可以在程序中启动它,只须在Server类中调用LocateRegistry.createRegistry(2008);把它写在Naming.bind方法前面就可以。这样就不需要手动执行rmiregistry 命令。
3. 关于策略文件,我们也可以用policytool.exe来编辑。