转载

Java远程方法调用RMI简单介绍

RMI介绍

RMI(Remote Method Invocation,远程方法调用)是用Java在JDK1.1中实现的。经过多个JDK版本迭代,目前RMI的实现方式跟最开始底层实现还是有很大差别的。远程方法调用允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

RMI使用JRMP(Java Remote Messaging Protocol)进行通信。JRMP是专为Java的远程对象制定的协议。因此,Java RMI具有Java的”Write Once,Run Anywhere”的优点,是分布式应用系统的百分之百纯Java解决方案。RMI主要使用在跨进程应用中或者分布式应用,因此一般开发过程中很少接触到RMI编程。

RMI底层是通过Socket进行的通信,但是使用RMI的好处在于我们不需要面向网络编程,摒除了复杂的网络解析那一层,仍然是采用面向对象的方式。由于RMI会创建一个类似客户辅助对象和服务辅助对象,客户调用客户辅助对象上的方法,仿佛客户辅助对象就是真正的服务。客户辅助对象再为我们转发这些请求。在服务端,服务辅助对象负责从客户辅助对象接收请求,将调用的信息解包,然后调用真正的服务方法。这种操作方式中,同服务对象交互的也是服务辅助对象。RMI辅助生成客户辅助对象和服务辅助对象。

Java远程方法调用RMI简单介绍

由于在使用RMI编程的时候,服务端和客户端不是同一个工程目录,因为他们之间调用不是类与类之间的直接调用,在上面也介绍了实际上RMI底层是通过Socket传输数据的,因此RMI中所有涉及到远程方法调用的变量都必须是可序列化的。

RMI一般将客户辅助对象称为Stub(桩),服务辅助对象称为Skeleton(骨架)。现在新版的Java已经不需要显示的Stub和Skeleton对象了,但是尽管如此,还是由一些东西负责Stub和Skeleton行为的。

RMI使用一般要遵循如下规则:

  • 远程接口必须为public属性(不能是“包访问”),否则一旦Client试图装载一个实现了远程接口的远程对象,就会得到一个错误;
  • 远程接口必须扩展(extends)接口java.rmi.Remote;
  • 除了应用程序本身可能抛出的Exception外,远程接口中的每个方法还必须在自己的throws从句中声明抛出java.rmi.RemoteException(否则运行Server时会抛出java.rmi.server.ExportException);
  • 作为参数或返回值传递的一个远程对象必须声明为远程接口,不可声明为实现类。

RMI举例

首先定义一个实体类,实现Serializable接口。

public class User implements Serializable {
 
	private static final long serialVersionUID = 1L;
 
	public String name;
	public int age;
 
	public User(String name, int age) {
		this.name = name;
		this.age = age;
	}
 
	@Override
	public String toString() {
		return "User [name=" + name + ", age=" + age + "]";
	}
 
}

定义一个远程接口以及该接口实现类。

public interface RemoteUser extends Remote {
	
	User getUser() throws RemoteException;
	
	int getAge() throws RemoteException;
	
}
public class RemoteUserImpl implements RemoteUser {
 
	@Override
	public User getUser() throws RemoteException {
		return new User("admin",20);
	}
 
	@Override
	public int getAge() throws RemoteException {
		return 20;
	}
 
}

创建导出远程对象,注册远程对象,向客户端提供远程对象服务。

public static void testUser(){
	try {
		RemoteUser user = new RemoteUserImpl();
		RemoteUser stub = (RemoteUser) UnicastRemoteObject.exportObject(user, 9999);
		LocateRegistry.createRegistry(1099);
		Registry registry = LocateRegistry.getRegistry();
		registry.bind("user", stub);
		System.out.println("绑定成功!");
	} catch (RemoteException e) {
		e.printStackTrace();
	} catch (AlreadyBoundException e) {
		e.printStackTrace();
	}
}

在本示例中UnicastRemoteObject是使用exportObject()方法来处理远程对象的,实际上也可以在远程接口的实现类直接继承自UnicastRemoteObject。直接使用继承的方式是比较简单的方式创建远程对象,但是需要提供一个无参的构造方法,并抛出RemoteException异常。

客户端将服务端创建的实体类以及远程接口需要Copy一份过来,包名不可以更改。

public static void testUser() {
	try {
		Registry registry = LocateRegistry.getRegistry("localhost");
		RemoteUser remoteUser = (RemoteUser) registry.lookup("user");
		User user = remoteUser.getUser();
		int age=remoteUser.getAge();
		System.out.println(user);     //User [name=admin, age=20]
		System.out.println(age);      //20
	} catch (RemoteException e) {
		e.printStackTrace();
	} catch (NotBoundException e) {
		e.printStackTrace();
	}
}

使用rmic命令查看编译后的源码

rmic是Java中RMI的编译命令。rmic编译的时候跟javac不一样,类名一定要写全,比如:rmic -classpath D:/workspace/bin com.sunny.server.RemoteUserImpl。而且文件名后面不能有.class。还有这个class一定要放在classpath下。

在JDK1.8中使用rmic命令可以看到只生成了RemoteUserImpl_Stub.class,但是rmic命令可以指定编译时使用的rmic版本号,当使用v1.1版本时仍然可以看到RemoteUserImpl_Skel.class文件。

JDK1.8 rmic生成的文件反编译如下

public final class RemoteUserImpl_Stub extends RemoteStub implements RemoteUser{
 
	private static final long serialVersionUID = 2L;
	private static Method $method_getAge_0;
	private static Method $method_getUser_1;
	
	static 
	{
		$method_getAge_0 = (com.sunny.server.RemoteUser.class).getMethod("getAge", new Class[0]);
		$method_getUser_1 = (com.sunny.server.RemoteUser.class).getMethod("getUser", new Class[0]);
	}
 
	static Class class$(String s){
	    ...
		return Class.forName(s);
	}
	
	public RemoteUserImpl_Stub(RemoteRef remoteref){
		super(remoteref);
	}
 
	public int getAge() throws RemoteException{
		Object obj = super.ref.invoke(this, $method_getAge_0, null, 0x6d7a3d73b0a83838L);
		return ((Integer)obj).intValue();	
	}
 
	public User getUser() throws RemoteException{
		Object obj = super.ref.invoke(this, $method_getUser_1, null, 0x57ab22fce7623f5eL);
		return (User)obj;
	}
 
}

使用rmic v1.1生成的类

public final class RemoteUserImpl_Stub extends RemoteStub implements RemoteUser{
 
	private static final Operation operations[] = {
		new Operation("int getAge()"), new Operation("com.sunny.server.bean.User getUser()")
	};
	private static final long interfaceHash = 0xeb847cb50cd67648L;
 
	public RemoteUserImpl_Stub(){}
 
	public RemoteUserImpl_Stub(RemoteRef remoteref){
		super(remoteref);
	}
 
	public int getAge()throws RemoteException{
		RemoteCall remotecall = super.ref.newCall(this, operations, 0, 0xeb847cb50cd67648L);
		super.ref.invoke(remotecall);
		ObjectInput objectinput = remotecall.getInputStream();
		int i = objectinput.readInt();
		super.ref.done(remotecall);
		...
		return i;
	}
 
	public User getUser() throws RemoteException{
	    ...
		User user = (User)objectinput.readObject();
		return user;
	}
 
}
public final class RemoteUserImpl_Skel implements Skeleton{
 
	private static final Operation operations[] = {
		new Operation("int getAge()"), new Operation("com.sunny.server.bean.User getUser()")
	};
	...
	public void dispatch(Remote remote, RemoteCall remotecall, int opnum, long hash)throws Exception{
		
		RemoteUserImpl remoteuserimpl = (RemoteUserImpl)remote;
		switch (opnum)
		{
		case 0: // '/0'
			remotecall.releaseInputStream();
			int j = remoteuserimpl.getAge();
			...
			objectoutput.writeInt(j);
			break;
 
		case 1: // '/001'
			remotecall.releaseInputStream();
			com.sunny.server.bean.User user = remoteuserimpl.getUser();
			ObjectOutput objectoutput1 = remotecall.getResultStream(true);
			objectoutput1.writeObject(user);
			...
			break;
			...
		}
	}
 
	public Operation[] getOperations(){
		return (Operation[])operations.clone();
	}
 
}

使用socket仿写rmi实现

上述示例如果Server端直接运行两次就会抛出类似如下异常:

Caused by: java.net.BindException: Address already in use: JVM_Bind
	at java.net.DualStackPlainSocketImpl.bind0(Native Method)
	at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
	at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
	at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
	at java.net.ServerSocket.bind(ServerSocket.java:375)
	at java.net.ServerSocket.<init>(ServerSocket.java:237)
	at java.net.ServerSocket.<init>(ServerSocket.java:128)
	at sun.rmi.transport.proxy.RMIDirectSocketFactory.createServerSocket(RMIDirectSocketFactory.java:45)
	at sun.rmi.transport.proxy.RMIMasterSocketFactory.createServerSocket(RMIMasterSocketFactory.java:345)
	at sun.rmi.transport.tcp.TCPEndpoint.newServerSocket(TCPEndpoint.java:666)
	at sun.rmi.transport.tcp.TCPTransport.listen(TCPTransport.java:330)

这个异常刚好跟我们使用Socket编程时,端口被占用的异常一样。所以我们也将服务端设置为一个ServerSocket,实体类和远程接口定义及实现跟上面示例一样,唯一不同的地方,我们这里抛出的异常是一个IOException,RemoteUser_Skel定义如下:

public class RemoteUser_Skel {
 
	private ServerSocket server;
	private RemoteUser remoteUser;
 
	public RemoteUser_Skel() {
		try {
			server = new ServerSocket(10086);
			remoteUser = new RemoteUserImpl();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
 
	public void dispatch() {
		System.out.println("server start");
		ObjectOutputStream oos = null;
		try {
			while (true) {
				Socket socket = server.accept();
				System.out.println("socket:"+socket);
				if (socket != null) {
					ObjectInputStream ois = new ObjectInputStream(
							socket.getInputStream());
					int index = ois.readInt();
					System.out.println("server:" + index);
					if (index == 1001) {
						oos = new ObjectOutputStream(socket.getOutputStream());
						User user = remoteUser.getUser();
						oos.writeObject(user);
						oos.flush();
 
					} else if (index == 1002) {
						oos = new ObjectOutputStream(socket.getOutputStream());
						int age = remoteUser.getAge();
						oos.writeInt(age);
						oos.flush();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
 
	}
}

客户端的代码这里就不贴出来了,可以直接 下载源码 查看详细示例。

其实这里的仿写可以更暴力一些,只要我们将服务端的代码的远程接口实现类直接序列化,通过序列化将RemoteUserImpl直接通过Socket传输到客户端,然后客户端可以直接反序列化出RemoteUserImpl实例,但是这种实现安全有个大问题,竟让将整个实现类在网络上传输。

本文就是简单介绍一下RMI的使用,这种模式的跨进程应用很类似Android中Binder机制,再加上设计模式中将RMI的实现称为远程代理,所以这次抽时间整理了一下相关知识点。

参考资料

java RMI原理详解

Java RMI原理与使用

RMI原理揭秘之远程对象

《Head First设计模式》

原文  http://www.sunnyang.com/792.html
正文到此结束
Loading...