菜鸟笔记
提升您的技术认知

系统间通信2:通信管理与远程方法调用rmi-ag真人游戏

rmi : remote method invocation,远程方法调用
rpc : remote procedure call protocol, 远程过程调用协议
esb : enterprise service bus, 企业服务总线
soa : service-oriented architecture, 面向服务的架构

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

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的工作方式):

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 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的具体实现
* 请注意这里继承的是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 queryalluserinfo() throws remoteexception { list users = new arraylist(); 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 users = remoteserviceinterface.queryalluserinfo();
        remoteclient.logger.info("users.size() = "  users.size());
    }
}

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

下图为remoteunicastmain的效果rmi 服务注册和执行效果:

可以看到,remoteunicastmain中的代码执行完成后整个应用程序没有退出。如下图:

这是因为这个应用程序要承担“真实的rmi server实现”的服务调用。如果它退出,rmi 注册表就无法请求真实的服务实现了
我们再来看下图,remoteclient调用rmi 服务的效果:

很明显控制台将返回:

0 [main] info testrmi.remoteclient - users.size() = 2

通过上面的两组代码,我们大概知道了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类(其他的业务代码不需要改变)

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

网站地图