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框架工作的系统,您至少需要做一下几个工作:
- 定义rmi remote接口
- 实现这个rmi remote接口
- 生成stub(桩)和skeleton(骨架)。这一步的具体操作视不同的jdk版本而有所不同(例如jdk1.5后,skeleton不需要手动);“rmi注册表”的工作方式也会影响“stub是否需要命令行生成”这个问题。
- 向“rmi注册表”注册在第2步我们实现的rmi remote接口。
- 创建一个remote客户端,通过java“命名服务”在“rmi注册表”所在的ip:port寻找注册好的rmi服务。
- 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.