在大型的分布式应用中,我们的某些要求并非前面讲述的方法能够满足的。举个例子来说,我们可能想同以前遗留下来的数据仓库打交道,或者需要从一个服务器对象里获取服务,无论它的物理位置在哪里。在这些情况下,都要求某种形式的“远程过程调用”(RPC),而且可能要求与语言无关。此时,CORBA可为我们提供很大的帮助。
CORBA并非一种语言特性,而是一种集成技术。它代表着一种具体的规范,各个开发商通过遵守这一规范,可设计出符合CORBA标准的集成产品。CORBA规范是由OMG开发出来的。这家非赢利性的机构致力于定义一个标准框架,从而实现分布式、与语言无关对象的相互操作。
利用CORBA,我们可实现对Java对象以及非Java对象的远程调用,并可与传统的系统进行沟通——采用一种“位置透明”的形式。Java增添了连网支持,是一种优秀的“面向对象”程序设计语言,可构建出图形化和非图形化的应用(程序)。Java和OMG对象模型存在着很好的对应关系;例如,无论Java还是CORBA都实现了“接口”的概念,并且都拥有一个引用(参考)对象模型。
ORB是对象间相互请求的一条通信总线。进行请求时,毋需关心对方的物理位置在哪里。这意味着在客户代码中看起来象一次方案调用的过程实际是非常复杂的一次操作。首先,必须存在与服务器对象的一条连接途径。而且为了创建一个连接,ORB必须知道具体实现服务器的代码存放在哪里。建好连接后,必须对方法自变量进行“汇集”。例如,将它们转换到一个二进制流里,以便通过网络传送。必须传递的其他信息包括服务器的机器名称、服务器进程以及对那个进程内的服务器对象进行标识的信息等等。最后,这些信息通过一种低级线路协议传递,信息在服务器那一端解码,最后正式执行调用。ORB将所有这些复杂的操作都从程序员眼前隐藏起来了,并使程序员的工作几乎和与调用本地对象的方法一样简单。
并没有硬性规定应如何实现ORB核心,但为了在不同开发商的ORB之间实现一种基本的兼容,OMG定义了一系列服务,它们可通过标准接口访问。
IDL是一种与语言无关的设计方法,可用它指定数据类型、属性、操作、接口以及更多的东西。IDL的语法类似于C++或Java语法。下面这张表格为大家总结了三种语言一些通用概念,并展示了它们的对应关系。
CORBA IDL Java C++
模块(Module) 包(Package) 命名空间(Namespace)
接口(Interface) 接口(Interface) 纯抽象类(Pure abstract class)
方法(Method) 方法(Method) 成员函数(Member function)
继承概念也获得了支持——就象C++那样,同样使用冒号运算符。针对需要由服务器和客户实现和使用的属性、方法以及接口,程序员要写出一个IDL描述。随后,IDL会由一个由厂商提供的IDL/Java编译器进行编译,后者会读取IDL源码,并生成相应的Java代码。
IDL编译器是一个相当有用的工具:它不仅生成与IDL等价的Java源码,也会生成用于汇集方法自变量的代码,并可发出远程调用。我们将这种代码称为“根干”(Stub and Skeleton)代码,它组织成多个Java源文件,而且通常属于同一个Java包的一部分。比如在启动的时候,服务器应用可创建一个服务器对象,将对象同命名服务绑定起来,然后等候客户发出请求。客户首先获得一个服务器引用,解析出字串名,然后通过引用发出对服务器的调用。
同样地,“命名服务”规范也属于CORBA的一部分,但实现它的应用程序是由ORB厂商(开发商)提供的。由于厂商不同,我们访问命名服务的方式也可能有所区别。
这儿显示的代码可能并不详尽,因为不同的ORB有不同的方法来访问CORBA服务,所以无论什么例子都要取决于具体的厂商(下例使用了JavaIDL,这是Sun公司的一个免费产品。它配套提供了一个简化版本的ORB、一个命名服务以及一个“IDL→Java”编译器)。除此之外,由于Java仍处在发展初期,所以在不同的Java/CORBA产品里并不是包含了所有CORBA特性。
我们希望实现一个服务器,令其在一些机器上运行,其他机器能向它查询正确的时间。我们也希望实现一个客户,令其请求正确的时间。在这种情况下,我们让两个程序都用Java实现。但在实际应用中,往往分别采用不同的语言。
IDL文件已分发给客户端的程序员,并成为两种语言间的桥梁。
下面这个例子展示了时间服务器的IDL描述情况:
module RemoteTime { interface ExactTime { string getTime(); }; };
这是对RemoteTime命名空间内的ExactTime接口的一个声明。该接口由单独一个方法构成,它以字串格式返回当前时间。
第二步是编译IDL,创建Java根干代码。我们将利用这些代码实现客户和服务器。与JavaIDL产品配套提供的工具是idltojava:
idltojava -fserver -fclient RemoteTime.idl
其中两个标记告诉idltojava同时为根和干生成代码。idltojava会生成一个Java包,它在IDL模块、RemoteTime以及生成的Java文件置入RemoteTime子目录后命名。_ExactTimeImplBase.java代表我们用于实现服务器对象的“干”;而_ExactTimeStub.java将用于客户。在ExactTime.java中,用Java方式表示了IDL接口。此外还包含了用到的其他支持文件,例如用于简化访问命名服务的文件。
大家在下面看到的是服务器端使用的代码。服务器对象是在ExactTimeServer类里实现的。RemoteTimeServer这个应用的作用是:创建一个服务器对象,通过ORB为其注册,指定对象引用时采用的名称,然后“安静”地等候客户发出请求。
import RemoteTime.*; import org.omg.CosNaming.*; import org.omg.CosNaming.NamingContextPackage.*; import org.omg.CORBA.*; import java.util.*; import java.text.*; // Server object implementation class ExactTimeServer extends _ExactTimeImplBase{ public String getTime(){ return DateFormat. getTimeInstance(DateFormat.FULL). format(new Date( System.currentTimeMillis())); } } // Remote application implementation public class RemoteTimeServer { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Create the server object and register it: ExactTimeServer timeServerObjRef = new ExactTimeServer(); orb.connect(timeServerObjRef); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Assign a string name to the // object reference (binding): NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ncRef.rebind(path, timeServerObjRef); // Wait for client requests: java.lang.Object sync = new java.lang.Object(); synchronized(sync){ sync.wait(); } } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } }
正如大家看到的那样,服务器对象的实现是非常简单的;它是一个普通的Java类,从IDL编译器生成的“干”代码中继承而来。但在与ORB以及其他CORBA服务进行联系的时候,情况却变得稍微有些复杂。
这里要简单介绍一下JavaIDL相关代码所做的工作(注意暂时忽略了CORBA代码与不同厂商有关这一事实)。main()的第一行代码用于启动ORB。而且理所当然,这正是服务器对象需要同它进行沟通的原因。就在ORB初始化以后,紧接着就创建了一个服务器对象。实际上,它正式名称应该是“短期服务对象”:从客户那里接收请求,“生存时间”与创建它的进程是相同的。创建好短期服务对象后,就会通过ORB对其进行注册。这意味着ORB已知道它的存在,可将请求转发给它。
到目前为止,我们拥有的全部东西就是一个timeServerObjRef——只有在当前服务器进程里才有效的一个对象引用。下一步是为这个服务对象分配一个字串形式的名字。客户会根据那个名字寻找服务对象。我们通过命名服务(Naming Service)完成这一操作。首先,我们需要对命名服务的一个对象引用。通过调用resolve_initial_references(),可获得对命名服务的字串式对象引用(在JavaIDL中是“NameService”),并将这个引用返回。这是对采用narrow()方法的一个特定NamingContext引用的模型。我们现在可开始使用命名服务了。为了将服务对象同一个字串形式的对象引用绑定在一起,我们首先创建一个NameComponent对象,用“ExactTime”进行初始化。“ExactTime”是我们想用于绑定服务对象的名称字串。随后使用rebind()方法,这是受限于对象引用的字串化引用。我们用rebind()分配一个引用——即使它已经存在。而假若引用已经存在,那么bind()会造成一个异常。在CORBA中,名称由一系列NameContext构成——这便是我们为什么要用一个数组将名称与对象引用绑定起来的原因。
服务对象最好准备好由客户使用。此时,服务器进程会进入一种等候状态。同样地,由于它是一种“短期服务”,所以生存时间要受服务器进程的限制。JavaIDL目前尚未提供对“持久对象”(只要创建它们的进程保持运行状态,对象就会一直存在下去)的支持。
现在,我们已对服务器代码的工作有了一定的认识。接下来看看客户代码:
import RemoteTime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*; public class RemoteTimeClient { public static void main(String args[]) { try { // ORB creation and initialization: ORB orb = ORB.init(args, null); // Get the root naming context: org.omg.CORBA.Object objRef = orb.resolve_initial_references( "NameService"); NamingContext ncRef = NamingContextHelper.narrow(objRef); // Get (resolve) the stringified object // reference for the time server: NameComponent nc = new NameComponent("ExactTime", ""); NameComponent path[] = {nc}; ExactTime timeObjRef = ExactTimeHelper.narrow( ncRef.resolve(path)); // Make requests to the server object: String exactTime = timeObjRef.getTime(); System.out.println(exactTime); } catch (Exception e) { System.out.println( "Remote Time server error: " + e); e.printStackTrace(System.out); } } }
前几行所做的工作与它们在服务器进程里是一样的:ORB获得初始化,并解析出对命名服务的一个引用。
接下来,我们需要用到服务对象的一个对象引用,所以将字串形式的对象引用直接传递给resolve()方法,并用narrow()方法将结果造型到ExactTime接口引用里。最后调用getTime()。
现在,我们已分别获得了一个服务器和一个客户应用,它们已作好相互间进行沟通的准备。大家知道两者都需要利用命名服务绑定和解析字串形式的对象引用。在运行服务或者客户之前,我们必须启动命名服务进程。在JavaIDL中,命名服务属于一个Java应用,是随产品配套提供的。但它可能与其他产品有所不同。JavaIDL命名服务在JVM的一个实例里运行,并(默认)监视网络端口900。
现在,我们已准备好启动服务器和客户应用(之所以按这一顺序,是由于服务器的存在是“短期”的)。若各个方面都设置无误,那么获得的就是在客户控制台窗口内的一行输出文字,提醒我们当前的时间是多少。当然,这一结果本身并没有什么令人兴奋的。但应注意一个问题:即使都处在同一台机器上,客户和服务器应用仍然运行于不同的虚拟机内。它们之间的通信是通过一个基本的集成层进行的——即ORB与命名服务的集成。
这只是一个简单的例子,面向非网络环境设计。但通常将ORB配置成“与位置无关”。若服务器与客户分别位于不同的机器上,那么ORB可用一个名为“安装库”(Implementation Repository)的组件解析出远程字串式引用。尽管“安装库”属于CORBA的一部分,但它几乎没有具体的规格,所以各厂商的实现方式是不尽相同的。正如大家看到的那样,CORBA还有许多方面的问题未在这儿进行详细讲述。但通过以上的介绍,应已对其有一个基本的认识。若想获得CORBA更详细的资料,最传真的起点莫过于OMB Web站点,地址是http://www.omg.org。这个地方提供了丰富的文档资料、白页、程序以及对其他CORBA资源和产品的链接。Java程序片可扮演一名CORBA客户的角色。这样一来,程序片就可访问由CORBA对象揭示的远程信息和服务。但程序片只能同最初下载它的那个服务器连接,所以程序片与它沟通的所有CORBA对象都必须位于那台服务器上。这与CORBA的宗旨是相悖的:它许诺可以实现“位置的透明”,或者“与位置无关”。
将Java程序片作为CORBA客户使用时,也会带来一些安全方面的问题。如果您在内联网中,一个办法是放宽对浏览器的安全限制。或者设置一道防火墙,以便建立与外部服务器安全连接。
针对这一问题,有些Java ORB产品专门提供了自己的解决方案。例如,有些产品实现了一种名为“HTTP通道”(HTTP Tunneling)的技术,另一些则提供了特别的防火墙功能。作为放到附录中的内容,所有这些主题都显得太复杂了。但它们确实是需要重点注意的问题。
我们已经知道,CORBA的一项主要特性就是对RPC(远程过程调用)的支持。利用这一技术,我们的本地对象可调用位置远程对象内的方法。当然,目前已有一项固有的Java特性可以做完全相同的事情:RMI(参考第15章)。尽管RMI使Java对象之间进行RPC调用成为可能,但CORBA能在用任何语言编制的对象之间进行RPC。这显然是一项很大的区别。
然而,可通过RMI调用远程、非Java代码的服务。我们需要的全部东西就是位于服务器那一端的、某种形式的封装Java对象,它将非Java代码“包裹”于其中。封装对象通过RMI同Java客户建立外部连接,并于内部建立与非Java代码的连接——采用前面讲到的某种技术,如JNI或J/Direct。
使用这种方法时,要求我们编写某种类型的“集成层”——这其实正是CORBA帮我们做的事情。但是这样做以后,就不再需要其他厂商开发的ORB了。
我们在这个附录讨论的都是从一个Java应用里调用非Java代码最基本的技术。每种技术都有自己的优缺点。但目前最主要的问题是并非所有这些特性都能在所有JVM中找到。因此,即使一个Java程序能调用位于特定平台上的固有方法,仍有可能不适用于安装了不同JVM的另一种平台。
Sun公司提供的JNI具有灵活、简单(尽管它要求对JVM内核进行大量控制)、功能强大以及通用于大多数JVM的优点。到本书完稿时为止,微软仍未提供对JNI的支持,而是提供了自己的J/Direct(调用Win32 DLL函数的一种简便方法)和RNI(特别适合编写高效率的代码,但要求对JVM内核有很深入的理解)。微软也提供了自己的专利Java/COM集成方案。这一方案具有很强大的功能,且将Java变成了编写COM服务器和客户的有效语言。只有微软公司的编译器和JVM能提供对J/Direct、RNI以及Java/COM的支持。我们最后研究的是CORBA,它使我们的Java对象可与其他对象沟通——无论它们的物理位置在哪里,也无论是用何种语言实现的。CORBA与前面提到的所有技术都不同,因为它并未集成到Java语言里,而是采用了其他厂商(第三方)的集成技术,并要求我们购买其他厂商提供的ORB。CORBA是一种有趣和通用的方案,但如果只是想发出对操作系统的调用,它也许并非一种最佳方案。