CORBA 程序开发,第 1 部分

作者:J. Mark Shacklette

CORBA(公共对象请求代理体系结构)是那些大多数人对其有一些“感觉”,其他人对其有一些兴趣,但极少有人真正了解的缩写词之一。这是三篇系列文章中的第一篇,我们将尝试增加前者,扩充后者并补救后者。我们将迅速指出 CORBA 的优势,但不会回避披露 CORBA 当前的一些缺点,这些缺点虽然数量不多,但仍然可能成为新手的绊脚石。

我们的目标是帮助 CORBA 新手 Linux 程序员通过检查一个非常简单的 CORBA 应用程序来入门。我们简单的分布式应用程序是使用 OmniORB 开发的,OmniORB 是来自英国剑桥 Oracle-Olivetti 研究中心的免费 ORB。OmniORB 是基本 CORBA 2.0 体系结构的快速、简洁的实现。我们已计算出其性能比其商业竞争对手快 2 到 15 倍(测量相同的代码),而且它在 Linux 上运行。它实现了 CORBA 标准的命名服务,但目前不支持一些更流行的 CORBA 服务,例如事件服务、交易服务或生命周期服务。(它有一种生命周期服务,但它是非标准的。)它也尚未提供动态调用接口、动态骨架接口或接口存储库。但是,它为 POSIX pThreads 提供了一个优秀的线程抽象,该抽象具有高度的可移植性(其中一位作者已将其移植到 HP 不稳定的 DCE 线程实现)。OMNIThread 抽象支持 Linuxthreads 0.5 及更高版本(POSIX 1003.1c-草案 10,并随 Red Hat 的 glibc 实现一起提供)和 MIT pThreads,非常值得下载。此处显示的简单示例不需要两台独立的联网机器,因此即使您只有一台机器,您仍然可以入门。CORBA 中没有要求对象实际上是物理分布的。该设计特性是完全可选的。

我们知道 CORBA 是一种分布式编程方法,但它到底是什么?CORBA 定义了一个协调规范,用于实现分布式对象通信。CORBA 是由对象管理组 (http://www.omg.org/) 管理的规范,该组织是由 800 多家不同的硬件和软件供应商组成的联盟,他们的各种利益都围绕着分布式计算的概念相交。OMG 通过 CORBA 实现的目标是定义一个与平台和语言无关的通信标准,重点是面向对象的范例。

分布式编程的主要动机是并行处理,或者实际上是将应用程序的活动分布在多台计算机上,以便它们同时工作以解决一个问题或一系列复杂的问题。资源在任何环境中通常都是有限的,对于那些滞留在一台机器上的应用程序,资源池(内存、磁盘、I/O 等)很容易变得紧张。当这种情况发生时,应用程序的整体吞吐量和性能会受到影响。分布式编程允许开发人员将应用程序的工作负载分布在多台不同的机器上,每台机器都有自己的一组资源。通过这样做,开发人员可以以非常积极的方式极大地影响应用程序的整体吞吐量。

CORBA 为开发人员提供了一个框架,他们可以在其中开发对象,这些对象可以在单台机器上或通过网络相互通信,而无需考虑硬件平台或编程语言。使用 CORBA,用 UNIX 上的 C++ 编写的对象可以与 Windows 上的 Java 编写的另一个对象或大型机上的 COBOL 编写的对象进行通信。几种技术试图提供类似的功能:TCP/IP、Berkeley Sockets、DCOM、远程过程调用(ONC 和 DCE)、Java 的远程方法调用等。System V IPC 提供了几种进程间通信功能,例如共享内存和消息队列,但它们默认是单机解决方案,而 CORBA 则以在网络上公开对象为中心。CORBA 的实现通常建立在 TCP/IP、套接字和 DCE 等这些组件之上,它提供了优于这些替代方案的独特优势包。

CORBA 允许几乎完全在对象模型内进行分布,抽象了对象通信的细节,以便开发人员只需担心更高级别的接口,而不是通信层的具体细节。任何 CORBA 开发人员都必须尽早意识到网络延迟方面的成本。从某种意义上说,单台计算机的资源限制被带宽和性能方面的网络限制所取代。请记住,每次调用远程对象都是网络调用。如果您的设计要求在远程对象上调用 100 个 set 方法来初始化它(这是一个非常糟糕的 CORBA 设计),您很快就会以全新的视角看到性能下降。

CORBA 对象可以用许多不同的编程语言在许多不同的平台上实现。为了使规范能够定义这种环境的框架,拥有一个与平台和语言无关的对象描述方法非常重要。为了满足这种需求,CORBA 定义了一种称为 IDL(接口定义语言)的映射语言,它的语法实际上与 C++ 非常相似。IDL 用于描述远程对象对外部世界的呈现方式,以及对象中存在的属性或方法。然后使用 IDL 编译器将 IDL 翻译成特定实现语言的源代码,例如 C++、Java、C、Ada、Smalltalk 和 COBOL。对于服务器,IDL 编译器创建 ORB 需要的源代码,以将对象公开给外部世界,并创建一个骨架,然后由开发人员用对象的实际实现“填充”。对于客户端,IDL 编译器创建存根,使远程对象看起来像是客户端本地的对象。为了保持平台和语言的独立性,IDL 有自己的变量类型。IDL 编译器将这些变量类型中的每一种都映射到客户端或服务器的本机语言中的代表性语言构造。

现在我们可以解决 CORBA 实际工作原理的问题了。首先也是最重要的组成部分是 ORB。在 CORBA 规范中,ORB(对象请求代理)是位于 CORBA 对象和 CORBA 对象用户之间的通信层。通过 ORB,客户端应用程序可以访问属性、传递参数、调用方法并从远程对象返回结果。人们普遍误解 ORB 是一个守护进程或服务,它实现了 CORBA——一些漂浮在网络上的虚无缥缈的中间人。实际上,ORB 是一个通信层,它同时部分驻留在客户端和部分驻留在服务器中。ORB 负责拦截对远程对象的调用,定位对象的远程实现并促进与远程对象的通信。因此,当我们谈论“ORB”时,我们谈论的是通过各自的存根和骨架以及通过这些存根和骨架对 ORB 实现的运行时库的调用提供给客户端和服务器的通信功能,这些运行时库提供低级通信和编组功能。

鉴于 ORB 是一个通信层,并且负责定位实现,因此它需要某种方法来查找远程对象。CORBA 通过为所有远程对象分配唯一的 IOR(可互操作对象引用)来实现这一点。IOR 就像一个电话号码,客户端应用程序可以通过它调用特定的远程对象。为了使客户端应用程序能够访问远程对象服务器,它必须首先能够获得 IOR。客户端可以通过几种不同的方法获得 IOR,最简单且最不实际的方法是在命令行上传递它。许多 CORBA 实现(例如 VisiBroker 和 Orbix)都有简单的专有 IOR 查找机制,允许使用“bind”调用将 IOR 传递给客户端。OMG 将命名服务定义为客户端获取远程对象 IOR 的首选方式。在本系列的第三部分中,我们将详细介绍命名服务。但是,对于我们的第一个示例,我们将服务器对象的 IOR 传递给客户端,放在一个通用文件中,客户端将在初始化期间读取该文件。

我们特意创建了一个简单的示例,以便代码可以轻松理解,并且我们提供了 Makefile 和 Make.rules,因为 omniORB 使用了一个相当复杂的 make 方案,很难理解。此示例代码是使用在 Red Hat Linux 5.1(内核 2.0.35)上运行的 omniORB 2.5.0 开发和测试的。该代码是使用 g++ 版本 egcs-2.90.27 980315(带有 Red Hat 的 egcs-1.0.2 默认 C++ 编译器)编译的。(我们还使用最新的 omniORB 2.7.0 和 egcs 1.1.1 编译并运行了代码。)

要构建和运行此示例,请从 http://www.orl.co.uk/omniORB/omniORB.html 下载 omniORB 2.5.0。填写表格,并下载适用于您的 Linux 版本的正确版本的 omniORB——它是免费的。您可能还想下载带有完整源代码的二进制版本。二进制版本期望您使用 gcc 2.7.2 附带的 g++。如果您正在运行 egcs(例如,Red Hat 5.x),您将需要继续重新编译 omniORB,以便它可以与 egcs g++ 编译器一起工作(在 config 目录的 config.mk 中选择 i586_linux_2.0_egcs)。您可以通过键入 g++ -v 来找出您正在运行的 g++ 编译器。按照 README* 文件中的说明了解有关构建和设置 omniORB 环境的说明。

一旦您安装并构建了 omniORB,您就可以从 ftp://ftp.linuxjournal.com/pub/lj/listings/issue61/3201.tgz 下载示例代码。然后解压缩 tar 文件。为了构建示例,您必须编辑 Make.rules 和 Makefile 文件。有关更改您的位置和编译器的信息,请参阅 README.build 文件。一旦您为您的位置编辑了相应的文件,只需键入 make 即可构建示例。

构建示例后,在一个窗口(或虚拟终端)中键入 server 来运行它。请注意,服务器对象的 IOR 会打印到 STDOUT。它也写入名为 ior.out 的文件。接下来,在另一个窗口中键入 client 来运行客户端。这将打开 IOR 文件,获取服务器对象的 IOR,然后将该 IOR 解析为对象引用,并对远程对象进行调用。对于远程连接,您需要将 ior.out 文件从服务器的目录移动到客户端将要运行的目录。您可以使用 FTP 在服务器启动并运行后将文件传输到客户端在另一台机器上的目录来完成此操作。

像大多数 CORBA 应用程序一样,我们简单的 CORBA 示例由三个项目组成。首先,一个服务器应用程序,它实例化一个 CORBA 对象,然后基本上永远阻塞,从而将 CORBA 对象公开给潜在的客户端。其次,CORBA 对象本身的实现,当客户端获得对服务器对象的引用时运行。第三,一个客户端,它绑定到 CORBA 对象并继续根据 CORBA 对象的 IDL 中定义的接口进行调用。

让我们从 IDL 开始。我们的示例定义了一个非常简单的接口 PushString。列表 1 显示该接口 PushString 具有一个名为 pushStr 的函数,该函数接受 IDL 类型字符串的单个输入参数并返回 IDL 布尔值。当这由 IDL 编译器 (omniidl2) 编译时,将创建以下文件

  • PushString.hh:存根/骨架头文件

  • PushStringSK.cc:存根/骨架代码

在 omniORB 实现中(存根和骨架的创建是 ORB 特有的),存根和骨架都在每个处理过的 IDL 文件的同一文件中定义。实现的骨架称为 _sk_PushString,它由 PushString 的实现继承,如下所示(在 PushString_i.h 中)

class PushString_i : public _sk_PushString
接口中定义的每个函数都在骨架类中声明为纯虚函数(在 PushString.hh 中)
virtual CORBA::Boolean pushStr ( const char * inStr ) = 0;
当 PushString (PushString_i) 的实现声明它继承自骨架 (public _sk_PushString) 时,它承诺实现骨架类中的每个纯虚函数。通过这种方式,强制执行了实现完全支持接口的承诺。未能实现骨架中声明的纯虚函数之一是编译器错误。

列表 2 (Srv_Main.C) 中,我们看到了服务器应用程序,它创建 CORBA 对象并通过 ORB 将其呈现给潜在的客户端。程序做的第一件事是打开一个名为 ior.out 的输出文件。这是它要写入它即将创建的对象的 IOR 的地方。然后,它初始化 ORB

CORBA::ORB_ptr orb =
CORBA::ORB_init(argc,argv,"omniORB2");

此调用将作为命令行参数传递给 orb 的参数传入,例如打开跟踪的标志、设置服务器名称等,并唯一地标识此初始化期望 omniORB2 ORB。

ORB 初始化后,轮到 BOA(基本对象适配器)了。服务器的 BOA 使用以下调用初始化

CORBA::BOA_ptr BOA =
orb->BOA_init(argc,argv,"omniORB2_BOA");

BOA 初始化是 ORB 相关的。在 omniORB 中,BOA 的名称设置为“omniORB2_BOA”,用户可以为 BOA 指定某些标志。通信层必须能够与某些东西通信,CORBA 规范中开发的替代方案之一是 BOA。BOA 驻留在 CORBA 服务器中,并负责在客户端请求访问时初始化远程对象。然后,BOA 在 ORB 通信的对象的远程表示和对象的实际物理实现之间提供转换层。

BOA 初始化后,将创建 CORBA 接口的实现,在我们的例子中是 PushString 接口。创建实现后,我们通过调用对象的 _obj_is_ready 函数并使用 BOA 本身的对象引用(由 BOA_init 调用返回)向 BOA 对象注册新创建的实现。向 BOA 注册对象实现的主要目的是让它知道实现正在运行,以便它可以将客户端对对象发出的调用分派给对象。

最后,我们在 BOA 上调用 impl_is_ready。我们这样做是为了让 BOA 知道它应该 现在 开始在其指定的端口上侦听客户端请求。在此调用之前,虽然实现已准备就绪并等待,但不会有任何流量到达,因为 BOA 没有侦听它。正是 impl_is_ready 调用告诉 BOA 开始代表此对象的实现侦听客户端连接。根据 ORB 实现,客户端可能会阻塞对远程对象的函数调用,或者如果未调用 impl_is_ready,则可能会抛出异常。

列表 3 (PushString_i.h) 中,我们看到了实现的头文件,它声明此实现将实现 pushStr 函数,但该类还必须定义自己的构造函数和析构函数。IDL 编译器不会为您创建实现头文件(某些编译器,例如 Orbix 的 idl2cpp 编译器,如果您请求,将创建一个“shell”实现头文件和 cpp 文件);通常您必须自己完成。请注意类声明行

class PushString_i : public _sk_PushString

我们声明 PushString_i 将通过继承骨架来实现 _sk_PushString 中定义的所有纯虚函数(我们只定义了一个)。

列表 4 (PushString_i.C) 中,我们实际上着手定义 PushString.idl 文件中定义的接口的实现。当然,我们将实现我们的构造函数和析构函数,但我们也将为我们的虚拟 pushStr 函数提供一个真正的正文。我们使用函数定义来做到这一点

CORBA::Boolean PushString_i::pushStr(
      const char * inStr)
   throw(CORBA::SystemException)
{
   int retval;
   cerr << "in PushStr\n";
   char * m_str = new char[strlen(inStr)+1];
   strcpy(m_str,inStr);
   // just for fun, mess with the boolean return
   if( strlen(m_str) > 5 )
      retval = 1;
   else
      retval = 0;
   cout << "The string pushed was "
        << m_str << endl;
   delete [] m_str;
   cerr << "Implementation leaving PushStr..."
        << endl;
   return(retval);
}

在这里,当客户端调用 pushStr 函数时,将字符串传递给它(请注意,IDL 字符串类型已在 C++ 中映射到 const char *),该函数会打印一条消息,告知我们我们正在实现中。然后,它将传入的字符串复制到本地缓冲区中,检查传入字符串的长度是否大于 5,如果是,则将函数的布尔返回值设置为 1;否则,它将返回值设置为 0。然后,pushStr 打印出复制的字符串,删除它,并通知我们我们现在正在离开实现。此时,我们将先前创建的布尔值 retval 返回给客户端。

列表 5 (Client.C) 中,我们看到了客户端代码。当我们进入 Client.C 代码时,我们做的第一件事是打开服务器创建的 ior.out 文件,其中包含服务器对象实现的 IOR。客户端期望此文件位于其当前工作目录中。打开该文件并将 IOR 存储在变量“IOR”中后,客户端代码开始让人联想到 Srv_Main.C 文件中的代码。也就是说,发生了相同的初始化 ORB 和 BOA 的调用,参数也相同。

然后,我们创建一个 PushString_var 类型的变量。PushString_var 类型本质上是一种辅助类型,它为编组和解组参数提供存根功能。它还提供其他基本功能,例如释放和复制对象引用,以及确定特定对象引用是否为空(空)的功能。本质上,PushString_var 类型充当客户端进程空间中 PushString 对象的真实实现的代理,该实现可能远在网络另一端。

创建 pushStringVar 后,我们进入一个 try/catch 块,该块调用 ORB 并要求它将字符串 IOR(我们从服务器创建的 ior.out 文件中获得)转换为实际的对象引用。但是,此处创建的对象引用是 CORBA::Object_var 类型的通用类型。为了实际对该对象的接口进行调用,我们必须将其“向下转换”为它表示的对象的实际类型,并且我们为此类型有一个实现。这是通过调用名为 narrow 的函数来完成的,该函数在存根 PushString 的抽象基类中定义(在 PushStringSK.cc 中定义)。一旦通用类型已解析为实际的接口实现,我们就可以对该接口进行调用。这是通过调用来完成的

pushStringVar->pushStr(src);

这会对远程对象的实现进行调用,并传入一个只包含短语“Hello World”的字符串。此时,如果没有在 try 块期间抛出任何异常,客户端会通知我们它已完成调用而没有异常并终止。

最后,您可能想知道是否总是需要通过文件传递 IOR。答案当然是否定的,但由于 omniORB 没有自己的专有绑定机制(这与 VisiBroker 或 Orbix 中的简单示例的实现方式相同),因此在不使用命名服务的情况下让客户端与服务器对象对话的唯一方法是通过服务器传递给客户端的 IOR。在未来关于 CORBA 服务的文章中,我们将展示如何使用 CORBA 命名服务仅通过名称(看起来很像 UNIX 中的绝对路径名)来获取对象引用。有了命名服务,我们将不再需要将 IOR 从服务器传递到客户端。

在我们的下一篇文章中,我们将讨论在 Linux 上使用 VisiBroker for Java,并用 Java 实现 CORBA。在关于 CORBA 服务的文章中,我们还将有一个更复杂的示例,我们将提供一个利用命名服务以及代表客户端创建对象的工厂的示例。

资源

CORBA Program Development, Part 1
Mark Shacklette 是芝加哥 Pierce, Leverett & McIntyre 的负责人,专门从事分布式对象技术。他拥有哈佛大学的学位,目前正在芝加哥大学社会思想委员会完成博士学位。他是奥克顿社区学院教授 UNIX 的兼职教授。他与妻子、两个儿子和一只猫住在伊利诺伊州德斯普兰斯。可以通过 jmshackl@plm-inc.com 与他联系。

CORBA Program Development, Part 1
Jeff Illian 是芝加哥 Energy Mark, Inc. 的负责人,专门从事电力公用事业解除管制和分布式交易技术。他拥有卡内基梅隆大学运筹学(应用数学)学位。他与妻子、儿子和女儿住在伊利诺伊州凯瑞。可以通过 jeff.illian@energymark.com 与他联系。
加载 Disqus 评论