38

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

 5 years ago
source link: http://www.sunnyang.com/792.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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辅助生成客户辅助对象和服务辅助对象。

7fQbmea.png!web

由于在使用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设计模式》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK