系统间通信2:通信管理与远程方法调用RMI

本文引用 : https://yinwj.blog.csdn.net/article/details/49120813

RMI : Remote Method Invocation,远程方法调用
RPC : Remote Procedure Call Protocol, 远程过程调用协议
ESB : Enterprise Service Bus, 企业服务总线
SOA : Service-Oriented Architecture, 面向服务的架构

1. 概述

在这个章节我将通过对RMI的详细介绍,引出一个重要的系统间通信的管理规范RPC,并且继续讨论一些RPC的实现;再通过分析PRC的技术特点,引出另一种系统间通信的管理规范ESB,并介绍ESB的一些具体实现。最后我们介绍SOA:面向服务的软件架构。

2. RMI基本使用

RMI(Remote Method Invocation,远程方法调用),是JAVA早在JDK 1.1中提供的JVM与JVM之间进行 对象方法调用的技术框架的实现(在JDK的后续版本中,又进行了改进)。通过RMI技术,某一个本地的JVM可以调用存在于另外一个JVM中的对象方法,就好像它仅仅是在调用本地JVM中某个对象方法一样。例如RMI客户端中的如下调用:

List< UserInfo > users = remoteServiceInterface.queryAllUserinfo();

看似remoteServiceInterface对象和普通的对象没有区别,但实际上remoteServiceInterface对象的具体方法实现却不在本地的JVM中,而是在某个远程的JVM中(这个远程的JVM可以是RMI客户端同属于一台物理机,也可以属于不同的物理机)

1.1 RMI使用场景

RMI是基于JAVA语言的,也就是说在RMI技术框架的描述中,只有Server端使用的是JAVA语言并且Client端也是用的JAVA语言,才能使用RMI技术(目前在codeproject.com中有一个开源项目名字叫做“RMI for C++”,可以实现JAVA To C++的RMI调用。但是这是一个第三方的实现,并不是java的标准RMI框架定义,所以并不在我们的讨论范围中)。

RMI适用于两个系统都主要使用JAVA语言进行构造,不需要考虑跨语言支持的情况。并且对两个JAVA系统的通讯速度有要求的情况。

RMI 是一个良好的、特殊的RPC实现:使用JRMP协议承载数据描述,可以使用BIO和NIO两种IO通信模型。

1.2 RMI框架的基本组成

虽然RMI早在JDK.1.1版本中就开放了。但是在JDK1.5的版本中RMI又进行改进。所以我们后续的代码示例和原理讲解都基于最新的RMI框架特性。

要定义和使用一套基于RMI框架工作的系统,您至少需要做一下几个工作:

  1. 定义RMI Remote接口
  2. 实现这个RMI Remote接口
  3. 生成Stub(桩)和Skeleton(骨架)。这一步的具体操作视不同的JDK版本而有所不同(例如JDK1.5后,Skeleton不需要手动);“RMI注册表”的工作方式也会影响“Stub是否需要命令行生成”这个问题。
  4. 向“RMI注册表”注册在第2步我们实现的RMI Remote接口。
  5. 创建一个Remote客户端,通过java“命名服务”在“RMI注册表”所在的IP:PORT寻找注册好的RMI服务。
  6. Remote客户端向调用存在于本地JVM中对象那样,调用存在于远程JVM上的RMI接口。

下图描述了上述几个概念名称间的关系,呈现了JDK.5中RMI框架其中一种运行方式(注意,是其中一种工作方式。也就是说RMI框架不一定都是这种运行方式,后文中我们还将描述另外一种RMI的工作方式):
系统间通信2:通信管理与远程方法调用RMI

1.3 RMI示例代码

在这个代码中,我们将使用“本地RMI注册表”(LocateRegistry),让RMI服务的具体提供者和RMI注册表工作在同一个JVM上,向您介绍最基本的RMI服务的定义、编写、注册和调用过程:

首先我们必须定义RMI 服务接口,代码如下:

package testRMI;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

import testRMI.entity.UserInfo;

public interface RemoteServiceInterface extends Remote {
    /**
     * 这个RMI接口负责查询目前已经注册的所有用户信息
     */
    public List<UserInfo> queryAllUserinfo() throws RemoteException;
}

很简单的代码,应该不用多解释什么了。这个定义的接口方法如果放在某个业务系统A中,您可以理解是查询这个系统A中所有可用的用户资料。注意这个接口所继承的java.rmi.Remote接口,是“RMI服务接口”定义的特点。

那么有接口定义了,自然就要实现这个接口:

package testRMI;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;

import testRMI.entity.UserInfo;

/**
 * RMI 服务接口RemoteServiceInterface的具体实现<br>
 * 请注意这里继承的是UnicastRemoteObject父类。
 * 继承于这个父类,表示这个Remote Object是“存在于本地”的RMI服务实现
 * (这句话后文会解释)
 * @author yinwenjie
 *
 */
public class RemoteUnicastServiceImpl extends UnicastRemoteObject implements RemoteServiceInterface {
    /**
     * 注意Remote Object没有默认构造函数
     * @throws RemoteException
     */
    protected RemoteUnicastServiceImpl() throws RemoteException {
        super();
    }

    private static final long serialVersionUID = 6797720945876437472L;

    /* (non-Javadoc)
     * @see testRMI.RemoteServiceInterface#queryAllUserinfo()
     */
    @Override
    public List<UserInfo> queryAllUserinfo() throws RemoteException {
        List<UserInfo> users = new ArrayList<UserInfo>();

        UserInfo user1 = new UserInfo();
        user1.setUserAge(21);
        user1.setUserDesc("userDesc1");
        user1.setUserName("userName1");
        user1.setUserSex(true);
        users.add(user1);

        UserInfo user2 = new UserInfo();
        user2.setUserAge(21);
        user2.setUserDesc("userDesc2");
        user2.setUserName("userName2");
        user2.setUserSex(false);
        users.add(user2);
        return users;
    }
}

还有我们定义的Userinfo信息,就是一个普通的POJO对象:

package testRMI.entity;

import java.io.Serializable;
import java.rmi.RemoteException;

public class UserInfo implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = -377525163661420263L;

    private String userName;
    private String userDesc;
    private Integer userAge;
    private Boolean userSex;

    public UserInfo() throws RemoteException {

    }

    /**
     * @return the userName
     */
    public String getUserName() {
        return userName;
    }

    /**
     * @param userName the userName to set
     */
    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * @return the userDesc
     */
    public String getUserDesc() {
        return userDesc;
    }

    /**
     * @param userDesc the userDesc to set
     */
    public void setUserDesc(String userDesc) {
        this.userDesc = userDesc;
    }

    /**
     * @return the userAge
     */
    public Integer getUserAge() {
        return userAge;
    }

    /**
     * @param userAge the userAge to set
     */
    public void setUserAge(Integer userAge) {
        this.userAge = userAge;
    }

    /**
     * @return the userSex
     */
    public Boolean getUserSex() {
        return userSex;
    }

    /**
     * @param userSex the userSex to set
     */
    public void setUserSex(Boolean userSex) {
        this.userSex = userSex;
    }
}

RMI Server 的接口定义和RMI Server的实现都有了,那么编写代码的最后一步是**将这个RMI Server注册到“RMI 注册表”中运行。这样 RMI的客户端就可以调用这个 RMI Server了。**下面的代码是将RMI Server注册到“本地RMI 注册表”中:

package testRMI;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteUnicastMain {
    public static void main(String[] args) throws Exception {
        /*
         * Locate registry,您可以理解成RMI服务注册表,或者是RMI服务位置仓库。
         * 主要的作用是维护一个“可以正常提供RMI具体服务的所在位置”。
         * 每一个具体的RMI服务提供者,都会讲自己的Stub注册到Locate registry中,以表示自己“可以提供服务”
         * 
         * 有两种方式可以管理Locate registry,一种是通过操作系统的命令行启动注册表;
         * 另一种是在代码中使用LocateRegistry类。
         * 
         * LocateRegistry类中有一个createRegistry方法,可以在这台物理机上创建一个“本地RMI注册表”
         * */
        LocateRegistry.createRegistry(1099);

        // 以下是向LocateRegistry注册(绑定/重绑定)RMI Server实现。
        RemoteUnicastServiceImpl remoteService = new RemoteUnicastServiceImpl();
        // 通过java 名字服务技术,可以讲具体的RMI Server实现绑定一个访问路径。注册到LocateRegistry中
        Naming.rebind("rmi://127.0.0.1:1099/queryAllUserinfo", remoteService);

        /*
         * 在“已经拥有某个可访问的远程RMI注册表”的情况下。
         * 下面这句代码就是向远程注册表注册RMI Server,
         * 当然远程RMI注册表的JVM-classpath中一定要有这个Server的Stub存在
         *  
         * (运行在另外一个JVM上的RMI注册表,可能是同一台物理机也可能不是同一台物理机)
         * Naming.rebind("rmi://192.168.61.1:1099/queryAllUserinfo", remoteService);
         * */
    }
}

这样我们后续编写的Client端就可以调用这个RMI Server了。下面的代码是RMI Client的代码:

package testRMI;

import java.rmi.Naming;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;

import testRMI.entity.UserInfo;

/**
 * 客户端调用RMI测试
 * @author yinwenjie
 *
 */
public class RemoteClient {
    static {
        BasicConfigurator.configure();
    }

    /**
     * 日志
     */
    private static final Log LOGGER = LogFactory.getLog(RemoteClient.class);

    public static void main(String[] args) throws Exception {
        // 您看,这里使用的是java名称服务技术进行的RMI接口查找。
        RemoteServiceInterface remoteServiceInterface = (RemoteServiceInterface)Naming.lookup("rmi://192.168.61.1/queryAllUserinfo");
        List<UserInfo> users = remoteServiceInterface.queryAllUserinfo();

        RemoteClient.LOGGER.info("users.size() = " +users.size());
    }
}

那么怎么来运行这段代码呢?如果您使用的是eclipse编写了您第一个RMI Server和RMI Client,并且您使用的是“本地RMI 注册表”。那么您不需要做任何的配置、脚本指定等工作(包括不需要专门设置JRE权限、不需要专门指定classpath、不需要专门生成Stub和Skeleton),就可以看到RMI的运行和调用效果了:

下图为RemoteUnicastMain的效果RMI 服务注册和执行效果:
系统间通信2:通信管理与远程方法调用RMI
可以看到,RemoteUnicastMain中的代码执行完成后整个应用程序没有退出。如下图:
系统间通信2:通信管理与远程方法调用RMI
这是因为这个应用程序要承担“真实的RMI Server实现”的服务调用。如果它退出,RMI 注册表就无法请求真实的服务实现了
我们再来看下图,RemoteClient调用RMI 服务的效果:
系统间通信2:通信管理与远程方法调用RMI
很明显控制台将返回:

0 [main] INFO testRMI.RemoteClient - users.size() = 2

3. JAVA RMI 原理

通过上面的两组代码,我们大概知道了RMI框架是如何使用的。下面我们来讲解一下RMI的基本原理。

3.1 Registry和Stub、Skeleton的关系

  • 一定要说明,在RMI Client实施正式的RMI调用前,它必须通过LocateRegistry或者Naming方式到RMI注册表寻找要调用的RMI注册信息。找到RMI事务注册信息后,Client会从RMI注册表获取这个RMI Remote Service的Stub信息。这个过程成功后,RMI Client才能开始正式的调用过程。

  • 另外要说明的是RMI Client正式调用过程,也不是由RMI Client直接访问Remote Service,而是由客户端获取的Stub作为RMI Client的代理访问Remote Service的代理Skeleton,如上图所示的顺序。也就是说真实的请求调用是在Stub-Skeleton之间进行的。

  • Registry并不参与具体的Stub-Skeleton的调用过程,只负责记录“哪个服务名”使用哪一个Stub,并在Remote Client询问它时将这个Stub拿给Client

3.2 Remote-Service线程管理

在上文中的演示我们看到了RemoteRegistryUnicastMain处理请求时,使用了线程池。这是JDK1.5到JDK1.6+版本中RMI框架的做的一个改进。包括JDK1.5在内,之前的版本都采用新建线程的方式来处理请求;在JDK1.6版本之后,改用了线程池,并且线程池的大小是可以调整的:

  • sun.rmi.transport.tcp.maxConnectionThreads:连接池的大小,默认为无限制。无限的大小肯定是有问题,按照Linux单进程可打开的最大文件数限制,建议的设置值为65535(生产环境)。如果同一时间连接池中的线程数量达到了最大值,那么后续的Client请求将会报错。测试环境/开发环境是否设置这个值,就没有那么重要了。

  • sun.rmi.transport.tcp.threadKeepAliveTime:如果当线程池中有闲置的线程资源的话,那么这个闲置线程资源多久被注销(单位毫秒),默认的设置是1分钟。

如果您使用的是linux或者window的命令控制台执行的话,您可以通过类似如下语句进行参数设置:

java -Dsun.rmi.transport.tcp.maxConnectionThreads=2 -Dsun.rmi.transport.tcp.threadKeepAliveTime=1000 testRMI.RemoteRegistryUnicastMain

3.3 Registry和Naming

Registry和Naming都可以进行RMI服务的bind/rebind/unbind,都可以用lookup方法查询RMI服务。Naming实际上是对Registry的封装。使用完整的URL方式对已注册的服务名进行查找。

3.4 UnicastRemoteObject和Activatable

在JDK1.2版本中,由Ann Wollrath执笔加入了一种新的RMI工作方式。即通过RMI“活化”模式,将Remote Service的真实提供者移植到RMI Registry注册表所在的JVM上。要使用这种工作模式的Remote Service实现不再继承UnicastRemoteObject类,而需要继承Activatable类(其他的业务代码不需要改变)

4. RMI:一种特殊的RPC服务实现

之所以介绍RMI,是因为要通过介绍RMI引出一种重要的系统间通讯管理框架RPC.

上一篇:rmi


下一篇:JavaRMI入门和使用