CORBA 程序开发,第 2 部分
在上一篇文章中,我们从高层次的角度介绍了使用 CORBA 进行分布式编程的概念。为了进一步充实 CORBA 基础设施,我们需要详细介绍 OMG(对象管理组织)定义的一些标准服务,这些服务应至少由大多数 ORB 供应商部分提供。其中包括交易服务、命名服务、事件服务、接口仓库和实现仓库。
OMG 仅定义了每个服务的接口,而没有尝试提供实现。这意味着 OMG 服务实际上只是用 IDL(接口定义语言)编写的 CORBA 接口。如果特定服务在特定 ORB 中不可用或实现不佳,开发人员始终可以选择为该接口编写自定义实现。事实上,如果供应商真正符合 CORBA 标准,则一个供应商的服务实现可以与另一个供应商的 ORB 实现一起使用。这种混合搭配符合 CORBA 标准的实现的能力为 CORBA 解决方案提供了灵活的方法。在本文中,我们将介绍两种最常用的 OMG 服务:命名服务和事件服务。我们的示例代码使用功能丰富且获得 GNU 许可的 MICO CORBA 实现编写,并演示如何在 C++ 中使用命名服务和事件服务。
上个月,我们介绍了 IOR(可互操作对象引用)的概念,我们说它就像远程对象的电话号码或邮寄地址。客户端应用程序可以使用 IOR 来定位远程对象并建立通信。在那篇文章中,我们通过将其写入文件并在启动时将文件传递给服务器应用程序,将 IOR 传递给客户端应用程序。实际上,这是一种不方便的系统设计方式。解决运行时对象定位问题最常见的方法之一是使用 OMG 命名服务。命名服务是数据库的接口,其中对象的名称与其 IOR 相关联。
为了理解命名服务,通常将命名服务视为 UNIX 目录结构会很有帮助。命名服务由称为命名上下文的对象组成。命名上下文可以被认为是文件系统中的目录,最终源自一个公共根目录(“根”上下文)。命名上下文中每个名称都必须是唯一的。由于命名上下文实际上是对象,因此可以将命名上下文注册到另一个命名上下文。实际上,这类似于在文件系统中的另一个目录中创建子目录。通过这种方法创建的层次结构称为命名图。为了简化在命名图中查找对象,命名服务允许通过复合名称引用对象,这类似于 UNIX 中的绝对路径名。
在命名服务中注册对象的名称完全是自由决定的,甚至不需要描述实际对象。在命名服务中,对象的名称由 NameComponent 对象定义。这些 NameComponent 对象随后存储在特定的命名上下文中。NameComponent 对象实际上由两部分组成,即“标识符”和“种类”。NameComponent 在 IDL 中表示为
struct NameComponent { Istring id; Istring kind; };
回到 UNIX 文件系统类比,一个名为 Consumer.C 的 UNIX 文件将具有标识符 Consumer 和种类 C。同样,一个对象可以存储在命名上下文中,标识符为 BusinessObject,种类为 java。因此,开发人员可以在使用命名服务定义对象时使用他希望的任何命名标准。
为了使 CORBA 客户端对象能够使用命名服务来查找其他对象,它必须知道在哪里找到命名服务。查找命名服务的首选方法是使用 OMG 方法 resolve_initial_references。在大多数 ORB 解决方案下,resolve_initial_references 将返回“根命名上下文”的 IOR,或者实际上是根目录节点。
简单来说,当服务器应用程序启动时,它会使用复合名称注册或“绑定”它希望公开的对象到命名服务。这是通过 bind 和 rebind 方法完成的。然后,客户端应用程序可以通过解析对象的复合名称来查找特定对象的 IOR,客户端必须知道该复合名称。客户端应用程序使用 resolve 方法从给定的复合名称中查找 IOR。一旦名称被解析并且 IOR 被获得,应用程序可以缩小(缩小对象是 CORBA 术语,表示向下转型)对象引用以解析实际的对象实现;从那时起,该对象可以像往常一样使用。稍后,我们的示例将演示如何使用命名服务来注册和定位对象实现。
另一个具有 OMG 定义接口的服务是事件服务。OMG 事件服务规范提供了 CORBA 对象之间解耦的消息传输。事件服务提供的通信解耦允许通信模式和方法的灵活性。具体来说,它允许一个对象(供应商)能够向另一个对接收这些消息感兴趣的对象(消费者)发送消息,而无需知道接收者在哪里,甚至接收者是否正在侦听。这种解耦提供了几个重要的好处
供应商和消费者不必实际处理通信,也不需要任何关于彼此的特定知识。它们只需连接到事件服务,事件服务会协调它们的通信。
供应商和消费者之间的消息传递是异步发生的。消息传递不需要阻塞(尽管拉取消费者可以选择阻塞,如果它愿意——见下文)。
事件通道可以设置为类型化或非类型化(并非所有 ORB 实现都支持类型化事件)。
事件通道将自动缓冲接收到的事件,直到合适的消费者表示对这些事件感兴趣。请注意,这并不意味着持久性或存储和转发功能。通常,事件通道中的一个独立队列将专用于每个消费者。这些内部队列通常基于 LIFO(后进先出)原则,当缓冲区已满且新消息到达时,会丢弃旧消息,而消费者没有足够快地提取消息。大多数 ORB 将允许您设置最大队列长度。
如果供应商实现了此功能,则可以确认事件并保证其交付。
供应商可以选择将事件推送到通道上(push),或者让通道从它们那里请求事件(pull)。同样,消费者可以请求同步(拉取)或异步(try_pull)从通道获取事件,或者让通道将事件传递给它们(推送)。
供应商和消费者之间不需要一一对应。可以通过事件服务将多个供应商连接到单个消费者,以及将单个供应商连接到一个或多个消费者。
供应商和消费者以及事件通道之间存在两种主要的交互方式:推送和拉取。
在推送方法中,供应商将连接到事件通道,并在准备好时启动事件到事件通道的推送。事件通道的责任是缓冲这些事件,直到它们被传递给一个或多个感兴趣的消费者。在推送模型中,是供应商启动事件流向事件通道。当供应商想要连接到事件通道时,它需要在事件通道中找到一个对象来“假装”它是消费者。这允许供应商简单地将事件传递给其“消费者”,而实际上,其消费者只是实际消费者的代理,该消费者位于事件通道之外。正是向这个代理“消费者”,推送供应商推送事件。因此,代理对象不是真正的消费者,而只是事件通道中的一个对象,它提供了一种交付机制,供应商可以通过该机制交付消息。
推送消费者也将连接到“代理”对象,该代理代表推送供应商。当事件通道有可用消息时,推送供应商代理将消息传递(推送)到实际的消费者对象。消息路径是从实际的推送供应商,通过其代理推送消费者,到代理推送供应商,最后到推送消费者本身。还有其他变体,正如我们稍后的示例将展示的那样。
在拉取方法中,事件通道将从供应商拉取数据。在拉取模型中,是消费者驱动消息的传递。拉取供应商将连接到代理拉取消费者。同样,就拉取供应商而言,它可以将此代理对象视为一个真正的消费者,它会定期请求事件。然后,感兴趣的拉取消费者对象将在事件通道的另一端连接到代理拉取供应商。当拉取消费者准备好接收事件时,它将对其代理拉取供应商启动 pull 或 try_pull 调用,这将反过来查询连接到实际拉取供应商的代理拉取消费者,以请求交付另一个事件。通过这种方式,消费者在准备好处理另一条消息时驱动数据。拉取方法的一些实现将允许代理拉取供应商定期从供应商拉取事件,以尝试保持缓冲区充满事件,供消费者在请求交付时使用。
关于事件通道抽象的好处是通信不需要完全是推送模型或拉取模型。推送供应商可以间接连接到一个或多个拉取消费者,并且多个拉取供应商可以连接到一个或多个推送消费者。正是事件通道逻辑允许对象之间存在这种相互关系的不成比例性。应用程序设计驱动关于供应商、消费者及其数量的决策。
无论供应商和消费者之间的关系如何,为了建立连接并通过事件通道传递事件,必须采取以下五个步骤
客户端(供应商或消费者)必须绑定到事件通道,事件通道必须已经由某人创建,可能是客户端。
客户端必须从事件通道获取管理对象。
客户端必须从管理对象获取代理对象——供应商客户端的消费者代理和消费者客户端的供应商代理。
通过连接调用将供应商或消费者添加到事件通道。
通过推送、拉取或 try_pull 调用在客户端和事件通道之间传输数据。
当消息通过事件通道传递时,它们可以是“类型化”或“非类型化”的。类型化消息是在 IDL 中定义的那些消息,在编译时进行类型检查。非类型化事件是最常见的,它们遵循标准事件服务接口,并打包为类型 CORBA::Any,它是所有已知 CORBA 类型的包装器。正是这种“Any”类型实际上是从供应商对象发送到消费者对象的。供应商将构造一个 Any,消费者在收到消息后,将从 Any 包装器中导出真实值。这为传递消息提供了极大的灵活性,因为供应商可以首先传递一个字符串,其次传递一个长整型值,第三次传递一个数组,所有这些都通过将值打包到 Any 中来完成。示例代码展示了如何创建、嵌入和提取 Any 类型的值。
我们的示例结合了命名服务查找以及供应商和消费者通过使用事件服务进行交互的实现。供应商实现推送供应商模型,消费者实现拉取消费者模型,从而说明模型不必都是一种类型。列表 1 显示了 Consumer.C,列表 2 显示了 Supplier.C。这些列表可以通过匿名下载在文件 ftp://ftp.linuxjournal.com/pub/lj/listings/issue62/3213.tgz 中获得。
消费者必须采取的第一步是找到根命名上下文。这是通过调用 resolve_initial_references,然后缩小返回的 IOR 来完成的。结果对象是根命名上下文,然后我们可以使用它来解析我们的事件服务。
CORBA::Object_var nsobj = orb->resolve_initial_references("NameService"); assert(! CORBA::is_nil(nsobj)); CosNaming::NamingContext_var context = CosNaming::NamingContext::_narrow(nsobj); assert(! CORBA::is_nil(context));
当我们转向代码的事件服务部分时,我们注意到消费者在从命名服务获取初始上下文后做的第一件事是解析和缩小 EventChannelFactory。
CosNaming::Name name; name.length(1); name[0].id = CORBA::string_dup("EventChannelFactory"); name[0].kind = CORBA::string_dup("factory"); CORBA::Object_var obj; obj = context->resolve(name);MICO 使用上面引用的工厂作为通用 CORBA::Object 来创建一个新的事件通道对象,方法是首先缩小通用引用,然后调用工厂的 create_eventchannel 函数
SimpleEventChannelAdmin::EventChannelFactory_var factory; CosEventChannelAdmin::EventChannel_var event_channel; factory = SimpleEventChannelAdmin::EventChannelFactory::_narrow(obj); event_channel = factory->create_eventchannel();然后我们使用命名服务通过命名服务的 bind 方法将这个新创建的事件通道对象绑定到名称 TestEventChannel。这样做是为了供应商在需要时能够通过名称 TestEventChannel 找到这个特定的事件通道。
name.length(1); name[0].id = CORBA::string_dup("TestEventChannel"); name[0].kind = CORBA::string_dup(""); context->bind(name,<\n> CosEventChannelAdmin::EventChannel:: _duplicate(event_channel));一旦事件通道被创建并命名,事件通道对象 (event_channel) 就被用来通过 for_consumers 函数获得对 ConsumerAdmin 对象的引用。ConsumerAdmin 对象为事件通道的消费者客户端提供代理。它允许消费者获得适当的供应商代理。在我们的例子中,我们使用 ConsumerAdmin 对象为我们(拉取消费者)提供一个代理拉取供应商。这允许我们的消费者对象表现得好像它直接与一个期望我们“拉取”事件的供应商通信一样。当然,事实并非如此。我们的供应商实际上是一个推送供应商,它将事件推送到事件通道上。代理解耦了消费者和供应商对象,并允许它们像直接连接一样运行,而实际上,它们的连接是间接的。一旦我们有了 ConsumerAdmin,我们就使用它来创建我们的推送消费者代理
CosEventChannelAdmin::ConsumerAdmin_var Consumer_admin; Consumer_admin = event_channel->for_consumers(); ... CosEventChannelAdmin::ProxyPullSupplier_var proxy_Supplier; proxy_Supplier = Consumer_admin->obtain_pull_Supplier();一旦消费者获得了对其供应商代理的引用,它就会通过调用代理的 connect_pull_Consumer 方法,通知事件通道它对接收来自它的事件感兴趣。事件服务的拉取消费者接口的实现被传递到 proxy_Supplier 以建立连接。
proxy_Supplier->connect_pull_Consumer (CosEventComm::PullConsumer::_duplicate(Consumer));连接后,可以对代理拉取供应商的 pull 或 try_pull 函数进行调用。PullSupplier 接口是
interface PullSupplier { any pull() raises(Disconnected); any try_pull(out boolean has_event) raises(Disconnected); void disconnect_pull_Supplier(); };在我们的例子中,我们让消费者派生一个工作线程,并将拉取供应商代理引用传递给该线程,该线程实际上进行了 try_pull 调用。try_pull 调用是一种异步轮询机制,允许消费者联系事件通道并“检查邮件”。如果事件通道中有消息,则该消息将作为 CORBA::Any 值返回,并且 try_pull 的 CORBA::Boolean 标志 has_event 将设置为 true。因此,try_pull 调用是从线程的“start”函数中以这种方式进行的
CORBA::Any* anyval; CORBA::Boolean has_event = 0; anyval = proxy_Supplier->try_pull(has_event);如果没有事件等待,则 has_event 标志设置为 false,并且不返回任何值;但是该调用不会阻塞(像 pull 函数那样),因此它会立即返回到客户端。这允许客户端继续执行其他工作,同时定期检查事件通道队列中是否有新的事件消息等待。
一旦 has_event 值为 true 并且检索到 Any 值,消费者必须首先确定它的类型,然后从 Any 包装器中提取该值才能使用它。执行此操作的代码使用 Any 的重载 >>= 运算符。这个看起来很奇怪的野兽将尝试将 Any 提取到目标类型中。如果 Any 中包含的类型与目标类型兼容,则该值将从 Any 中提取;否则,返回 null。检查值的常用方法是执行以下操作
if( *anyval >>= shortval ) { cerr << "Consumer: thread pulled short: " << shortval << endl; } else if( *anyval >>= doubleval ) { cerr << setiosflags(ios::fixed); cerr << "Consumer: thread pulled double: " << doubleval << endl; }
在我们的例子中,当我们从 Any 中提取正确的类型时,我们会将其打印出来,并立即开始再次通过 try_pull 调用检查事件。
我们的供应商实现稍微简单一些。在绑定到 ORB 后,它会创建一个实现 CORBA PushSupplier IDL 的类的实现
class PushSupplierImpl : virtual public CosEventComm::PushSupplier_skel { public: PushSupplierImpl() { } void disconnect_push_Supplier(); }; ... PushSupplierImpl * Supplier = new PushSupplierImpl();
这个类实现了 IDL PushSupplier 接口,该接口只有一个函数需要实现:disconnect_push_Supplier。实现对象 PushSupplierImpl * Supplier 将在稍后用于连接到事件通道并注册我们对向通道提供事件的兴趣。
正如消费者开始通过查找根命名上下文一样,我们的供应商也开始调用 resolve_initial_references。使用 resolve_initial_references 返回的 IOR,供应商可以缩小到命名上下文对象。
CORBA::Object_var nsobj = orb->resolve_initial_references("NameService"); assert(! CORBA::is_nil(nsobj)); cerr << "Supplier: successful call to \ resolve_initial_references()" << endl; CosNaming::NamingContext_var context = CosNaming::NamingContext::_narrow(nsobj); assert(! CORBA::is_nil(context));
一旦名称被解析和缩小,供应商就会尝试通过调用事件通道的 for_suppliers 函数来检索 SupplierAdmin 对象。
CosNaming::Name name; name.length(1); name[0].id = CORBA::string_dup("TestEventChannel"); name[0].kind = CORBA::string_dup(""); CORBA::Object_var obj; ... obj = context->resolve(name); ... CosEventChannelAdmin::EventChannel_var event_channel; CosEventChannelAdmin::SupplierAdmin_var Supplier_admin; ... event_channel = CosEventChannelAdmin::EventChannel::_narrow(obj); Supplier_admin = event_channel->for_suppliers();一旦检索到 SupplierAdmin 对象,就会调用其 obtain_push_Consumer 函数,以便供应商获得用于通信的代理推送消费者。
CosEventChannelAdmin::ProxyPushConsumer_var proxy_Consumer; ... proxy_Consumer = Supplier_admin->obtain_push_Consumer();一旦获得代理,我们需要通过以下调用将供应商连接到代理
proxy_Consumer->connect_push_Supplier( CosEventComm::PushSupplier::_duplicate(Supplier));此调用注册了我们对向事件通道提供事件的兴趣。PushConsumer 的 IDL 接口(ProxyPushConsumer 继承了该接口的实现)是
interface PushConsumer { void push(in any data) raises(Disconnected); void disconnect_push_Consumer(); };一旦获得了代理推送消费者,就可以对其 push 函数进行调用,传入一个 CORBA::Any 值。这非常简单地完成
CORBA::Any any; any <<=(CORBA::ULong) 555555555; proxy_Consumer->push(any);此时,Any 值被传递到事件通道,事件通道负责使该事件消息可用于上面描述的消费者的 try_pull 调用。因此,我们在关于供应商/消费者在与事件服务交互中的角色的讨论中已经完成了整个循环。
我们的示例是使用 egcs 1.1b C++ 编译器和 MICO 2.2.1 构建的。为了构建和运行示例,一旦您解压缩了 tar 文件,您只需更新 Makefile 中的变量 MICO_BASEDIR 以指向您的 Mico 基本安装目录,然后键入 make。这将构建供应商和消费者。为了运行应用程序,我们提供了一个简单的脚本,该脚本会自动为您启动相当冗长的 MICO 命名和事件服务,然后启动消费者(创建事件通道),然后启动供应商。要运行脚本,只需键入 runit。您将看到供应商将消息写入事件通道的进度,以及消费者从事件通道中提取消息的进度;当它这样做时,它会将它们打印出来。我们的供应商将依次推送一个 long、一个 short、一个 double、一个 string,最后是另一个 long(数字 13),这向消费者发出信号,表明它已完成。此时,消费者线程退出,应用程序被 runit 脚本杀死。
我们的下一篇文章将讨论 VisiBroker for Java 的实现,该实现可用于完全在 Linux 上使用 Sun 的 JDK 开发客户端和服务器。
资源
对象管理组织主页:http://www.omg.org/
CORBA 简介:http://www.omg.org/news/begin.htm
免费 CORBA 页面:http://adams.patriot.net/~tvalesky/freecorba.html
Linux 的 Java 端口:http://java.blackdown.org/
CORBA 常见问题解答:http://www.cerfnet.com/~mpcline/Corba-FAQ

