通过一个简单的实例,详细介绍基于CIAO的CCM组件开发过程。
前面讲过,CCM是以EJB为蓝本来定义的,因此,二者在组件分类(与EJB被分为Session、Entity、Message Driven三种类型一样,CCM组件被分为Service、Session、Process、Entity四种类型)、组件的基本组成、开发/部署基本流程等方面十分相似,但由于目前CCM组件应用服务器/应用框架等远不如EJB成熟,在开发环境的支持方面也远不如EJB完善,因此,其开发过程还比较繁琐。
下面将围绕如下的应用需求详细讨论CIAO平台下CCM开发的详细过程:
一个Monitor程序负责监视多台设备的状态,且每个设备上均运行一个设备控制程序Controller,Controller的客户端通过该组件提供的接口来操控Controller;按照常规的方法,可以采用轮询的方式,Monitor定期向Controller查询设备目前的状态信息,也可以通过Event Service在Controller与Monitor间建立事件通道。在这里,我们采用CCM来解决上述问题。
整个系统包括两个组件Controller和Monitor;当设备启动时,Controller通过Monitor提供的DeviceIDAllocator接口获得由Monitor分配的唯一标识信息(如IP地址等信息,本例中为简化问题,该唯一标识仅由一个表示名称的字符串组成,且Monitor不会记忆已分配给每个设备的标识);当Controller状态发生变化时,向Monitor发布DeviceStatus状态变化通知事件。
IDL文件被用于描述组件Controller、Monitor之间的通信接口、组件的基本组成(包括组件所支持的Facet/Receptacle、eventtype、Event Source/Event Sink、Component Home、组件的Attribute等)以及组件对外提供的接口。
1、建立一个目录DeviceAdmin,在该目录下创建三个子目录:Controller、DeviceBase、Monitor,分别作为Controller组件工程文件、组件公共工程文件、Monitor组件工程文件的存放目录。
2、在DeviceBase下建立如下idl文件,其中定义了一些组件间通信需要用到的基本接口及结构。
//DeviceBase.idl
#include <Components.idl>
module Device {
const long MAX_RUN_LEVEL = 5;
const long MIN_RUN_LEVEL = 0;
// a demo device unique id
struct DeviceID {
string device_name;
};
// a demo device status
struct DeviceStatus {
long run_level;
};
struct StatusPair {
DeviceID device_id;
DeviceStatus device_status;
};
/**
* @event DeviceStatus
*
* @brief component event between Monitor and Controller
* Controller publishes this event when status change.
*/
eventtype StatusEvent {
public StatusPair status_pair;
};
/**
* @interface DeviceIDAllocator
*
* @brief Controller use this facet to get a unique id from Monitor
*/
interface DeviceIDAllocator {
DeviceID get_id();
};
/**
* @interface DeviceOperate
*
* @brief a interface exposed to client by Controller
*/
interface DeviceOperate {
void power_on();
void power_off();
DeviceID get_device_id();
DeviceStatus get_device_status();
boolean tune(in boolean tune_up);
};
};
其中,DeviceID表示设备的唯一标识,DeviceStatus表示设备的状态信息,StatusEvent是Controller、Monitor间传递的通知事件,DeviceIDAllocator是由Monitor提供的一个Facet接口,DeviceOperate是Controller组件支持的一个普通接口。
3、在Controller目录下建立如下的idl文件,用于声明Controller组件对外的接口及与其他组件的通信接口。Controller组件支持DeviceOperate接口,并有一个DeviceIDAllocator类型的receptacle,和可对外发布StatusEvent类型的通知事件,此外,Controller组件还有两个属性,分别表示设备的唯一标识和设备的当前状态,其中唯一标识是只读属性。
//Controller.idl
#include "../DeviceBase/DeviceBase.idl"
module Device
{
/**
* @class Controller
*
* @brief component
*/
component Controller supports DeviceOperate {
publishes StatusEvent notify_out;
uses DeviceIDAllocator id_allocator;
readonly attribute DeviceID device_id;
attribute DeviceStatus device_status;
};
/**
* @class ControllerHome
*
* @brief home for Controller component
*/
home ControllerHome manages Controller { };
};
4、在Monitor目录下添加如下idl文件,用于声明Monitor组件对外的接口及与其他组件的通信接口。Monitor组件支持一个DeviceIDAllocator类型的facet,并可接收StatusEvent通知事件。
//Monitor.idl
#include "../DeviceBase/DeviceBase.idl"
module Device
{
/**
* @class Monitor
*
* @brief component
*/
component Monitor {
provides DeviceIDAllocator id_allocator;
consumes StatusEvent notify_in;
};
/**
* @class MonitorHome
*
* @brief home for Monitor component
*/
home MonitorHome manages Monitor {};
};
cidl文件用于描述组件和组件Home接口的实现和持久状态,cidl编译器cidlc可以根据idl和cidl文件为我们自动生成组件程序框架,从而大大简化组件的开发。CIDL所生成的实现称为executor,executor包含了一些自动实现,并提供了钩子方法以允许开发人员可以增加定制的组件专门的逻辑。executor可以打包到DLL中,并可以安装到支持特定目标平台和编程语言的组件Server中。
1、进入Controller目录,添加如下cidl文件:
//Controller.idl
#include "Controller.idl"
composition session Controller_Impl {
home executor ControllerHome_Exec {
implements Device::ControllerHome;
manages Controller_Exec;
}
};
并执行:
%CIAO_ROOT%/bin/cidlc -I%CIAO_ROOT% -I%CIAO_ROOT%/DAnCE -I%CIAO_ROOT%/ciao -I%TAO_ROOT% -I%TAO_ROOT%/tao -I%TAO_ROOT%/orbsvcs --gen-exec-impl -- Controller.cidl
以生成最终实现类的基本结构,通过执行上述命令,我们将得到Controller_exec.h和Controller_exec.cpp(以及servant类和其它几个文件,但只有上述两个文件是我们需要手工修改的),这是我们实现Controller组件方法和ControllerHome接口的地方,其中包含了为实现组件需要实现的各方法(包括属性的accessor/mutator方法、组件支持的接口所包含的方法、其它CCM相关的基本方法,如ccm_activate(), ccm_passivate(), ccm_remove()、set_session_context()等)的声明和空的函数体,你可以无需任何修改即可将上述文件加入组件工程完成编译。
2、进入Monitor目录,添加如下cidl文件:
//Monitor.idl
#include "Monitor.idl"
composition session Monitor_Impl {
home executor MonitorHome_Exec {
implements Device::MonitorHome;
manages Monitor_Exec;
}
};
并执行:
%CIAO_ROOT%/bin/cidlc -I%CIAO_ROOT% -I%CIAO_ROOT%/DAnCE -I%CIAO_ROOT%/ciao -I%TAO_ROOT% -I%TAO_ROOT%/tao -I%TAO_ROOT%/orbsvcs --gen-exec-impl -- Monitor.cidl
以生成最终实现类的基本结构,通过执行上述命令,我们将得到Monitor_exec.h和Monitor_exec.cpp。
虽然上面已经生成了组件实现类的基本架构,但我们还需要借助其他CCM自动化工具生成我们的工程文件。
1、进入DeviceBase目录,依次执行如下命令:
%CIAO_ROOT%/bin/generate_component_mpc.pl -n DeviceBase
生成DeviceBase基础工程描述文件。
由于DeviceBase工程仅用于编译我们的组件工程依赖的基本接口及结构信息,并不是一个组件,因此,我们需要手动删除DeviceBase_svnt工程的部分内容。打开DeviceBase.mwc文件,删除DeviceBase_svnt工程CIDL_Files、IDL_Files两个说明项,仅保留Source_Files说明项下的DeviceBaseS.cpp文件。修改后的DeviceBase_svnt工程部分的内容应该是:
project(DeviceBase_svnt) : ciao_servant_dnc {
after += DeviceBase_stub
sharedname = DeviceBase_svnt
libs += DeviceBase_stub
idlflags += -Wb,export_macro=DEVICEBASE_SVNT_Export -Wb,export_include=DeviceBase_svnt_export.h
dynamicflags = DEVICEBASE_SVNT_BUILD_DLL
Source_Files {
DeviceBaseS.cpp
}
}
2、进入Controller目录,依次执行如下命令:
%CIAO_ROOT%/bin/generate_component_mpc.pl -p DeviceBase Controller
生成Controller组件工程描述文件。
3、进入Monitor目录,依次执行如下命令:
%CIAO_ROOT%/bin/generate_component_mpc.pl -p DeviceBase Monitor
生成Monitor组件工程描述文件。
4、进入DeviceAdmin目录,并在该目录下执行:
mwc.pl -type vc8
以生成整个Solution文件。打开生成的.sln文件,将看到该Solution下包含有8个Project,分别是:
DeviceBase_stub
DeviceBase_svnt
DeviceBase_Controller_exec
DeviceBase_Controller_stub
DeviceBase_Controller_svnt
DeviceBase_Monitor_exec
DeviceBase_Monitor_stub
DeviceBase_Monitor_svnt
且各工程间的依赖关系已建立好,你只需编译DeviceBase_Controller_exec、DeviceBase_Monitor_exec即可完成整个工程的编译,试试编译这两个工程,你应该可以顺利通过编译,但由于还没有添加实现代码,组件什么也做不了。
仔细查看自动生成的Monitor_exec.h/Monitor_exec.cpp、Controller_exec.h/Controller_exec.cpp会发现:
1)对于组件的每个普通attribute,会生成一个accessor和一个mutator方法,而对于readonly attribute,仅会生成一个accessor方法;
2)自动工具还自动生成了组件所支持的对外接口的框架;
3)对于组件所支持的Facet,会单独生成一个框架类,其中包含了该Facet所有方法的空的实现;而Receptacle一方可以通过相应Context类提供的“get_ + 接口名”方法来访问由对方组件提供的服务。
4)对于Event的接收方,会自动生成一个以“push_ + 事件名”命名的方法;而对于Event的发送方,相应的Servant类以包含了发送事件相关的代码,我们只需调用相应Context类的“push_ + 事件名”方法即可。
为了节省篇幅,这里不详细介绍实现的具体内容,读者可以比较刚创建的工程文件和附件中的工程文件,以找出其中被修改的地方,所有的修改均集中在Monitor_exec.h/Monitor_exec.cpp、Controller_exec.h/Controller_exec.cpp几个文件中。
现在到了整个组件应用开发中最不令人愉快的阶段,我们要用CoMIC这个工具来描述我们的组件,以生成组件的descriptor文件。CoMIC是一个用于生成组件部署descriptor的MDD工具,其安装请参照该项目的安装说明,虽然CoMIC想用尽可能简便、直观的方式来帮助我们编写descriptor,但其过程仍然有些烦琐。我不久前曾建议CIAO的作者实现一个类似idl的组件描述语言,以便通过编译该文件获得组件的描述信息,但Schmdit博士似乎对此不感兴趣,并认为可以借助另一项目Cadena提供的支持来实现相关功能,而Cadena是一个Eclipse的插件(它主要面向的是另一基于Java的CCM实现OpenCCM,虽然它也支持CIAO),对于Java开发者来说,Eclipse简直太完美了,但用Eclipse来开发C++应用实在不是什么让人愉快的事情。
作为一个MDD工具,CoSMIC允许我们用类似绘制UML图的方式来描述系统内各组件间的关系,以及系统内包含的等。
运用CoSMIC描述组件的基本流程如下:
1、运行idl_to_picml命令解析各idl文件,生成可被CoSMIC导入的平台无关组件模型语言(PICML,Platform-Independent Component Modeling Language)xml描述文件。
2、添加ComponentImplementation,以虚拟组件的形式描述各组件间Facet/Receptacle、Event Source/Event Sink等组件端口(Port)的集成关系;
3、添加ComponentPackage,描述组件与组件实现、组件端口之间的集成关系。
4、添加PackageConfiguration,描述组件与相应ComponentPackage之间的集成关系。
5、添加ToplevelPackage,描述顶层包与虚拟组件包之间的集成关系。
6、添加Targets描述,以定义可供组件驻留的节点;
7、添加DeploymentPlan,以定义组件与节点之间的关系。
限于篇幅,这里不详细介绍CoSMIC描述组件的细节,具体过程请参照%CIAO_ROOT%/docs/tutorials/CoSMIC,附件中包含了最终完成的CoSMIC工程文件,可供读者对照。
完成CoSMIC工程后,在工程目录DeviceAdmin下创建一个目录descriptors目录,选择工具栏上的Generate Package Descriptors、Generate Domain Descriptors、Generate Flattend DeploymentPlan生成工程的部署描述文件,将所有输出文件保存到descriptors目录下。(提示:如果你在使用GME时无法看到整个CoSMIC工具栏,请将其拖到可见的范围内。)
由于CoSMIC与generate_component_mpc.pl采用了不同的Home方法命名方式,你需要手工修改最后生成的Plan.cdp文件中的各Home方法的声明,如:将createControllerHome_Servant改为create_Device_ControllerHome_Servant,将createControllerHome_Impl改为create_Device_ControllerHome_Impl,Monitor组件的两处修改类似。
CCM所定义的组件模型并不会对我们的客户程序造成影响,所有IDL3所提供的新特性仅被用于组件之间的通信(除了supports关键字),因此我们可以像对待CORBA2.x服务程序一样的方式通过CCM组件所支持的接口来访问CCM组件程序。但同时,如果我们变换角度,将整个应用系统看作一个组件应用,则我们原来的客户程序可能变化成组件应用程序的一部分(但我们仍然需要一个客户程序来访问组件所提供的功能)。
附件中的Operator客户程序以Controller组件的IOR为输入参数,对Controller组件支持的所有接口方法进行了测试,该程序与普通的CORBA客户程序并无任何差异。
1、先通过NodeManager启动可供组件部署、运行的节点
在descriptors目录下执行:
run_NodeDaemons.pl
启动两个可供Controller、Monitor组件部署、运行的节点,你也可以分别在两个终端窗口下运行:
%CIAO_ROOT%/DAnCE/NodeManager/NodeManager -ORBEndpoint iiop://localhost:40001 -s %CIAO_ROOT%/DAnCE/NodeApplication/NodeApplication
%CIAO_ROOT%/DAnCE/NodeManager/NodeManager -ORBEndpoint iiop://localhost:40002 -s %CIAO_ROOT%/DAnCE/NodeApplication/NodeApplication
来完成相同的工作。建议读者用第二种方式,这种方式更加直观。
2、运行Execution_Manager完成逻辑节点与具体NodeApplication的关联
运行Execution_Manager需要一个节点与NodeApplication之间的映射关系的描述文件,其大致内容如下:
ControllerNode corbaloc:iiop:localhost:40001/NodeManager
MonitorNode corbaloc:iiop:localhost:40002/NodeManager
将上述内容保存为NodeManagerMap.dat,并执行:
%CIAO_ROOT%/DAnCE/ExecutionManager/Execution_Manager -o ior.out -i NodeManagerMap.dat
即可完成逻辑节点与具体NodeApplication的关联。
以后,在Execution_Manager收到Plan_Launcher发送的部署描述信息时便可将组件部署到相应的NodeApplication上。
3、运行Plan_Launcher完成组件的部署
执行
%CIAO_ROOT%/DAnCE/Plan_Launcher/Plan_Launcher -p Plan.cdp -k file://ior.out
其中的Plan.cdp是本文第四节中生成的组件部署描述文件,ior.out则是上一步输出的Execution_Manager实例的IOR信息。
注:如果你在这一步失败了,请认真查看错误提示确定错误原因。在执行1、2、3步之前执行:
set CIAO_DEBUG_LEVEL=11
打印调试信息可以帮助你定位错误。
4、运行测试程序
组件部署完毕,下面可以启动客户程序来操纵Controller组件了,在descriptors目录下运行如下命令:
../debug/operator file://Controller.ior
将输出如下如下信息:
Power on Device...
Tune device run level...
Device's name is [Device-0]
[Device-0] is running at level [2]
Power off device...
而MonitorNode对应NodeApplication所在的终端将输出:
Device: [Device-0] is running at level [1]
Device: [Device-0] is running at level [2]
Device: [Device-0] is running at level [1]
Device: [Device-0] is running at level [0]
这些信息是在Monitor组件收到Controller组件的DeviceStatus改变通知事件时输出的。
CCM通过引入新的组件集成、配置规范,利用IDL3扩展简化和规范了对组件间通信机制的描述,使得基于组件的开发成为可能;同时,基于组件的开发方法由Container来完成组件间的通信,避免了组件间的直接访问,从而使得我们可以像组装零件一样通过集成、配置来进行系统的设计、开发。但目前组件市场并没有随着基于组件的开发(Component-Based Development,CBD)这一概念的提出来而迅速发展,从而使得基于组件的开发并不可能那么轻松和自由,但作为软件重用方法的一种较为高级的形式,CBD仍是十分有益的。
与EJB相比,CCM大量借鉴了EJB的设计思想,可以认为是EJB面向多语言层次的扩展,但由于这种扩展所带来的众多问题,使得CCM容器比EJB容器更难实现;与众多EJB服务器不同,CIAO没有一个便于操作和管理的CCM服务器,同时也没有一个合适的集成开发环境可以支撑整个CCM组件的开发过程,使得其开发过程还比较繁琐,这些都在一定程度上制约了CCM的发展;C++与Java语言的差异和适用范围,也是造成CCM/EJB命运迥异的一个重要原因;由于CCM的复杂性,研究人员预期的EJB标准进行扩展以与CCM融合的局面也没有出现。
对于CIAO而言,设计者的高明之处在于,CIAO被设计成一个面向分布式嵌入式应用的CCM容器,在该领域,C++语言相对于Java具有明显的优势,因此也使得CCM比EJB更适用。在其他EJB广泛应用的领域,虽然已有OpenCCM、K2-CCM等面向Java的CCM实现,但在这些领域向应用开发者推广CCM是非常困难的,毕竟EJB在这些领域具有明显的优势:简单,而且成熟。
附:范例组件工程、测试程序源代码及CoSMIC工程文件
1. Ming Xiong, Building a Stock Quoter with CoSMIC and DAnCE. http://www.dre.vanderbilt.edu/~mxiong/CoSMIC/.
2. Douglas C. Schmidt and Steve Vinoski, Object Interconnections: The CORBA Component Model: Part 1, Evolving Towards Component Middleware, C/C++ Users Journal, February, 2004.
3. Douglas C. Schmidt and Steve Vinoski, Object Interconnections: The CORBA Component Model: Part 2, Defining Components with the IDL 3.x Types, C/C++ Users Journal, April, 2004.
4. Bala Natarajan, Douglas C. Schmidt, and Steve Vinoski, The CORBA Component Model Part 3: The CCM Container Architecture and Component Implementation Framework, C/C++ Users Journal, September, 2004.
5. Bala Natarajan, Douglas C. Schmidt, and Steve Vinoski, The CORBA Component Model Part 4: The CORBA Component Model Part 4: Implementing Components with CCM, C/C++ Users Journal, October, 2004.