Java RMI(Java远程方法调用)入门示例 - 高飞网
781 人阅读

Java RMI(Java远程方法调用)入门示例

2017-07-28 02:09:46

    本篇教程,将展示给你使用Java Remote Method Invocation(Java RMI,Java远程方法调用)来创建一个分布式版本的HelloWorld程序的步骤。在学习该示例的过程中,你可能有一些问题,可以参考这里来得到答案 Java RMI FAQ

    该分布式的HelloWorld示例使用一个简易的客户端触发一个方法调用,调用可能运行在远程计算机上的服务的方法。客户端会从服务端接收“Hello World”消息。

    该教程包含以下步骤:

  1. 定义远程接口(remote interface)
  2. 实现远程服务
  3. 实现客户端
  4. 编译源文件
  5. 启动Java RMI注册器,服务和客户端

    该教程包含的源文件有:

  1. Hello.java -- 一个远程接口
  2. Server.java -- 一个实现了远程接口的实现类
  3. Client.java -- 一个简易的客户端,用来调用远程服务接口。


定义远程接口

    一个远程对象是一个实现了远程接口的实例。所谓远程接口是指继承了接口java.rmi.Remote接口并声明了一系列远程方法。每个远程方法声明抛出java.rmi.RemoteException异常(或该异常的超类),此外还有任意的应用级异常。

    以下是这个示例代码:

package rmi.example.hello;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * 远程接口
 * @author xxx
 * @data 2017年3月31日 上午11:26:39
 */
public interface Hello extends Remote {
	/**
	 * 远程方法
	 * 
	 * @param name
	 * @return
	 * @throws RemoteException
	 */
	String sayHello(String name) throws RemoteException;
}

    相对于本地的方法调用,网络调用很可能会失败(由于网络原因或服务端问题),因此可以通过声明的java.rmi.RemoteException异常来报告。

实现服务

    本段中说的“Server”类,它有一个main方法,其中创建了一个远程对象实现的实例,并发布了这个远程对象,然后将之通过指定名字绑定在Java RMI注册器中。

    该示例中,Server中定义了main方法,该类同时实现了Hello接口,main方法中做了如下事情:

  1. 创建并发布远程对象
  2. 注册远程对象到Java RMI注册器上。

    以下是Server类的源码:

package rmi.example.hello;

import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server implements Hello {

	@Override
	public String sayHello(String name) throws RemoteException {
		System.out.println("receive msg :" + name);
		return "Hello :" + name;
	}

	public static void main(String[] args) {
		try {
			Server obj = new Server();
			Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);

			Registry registry = LocateRegistry.getRegistry();
			if(registry.lookup("Hello")!=null){
				registry.unbind("Hello");
			}
			registry.bind("Hello", stub);

			System.out.println("Server ready.");
		} catch (RemoteException | AlreadyBoundException | NotBoundException e) {
			System.err.println("Server exception: " + e.getMessage());
			e.printStackTrace();
		}
	}
}

    这个Server类实现了Hello接口,提供了远程方法sayHello的实现。sayHello在这里并不需要声明抛出任何异常,因为该方法实现本身没有抛出RemoteException或其他异常。

    注:该类可以定义一些接口Hello中没有的方法,只是这些方法只能在本地虚拟机中调用,不能运行在服务中供远程调用。

    笔者注:这时先不要急着运行Server,还有后续一些配置没做,直接运行会报错。

创建并发布远程对象

    main方法中需要创建远程对象以提供服务。此外,远程对象必须在Java RMI运行时导出,以便它可以接收远程的调用。

Server obj = new Server();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 0);

    UnicastRemoteObject.exportObject,这个静态方法,发布了一个远程对象,该对象可以监听在任意的TCP端口上,接收远程方法调用,并返回一个传递给客户端的存根(stub)。当调用这个方法时,RMI的运行时就会在一个新socket或共享的socket上开始监听,并接收来自远程对象的方法调用。返回的存根作为远程对象类,与远程接口一样,并包含了远程对象可以访问的主机名和端口号。

    注:在J2SE5.0发布后,远程对象的存根类不再需要使用rmic命令(JAVA_HOME/bin下的一个命令)来编译生成存根了。除非远程对象需要在5.0之前的虚拟机上运行。如果你需要支持这样的客户端,你就必需生成这样的存根,并发布这样的存根类集给客户端下载使用。要了解如何生成这样的存根,可以查看rmic的工具文档。

用RMI 注册器注册远程对象

    为了远程调用者(客户端,peer或applet)可以在远程对象上调用方法,调用者必须首先得到一个远程对象的存根。为了启动引导,Java RMI为应用提供了一个注册API,将一个远程对象的存根绑定在一个名字上,客户端就可以通过这个名字去发现远程对象以获取这个存根。

    Java的RMI注册,就是一个简化的名字服务,允许客户端得到一个远程对象的引用(存根)。通常情况下,注册表只会在客户端定位第一个远程对象时使用。然后这个对象转而提供给指定应用支持以发现其他的对象。例如,引用可以作为参数被获取,返回值,或其他的方法调用。要明白它的工作原理,可以查看下Java RMI的工厂模式应用

    远程对象在服务上注册后,调用者就可以通过名字来找到对象,获取远程对象的引用,既而调用该对象的远程方法。

    下面是服务端的代码,在本地主机上注册得到一个存根,使用默认的注册端口和注册存根来绑定到名字“Hello”上。

Registry registry = LocateRegistry.getRegistry();
if(registry.lookup("Hello")!=null){
	registry.unbind("Hello");
}
registry.bind("Hello", stub);

    静态方法LocateRegistry.getRegistry(),没有参数,返回一个实现了远程接口java.rmi.registry.Registry的存根,并发送调用消息给本地服务的注册表上,监听在默认的1099端口上。bind方法被注册存根调用,以绑定远程对象的存根到名字“Hello”上。

    注:调用LocateRegistry.getRegistry简单地返回了注册表对应的存根,这个调用并没有检查注册ge是否正在运行。如果本地主机的注册器没有在1099端口上运行,那么当使用bing方法时,服务端将会报告失败并返回RemoteException.

实现客户端

    客户端程序获取服务端机器的注册器,通过名字查找远程对象存根,然后使用存根调用远程对象的sayHello方法。

package rmi.example.hello;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
	public static void main(String[] args) {
		try {
			Registry registry = LocateRegistry.getRegistry(0);
			Hello hello = (Hello) registry.lookup("Hello");
			String result = hello.sayHello("xuyh");
			System.out.println("server :" + result);
		} catch (RemoteException | NotBoundException e) {
			e.printStackTrace();
		}
	}
}

    客户端首先通过静态方法LocateRegistry.getRegistry(0),获取指定主机上的注册存根。如果主机没有被指定,会默认使用本地主机。

    之后,客户端调用远程方法lookup查找注册存根并包含远程注册器上的远程对象的存根。

    最后,客户端调用远程对象存根上的sayHello方法,这会导致下面系列行为的发生:

  1. 客户端运行时打开一个到指定主机和端口的服务端,该服务端上运行着远程对象的存根,并序列化调用数据
  2. 服务端运行时接收进来的调用请求,分发调用到远程对象上,然后将返回结果序列化发送回客户端
  3. 客户端运行时接收并反序列化结果信息

    从远程对象上远程调用返回的消息会被打印出来到标准输出中System.out

编译并运行

    编译略过,使用eclipse时会自动编译代码。

    要运行该示例,需要经过以下的步骤:

  1. 启动Java RMI 注册器
  2. 启动Server
  3. 运行Client

启动Java RMI注册器

    在服务主机,运行rmiregistry命令。该命令运行成功时,没有任何输出,且通常在后台运行。要了解详情可查看相关文档。

Linux下:

$ rmiregistry

windows下:

D:\software\Java\jdk1.7.0_71\bin>start rmiregistry.exe

    默认情况下,注册器运行在1099的TCP端口上。当然也可以指定端口运行,如:

D:\software\Java\jdk1.7.0_71\bin>start rmiregistry.exe 2001

这样注册器就运行在2001端口上了。当注册器运行在非1099端口时,在使用 LocateRegistry.getRegistry方法时,就需要指定端口了,如:

Registry registry = LocateRegistry.getRegistry(2001);


启动服务

    直接在eclipse中运行Server即可。

但你可能遇到下面的异常:

Server exception: Connection refused to host: 192.168.11.158; nested exception is: 
	java.net.ConnectException: Connection refused: connect
java.rmi.ConnectException: Connection refused to host: 192.168.11.158; nested exception is: 
	java.net.ConnectException: Connection refused: connect
	at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
	at sun.rmi.transport.tcp.TCPChannel.createConnection(TCPChannel.java:216)
	at sun.rmi.transport.tcp.TCPChannel.newConnection(TCPChannel.java:202)
	at sun.rmi.server.UnicastRef.newCall(UnicastRef.java:341)
	at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source)
	at rmi.example.hello.Server.main(Server.java:24)
Caused by: java.net.ConnectException: Connection refused: connect
	at java.net.DualStackPlainSocketImpl.connect0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79)
	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
	at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
	at java.net.Socket.connect(Socket.java:579)
	at java.net.Socket.connect(Socket.java:528)
	at java.net.Socket.<init>(Socket.java:425)
	at java.net.Socket.<init>(Socket.java:208)
	at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(RMIDirectSocketFactory.java:40)
	at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(RMIMasterSocketFactory.java:147)
	at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:613)
	... 5 more
    遇到这个异常,很显然,你的注册服务还没有启动,或者启动的端口号与获取注册器时使用的端口号不一致。
或者还可能遇到这样的异常:
Server exception: RemoteException occurred in server thread; nested exception is: 
	java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: 
	java.lang.ClassNotFoundException: rmi.example.hello.Hello
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
	java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: 
	java.lang.ClassNotFoundException: rmi.example.hello.Hello
	at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
	at sun.rmi.transport.Transport$1.run(Transport.java:177)
	at sun.rmi.transport.Transport$1.run(Transport.java:174)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:173)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:556)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:811)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:670)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:745)
	at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:275)
	at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
	at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:378)
	at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
	at rmi.example.hello.Server.main(Server.java:27)
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: 
	java.lang.ClassNotFoundException: rmi.example.hello.Hello
	at sun.rmi.registry.RegistryImpl_Skel.dispatch(Unknown Source)
	at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:409)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
	at sun.rmi.transport.Transport$1.run(Transport.java:177)
	at sun.rmi.transport.Transport$1.run(Transport.java:174)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:173)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:556)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:811)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:670)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ClassNotFoundException: rmi.example.hello.Hello
	at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
	at sun.rmi.server.LoaderHandler$Loader.loadClass(LoaderHandler.java:1206)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:274)
	at sun.rmi.server.LoaderHandler.loadClassForName(LoaderHandler.java:1219)
	at sun.rmi.server.LoaderHandler.loadProxyInterfaces(LoaderHandler.java:729)
	at sun.rmi.server.LoaderHandler.loadProxyClass(LoaderHandler.java:673)
	at sun.rmi.server.LoaderHandler.loadProxyClass(LoaderHandler.java:610)
	at java.rmi.server.RMIClassLoader$2.loadProxyClass(RMIClassLoader.java:646)
	at java.rmi.server.RMIClassLoader.loadProxyClass(RMIClassLoader.java:311)
	at sun.rmi.server.MarshalInputStream.resolveProxyClass(MarshalInputStream.java:255)
	at java.io.ObjectInputStream.readProxyDesc(ObjectInputStream.java:1558)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
	... 13 more
    即找不到rmi.example.hello.Hello接口,这是因为,rmiregister启动时,需要指定相应的classpath
D:\software\Java\jdk1.7.0_71\bin>set classpath=d:\rmi\target\

D:\software\Java\jdk1.7.0_71\bin>start rmiregistry.exe

    现在再运行Server类,就能正常启动了:

Server ready.

运行客户端

    服务端启动后,客户端就也可以启动了。在eclipse直接运行即可,如下,启动客户端后,服务端会收到

Server ready.
receive msg :xuyh
    客户端会收到服务端的响应:
server :Hello :xuyh







参考:

http://docs.oracle.com/javase/7/docs/technotes/guides/rmi/hello/hello-world.html

http://docs.oracle.com/javase/7/docs/technotes/guides/rmi/Factory.html


还没有评论!