CORBA 程序开发,第 3 部分
在过去的几个月中,我们一直致力于概述在 Linux 上使用 CORBA 进行分布式应用程序开发。在第一篇文章中,我们探讨了“什么是 CORBA?”这个问题,并介绍了使用 ORB 以及简单的客户端和服务器的基础知识。第二篇文章介绍了两个最常见的 OMG(对象管理组)支持的服务,即命名服务和事件服务,并提供了一个使用两者的示例。在本文,我们的第三篇也是最后一篇文章中,我们将通过介绍“tie”机制来更深入地研究到目前为止使用的方法,“tie”是一种绑定到远程对象的委托方法。到目前为止,我们所有的示例都是用 C++ 实现的,但我们必须记住,CORBA 的设计围绕平台和语言独立性的概念。为了进一步演示平台和语言独立性的概念,这次我们将展示一个使用两个不同操作系统平台的示例,其中一个是 Linux,我们的实现将使用 Java 而不是 C++。
虽然我们在撰写本文时使用了 Linux 和 Windows 的组合,但我们的代码应该可以在任何 Java 启用的平台组合上运行。也就是说,您应该期望此代码在 Linux+Solaris 环境或 HPUX+IRIX 环境中同样运行良好。对于第 3 部分,我们选择使用最流行的基于 Java 的 ORB 之一 VisiBroker(来自 Inprise 公司)来实现我们的示例。Inprise 公司友好且正式地认可了我们进行的所有研究。
正如我们在第二部分中讨论的那样,OMG 规范描述了 ORB 如何引导自身以找到公共对象服务 (COS) 的位置,例如命名服务、交易服务或实现仓库。此引导过程隐藏在每个供应商的 resolve_initial_references 方法的实现中。实现 ORB 之间互操作性的真正诀窍是弄清楚如何针对另一个 ORB 进行引导。VisiBroker 以名为 osagent 的二进制可执行文件的形式提供了一个简单的专有命名服务。osagent 提供这种基本的位置服务,同时提供一定程度的容错能力和对象的负载平衡。在 VisiBroker 的情况下,resolve_initial_references 的实现能够定位 osagent,然后向其请求对其他 COS 服务的直接引用。此外,VisiBroker 的命名服务和事件服务在启动时都会尝试查找 osagent,以便注册其 IOR(可互操作对象引用)。在我们的示例中,osagent 将为我们的示例客户端和服务器提供对 VisiBroker 命名服务和事件服务以及任何依赖对象的引用。
不幸的是,osagent 是一个平台特定的二进制应用程序,尚未移植到 Linux。好消息是,对于单个或一组基于 VisiBroker 的应用程序,本地网络上只需要运行一个 osagent。对于我们的示例,我们将在 Inprise 支持的操作系统上运行 osagent。直接引用 VisiBroker 3.4 发行说明,“除了 osagent、osfind 和 locserv 可执行文件外,VisiBroker for Java ORB 完全用 Java 编写,可以在任何 Java 1.1 或 Java 2 兼容的环境中运行。” 因此,我们将在受支持的平台上运行 osagent,并在 Linux 上完成所有其他工作。由于 Java 2 也捆绑了一个简单的 ORB,因此在尝试使用 Java 2 运行 VisiBroker 之前,请务必阅读发行说明。重要的新闻是,根据 VisiBroker 高级产品经理 James Ferguson 的说法,“Inprise 已经看到企业对 Linux 上 VisiBroker 的需求不断增长。这只是 Linux 在企业中快速增长的另一个迹象。为了参与这个新的增长市场,Inprise 将发布关于 Linux 上 VisiBroker for Java 和 C++ 可用性的重要公告。” 如果您想注册您对该方向的支持或了解更多信息,Inprise 建议您将反馈直接发送至 news://forums.inprise.com/inprise.public.visibroker。
本文中的示例代码使用了流行的 OMG CORBA 概念,称为“tie”机制。在第一部分中,我们的基于服务器的对象是通过从骨架实现 _sk_InterfaceName 继承来实现的,这是一个由 OmniORB 的 IDL(接口定义语言)到 C++ 编译器生成的类。通过从这个基类继承,开发人员可以专注于仅实现 IDL 中定义的接口,而不必担心所有实际使 CORBA 通信成为可能的代码。有时使用多重继承来允许继承 IDL 编译器提供的骨架,有时称为 BOA(基本对象适配器)实现,同时也允许实现从其他父类继承。当实现是用 Java 编写时,Java 不支持多重继承,这种情况变得有问题。如果没有多重继承,就不可能同时继承骨架基类和另一个基类,例如应用程序框架。
为了解决这个问题,OMG 规范定义了一个名为 tie 类的委托类。OmniORB 的 IDL 编译器生成一个名为 _tie_InterfaceName 的接口来处理此角色,而 VisiBroker 的 Java IDL 编译器生成一个名为 InterfaceNameOperations 的 Java 接口。开发人员不是从基实现类继承,而是实现一个生成的接口,称为操作接口,该接口除了 IDL 中定义的那些方法、属性或特性外,不包含任何其他方法、属性或特性。然后,操作类在构造函数中传递给包装器 tie 类,该 tie 类通过委托给操作接口对象来实现接口中的每个方法。tie 类实现由生成的基实现提供的 orb 功能。然后,tie 对象是实际绑定到 orb 的组件。由于我们的接口实现不再从基实现继承,这使开发人员可以自由地从另一个基类(例如应用程序框架)继承。
为了更好地理解如何在 Java 中实现 CORBA 解决方案,让我们将 Java 实现与 C++ 实现进行比较,第一部分和第二部分的读者应该对此很熟悉。在 Java 与 C++ 中实现 CORBA 应用程序的方式存在一些差异。专注于 VisiBroker for Java 实现和 VisiBroker for C++ 实现,最明显的区别可以在 Java idl2java 编译器和 C++ idl2cpp 编译器生成的文件数量中看到。基本上,idl2java 创建的文件数量大约是 idl2cpp 编译器的两倍。idl2java 编译器甚至创建一个新的子目录来保存它生成的所有新文件。某些标志可以帮助控制生成的文件数量和类型。当您在没有任何标志的情况下运行 idl2cpp 时,将生成这个非常简单的 IDL 文件 (example.idl)
{ interface SimpleInterface { void SimpleOperation(in short x); }; };
同时,还创建以下文件
example_c.hh:包含 SimpleInterface 的类定义以及支持类。
example_c.cc:包含要与客户端编译和链接的桩代码,它提供支持函数(例如 _ptr 和 _var 定义)。
example_s.hh:包含 _sk_Account 骨架类的定义,用于与 bind 方法继承,以及 tie 方法中委托的 tie 类。
example_s.cc:包含内部骨架的编组代码等。
SimpleInterface.java:为 IDL 中声明的 SimpleInterface 提供简单的公共接口定义。此接口只是在 Java 中模仿 IDL 中定义的接口。此 Java 接口的实际实现包含在 _SimpleInterfaceImplBase 类中(由您将编写的实际实现补充)。
SimpleInterfaceHelper.java:为 SimpleInterface 客户端提供助手方法。在这些助手方法中,最重要的 bind 方法重载以及 narrow 函数(用于 tie 方法)。
SimpleInterfaceHolder.java:提供一个 Java 类,该类保存 SimpleInterface 对象的公共实例。它为 SimpleInterface 对象提供了一个包装器类,这对于允许在 IDL 接口中声明的函数调用中将 SimpleInterface 对象作为 out 和 inout 参数传递是必要的。
SimpleInterfaceOperations.java:提供辅助实现 tie 方法的类。(如果未给出 -no_tie 标志,则不会创建)。
_SimpleInterfaceImplBase.java:提供一个抽象的公共 Java 类,该类实现 SimpleInterface 的服务器端骨架。此基类本身扩展了 org.omg.CORBA.protable.Skeleton,并实现了 Example.SimpleInterface。当使用 bind 而不是 tie 方法时,您的实现会从此基类继承。
_example_SimpleInterface.java:提供简单的代码,您可以填写这些代码以在服务器端实现 SimpleInterface 对象。
_st_SimpleInterface.java:提供一个 Java 类,该类实现客户端桩,它代表客户端代理 SimpleInterface 对象。客户端在此代理上进行调用。
_tie_SimpleInterface.java:提供用于在服务器端实现 tie 方法的委托类。
idl2java 编译器还为每个接口生成一个 helper 类。Helper 类提供了许多静态方法,这些方法为客户端提供了重要的功能。其中包括 bind 和 narrow 方法,这些方法允许客户端连接到基于服务器的对象。它们还提供了 read 和 write 方法,以帮助 Holder 类在 I/O 流和本机对象类型之间进行转换。它们还提供类型代码信息,这些信息在处理 Any 类型以及动态调用和动态骨架接口时非常有用。类型代码提供运行时类型不匹配的检测,以及运行时类型信息的元数据支持。由于 Java 主要是一种解释型语言,因此它必须注意增加的内存约束。Helper 类通过卸载一些很少使用的方法(例如 bind 和 narrow)来提供帮助,以便实际的对象实现可以避免加载这些方法。您可能每秒调用 calculate 方法一百次,但通常只调用 bind 一次。
除了生成的 helper 类之外,使用 CORBA 的 Java 和 C++ 实现看起来非常相似。例如,在 C++ 和 Java 下查找命名上下文之间唯一真正的区别是使用 helper 类来执行 narrow。
Mico C++: CORBA::Object_var nsobj = orb->resolve_initial_references ("NameService"); CosNaming::NamingContext_var nc = CosNaming::NamingContext::_narrow (nsobj); VisiBroker Java: org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService"); org.omg.CosNaming.NamingContext rootContext = org.omg.CosNaming.NamingContextHelper.narrow (objRef);
我们的示例演示了一个简单的日志记录工具,该工具利用了 VisiBroker for Java 命名服务和事件服务,并演示了 Java 中 tie 机制的使用。该示例提供了一个日志服务和两个客户端:一个向日志服务提供事件(消息),另一个使用(读取)这些消息或事件。
本文的示例是一个简单的消息传递服务,形式为记录器,使用与 VisiBroker for Java 事件服务交互的类实现。Supplier 生成字符串,然后将其传递给日志服务,日志服务是一个扩展 Push Supplier 接口的 Java 类。日志服务发布一个名为 send 的函数,该函数允许其客户端之一(Supplier)将事件(发送消息)发布到事件队列。send 方法通过将事件推送到事件队列来转发该事件。PullConsumer 是场景中的另一个客户端,它绑定到事件通道,然后继续从队列中拉取 Supplier 发出的事件。该示例演示了 VisiBroker for Java 中命名服务和事件服务的使用以及 tie 机制。(LogService 是使用 tie 机制实现的。)该示例保持简单,以便轻松传达所涉及的问题。例如,错误处理已保持在绝对最小值,以免掩盖基本要素。因此,字符串在系统中传播的路径如下
Supplier 创建字符串。
在日志服务上调用 send。
日志服务通过 push 将字符串转发到事件通道。
事件通道缓冲供 Consumer 使用的字符串。
Consumer 轮询事件通道以获取新字符串。
Consumer 从事件通道检索字符串。
我们的日志记录工具的 IDL 非常简单(请参阅清单 1)。它定义了一个名为 logging 的模块和一个名为 LogService 的单个接口,该接口实现了一个名为 send 的单个函数,该函数接受单个字符串参数。此字符串传递给日志服务,然后放置在事件通道上,等待 Consumer 读取。Consumer 定期轮询事件通道,检查是否已传递新事件(字符串消息)。如果已传递,它将从事件通道中拉取该字符串并将其打印到 STDOUT。
在 CORBA 的 Java 映射中,IDL 模块映射到 Java 包。因此,IDL 中的 logging 模块映射到 logging 包,在 Java 中,默认情况下,该包是 IDL 文件所在目录下的一个子目录。正是 logging 包(目录)包含 idl2java 编译器生成的所有文件。构建后,logging 目录包含八个 Java 文件,这些文件由 idl2java 编译器生成。该目录包含 LogService 接口、Helper 和 Holder 类的类定义(我们在上面提到过),以及用于委托和绑定的 tie 和 ImplBase 类。
清单 2 显示了 LJEventChannel.java 源代码,该源代码定义了两个类。LogServiceImpl 和 LJEventChannel。LogServiceImpl 类扩展了 _PushSupplierImplBase 基类,并实现了 LogServiceOperations。LogServiceOperations 类具有 tie 机制所需的必要功能,我们将使用该机制连接到 LogServiceImpl 对象。LogServiceImpl 类为绑定到正确的 VisiBroker 事件服务通道提供了核心功能。由于 LogServiceImpl 扩展了 _PushSupplierImplBase,因此它能够充当事件服务的 Push Supplier 角色(有关更多信息,请参阅上个月的文章)。
LogServiceImpl 类的核心在其构造函数中。当创建一个新的 LogServiceImpl 对象时,构造函数首先通过 org.omg.CORBA.ORB.init 调用绑定到 ORB。然后,它连接到特定的事件通道“channel_server”,以便通过它创建的 Push Consumer 代理传递字符串。连接到事件通道的实际过程在上个月的文章中已详细介绍;但是,我们在此处简要总结步骤。
首先,在 EventChannelHelper 类上调用 bind 方法,该类是 VisiBroker 事件服务的一部分。命名服务对于连接到事件服务不是必需的,因为 osagent 通过使用其自己的简单命名服务来促进绑定。
一旦绑定了 EventChannel,就从事件通道获取 Push Consumer 代理,然后将我们的 LogServiceImpl 对象连接到该代理。这使我们可以在代理上进行调用。从那时起,任何在我们的 LogServiceImpl 对象上调用 send 方法的 supplier 都将导致我们的实现在其 Push Consumer 代理上调用 push 方法。这是通过代码行 _pushConsumer.push(message) 完成的。请注意,像往常一样,我们将要发送的字符串打包在 Any 类型中,这是事件服务传输所需的。
LJEventChannel 类由一个 main 方法组成,该方法在绑定到 ORB 并初始化我们对象的 Basic Object Adapter 后,创建一个新的 LogServiceImpl 对象(如上所述)。tie 方法用于绑定过程中,这意味着我们将在与对象的实现进行通信时使用委托而不是继承。在我们 tie 到名为“new_service”的新 LogServiceImpl 委托后,我们将该服务对象绑定到命名服务,组件路径为 Linux Journal:LJEventChannel。这允许可见网络中任何机器上的任何客户端对象通过此命名约定连接到我们的 new_service 对象,而无需知道托管机器的名称或 IP 地址。
一旦 new_service 委托已绑定到命名服务,BOA 就会被告知该对象已准备就绪且可用,并且服务器的实现已完成。
此时,已创建并发布了 IDL 中定义的 LogService 接口的实现,现在客户端可以调用它,希望使用其 send 方法向事件通道发布消息。
清单 3 显示了 PushSupplier.java,它是我们应用程序中的 supplier。PushSupplier 类完全由一个 main 方法组成,该方法在使用 org.mg.CORBA.ORB.init 初始化 orb 后,存储在 supplier 启动时可选地在命令行中输入的 supplier 名称。这个任意名称允许您命名您的 supplier,例如 Supplier1、Supplier2 等,以便您在 consumer 端知道获得了哪个 supplier 的字符串。在初始化 ORB 后,创建一个名为 logger 的 LogService 引用。然后我们输入一个 try 代码块,该代码块尝试使用命名服务绑定到已创建的 LogServiceImpl 对象。supplier 在 orb 对象上调用 resolve_initial_references 方法,获取根上下文,创建适当的名称组件,并在根上下文上使用创建的名称组件数组调用 resolve。然后,通过调用使用 LogServiceHelper 对象的 narrow 来缩小返回的通用对象引用。
假设 logger 对象不为空,然后我们进入一个循环,该循环不断地通过其 send 方法向 LogServiceImpl 对象发送字符串。这将持续到用户使用 ctrl-C 中断 supplier。
清单 4 显示了 PullConsumer 类,该类扩展了 VisiBroker 事件服务的 _PullConsumerImplBase 基类。在初始化 ORB 和 BOA 后,PullConsumer 对象尝试通过调用 EventChannelHelper 对象上的 bind 方法来绑定到事件通道。然后创建一个新的 PullConsumer 对象,该对象实现了 PullConsumer 所需的 disconnect_pull_consumer 方法。必须创建一个新的 PullConsumer 的原因是我们需要一个对象引用来传递给 BOA 的 obj_is_ready 和代理 Pull Supplier 的 connect_pull_consumer 方法。由于我们在静态 main 方法中,因此没有“隐式 this”引用可供我们使用。因此,我们需要创建一个新的对象才能获得要传递的引用。一旦创建了一个新的 PullConsumer 对象,BOA 就会被告知该对象已准备就绪。之后,通过调用绑定通道的 obtain_pull_supplier 方法来创建 Pull Supplier 代理。创建代理后,通过在代理上调用 connect_pull_consumer 并将 PullConsumer 对象传递给它,将 PullConsumer 对象连接到代理。
此时,进入一个 while 循环,consumer 不断在 Pull Supplier 代理上调用 try_pull。如果代理找到一个事件,则返回该事件,PullConsumer 对象将该消息打印到标准输出,并且循环重新开始。
您可以通过首先在同一 Linux 框上本地运行单个 PushSupplier 和 PullConsumer 来试用此应用程序。(有关应用程序的设置、构建和启动的详细信息,请参阅随代码附带的 README.install 和 README.run 说明。)然后您可能想要启动另一个 PushSupplier,并注意 PullConsumer 也会自动开始处理来自新 PushSupplier 的事件。(您可能想要在启动 PushSupplier 时对其进行命名——有关如何执行此操作,请参阅 README.run 说明。)然后在 Windows(或其他 OS)框上启动一个新的 PullConsumer,并观察来自两个 supplier 的事件如何传送到两个 consumer,其中一个 consumer 现在在 Windows 机器上运行。最后,在 Windows 机器上启动另一个 PushSupplier,并观察两个 consumer 如何处理由三个不同的 supplier 创建并传递到同一事件通道的字符串。即使代码很简单,此处实现的项目也非常强大,并且具有一些您应该探索的广泛含义。
在这三篇文章中,我们试图向您介绍 Linux 上的 CORBA 编程。Linux 是开发 CORBA 应用程序的强大平台,CORBA 在其功能、服务、平台独立性和语言独立性方面非常通用。我们希望这些文章激发了您对 CORBA 和 Linux 的兴趣,并祝您在自行更全面地探索这些问题方面取得成功。对于那些希望了解更多信息的人,请访问免费 CORBA 页面,网址为 adams.patriot.net/~tvalesky/freecorba.html。它的主题和质量都在不断增长,包括有关 Java 2 中 CORBA 实现的一些信息。

