深究Java中的RMI

前言:随着一个系统被用户认可,业务量、请求量不断上升,那么单机系统必然就无法满足了,于是系统就慢慢走向分布式了,随之而来的是系统之间“沟通”的障碍。一般来说,解决系统之间的通信可以有两种方式:即远程调用和消息。RMI(Remote Method Invocation)就是远程调用的一种方式,也是这篇文章主要介绍的。

一、RPC和RMI的关系

从目标上来看,它们都是为了实现远程调用的。不同点在于RMI具有语言的局限性,仅限于Java语言,而RPC则没有限制,更加通用。相对地,RMI有其较之RPC不可比拟的优点,那就是提供了面向对象的支持,这也是为了呼应Java作为面向对象语言的特性。

二、RMI通信的一个简单示例

这个示例拆分为服务端和客户端,放在两个idea项目中,并且通过了单机和双机两种环境的测试,是真正意义上的分布式应用了。

项目结构

服务端应用: Server

主程序:        com.jnu.wwt.entry.Server

服务接口:     com.jnu.wwt.service.IOperation

服务实现:     com.jnu.wwt.service.impl.OperationImpl

客户端应用: Client

主程序:        com.jnu.wwt.entry.Client

服务接口:    com.jnu.wwt.service.IOperation

源码:

Server.java

/**
 * Created by wwt on 2016/9/14.
 */
public class Server {

    public static void main(String args[]) throws Exception{

        //以1099作为LocateRegistry接收客户端请求的端口,并注册服务的映射关系
        Registry registry=LocateRegistry.createRegistry(1099);

        IOperation iOperation=new OperationImpl();
        Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation);

        System.out.println("service running...");
    }

}

IOperation.java(服务端和客户端各需要一份)

/**
 * 服务端接口必须实现java.rmi.Remote
 * Created by wwt on 2016/9/14.
 */
public interface IOperation extends Remote{

    /**
     * 远程接口上的方法必须抛出RemoteException,因为网络通信是不稳定的,不能吃掉异常
     * @param a
     * @param b
     * @return
     */
    int add(int a, int b) throws RemoteException;

}

OperationImpl.java

/**
 * Created by wwt on 2016/9/14.
 */
public class OperationImpl extends UnicastRemoteObject implements IOperation{

    public OperationImpl() throws RemoteException {
        super();
    }

    @Override
    public int add(int a, int b) throws RemoteException{
        return a+b;
    }

}

Client.java

/**
 * Created by wwt on 2016/9/15.
 */
public class Client {

    public static void main(String args[]) throws Exception{
        IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
        System.out.println(iOperation.add(1,1));
    }

}

运行结果

先运行Server应用,服务就起来了。然后切换到Client应用,点击运行,Client调用Server的服务,返回结果。

三、RMI做了些什么

现在我们先忘记Java中有RMI这种东西。假设我们需要自己实现上面例子中的效果,怎么办呢?可以想到的步骤是:

a.编写服务端服务,并将其通过某个服务机的端口暴露出去供客户端调用。

b.编写客户端程序,客户端通过指定服务所在的主机和端口号、将请求封装并序列化,最终通过网络协议发送到服务端。

c.服务端解析和反序列化请求,调用服务端上的服务,将结果序列化并返回给客户端。

d.客户端接收并反序列化服务端返回的结果,反馈给用户。

这是大致的流程,我们不难想到,RMI其实也是帮我们封装了一些细节而通用的部分,比如序列化和反序列化,连接的建立和释放等,下面是RMI的具体流程:

这里涉及到几个新概念:

Stub和Skeleton:这两个的身份是一致的,都是作为代理的存在。客户端的称作Stub,服务端的称作Skeleton。要做到对程序员屏蔽远程方法调用的细节,这两个代理是必不可少的,包括网络连接等细节。

Registry:顾名思义,可以认为Registry是一个“注册所”,提供了服务名到服务的映射。如果没有它,意味着客户端需要记住每个服务所在的端口号,这是极不优雅的。

四、按照上面的时序图,来看看RMI的底层细节

主要用到的类:

主要的类的层次结构:

1.启动Registry服务

//以1099作为LocateRegistry接收客户端请求的端口,并注册服务的映射关系
Registry registry=LocateRegistry.createRegistry(1099);

调用以上方法,将会以1099端口为参数实例化一个RegistryImpl对象。这里做了一个判断,假如指定的端口是1099并且打开了安全管理器,那么对于指定的权限(listen,accept)不需要做权限控制。反之则需要进行安全校验。提升了效率。

public RegistryImpl(final int var1) throws RemoteException {
    if(var1 == 1099 && System.getSecurityManager() != null) {
        try {
            AccessController.doPrivileged(new PrivilegedExceptionAction() {
                public Void run() throws RemoteException {
                    LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
                    RegistryImpl.this.setup(new UnicastServerRef(var1x));
                    return null;
                }
            }, (AccessControlContext)null, new Permission[]{new SocketPermission("localhost:" + var1, "listen,accept")});
        } catch (PrivilegedActionException var3) {
            throw (RemoteException)var3.getException();
        }
    } else {
        LiveRef var2 = new LiveRef(id, var1);
        this.setup(new UnicastServerRef(var2));
    }

}

接下来会调用到RegistryImpl的setUp()方法,setUp方法又会调用到UnicastServerRef的exportObject()方法,如下:

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
    Class var4 = var1.getClass();

    Remote var5;
    try {
        var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
    } catch (IllegalArgumentException var7) {
        throw new ExportException("remote object implements illegal remote interface", var7);
    }

    if(var5 instanceof RemoteStub) {
        this.setSkeleton(var1);
    }

    Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
    this.ref.exportObject(var6);
    this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
    return var5;
}

很明显,这里创建并设置了服务端的Skeleton对象。

2.创建服务对象,乍一看没啥好说的,其实不然。从上面的例子中,我们可以看到OperationImpl继承了UnicastRemoteObject对象,并且调用了其构造器,我们就来看看它做了什么。从Operation的super()开始,追溯到UnicastRemoteObject的exportObject()方法。

private static Remote exportObject(Remote obj, UnicastServerRef sref)
    throws RemoteException
{
    // if obj extends UnicastRemoteObject, set its ref.
    if (obj instanceof UnicastRemoteObject) {
        ((UnicastRemoteObject) obj).ref = sref;
    }
    return sref.exportObject(obj, null, false);
}

这里做了一个判断,如果我们需要暴露出去的服务实现类是UnicastRemoteObject的子类,那么我们只要设置这个服务实现类从RemoteObject继承的RemoteRef对象为传入的UnicastServerRef对象即可。否则(不继承UnicastRemoteObject也是可以通过其他方法实现)调用UnicastServerRef的exportObject()方法。这里我们是第一种情况,直接为ref赋值即可。可以看到第二种情况也是调用到UnicastServerRef的exportObject()方法。

3.注册服务对象。

Naming.rebind("rmi://127.0.0.1:1099/Operation",iOperation);

通过源码我们可以看到,其实Naming调用的都是Registry的方法。辗转调用到RegistryImpl类的rebind方法,这个方法做的事情是将服务名称和服务的实现映射存在一个map里面。

public void rebind(String var1, Remote var2) throws RemoteException, AccessException {
    checkAccess("Registry.rebind");
    this.bindings.put(var1, var2);
}

4.客户端访问服务器并通过服务名查找服务

IOperation iOperation= (IOperation) Naming.lookup("rmi://127.0.0.1:1099/Operation");
public static Remote lookup(String name)
    throws NotBoundException,
        java.net.MalformedURLException,
        RemoteException
{
    ParsedNamingURL parsed = parseURL(name);
    Registry registry = getRegistry(parsed);

    if (parsed.name == null)
        return registry;
    return registry.lookup(parsed.name);
}

上面主要做了两件事情,通过主机和端口(1099)获取服务端Registry对象的Stub(RegistryImpl_Stub)。通过Registry的Stub对象lookUp()找到服务实现的Stub对象。这里主要看看客户端如何获取服务实现的Stub对象,查看RegistryImpl_Stub的lookUp()方法。核心代码如下:

RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
    ObjectOutput var3 = var2.getOutputStream();
    var3.writeObject(var1);
} catch (IOException var18) {
    throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
    ObjectInput var6 = var2.getInputStream();
    var23 = (Remote)var6.readObject();
} catch (IOException var15) {
    throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
    throw new UnmarshalException("error unmarshalling return", var16);
} finally {
    super.ref.done(var2);
}

return var23;

这段代码做的事情是构造了RemoteCall对象,将服务名写入输出流中,调用方法。这样就调用了服务端的lookUp()方法。服务端将服务实现的Stub传递给客户端。于是客户端便有了Stub对象。

5.最后再来看看Stub,Skeleton,服务实现如何之间的交互在代码中的体现。这里博主试过用rmic编译产生这两个文件,但是失败了,所以暂且参考一下其他大神的博文里面的示例。java RMI原理详解

Stub类:

  1. public class Person_Stub implements Person {
  2. private Socket socket;
  3. public Person_Stub() throws Throwable {
  4. // connect to skeleton
  5. socket = new Socket("computer_name", 9000);
  6. }
  7. public int getAge() throws Throwable {
  8. // pass method name to skeleton
  9. ObjectOutputStream outStream =
  10. new ObjectOutputStream(socket.getOutputStream());
  11. outStream.writeObject("age");
  12. outStream.flush();
  13. ObjectInputStream inStream =
  14. new ObjectInputStream(socket.getInputStream());
  15. return inStream.readInt();
  16. }
  17. public String getName() throws Throwable {
  18. // pass method name to skeleton
  19. ObjectOutputStream outStream =
  20. new ObjectOutputStream(socket.getOutputStream());
  21. outStream.writeObject("name");
  22. outStream.flush();
  23. ObjectInputStream inStream =
  24. new ObjectInputStream(socket.getInputStream());
  25. return (String)inStream.readObject();
  26. }
  27. }

可以看到,Stub对象做的事情是建立到服务端Skeleton对象的Socket连接。将客户端的方法调用转换为字符串标识传递给Skeleton对象。并且同步阻塞等待服务端返回结果。

Skeleton类:

  1. public class Person_Skeleton extends Thread {
  2. private PersonServer myServer;
  3. public Person_Skeleton(PersonServer server) {
  4. // get reference of object server
  5. this.myServer = server;
  6. }
  7. public void run() {
  8. try {
  9. // new socket at port 9000
  10. ServerSocket serverSocket = new ServerSocket(9000);
  11. // accept stub‘s request
  12. Socket socket = serverSocket.accept();
  13. while (socket != null) {
  14. // get stub‘s request
  15. ObjectInputStream inStream =
  16. new ObjectInputStream(socket.getInputStream());
  17. String method = (String)inStream.readObject();
  18. // check method name
  19. if (method.equals("age")) {
  20. // execute object server‘s business method
  21. int age = myServer.getAge();
  22. ObjectOutputStream outStream =
  23. new ObjectOutputStream(socket.getOutputStream());
  24. // return result to stub
  25. outStream.writeInt(age);
  26. outStream.flush();
  27. }
  28. if(method.equals("name")) {
  29. // execute object server‘s business method
  30. String name = myServer.getName();
  31. ObjectOutputStream outStream =
  32. new ObjectOutputStream(socket.getOutputStream());
  33. // return result to stub
  34. outStream.writeObject(name);
  35. outStream.flush();
  36. }
  37. }
  38. } catch(Throwable t) {
  39. t.printStackTrace();
  40. System.exit(0);
  41. }
  42. }
  43. }

Skeleton对象做的事情是将服务实现传入构造参数,获取客户端通过socket传过来的方法调用字符串标识,将请求转发到具体的服务上面。获取结果之后返回给客户端。

时间: 2024-10-07 09:44:40

深究Java中的RMI的相关文章

JAVA 中的RMI是什么

RMI的概念 RMI(Remote Method Invocation)远程方法调用是一种计算机之间利用远程对象互相调用实现双方通讯的一种通讯机制.使用这种机制,某一台计算机上的对象可以调用另外 一台计算机上的对象来获取远程数据.RMI是Enterprise JavaBeans的支柱,是建立分布式Java应用程序的方便途径.在过去,TCP/IP套接字通讯是远程通讯的主要手段,但此开发方式没有使用面向对 象的方式实现开发,在开发一个如此的通讯机制时往往令程序员感觉到乏味,对此RPC(Remote

关于metaspolit中进行JAVA反序列化渗透RMI的原理分析

一.背景: 这里需要对java反序列化有点了解,在这里得推广下自己的博客嘛,虽然写的不好,广告还是要做的.原谅我: 1.java反序列化漏洞原理研习 2.java反序列化漏洞的检测 二.攻击手法简介 针对这个使用msf攻击需要大家看一篇文章:JMX RMI Exploit 实例 , 鸣谢,确实学到了很多,膜拜大牛 , 简要介绍下攻击手法: (1)下载mjet模块:下载连接mjet,如果是mac电脑安装好metaspolit以后可以直接使用git clone命令下载到metaspolit的父目录下

关于<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)>看后的一些总结-1

原文地址:https://www.anquanke.com/post/id/194384#h3-3 1.java rmi 关于rmi客户端和服务端通信的过程,java的方法都实现在rmi服务端,客户端实际上是通过访问rmi注册表拿到stub,然后再通过它调用服务端方法,那么调用方法时要传递参数,参数可以为一般类型,也可以为引用类型,那么如果为引用类型,就能够利用服务端已经有的gaget chain来打server,因为参数实际上是序列化传输的,那么数据到达服务端后必定会经过反序列化. 客户端:

java中的回调机制的理解(小例子)

这样的解释似乎还是比较难懂,这里举个简单的例子,程序员A写了一段程序(程序a),其中预留有回调函数接口,并封装好了该程序.程序员B要让a调用自己的程序b中的一个方法,于是,他通过a中的接口回调自己b中的方法.目的达到.在C/C++中,要用回调函数,被掉函数需要告诉调用者自己的指针地址,但在JAVA中没有指针,怎么办?我们可以通过接口(interface)来实现定义回调函数. 正常情况下开发人员使用已经定义好的API,这个过程叫Call.但是有时这样不能满足需求,就需要程序员注册自己的程序,然后让

Java中的对象序列化

好久没翻译simple java了,睡前来一发.译文链接:http://www.programcreek.com/2014/01/java-serialization/ 什么是对象序列化 在Java中,对象序列化指的是将对象用字节序列的形式表示,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到数据库或文件中,并且支持从数据库或文件中反序列化,从而在内存中重建对象: 为什么需要序列化 序列化经常被用于对象的网络传输或本地存储.网络基础设施和硬盘只能识别位和字节信息,而不能识别Jav

java中serializable

java中serializable是一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才是可序列化的.因此如果要序列化某些类的对象,这些类就必须实现Serializable接口.而实际上,Serializable是一个空接口,没有什么具体内容,它的目的只是简单的标识一个类的对象可以被序列化. 什么情况下需要序列化 a)当你想把的内存中的对象写入到硬盘的时候:b)当你想用套接字在网络上传送对象的时候:c)当你想通过RMI传输对象的时候: 再稍微解释一下:a)比如说你的内存

Java中的SerialVersionUID

序列化及SergalVersionUID困扰着许多Java开发人员.我经常会看到这样的问题,什么是SerialVersionUID,如果实现了Serializable接口的类中没有定义SerialVersionUID的话会怎样?抛开它的复杂性以及不太常用不说,一个原因就是Eclipse在缺少了SerialVersionUID之后的给出的警告提示:"The Serializable class Customer does not declare a static final SerialVersi

如何在Spring框架中使用RMI技术

在博客<RMI远程方法调用技术>中使用纯Java代码演示了RMI技术的使用,本篇博客将介绍如何在Spring中使用RMI技术. 正如博客<RMI远程方法调用技术>提到的那样--RMI技术的应用通常包括在两个独立的应用程序中:RMI服务端应用程序和RMI客户端应用程序.本博客也从这两方面入手:        RMI服务端应用程序: 1.自定义远程接口 代码如下: package com.ghj.packageofrmi; /** * 定义远程接口. * * @author 高焕杰 *

关于java中split的使用

之前在博客中已经叙述过这个问题,但是最近一次笔试中居然有碰到了这个知识点,而且还做错了,囧!学艺不精啊.题目大概是这样的: Java代码 String s2="this is a test"; String sarray[]=s2.split("/s"); System.out.println("sarray.length="+sarray.length); 这个输出是什么还是编译出错?我想那个split方法中的参数要是"s"