第1章OPC概述
关键字:COMDCOMOPCDA通讯规范CLIENTSERVERGROUPITEM自定义接口自动化接口同步异步回调
随着计算机科学技术、工业控制等各方面新技术的迅速发展,计算机监控系统由早期的集中式监控向全分布式的方向发展,计算机监控系统软件随着面向对象技术、分布式对象计算、多层次Client/Server技术的成熟,也从早期面向功能的系统软件,发展为面向具体现场设备为特征的面向对象的监控系统软件。
同时,计算机监控系统规模越来越大,不同厂家生产的现场设备的种类在不断增加,由于不同厂家所提供的现场设备的通讯机制并不尽相同,计算机监控系统软件需要开发的硬件设备通信驱动程序也就越来越多,造成了硬件通讯驱动程序需要不断开发的现象,而基于COM/DCOM技术的OPC技术,提供了一个统一的通讯标准,不同厂商只要遵循OPC技术标准就可以实现软硬件的互操作性。
OPC(OLEforProcessControl,用于过程控制的OLE)是为过程控制专门设计的OLE技术,由一些世界上技术占领先地位的自动化系统和硬件、软件公司与微软公司(Microsoft)紧密合作而建立的,并且成立了专门的OPC基金会来管理,OPC基金会负责OPC规范的制定和发布。OPC提出了一套统一的标准,采用典型的CLIENT/SERVER模式,针对硬件设备的驱动程序由硬件厂商或专门的公司完成,提供具有统一OPC接口标准的SERVER程序,软件厂商只需按照OPC标准编写CLIENT程序访问(读/写)SERVER程序,即可实现与硬件设备的通信。
如图1.1所示,与传统的通讯开发方式相比,OPC技术具有以下优势:
·硬件厂商熟悉自己的硬件设备,因而设备驱动程序性能更可靠、效率更高。
·软件厂商可以减少复杂的设备驱动程序的开发周期,只需开发一套遵循OPC标准的程序就可以实现与硬件设备的通信,因此可以把人力、物力资源投入到系统功能的完善中。·可以实现软硬件的互操作性。
·OPC把软硬件厂商区分开来,使得双方的工作效率有了很大的提
高。
图1.1OPC技术应用前后比较
因此OPC技术的出现得到了广大软硬件厂商的支持,并迅速发展。自从1997年9月发布OPCDA1.0规范以来,经过多年的发展,OPC规范已经被工控领域大多数厂商接受,并成了工控软件的技术标准。目前OPC规范主要有DA(DataAccess)规范,AE(alarmandevent)规范,HDA(historydataaccess)规范等。而且随着OPC技术与企业整体信息系统集成的需求变得日益迫切,对OPC技术的跨平台性能和Internet特性提出了更高要求。为此,OPC基金会开始以XML为基础着手制定一系列新的标准。2002年3月OPC基金会正式发布了OPCXML-DA规范,并与2004年12月正式发布了OPCXML-DA1.01规范,为OPC进一步提高工业控制系统的互操作性揭开了新的篇章。本书仅仅以符合DA规范的OPC服务器和客户程序为例介绍OPC技术,对于其它规范的OPC技术,本书未能介绍。
COM/DCOM1.1OPC技术的本质————COM/DCOM
随着计算机网络技术的发展,计算机监控系统也普遍的采用了分布式结构,因而系统的异构性是一个非常显著的特点。OPC技术本质是采用了Microsoft的COM/DCOM(组件对象模型/分布式组件对象模型)技术,COM主要是为了实现软件复用和互操作,并且为基于WINDOWS的程序提供了统一的、可扩充的、面向对象的通讯协议,DCOM是COM技术在分布式计算领域的扩展,使COM可以支持在局域网、广域网甚至Internet上不同计算机上的对象之间的通讯。
COM是由Microsoft提出的组件标准,它不仅定义了组件程序之间进行交互的标准,并且也提供了组件程序运行所需的环境。在COM标准中,一个组件程序也被称为一个模块,它可以是一个动态链接库,被称为进程内组件(in-processcomponent);也可以是一个可执行程序(即EXE程序),被称作进程外组件(out-of-processcomponent)。一个组件程序可以包含一个或多个组件对象,因为COM是以对象为基本单元的模型,所以在程序与程序之间进
行通信时,通信的双方应该是组件对象,也叫做COM对象,而组件程序(或称作COM程序)是提供COM对象的代码载体。
COM标准为组件软件和应用程序之间的通信提供了统一的标准,包括规范和实现两部分,规范部分规定了组件间的通信机制。由于COM技术的语言无关性,在实现时不需要特定的语言和操作系统,只要按照COM规范开发即可。然而由于特定的原因,目前COM技术仍然是以Windows操作系统为主,在非Windows操作系统上开发OPC,具有很大的难度。COM的模型是C/S(客户/服务器)模型,OPC技术的提出就是基于COM的C/S模式,因此OPC的开发分为OPC服务器开发和OPC客户程序开发,对于硬件厂商,一般需要开发适用于硬件通讯的OPC服务器,对于组态软件,一般需要开发OPC客户程序。
对于OPC服务器的开发,由于多种编程语言在实现时都提供了对COM的支持,如MicrosoftC/C++,VisualBasic,Borland公司的Delphi等。但是开发OPC服务器的语言最好是C或者是C++语言。在本书中选用VisualC++6.0为开发语言。
对于OPC客户程序的开发,可根据实际需求,选用比较合适的,能够快速开发的语言。
1.2OPCDA204规范简述
OPCDA204规范(OPCDataAccessCustomInterfaceSpecification2.04)是2000年9月OPC基金会发布的OPCDA自定义接口规范。该规范制定了OPC服务器和OPC客户程序的COM接口标准,通过制定标准的接口来实现多个厂家的OPC服务器和OPC客户程序开发。本书附带OPCDA204规范的WORD文档。
1.2.1OPC客户程序和OPC服务器
一个OPC客户可以连接一个或多个OPC服务器,而多个OPC客户也可以同时连接同一个OPC服务器,如图1.2
所示。
图1.2OPC客户程序/OPC服务器关系
1.2.2OPC服务器的对象组成
一个OPC服务器由三个对象组成:服务器(Server),组(Group),项(Item)。OPC服务器对象用来提供关于服务器对象自身的相关信息,并且作为OPC组对象的容器。OPC组对象用来提供关于组对象自身的相关信息,并提供组织和管理项的机制。
OPC组对象提供了OPC客户程序用来组织数据的一种方法。例如一个组对象代表了一个(可编程控制器)中的需要读写的寄存器组。一个客户程序可以设置组对象的死区,刷新频率,需要组织的项等。OPC规范定义了2种组对象:公共组和私有组。公共组由多个客户共享,局部组只隶属于一OPC客户。全局组对所有连接在服务器的应用程序都有效,而私有组只能对建立它的CLIENT有效。在一个SERVER中,可以有若干个组。
OPC项代表了OPC服务器到数据源的一个物理连接。数据项是读写数据的最小逻辑单位。一个OPC项不能被OPC客户程序直接访问,因此在OPC规范中没有对应于项的COM接口,所有与项的访问需要通过包含项的OPC组对象来实现。简单的讲,对于一个项而言,一个项可以是PLC中的一个寄存器,也可以是PLC中的一个寄存器的某一位。在一个组对象中,客户可以加入多个OPC数据项。每个数据项包括3个变量:值(Value)、品质(Quality)和时间戳(TimeStamp)。数据值是以VARIANT
形式表示的。
图1.3Server/Group/Item关系
这里最需要注意的是项并不是数据源,项代表了到数据源的连接。例如一个在一个系统中的TAG不论OPC客户程序是否访问都是实际存在的。项应该被认为是到一个地址的数据。大家一定要注意项的概念。不同的组对象里可以拥有相同的项,如组1中有对应于一个开关的ITEMAAA,组2中也可以有同样意义对应于一个开关的ITEMAAA,即同样的项可以出现在不同的组中。
1.2.3OPC接口体系
OPC规范提供两种接口:自定义接口(theOPCCustomInterfaces),自动化接口(theOPCAutomationinterfaces)。
图1.4OPC接口
如前所述,象所有的COM结构一样,OPC是典型的CLIENT/SERVER结构,OPC服务器提供标准的OPC接口供OPC客户程序访问。OPC服务器必须提供自定义接口,对于自动化接口,在OPC
规范定义中是可选的。
图1.5典型OPC结构
1.3OPC对象接口定义
本节主要对OPC服务器对象和OPC组对象的接口进行简要的介绍。
OPC服务器对象提供一些方法去读取或连接一些数据源。OPC客户程序连接到OPC服务器对象,并通过标准接口与OPC服务器联系。OPC服务器对象提供接口(AddGroup)供OPC客户程序创建组对象并将需要操作的项添加到组对象中,并且组对象可以被激活,也可以被赋予未激活状态。对于OPC客户程序而言,所有OPC服务器和OPC组对象可见的仅仅是COM接口。
下面的两个图例是OPC规范中定义的OPC服务器对象和OPC组对象的COM接口,其中任选的接口均以[]表示。(注:任选指开发OPC服务器时,这些接口可以根据实际情况选择实现还是不实现,除任选项外的接口在开发时必须全部实现。)
图1.6标准OPC服务器对象及接口
IOPCServerPublicGroups,IOPCBrowseServerAddressSpace和
IPersistFile为任选(optional)接口,OPC服务器提供商可根据需要选择是否需要实现。其它接口为OPC服务器必须实现的接口。其中:IOPCServerPublicGroups接口用于对公共组进行管理。IPersistFile接口允许用户装载和保存服务器的设置,这些设置包括服务器通信的波特率、现场设备的地址和名称等,这样用户就可以知道服务器启动和配置的改变而不需要启动其它的程序。
IOPCBrowseServerAddressSpace允许用户浏览服务器中的有用的组员的数据,为用户提供OPC服务器各个组员的定义列表。IOPCCommon接口是其它OPC服务器(例如OPC报警与事件服务器)也使用的接口。通过该接口可为某个特定的客户/服务器对话(session)设置和查询本地标识(LocateID)。这样,一个客户程序的操作将不会影响其它客户程序。IConnectionPointContainer接口服务器(OPC服务器对象接口)支持可连接点对象,当OPC服务器关闭时需要通知所有的客户程序释放OPC组对象和其中的OPC组员,此时可利用该接口调用客户程序方的IOPCShutdown接口实现服务器的正常关闭。
IOPCServer接口及成员函数主要用于对组对象进行创建、删除、枚举和获取当前状态等操作。是OPC服务器对象的主要接口。接口及成员函数定义为:
图1.7标准OPC组对象及接口
其中:IOPCItemMgt接口及成员函数用于OPC客户程序添加、删除和组对象中组员等控制操作。IOPCGroupStateMgt接口及其成员函数允许OPC客户程序操作或获取用户组对象的全部状态(主要是组对象的刷新率和活动状态,刷新率的单位为毫秒)。IOPCPublicGroupStateMgt为任选接口,用于将私有组对象(privategroup)转化为公有组对象(publicgroup),这个接口一般不用,在很多商业的OPC服务器中,此接口都没有开发。可选接口IOPCAsyncIO和IdataObject接口用于异步数据传输(在OPC数据访问规范1.0中定义,现在其功能已经被IOPCAsyncIO2和IConnectionPointContainer接口取代)。IOPCSyncIO用于同步数据访问。IOPCAsyncIO2用于异步数据访问。这两个接口是数据访问规范进行数据访问最重要的接口。
有关OPC服务器对象和OPC组对象的COM接口详细定义请看OPC规范定义,除在开发实例中用到的COM接口,其它接口本文不再详述。
1.4OPC同步异步通讯
OPCDA规范规定了两种通讯方式:同步通讯和异步通讯。这两种通讯方式与常见的串口同步通讯、异步通讯以及以太网的同步通讯、异步通讯的功能差不多。
同步通讯时,OPC客户程序对OPC服务器进行相关操作时,OPC客户程序必须等到OPC服务器对应的操作全部完成以后才能返回,在此期间OPC客户程序一直处于等待状态,如进行读操作,那么必须等待OPC服务器完成读后才返回。因此在同步通讯时,如果有大量数
据进行操作或者有很多OPC客户程序对OPC服务器进行读、写操作,必然造成OPC客户程序的阻塞现象。因此同步通讯适用于OPC客户程序较少,数据量较小时的场合。
异步通讯时,OPC客户程序对服务器进行相关操作时,OPC客户程序操作后立刻返回,不用等待OPC服务器的操作,可以进行其他操作。当OPC服务器完成操作后再通知OPC客户程序,如进行读操作,OPC客户程序通知OPC服务器后离开返回,不等待OPC服务器的读完成,而OPC服务器完成读后,会自动的通知OPC客户程序,把读结果传送给OPC客户程序。因此相对于同步通讯,异步通讯的效率更高,适用于多客户访问同一OPC服务器和大量数据的场合。
OPC的异步通讯有四种方式:
·数据订阅,客户端通过订阅方式后,服务器端将变化的数据通过回调传送给客户程序。·异步读,返回操作结果和数据值。
·异步写,返回操作结果,成功、失败。
·异步刷新,异步读所有Item的值
1.5OPC服务器开发方式
OPC服务器本身就是一个可执行程序,该程序以一定的速率不断地同物理设备进行数据交互。服务器内有一个数据缓冲区,其中存有最新的数据值,数据质量戳和时间戳。OPC数据服务器的设计与实现是一个较为复杂与繁重的任务,设计者既需要熟悉OPC规范,同时也必须掌握相应的硬件产品特性。OPC数据服务器大致可以分解为不同的功能模块。OPC对象接口管理模块,Item数据项管理模块以及服务器界面和设置等等。一个设备的OPCServer主要有两部组成,一是OPC标准接口的实现,二是与硬件设备的通信模块。
虽然COM技术本质上具有语言无关性,可以用各种语言开发,但由于最适合COM开发的语言仍然是C++,因此一般都选择采用VisualC++进行开发。
目前用VisualC++开发COM组件主要有三种方式:使用COMSDK直接开发COM组件;通过MFC提供的COM支持实现COM组件;通过ATL来实现COM组件。
此外,目前国内外很多的工控软件厂商也推出了一系列的OPC快速开发工具包,使用专门的OPC开发工具包,开发者只需具备基本的编程基础即可快速上手,无需掌握ATL,COM/DCOM,也无需了解OPC技术的细节,而且大多数的OPC开发工具都支持多种常用编程语言,如VB,VC等。网站也提供OPC开发工具,有兴趣的读者可以到网站下载DEMO开发工具或者与QQ:41063473联系购买事宜。http://www.opc-china.com)
建议所有的学生或有志向的开发人员可以尝试独立开发OPC服务器,如果是公司使用,建议购买OPC服务器开发工具。
C重点:何为OPC?OPCDA有哪些对象?OPCDA有哪些接口?OPCDA的通讯方式?OPOPC
DA的开发方式?(工控帮http://www.opc-china.com)
第2章ATL简介
关键字:ATL类厂接口标识符IDL组件聚合双重接口自定义接口自动化接口连接点事件注册属性方法客户程序
VisualC++从4.0版本就已经提供全面的COM支持,尤其在5.0和6.0版本中,不仅MFC类库提供COM应用的支持,而且VisualC++的集成开发环境VisualStudio也为COM应用提供了各种向导支持,并且,VisualC++还提供了另一套模板库ATL专门用于COM应用程序的开发。在上一章中介绍了采用VisualC++进行OPC服务器开发的几种方式,因为ATL是专为COM应用程序开发,因此本书的OPC服务器开发采用了ATL的模式,在本章中首先对ATL进行简要介绍,并以实际例子介绍如何进行ATL编写COM组件。
ATL(ActiveTemplateLibrary)是VisualC++提供的一套基于模板的C++类库,利用这些模板类,开发人员可以快速的开发COM组件程序。所以说ATL专门针对于COM应用开发的,它内部的模板类实现COM的一些基本特征,比如一些基本COM接口IUnknown,IClassFactory,IDispatch等,也支持COM的一些高级特性,如双接口(dualinterface)、连接点(connectionpoint)、Activex控制等。ATL最初的设计是快速的开发小型的组件,ATL2.0版本添加了模板库用来支持可视化的控件开发。
ATL所具有的特点:
•包含所有C++的功能。
•无需运行库,除非你想使用它。
•引用计数。
•高水平的对象和接口实现方法。
•类厂自动操作,对象创造,接口查询。
ATL开发应用程序并不像开发MFC应用程序那样容易。但VisualStduio提供某种帮助使开发者迅速开发应用程序。可利用ATLCOMWinzard(活动模板库组件向导)和ATLObjectWizard(活动模板库对象向导)开发ATL应用程序。
从目前介绍ATL技术的书籍来看,国内的书籍较少,本章主要通过介绍如何创建一个ATL应用程序来使大家对ATL技术有一定的了解,如果读者了解ATL,可以略过此章。
2.1COM基础
本节主要对COM的两个基本概念(接口,组件)做简要介绍。
2.1.1COM
图2.1接口结构
从理论上讲,完整的COM编程系统是基于接口的。
从技术上讲,接口是包含了一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的所有信息,客户程序利用这些函数获得组件对象的服务。需要注意的是在接口成员函数中,字符串变量必须用Unicode字符指针,COM规范要求使用Unicode字符,而且COM库中提供的COMAPI函数也使用Unicode字符。所以如果在组件程序内部使用到了ANSI字符的话,则应该进行两种字符表达的转换。当然,在即建立组件程序又建立客户程序的情况下,可以使用自己定义的参数类型,只要它们与COM所能识别的参数类型兼容。这里需要特别注意的是COM需要使用Unicode字符。
COM接口可以分为以下两类:
标准接口
自定义接口
标准接口之IUnknown,是所有接口的基接口。自定义接口也是基于IUnknown接口。所有的COM组件都必须以这个接口为基础。IUnknown提供了两个非常重要的特性,一个用于组件对象的生命周期管理,也可以查询被组件对象使用的其他接口。客户程序只能通过接口
与COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。如果客户还要继续对对象进行操作,则它必须保证对象能一直存在于内存中;如果客户对象的操作已经完成,以后不再需要该对象了,则它必须及时地把该对象释放掉,以提高资源的利用率。IUnknown引入了“引用计数”方法,可以有效地控制对象的生存期。另一方面,如果一个COM对象实现了多个接口,在初始时刻,客户程序不太可能得到该对象的所有接口指针,它只会拥有一个接口指针。如果客户程序需要其它的指针,则可利用IUnknown的“接口查询”方法来完成接口之间的跳转。
IUnknown的IDL定义:
interfaceIUnknown
{
HRESULTQueryInterface([in]REFIIDiid,[out]void**ppv);
ULONG
ULONG
}
IUnkown的C++定义:
classIUnknown
{
virutalHRESULT_stdcallQueryInterface(constIID&iid,void**ppv)=0;
virtualULONG
virutalULONG
}
标准接口之IDispatch,此接口用于脚本语言(如VisualBasic)访问组件。当脚本语言调用组件对象的方法时,此接口查询函数的地址并执行。
标准接口之IClassFactory(类厂)接口用于创建新的COM对象的实例。类厂(classfactory)是COM对象的生产基地,COM库通过类厂创建COM对象;对应每一个COM类,有一个类厂专门用于该COM类的对象创建操作。类厂本身也是一个COM对象,它支持一个特殊的接口IClassFactory:
classIClassFactory:publicIUnknown
{
virtualHRESULT_stdcallCreateInstance(IUnknown*pUnknownOuter,_stdcallAddRef()=0;_stdcallRelease()=0;AddRef(void);Release(void);
constIID&iid,void**ppv)=0;
virtualHRESULT_stdcallLockServer(BOOLbLock)=0;
}
如果你还没有学过COM,那么你来创建一个组件的时候很可能会采用C++的new操作符,这样的话会返回一个奇怪的错误。这也是一个非常常见的ATL错误,一个初学者不太理解的错误。实际上,创建一个对象,需要调用CoCreateInstance来创建一个COM对象的实例。在这里要注意的是创建COM对象不是用new操作符。在后面的章节中同样可以看到释放一个对象时,不能用delete,而是要通过Release接口来释放对象。
自定义接口的目的是提供更多的功能给用户,开发者可以自己定义基于IUnknown的接口提供更多的功能。
COM标识符UUID/GUID用来标识组件,通过唯一标识行UUID来唯一标识组件,如同身份证的意义,可以在系统中标识COM组件。在COM中,UUID是指全局唯一标识符GUID。GUID分为CLSID、IID和LIBID三类。
COM规范采用了128位全局唯一标识符GUID来标识对象和接口,这是一个随机数,并不需要专门机构进行分配和管理。因为GUID是个随机数,所以并不绝对保证唯一性,但发生标识符相重的可能性非常小。从理论上讲,如果一台机器每秒产生10000000个GUID,则可以保证(概率意义上)的3240年不重复。
接口描述语言IDL。COM规范在采用OSF(开放软件基金会)的DCE(分布式计算环境)规范描述远程调用接口IDL(interfacedescriptionlanguage,接口描述语言)的基础上,进行扩展形成了COM接口的描述语言。接口描述语言提供了一种不依赖于任何语言的接口描述方法,因此,它可以成为组件程序和客户程序之间的共同语言。MicrosoftVisualC++提供了MIDL工具,可以把IDL接口描述文件编译成C/C++兼容的接口描述头文件(.h)。按照COM规范,按照接口成员函数的参数类型的不同,有三种情况:
In参数:对于in参数,由OPC客户程序分配和释放内存;
Out参数:Out参数由OPC服务器分配内存,由OPC客户程序释放内存,采用标准COM内存分配器。
In-out参数:in-out参数最初由OPC客户程序分配内存,然后由OPC服务器释放和再分配(如果需要)。和out参数一样,OPC客户程序负责最后返回值的释放。
如不能正确释放内存,将会引起难以发现的内存泄漏(memoryleak),造成系统可用的内存资源越来越少直至系统崩溃。因此,编写程序时可以参考IDL文件来查找out参数,并且
针对每种类型的结构编写一段子程序处理内存释放。在每次客户程序调用服务器函数的过程中,不管函数执行正确与否,服务器程序都必须为每个out参数定义好返回值,而客户程序则负责释放相应的内存资源。
COM接口所有方法返回类型都为HRESULT。
2.1.2组件
一般而言,组件具有三种类型:进程内组件,进程外组件,远程组件。
进程内组件是采用动态连接库方式实现的组件。客户程序调用组件时,客户程序会把组件程序装入自己的进程空间,即客户程序和组件程序在同一个进程地址空间内。在客户端和服务端组件间有大量数据转移操作的情况下是理想的。进程内服务器会更快地装载。由于它占用和客户端应用程序同样的地址空间,它可以与客户端更快的通信。进程内服务器是通过将组件作为动态连接库(DLL)的形式来实现的。DLL允许特定的一套功能以分离于可执行的、以DLL为扩展名的文件进行存储。只有当程序需要它们时,DLL才将其装入内存中,客户程序将组件程序加载到自己的进程地址空间后再调用组件程序的函数。
本地(即进程外)组件,进程外组件是以EXE方式实现的组件,具有独立的进程,因此客户程序和组件程序分别处在不同的进程空间地址中。在COM中,采用了本地过程调用(localprocedurecall,LPC)来进行本地通信。进程外服务器对需要运行于独立的处理空间或作为独立客户端应用程序的线程的组件是理想的。由于数据必须从一个地址空间移到另一个地址空间,因此这些服务器就会慢得多。由于进程外服务器是可执行的,它们运行在自己的线程内。当客户端代码正在执行时,客户端不锁住服务器。进程外服务器对需要表现为独立的应用程序的组件也是理想的。例如,MicrosoftInternetExplorer的应用程序是本地服务器的例子。客户端和服务端的通信是通过进程内的通信协议进行的,这个通信协议是IPC(进程间通信)。
远程组件,远程服务器与本地服务器类似,除了远程服务器是运行在通过网络连接的分离的计算机上。这种功能是使用DCOM实现的。DCOM的优点在于它并不要求任何特别的编程来使具有功能。另外服务端和客户端通信是通过RPC(Remoteprocedurecall,RPC)通信协议进行的。对于这三种不同的服务器组件,客户程序和组件程序交互的内在方式是完全不同的。但是对于功能相同的进程内和进程外组件,从程序编写的角度看,客户程序是以同样的方法来使用组件程序的,客户程序不需要做任何的修改。
2.2ATL应用程序向导创建应用程序
应用ATL应用程序向导来开发ATL程序是快速而且有效的,本节主要以MSDN的例子为例来介绍如何应用ATL应用程序向导来创建新的ATL程序。
创建新的ATL应用程序第一步,生成一个新的ATL工程,从VisualStudio菜单中选择File—>New,选中Projects(工程)选项卡。在图2.2中选择ATLCOMAppWizard,输入工程名BeepCtrlMod。
图2.2VisualStud
io的ATLCOMAppWizard
单击New对话框的OK,打开ATLCOMAppWizard对话框。MFCAppWizard由许多步骤组成,而ATLCOMWizard仅需要一步,即必须选定开发应用程序类型。由图2.3可以看出,一共有三种类型的组件,动态连接库,可执行,服务三种类型,分别对应着进程内组件,进程外组件,服务。
选择动态连接库(进程内组件)。单击Finish,打开NewProjectInformation对话框,然后单击OK完成后,一个新的组件的框架已经建立。
图2.3ATLCOMAppW
izard选择组件类型
经过AppWizard创建得到的应用仅仅是一个程序框架,可以看到包含一个全局成员:CComModule_Module;
•一个包含着最初的类型库的说明的.idl文件
•一个.def文件。
•一个.rc资源文件
•一个包含着资源ID定义的头文件
•stdafx.h和stdafx.cpp文件
•其它的.cpp文件,用来实现全局功能应用的源文件。
可以看出与MFC应用程序不同的一点是多了一个后缀为.idl的文件。每一个ATL工程都有一个与工程同名的IDL文件,此IDL文件记录了该工程中所用到COM接口或COM对象的定义。ATLAppWizard或ATLObjectWizard会自动维护此IDL文件,也可以手动的修改此文件加入需要的COM接口的IDL定义。
2.3源文件说明
本节对BeepCntMod.cpp文件做简要说明,从而了解主文件的程序结构。
#include"stdafx.h"
#include"resource.h"
#include
#include"BeepCntMod.h"
#include"BeepCntMod_i.c"
CComModule_Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
Initguid.h文件是一个标准OLE系统文件,需要在工程中引用,用来定义GUID(thegloballyuniqueidentifier)结构。
如果现在编译和连接一下应用程序,会发现多了一个与工程的IDL文件同名的.h头文件(BeepCntMod.h)和一个._i.c(BeepCntMod_i.c)文件。这是集成环境调用MIDL编译器对工程的IDL文件进行编译生成的。在进行第一次编译之前,看不到这两个文件,必须编译以后才可以看这两个文件。
BeepCntMod.h包含了接口和组件的定义。BeepCntMod_i.c包含着GUID的定义。
_Module是一个全局变量,定义了类厂以及类厂所有的初始化代码功能,如类厂的注册等,如果不用ATL进行开发,而是采用MFC开发,类厂的注册等都需要手动加入。可以通过MSDN来查看CComModule的详细文档。
最后一部分,是一个空的对象映射,对象向导会将需要的映射加入进去。每一个元素的对象映射都有一个CLSID和一个类名。_Module对象读取这些映射根据CLSID来创建对象。接下来看一下DllMain函数,它简单的调用了_Module对象的Init和Term函数。
//DLLEntryPoint
extern"C"
BOOLWINAPIDllMain(HINSTANCEhInstance,DWORDdwReason,LPVOID/*lpReserved*/)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap,hInstance,
&LIBID_BEEPCNTMODLib);
DisableThreadLibraryCalls(hInstance);
}
elseif(dwReason==DLL_PROCESS_DETACH)
_Module.Term();
returnTRUE;
}
最后的四个函数如下://ok
STDAPIDllCanUnloadNow(void)
{
return(_Module.GetLockCount()==0)?S_OK:S_FAL
SE;
}
STDAPIDllGetClassObject(REFCLSIDrclsid,REFIIDriid,LPVOID*ppv)
{
return_Module.GetClassObject(rclsid,riid,ppv);
}
STDAPIDllRegisterServer(void)
{
//registersobject,typelibandallinterfacesin
typelib
return_Module.RegisterServer(TRUE);
}
STDAPIDllUnregisterServer(void)
{
return_Module.UnregisterServer(TRUE);
}
这些函数除非调用_Module对象的合适的函数,什么也不做。有关这四个函数的详细说明请参考有关COM书籍和MSDN。
2.4添加组件对象
通过上面的介绍,开始对ATL有了一定的认识,下面为上面的应用程序添加一个COM组件,本节的主要目的是熟悉如何通过ATL添加组件对象。
最简单的添加组件的方法是通过ATLObjectWizard。从Insert菜单下选择NewATLObject...,你将看到图2.4
所示的对话框。
图2.4ATLObjectWizard
这里选择SimpleObject,单击Next,图2.5所示的属性页显示出来,在这个属性页可以输入组件的名称和组件的属性。在ShortName编辑框里输入“BeepCnt”,也可以输入其它名字。输入“BeepCnt”
其它七个编辑框的内容会自动生成。
图2.5ATLObjectWizardProperties
单击属性页上的Attributes,出现图2.6所示的属性框,在这个框中可以选择组件相应的属性,本例中选择默认。单击OK。
图2.6ATLObjectWizardProperties
ApartmentThreadingModel的意思是指这个对象可以被一个或者多个线程所调用。实际上每个COM对象的实例都是单线程的,需要注意的是假设COM对象有多个线程在调用它,如果在程序的方法中引用了全局变量,必须保证程序多个线程访问的安全问题。不管如何,只要引用了全局变量,需要考虑程序多线程访问时的安全问题,或者在此选择SingleThreadingModel.
在这里,选择DualInterface,双重接口的意思是指创建的组件支持自定义接口,也支持自动化接口。自动化接口是用来支持脚本语言访问的接口,考虑到客户程序自动化的实现比自定义的实现简单的多,所以选择了双接口的方式。
Aggregation,汉语意思为聚合。包容和聚合是COM的两种重要类型。聚合是指组件对象一种特殊的包容方法,在一个组件对象里面可以拥有另外的组件对象。一般而言,采用COM自己实现聚合的的方式是复杂的,而ATL可以帮助我们操作聚合的实现。如果组件支持聚合,那么组件将是更加灵活的,因此我们选择支持聚合。
ISupportErrorInfo,创建ISupportErrorInfo接口支持,以便对象可将错误信息返回到客户端。我们不选择它。
ConnectionPoints,连接点用来支持事件,通过使对象的类从IConnectionPointContainerImpl导出来启用对象的连接点,连接点用来支持事件,所谓事件是指COM对象中当某个属性发生改变时,对象产生一个事件,通知到客户程序,客户程序可以处理这些事件。本例中,不选择连接点。
Free-ThreadedMarshaller,自由线程封送拆收器,创建自由线程封送拆收器对象,以有效地在同一进程中的两个线程之间封送接口指针。对指定“两者”或“自由”作为线程模型的对象可用。用于多线程控制的特定类型。本例中,不选择它。
创建完成后的组件对象由于支持双接口(自动化接口,自定义接口),聚合,多线程访问,因此它是一个简单但是易于扩展的组件。
现在工程中又多了两个ATL对象向导创建的文件:beepcnt.cpp和beepcnt.h。beepcnt.cpp基本上是空的,因为没有为组件对象添加任何的属性。所有将来添加的对象方法和属性都将在这个文件中实现。
beepcnt.h包含了对象的类的定义。
classATL_NO_VTABLECBeepCnt:
publicCComObjectRootEx,
publicCComCoClass,
publicIDispatchImpl
&LIBID_BEEPCNTMODLib>
{
public:
CBeepCnt()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_BEEPCNT)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CBeepCnt)
COM_INTERFACE_ENTRY(IBeepCnt)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
//IBeepCnt
public:
};
可以看出,CBeepCnt类基于三个模板类。CComObjectRootEx操作了组件的引用计数;CComCoClass用来定义该对象的默认类工厂和聚合模型;IDispatchImpl,提供了一个双重接口IBeepCnt(接口ID是IID_IbeepCnt,支持自动化接口和自定义接口)。
CBeepCnt类同样提供了一组宏,ATL_NO_VTABLE会告诉编译器不要再为CBeepCnt类生成虚函数,因为ATL_NO_VTABLE必须只能与一个无法直接创建的基类一起使用。在项目中一定不能将declspec(novtable)与基本上是导出的类一起使用,因为该类(通常是CComObject、CComAggObject或CComPolyObject)为项目初始化vtable指针。一定不能从任何使用declspec(novtable)的对象的构造函数调用虚函数。应该将那些调用移动到FinalConstruct(大家要记着这个方法)方法。需要我们切记的是,我们可以尝试着添加一个虚函数,但是编译器会编译错误,切记ATL_NO_VTABLE意味着没有虚函数,不要为基于ATL_NO_VTABLE的类添加虚函数。
当对象创建时,对象实际上是CComObject。换言之,用的是把对象类型作为一个参数的CComObject
模板类。一个派生的图如下图所示。
图2.7CcomObject派生图
CComObject的主要工作是提供IUnknown方法的实现。这些必须由最低层的派生类提供,以便所有由IUnknown派生的类可以共享IUnknown方法的实现。CComObject的方法仅仅调用CComObjectRootEx中的实现。
另外需要注意的是COM接口映射,COM接口映射包含着我们实现自定义接口和IDispatch的宏。COM接口映射中包含的接口是可以通过QueryInterface返回的接口指针的接口。这里还有一对宏。一个用来根据资源ID定义注册入口,一个用来改变对象创建的方法来保证对象不会被意外的删除。
与非采用ATL方式的COM相比,可以看到采用ATL开发的组件中没有引用计数的代码,是因为引用计数的工作由CComObjectRootEx类操作,而不需要我们来操作了。
接着讨论的新文件,beepcnt.rgs,包含了为ATL处理的代码的源脚本。大部分直接反映了为了COM运行时能够找到组件的注册入口。文件如下:
HKCR
{
BeepCntMod.BeepCnt.1=s'BeepCntClass'
{
CLSID=s'{AE73F2F8-4E95-11D2-A2E1-00C04F8
EE2AF}'
}
BeepCntMod.BeepCnt=s'BeepCntClass'
{
CLSID=s'{AE73F2F8-4E95-11D2-A2E1-00C04F8
EE2AF}'
CurVer=s'BeepCntMod.BeepCnt.1'
}
NoRemoveCLSID
{
ForceRemove{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}=s'BeepCntClass'
{
ProgID=s'BeepCntMod.BeepCnt.1'
VersionIndependentProgID=s'BeepCntMod.BeepC
nt'
ForceRemove'Programmable'
InprocServer32=s'%MODULE%'
{
valThreadingModel=s'Apartment'
}
'TypeLib'=s'{170BBD8D-4DE8-11D2-A2E0-00C04F8E
E2AF}'
}
}
}
这段脚本用来注册和注销组件。默认注册时,所有的键都会添加到注册表,不管这些键是否在注册表中。当注销时,默认的方式是删除所有在脚本中列出的键。一般情况下,你不需要编辑这个文件,当你添加对象和接口时,向导会自动的实现它。
最后讨论的文件是“BeepCntMod_i.c”,在此文件中,定义了组件的CLSID。CLSID是唯一地标识类或组件的GUID,传统地,CLSID的一般形式为CLSID_。TypeLibID是标识系统上的类型库。按照惯例,TypeLibID的一般形式为LIBID_Lib。
#ifdef__cplusplus
extern"C"{
#endif
#ifndef__IID_DEFINED__
#define__IID_DEFINED__
typedefstruct_IID
{
unsignedlongx;
unsignedshorts1;
unsignedshorts2;
unsignedcharc[8];
}IID;
#endif//__IID_DEFINED__
#ifndefCLSID_DEFINED
#defineCLSID_DEFINED
typedefIIDCLSID;
#endif//CLSID_DEFINED
constIIDLIBID_BEEPCNTMODLib=
{0x170BBD8D,0x4DE8,0x11D2,{0xA2,0xE0,0x00,0xC0,0x4F,0x8E,0xE2,0xAF}};
#ifdef__cplusplus
}
#endif
与添加组件对象前相比,已经存在的文件也有一些小的变化。
•BeepCntMod.cpp现在添加了#includesBeepCnt.h,并且为CbeepCnt添加了一个对象映射入口,每一个COM对象都有一个对象映射入口:
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_BeepCnt,CBeepCnt)
END_OBJECT_MAP()
•.idl文件里现在有了IDispatch派生的IBeepCnt接口入口,一个为BeepCnt类的在类型库的Coclass入口。
当重新编译程序时,MIDL生成了一个新的反映新建组件对象的BeepCntMod.h文件。编译后就生成了组件的DLL文件,在编译的同时完成组件的注册。
2.5添加组件对象的属性和方法(函数)
上面已经介绍了如何用ATL组件。然而生成的对象是个空的对象,属于这个对象的属性和方法都是空的。如何添加组件对象的属性和方法是本节要讨论的内容。
从ClassView中右击IbeepCnt,选择“AddMethod…”如图2所示的对话框出现。在MethodName编辑框中,选择返回值类型为HRESULT,方法名输入Beep,参数一项为空,单击OK,完成Beep方法的添加,向导会把所有Beep的定义的代码自动添加到BeepCnt.cpp和BeepCnt.h
文件中。
图2.8向接口添加方法
添加一个属性也是类似的。右击接口,选择“AddProperty…”,如图2.9所示的对话框出现。选择合适的类型,输入属性的名称,单击OK完成添加。
图2.9向接口添加属性
完成了方法和属性的添加后,向导向.idl文件添加了方法和属性的定义。
interfaceIBeepCnt:IDispatch
{
[id(1),helpstring("methodBeep")]HRESULTBee
p();
[propget,id(0),helpstring("propertyCount")]
HRESULTCount([out,retval]long*pVal);
[propput,id(0),helpstring("propertyCount")]
HRESULTCount([in]longnewVal);
};
BeepCnt.h包含了三个新函数的定义,BeepCnt.cpp文件包含了这些函数的框架。
STDMETHODIMPCBeepCnt::Beep()
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
STDMETHODIMPCBeepCnt::get_Count(long*pVal)
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
STDMETHODIMPCBeepCnt::put_Count(longnewVal)
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
为了让创建的组件可以做一些我们能感觉到的事情,需要添加一些代码,首先,为CBeepCnt类添加一个计数器,并且在CBeepCnt()构造函数中把它初始化为1。
longcBeeps;
CBeepCnt():CBeeps(1){}
接着编写一部分代码在CBeepCnt的方法里。注意属性有两个功能,一个是设置计数器(put_Count),一个是获得计数器(get_Count)。
STDMETHODIMPCBeepCnt::Beep()
{
for(inti=0;i
{
MessageBeep((UINT)-1);
Sleep(1000);
}
returnS_OK;
}
STDMETHODIMPCBeepCnt::get_Count(long*pVal)
{
*pVal=cBeeps;
returnS_OK;
}
STDMETHODIMPCBeepCnt::put_Count(longnewVal)
{
cBeeps=newVal;
returnS_OK;
}
现在一个简单功能的组件诞生了,它可以用来发出嘟嘟声。
2.6测试组件
如何来测试这个组件呢?一般而言,采用VisualBasic是个不错的选择,可以快速的编写客户程序来实现测试。可以编写一个图2.10
所示的对话框程序来测试它。
图2.10测试界面
采用VisualBasic6.0生成的代码如下:
DimBeeperCntAsBeepCnt
PrivateSubBeep_Click()
Text1=BeeperCnt
BeeperCnt.Beep
EndSub
PrivateSubSet_Click()
BeeperCnt=Val(Text1)
Text1=BeeperCnt
EndSub
PrivateSubForm_Load()
SetBeeperCnt=NewBeepCnt
Text1=BeeperCnt
EndSub
需要注意的是需要引用BeepCntMod1.0TypeLibrary。
图2.11引用库
运行这个程序,是不是听到了嘟嘟声,而且可以设置嘟嘟声的次数,类似的,你可以添加更多的属性和方法,来增强这个组件的功能,是不是很简单呢。
重点:何为ATL?何为类厂?COM对象如何创建?如何测试COM组件功能?
第4章OPC客户程序实例
关键字:同步异步VCVB
4.1OPC客户程序开发环境
无论开发者还是最终使用者都必须安装OPC代理/存根(Proxy/Stub)DLL,并进行环境设置。这些文件(opc_aeps.dll,opccomn_ps.dll,
opchda_ps.dll,opcproxy.dll,aprxdist.exe,opcenum.exe)可以从OPC基金会网站下载,也可以在http://www.opc-china.com下载。所有文件必须安装在客户端机器和服务器端机器上。
安装步骤如下:
1.复制所有的文件到你的Windows系统路径,如:
copyopcproxy.dllc:\winnt\system32
copyopccomn_ps.dllc:\winnt\system32
copyopc_aeps.dllc:\winnt\system32
copyopchda_ps.dllc:\winnt\system32
copyaprxdist.exec:\winnt\system32
copyopcenum.exec:\winnt\system32
2.安装代理DLL。
REGSVR32opcproxy.dll
REGSVR32opccomn_ps.dll
REGSVR32opc_aeps.dll
REGSVR32opchda_ps.dll
3.如果aprxdist.dll不存在,可以运行aprxdist.exe生成aprxdist.dll。
4.安装opcenum.exe
opcenum/regserver
在本书的VB客户程序中引用了OPCAutomation2.0库,库文件为
opcdaauto.dll。可以在本书附送的源码中找到,注册方式为:REGSVR32opcdaauto.dll。
4.3OPC客户程序(VB篇——同步)
正文:建立如下窗体:
引用如下:
代码如下:
OptionExplicit
DimWithEventsServerObjAsOPCServer
DimWithEventsGroupObjAsOPCGroup
DimItemObjAsOPCItem
PrivateSubCommand_Start_Click()
DimOutTextAsString
OnErrorGoToErrorHandler
Command_Start.Enabled=False
Command_Read.Enabled=True
Command_Write.Enabled=True
Command_Exit.Enabled=True
OutText="连接OPC服务器"
SetServerObj=NewOPCServer
ServerObj.Connect("XXXSERVER")@#XXXSERVER为某OPC服务器名称
OutText="添加组"
SetGroupObj=ServerObj.OPCGroups.Add("Group")
OutText="AddinganItemtothegroup"
SetItemObj=GroupObj.OPCItems.AddItem("XXXITEM",1)@#XXXITEM为添加的ITEM名称
ExitSub
ErrorHandler:@#如果出现异常,则报出错误。
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Read_Click()@#同步读
DimOutTextAsString
DimmyValueAsVariant
DimmyQualityAsVariant
DimmyTimeStampAsVariant
OnErrorGoToErrorHandler
OutText="读ITEM值"
ItemObj.ReadOPCDevice,myValue,myQuality,myTimeStampEdit_ReadVal=myValue
Edit_ReadQu=GetQualityText(myQuality)
Edit_ReadTS=myTimeStamp
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Write_Click()@#同步写
DimOutTextAsString
DimServerhandles(1)AsLong
DimMyValues(1)AsVariant
DimMyErrors()AsLong
OutText="写值"
OnErrorGoToErrorHandler
Serverhandles(1)=ItemObj.ServerHandle
MyValues(1)=Edit_WriteVal
GroupObj.SyncWrite1,Serverhandles,MyValues,MyErrors
Edit_WriteRes=ServerObj.GetErrorString(MyErrors(1))
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Exit_Click()@#停止,删除ITEM,删除GROUP,删除SERVER。DimOutTextAsString
OnErrorGoToErrorHandler
Command_Start.Enabled=True
Command_Read.Enabled=False
Command_Write.Enabled=False
Command_Exit.Enabled=False
OutText="删除对象"
SetItemObj=Nothing
ServerObj.OPCGroups.RemoveAll
SetGroupObj=Nothing
ServerObj.Disconnect
SetServerObj=Nothing
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateFunctionGetQualityText(Quality)AsString
SelectCaseQuality
Case0:GetQualityText="BAD"
Case64:GetQualityText="UNCERTAIN"Case192:GetQualityText="GOOD"
Case8:GetQualityText="NOT_CONNECTED"Case13:GetQualityText="DEVICE_FAILURE"Case16:GetQualityText="SENSOR_FAILURE"Case20:GetQualityText="LAST_KNOWN"Case24:GetQualityText="COMM_FAILURE"Case28:GetQualityText="OUT_OF_SERVICE"Case132:GetQualityText="LAST_USABLE"Case144:GetQualityText="SENSOR_CAL"Case148:GetQualityText="EGU_EXCEEDED"Case152:GetQualityText="SUB_NORMAL"Case216:GetQualityText="LOCAL_OVERRIDE"CaseElse:GetQualityText="UNKNOWNERROR"EndSelect
EndFunction
第1章OPC概述
关键字:COMDCOMOPCDA通讯规范CLIENTSERVERGROUPITEM自定义接口自动化接口同步异步回调
随着计算机科学技术、工业控制等各方面新技术的迅速发展,计算机监控系统由早期的集中式监控向全分布式的方向发展,计算机监控系统软件随着面向对象技术、分布式对象计算、多层次Client/Server技术的成熟,也从早期面向功能的系统软件,发展为面向具体现场设备为特征的面向对象的监控系统软件。
同时,计算机监控系统规模越来越大,不同厂家生产的现场设备的种类在不断增加,由于不同厂家所提供的现场设备的通讯机制并不尽相同,计算机监控系统软件需要开发的硬件设备通信驱动程序也就越来越多,造成了硬件通讯驱动程序需要不断开发的现象,而基于COM/DCOM技术的OPC技术,提供了一个统一的通讯标准,不同厂商只要遵循OPC技术标准就可以实现软硬件的互操作性。
OPC(OLEforProcessControl,用于过程控制的OLE)是为过程控制专门设计的OLE技术,由一些世界上技术占领先地位的自动化系统和硬件、软件公司与微软公司(Microsoft)紧密合作而建立的,并且成立了专门的OPC基金会来管理,OPC基金会负责OPC规范的制定和发布。OPC提出了一套统一的标准,采用典型的CLIENT/SERVER模式,针对硬件设备的驱动程序由硬件厂商或专门的公司完成,提供具有统一OPC接口标准的SERVER程序,软件厂商只需按照OPC标准编写CLIENT程序访问(读/写)SERVER程序,即可实现与硬件设备的通信。
如图1.1所示,与传统的通讯开发方式相比,OPC技术具有以下优势:
·硬件厂商熟悉自己的硬件设备,因而设备驱动程序性能更可靠、效率更高。
·软件厂商可以减少复杂的设备驱动程序的开发周期,只需开发一套遵循OPC标准的程序就可以实现与硬件设备的通信,因此可以把人力、物力资源投入到系统功能的完善中。·可以实现软硬件的互操作性。
·OPC把软硬件厂商区分开来,使得双方的工作效率有了很大的提
高。
图1.1OPC技术应用前后比较
因此OPC技术的出现得到了广大软硬件厂商的支持,并迅速发展。自从1997年9月发布OPCDA1.0规范以来,经过多年的发展,OPC规范已经被工控领域大多数厂商接受,并成了工控软件的技术标准。目前OPC规范主要有DA(DataAccess)规范,AE(alarmandevent)规范,HDA(historydataaccess)规范等。而且随着OPC技术与企业整体信息系统集成的需求变得日益迫切,对OPC技术的跨平台性能和Internet特性提出了更高要求。为此,OPC基金会开始以XML为基础着手制定一系列新的标准。2002年3月OPC基金会正式发布了OPCXML-DA规范,并与2004年12月正式发布了OPCXML-DA1.01规范,为OPC进一步提高工业控制系统的互操作性揭开了新的篇章。本书仅仅以符合DA规范的OPC服务器和客户程序为例介绍OPC技术,对于其它规范的OPC技术,本书未能介绍。
COM/DCOM1.1OPC技术的本质————COM/DCOM
随着计算机网络技术的发展,计算机监控系统也普遍的采用了分布式结构,因而系统的异构性是一个非常显著的特点。OPC技术本质是采用了Microsoft的COM/DCOM(组件对象模型/分布式组件对象模型)技术,COM主要是为了实现软件复用和互操作,并且为基于WINDOWS的程序提供了统一的、可扩充的、面向对象的通讯协议,DCOM是COM技术在分布式计算领域的扩展,使COM可以支持在局域网、广域网甚至Internet上不同计算机上的对象之间的通讯。
COM是由Microsoft提出的组件标准,它不仅定义了组件程序之间进行交互的标准,并且也提供了组件程序运行所需的环境。在COM标准中,一个组件程序也被称为一个模块,它可以是一个动态链接库,被称为进程内组件(in-processcomponent);也可以是一个可执行程序(即EXE程序),被称作进程外组件(out-of-processcomponent)。一个组件程序可以包含一个或多个组件对象,因为COM是以对象为基本单元的模型,所以在程序与程序之间进
行通信时,通信的双方应该是组件对象,也叫做COM对象,而组件程序(或称作COM程序)是提供COM对象的代码载体。
COM标准为组件软件和应用程序之间的通信提供了统一的标准,包括规范和实现两部分,规范部分规定了组件间的通信机制。由于COM技术的语言无关性,在实现时不需要特定的语言和操作系统,只要按照COM规范开发即可。然而由于特定的原因,目前COM技术仍然是以Windows操作系统为主,在非Windows操作系统上开发OPC,具有很大的难度。COM的模型是C/S(客户/服务器)模型,OPC技术的提出就是基于COM的C/S模式,因此OPC的开发分为OPC服务器开发和OPC客户程序开发,对于硬件厂商,一般需要开发适用于硬件通讯的OPC服务器,对于组态软件,一般需要开发OPC客户程序。
对于OPC服务器的开发,由于多种编程语言在实现时都提供了对COM的支持,如MicrosoftC/C++,VisualBasic,Borland公司的Delphi等。但是开发OPC服务器的语言最好是C或者是C++语言。在本书中选用VisualC++6.0为开发语言。
对于OPC客户程序的开发,可根据实际需求,选用比较合适的,能够快速开发的语言。
1.2OPCDA204规范简述
OPCDA204规范(OPCDataAccessCustomInterfaceSpecification2.04)是2000年9月OPC基金会发布的OPCDA自定义接口规范。该规范制定了OPC服务器和OPC客户程序的COM接口标准,通过制定标准的接口来实现多个厂家的OPC服务器和OPC客户程序开发。本书附带OPCDA204规范的WORD文档。
1.2.1OPC客户程序和OPC服务器
一个OPC客户可以连接一个或多个OPC服务器,而多个OPC客户也可以同时连接同一个OPC服务器,如图1.2
所示。
图1.2OPC客户程序/OPC服务器关系
1.2.2OPC服务器的对象组成
一个OPC服务器由三个对象组成:服务器(Server),组(Group),项(Item)。OPC服务器对象用来提供关于服务器对象自身的相关信息,并且作为OPC组对象的容器。OPC组对象用来提供关于组对象自身的相关信息,并提供组织和管理项的机制。
OPC组对象提供了OPC客户程序用来组织数据的一种方法。例如一个组对象代表了一个(可编程控制器)中的需要读写的寄存器组。一个客户程序可以设置组对象的死区,刷新频率,需要组织的项等。OPC规范定义了2种组对象:公共组和私有组。公共组由多个客户共享,局部组只隶属于一OPC客户。全局组对所有连接在服务器的应用程序都有效,而私有组只能对建立它的CLIENT有效。在一个SERVER中,可以有若干个组。
OPC项代表了OPC服务器到数据源的一个物理连接。数据项是读写数据的最小逻辑单位。一个OPC项不能被OPC客户程序直接访问,因此在OPC规范中没有对应于项的COM接口,所有与项的访问需要通过包含项的OPC组对象来实现。简单的讲,对于一个项而言,一个项可以是PLC中的一个寄存器,也可以是PLC中的一个寄存器的某一位。在一个组对象中,客户可以加入多个OPC数据项。每个数据项包括3个变量:值(Value)、品质(Quality)和时间戳(TimeStamp)。数据值是以VARIANT
形式表示的。
图1.3Server/Group/Item关系
这里最需要注意的是项并不是数据源,项代表了到数据源的连接。例如一个在一个系统中的TAG不论OPC客户程序是否访问都是实际存在的。项应该被认为是到一个地址的数据。大家一定要注意项的概念。不同的组对象里可以拥有相同的项,如组1中有对应于一个开关的ITEMAAA,组2中也可以有同样意义对应于一个开关的ITEMAAA,即同样的项可以出现在不同的组中。
1.2.3OPC接口体系
OPC规范提供两种接口:自定义接口(theOPCCustomInterfaces),自动化接口(theOPCAutomationinterfaces)。
图1.4OPC接口
如前所述,象所有的COM结构一样,OPC是典型的CLIENT/SERVER结构,OPC服务器提供标准的OPC接口供OPC客户程序访问。OPC服务器必须提供自定义接口,对于自动化接口,在OPC
规范定义中是可选的。
图1.5典型OPC结构
1.3OPC对象接口定义
本节主要对OPC服务器对象和OPC组对象的接口进行简要的介绍。
OPC服务器对象提供一些方法去读取或连接一些数据源。OPC客户程序连接到OPC服务器对象,并通过标准接口与OPC服务器联系。OPC服务器对象提供接口(AddGroup)供OPC客户程序创建组对象并将需要操作的项添加到组对象中,并且组对象可以被激活,也可以被赋予未激活状态。对于OPC客户程序而言,所有OPC服务器和OPC组对象可见的仅仅是COM接口。
下面的两个图例是OPC规范中定义的OPC服务器对象和OPC组对象的COM接口,其中任选的接口均以[]表示。(注:任选指开发OPC服务器时,这些接口可以根据实际情况选择实现还是不实现,除任选项外的接口在开发时必须全部实现。)
图1.6标准OPC服务器对象及接口
IOPCServerPublicGroups,IOPCBrowseServerAddressSpace和
IPersistFile为任选(optional)接口,OPC服务器提供商可根据需要选择是否需要实现。其它接口为OPC服务器必须实现的接口。其中:IOPCServerPublicGroups接口用于对公共组进行管理。IPersistFile接口允许用户装载和保存服务器的设置,这些设置包括服务器通信的波特率、现场设备的地址和名称等,这样用户就可以知道服务器启动和配置的改变而不需要启动其它的程序。
IOPCBrowseServerAddressSpace允许用户浏览服务器中的有用的组员的数据,为用户提供OPC服务器各个组员的定义列表。IOPCCommon接口是其它OPC服务器(例如OPC报警与事件服务器)也使用的接口。通过该接口可为某个特定的客户/服务器对话(session)设置和查询本地标识(LocateID)。这样,一个客户程序的操作将不会影响其它客户程序。IConnectionPointContainer接口服务器(OPC服务器对象接口)支持可连接点对象,当OPC服务器关闭时需要通知所有的客户程序释放OPC组对象和其中的OPC组员,此时可利用该接口调用客户程序方的IOPCShutdown接口实现服务器的正常关闭。
IOPCServer接口及成员函数主要用于对组对象进行创建、删除、枚举和获取当前状态等操作。是OPC服务器对象的主要接口。接口及成员函数定义为:
图1.7标准OPC组对象及接口
其中:IOPCItemMgt接口及成员函数用于OPC客户程序添加、删除和组对象中组员等控制操作。IOPCGroupStateMgt接口及其成员函数允许OPC客户程序操作或获取用户组对象的全部状态(主要是组对象的刷新率和活动状态,刷新率的单位为毫秒)。IOPCPublicGroupStateMgt为任选接口,用于将私有组对象(privategroup)转化为公有组对象(publicgroup),这个接口一般不用,在很多商业的OPC服务器中,此接口都没有开发。可选接口IOPCAsyncIO和IdataObject接口用于异步数据传输(在OPC数据访问规范1.0中定义,现在其功能已经被IOPCAsyncIO2和IConnectionPointContainer接口取代)。IOPCSyncIO用于同步数据访问。IOPCAsyncIO2用于异步数据访问。这两个接口是数据访问规范进行数据访问最重要的接口。
有关OPC服务器对象和OPC组对象的COM接口详细定义请看OPC规范定义,除在开发实例中用到的COM接口,其它接口本文不再详述。
1.4OPC同步异步通讯
OPCDA规范规定了两种通讯方式:同步通讯和异步通讯。这两种通讯方式与常见的串口同步通讯、异步通讯以及以太网的同步通讯、异步通讯的功能差不多。
同步通讯时,OPC客户程序对OPC服务器进行相关操作时,OPC客户程序必须等到OPC服务器对应的操作全部完成以后才能返回,在此期间OPC客户程序一直处于等待状态,如进行读操作,那么必须等待OPC服务器完成读后才返回。因此在同步通讯时,如果有大量数
据进行操作或者有很多OPC客户程序对OPC服务器进行读、写操作,必然造成OPC客户程序的阻塞现象。因此同步通讯适用于OPC客户程序较少,数据量较小时的场合。
异步通讯时,OPC客户程序对服务器进行相关操作时,OPC客户程序操作后立刻返回,不用等待OPC服务器的操作,可以进行其他操作。当OPC服务器完成操作后再通知OPC客户程序,如进行读操作,OPC客户程序通知OPC服务器后离开返回,不等待OPC服务器的读完成,而OPC服务器完成读后,会自动的通知OPC客户程序,把读结果传送给OPC客户程序。因此相对于同步通讯,异步通讯的效率更高,适用于多客户访问同一OPC服务器和大量数据的场合。
OPC的异步通讯有四种方式:
·数据订阅,客户端通过订阅方式后,服务器端将变化的数据通过回调传送给客户程序。·异步读,返回操作结果和数据值。
·异步写,返回操作结果,成功、失败。
·异步刷新,异步读所有Item的值
1.5OPC服务器开发方式
OPC服务器本身就是一个可执行程序,该程序以一定的速率不断地同物理设备进行数据交互。服务器内有一个数据缓冲区,其中存有最新的数据值,数据质量戳和时间戳。OPC数据服务器的设计与实现是一个较为复杂与繁重的任务,设计者既需要熟悉OPC规范,同时也必须掌握相应的硬件产品特性。OPC数据服务器大致可以分解为不同的功能模块。OPC对象接口管理模块,Item数据项管理模块以及服务器界面和设置等等。一个设备的OPCServer主要有两部组成,一是OPC标准接口的实现,二是与硬件设备的通信模块。
虽然COM技术本质上具有语言无关性,可以用各种语言开发,但由于最适合COM开发的语言仍然是C++,因此一般都选择采用VisualC++进行开发。
目前用VisualC++开发COM组件主要有三种方式:使用COMSDK直接开发COM组件;通过MFC提供的COM支持实现COM组件;通过ATL来实现COM组件。
此外,目前国内外很多的工控软件厂商也推出了一系列的OPC快速开发工具包,使用专门的OPC开发工具包,开发者只需具备基本的编程基础即可快速上手,无需掌握ATL,COM/DCOM,也无需了解OPC技术的细节,而且大多数的OPC开发工具都支持多种常用编程语言,如VB,VC等。网站也提供OPC开发工具,有兴趣的读者可以到网站下载DEMO开发工具或者与QQ:41063473联系购买事宜。http://www.opc-china.com)
建议所有的学生或有志向的开发人员可以尝试独立开发OPC服务器,如果是公司使用,建议购买OPC服务器开发工具。
C重点:何为OPC?OPCDA有哪些对象?OPCDA有哪些接口?OPCDA的通讯方式?OPOPC
DA的开发方式?(工控帮http://www.opc-china.com)
第2章ATL简介
关键字:ATL类厂接口标识符IDL组件聚合双重接口自定义接口自动化接口连接点事件注册属性方法客户程序
VisualC++从4.0版本就已经提供全面的COM支持,尤其在5.0和6.0版本中,不仅MFC类库提供COM应用的支持,而且VisualC++的集成开发环境VisualStudio也为COM应用提供了各种向导支持,并且,VisualC++还提供了另一套模板库ATL专门用于COM应用程序的开发。在上一章中介绍了采用VisualC++进行OPC服务器开发的几种方式,因为ATL是专为COM应用程序开发,因此本书的OPC服务器开发采用了ATL的模式,在本章中首先对ATL进行简要介绍,并以实际例子介绍如何进行ATL编写COM组件。
ATL(ActiveTemplateLibrary)是VisualC++提供的一套基于模板的C++类库,利用这些模板类,开发人员可以快速的开发COM组件程序。所以说ATL专门针对于COM应用开发的,它内部的模板类实现COM的一些基本特征,比如一些基本COM接口IUnknown,IClassFactory,IDispatch等,也支持COM的一些高级特性,如双接口(dualinterface)、连接点(connectionpoint)、Activex控制等。ATL最初的设计是快速的开发小型的组件,ATL2.0版本添加了模板库用来支持可视化的控件开发。
ATL所具有的特点:
•包含所有C++的功能。
•无需运行库,除非你想使用它。
•引用计数。
•高水平的对象和接口实现方法。
•类厂自动操作,对象创造,接口查询。
ATL开发应用程序并不像开发MFC应用程序那样容易。但VisualStduio提供某种帮助使开发者迅速开发应用程序。可利用ATLCOMWinzard(活动模板库组件向导)和ATLObjectWizard(活动模板库对象向导)开发ATL应用程序。
从目前介绍ATL技术的书籍来看,国内的书籍较少,本章主要通过介绍如何创建一个ATL应用程序来使大家对ATL技术有一定的了解,如果读者了解ATL,可以略过此章。
2.1COM基础
本节主要对COM的两个基本概念(接口,组件)做简要介绍。
2.1.1COM
图2.1接口结构
从理论上讲,完整的COM编程系统是基于接口的。
从技术上讲,接口是包含了一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的所有信息,客户程序利用这些函数获得组件对象的服务。需要注意的是在接口成员函数中,字符串变量必须用Unicode字符指针,COM规范要求使用Unicode字符,而且COM库中提供的COMAPI函数也使用Unicode字符。所以如果在组件程序内部使用到了ANSI字符的话,则应该进行两种字符表达的转换。当然,在即建立组件程序又建立客户程序的情况下,可以使用自己定义的参数类型,只要它们与COM所能识别的参数类型兼容。这里需要特别注意的是COM需要使用Unicode字符。
COM接口可以分为以下两类:
标准接口
自定义接口
标准接口之IUnknown,是所有接口的基接口。自定义接口也是基于IUnknown接口。所有的COM组件都必须以这个接口为基础。IUnknown提供了两个非常重要的特性,一个用于组件对象的生命周期管理,也可以查询被组件对象使用的其他接口。客户程序只能通过接口
与COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。如果客户还要继续对对象进行操作,则它必须保证对象能一直存在于内存中;如果客户对象的操作已经完成,以后不再需要该对象了,则它必须及时地把该对象释放掉,以提高资源的利用率。IUnknown引入了“引用计数”方法,可以有效地控制对象的生存期。另一方面,如果一个COM对象实现了多个接口,在初始时刻,客户程序不太可能得到该对象的所有接口指针,它只会拥有一个接口指针。如果客户程序需要其它的指针,则可利用IUnknown的“接口查询”方法来完成接口之间的跳转。
IUnknown的IDL定义:
interfaceIUnknown
{
HRESULTQueryInterface([in]REFIIDiid,[out]void**ppv);
ULONG
ULONG
}
IUnkown的C++定义:
classIUnknown
{
virutalHRESULT_stdcallQueryInterface(constIID&iid,void**ppv)=0;
virtualULONG
virutalULONG
}
标准接口之IDispatch,此接口用于脚本语言(如VisualBasic)访问组件。当脚本语言调用组件对象的方法时,此接口查询函数的地址并执行。
标准接口之IClassFactory(类厂)接口用于创建新的COM对象的实例。类厂(classfactory)是COM对象的生产基地,COM库通过类厂创建COM对象;对应每一个COM类,有一个类厂专门用于该COM类的对象创建操作。类厂本身也是一个COM对象,它支持一个特殊的接口IClassFactory:
classIClassFactory:publicIUnknown
{
virtualHRESULT_stdcallCreateInstance(IUnknown*pUnknownOuter,_stdcallAddRef()=0;_stdcallRelease()=0;AddRef(void);Release(void);
constIID&iid,void**ppv)=0;
virtualHRESULT_stdcallLockServer(BOOLbLock)=0;
}
如果你还没有学过COM,那么你来创建一个组件的时候很可能会采用C++的new操作符,这样的话会返回一个奇怪的错误。这也是一个非常常见的ATL错误,一个初学者不太理解的错误。实际上,创建一个对象,需要调用CoCreateInstance来创建一个COM对象的实例。在这里要注意的是创建COM对象不是用new操作符。在后面的章节中同样可以看到释放一个对象时,不能用delete,而是要通过Release接口来释放对象。
自定义接口的目的是提供更多的功能给用户,开发者可以自己定义基于IUnknown的接口提供更多的功能。
COM标识符UUID/GUID用来标识组件,通过唯一标识行UUID来唯一标识组件,如同身份证的意义,可以在系统中标识COM组件。在COM中,UUID是指全局唯一标识符GUID。GUID分为CLSID、IID和LIBID三类。
COM规范采用了128位全局唯一标识符GUID来标识对象和接口,这是一个随机数,并不需要专门机构进行分配和管理。因为GUID是个随机数,所以并不绝对保证唯一性,但发生标识符相重的可能性非常小。从理论上讲,如果一台机器每秒产生10000000个GUID,则可以保证(概率意义上)的3240年不重复。
接口描述语言IDL。COM规范在采用OSF(开放软件基金会)的DCE(分布式计算环境)规范描述远程调用接口IDL(interfacedescriptionlanguage,接口描述语言)的基础上,进行扩展形成了COM接口的描述语言。接口描述语言提供了一种不依赖于任何语言的接口描述方法,因此,它可以成为组件程序和客户程序之间的共同语言。MicrosoftVisualC++提供了MIDL工具,可以把IDL接口描述文件编译成C/C++兼容的接口描述头文件(.h)。按照COM规范,按照接口成员函数的参数类型的不同,有三种情况:
In参数:对于in参数,由OPC客户程序分配和释放内存;
Out参数:Out参数由OPC服务器分配内存,由OPC客户程序释放内存,采用标准COM内存分配器。
In-out参数:in-out参数最初由OPC客户程序分配内存,然后由OPC服务器释放和再分配(如果需要)。和out参数一样,OPC客户程序负责最后返回值的释放。
如不能正确释放内存,将会引起难以发现的内存泄漏(memoryleak),造成系统可用的内存资源越来越少直至系统崩溃。因此,编写程序时可以参考IDL文件来查找out参数,并且
针对每种类型的结构编写一段子程序处理内存释放。在每次客户程序调用服务器函数的过程中,不管函数执行正确与否,服务器程序都必须为每个out参数定义好返回值,而客户程序则负责释放相应的内存资源。
COM接口所有方法返回类型都为HRESULT。
2.1.2组件
一般而言,组件具有三种类型:进程内组件,进程外组件,远程组件。
进程内组件是采用动态连接库方式实现的组件。客户程序调用组件时,客户程序会把组件程序装入自己的进程空间,即客户程序和组件程序在同一个进程地址空间内。在客户端和服务端组件间有大量数据转移操作的情况下是理想的。进程内服务器会更快地装载。由于它占用和客户端应用程序同样的地址空间,它可以与客户端更快的通信。进程内服务器是通过将组件作为动态连接库(DLL)的形式来实现的。DLL允许特定的一套功能以分离于可执行的、以DLL为扩展名的文件进行存储。只有当程序需要它们时,DLL才将其装入内存中,客户程序将组件程序加载到自己的进程地址空间后再调用组件程序的函数。
本地(即进程外)组件,进程外组件是以EXE方式实现的组件,具有独立的进程,因此客户程序和组件程序分别处在不同的进程空间地址中。在COM中,采用了本地过程调用(localprocedurecall,LPC)来进行本地通信。进程外服务器对需要运行于独立的处理空间或作为独立客户端应用程序的线程的组件是理想的。由于数据必须从一个地址空间移到另一个地址空间,因此这些服务器就会慢得多。由于进程外服务器是可执行的,它们运行在自己的线程内。当客户端代码正在执行时,客户端不锁住服务器。进程外服务器对需要表现为独立的应用程序的组件也是理想的。例如,MicrosoftInternetExplorer的应用程序是本地服务器的例子。客户端和服务端的通信是通过进程内的通信协议进行的,这个通信协议是IPC(进程间通信)。
远程组件,远程服务器与本地服务器类似,除了远程服务器是运行在通过网络连接的分离的计算机上。这种功能是使用DCOM实现的。DCOM的优点在于它并不要求任何特别的编程来使具有功能。另外服务端和客户端通信是通过RPC(Remoteprocedurecall,RPC)通信协议进行的。对于这三种不同的服务器组件,客户程序和组件程序交互的内在方式是完全不同的。但是对于功能相同的进程内和进程外组件,从程序编写的角度看,客户程序是以同样的方法来使用组件程序的,客户程序不需要做任何的修改。
2.2ATL应用程序向导创建应用程序
应用ATL应用程序向导来开发ATL程序是快速而且有效的,本节主要以MSDN的例子为例来介绍如何应用ATL应用程序向导来创建新的ATL程序。
创建新的ATL应用程序第一步,生成一个新的ATL工程,从VisualStudio菜单中选择File—>New,选中Projects(工程)选项卡。在图2.2中选择ATLCOMAppWizard,输入工程名BeepCtrlMod。
图2.2VisualStud
io的ATLCOMAppWizard
单击New对话框的OK,打开ATLCOMAppWizard对话框。MFCAppWizard由许多步骤组成,而ATLCOMWizard仅需要一步,即必须选定开发应用程序类型。由图2.3可以看出,一共有三种类型的组件,动态连接库,可执行,服务三种类型,分别对应着进程内组件,进程外组件,服务。
选择动态连接库(进程内组件)。单击Finish,打开NewProjectInformation对话框,然后单击OK完成后,一个新的组件的框架已经建立。
图2.3ATLCOMAppW
izard选择组件类型
经过AppWizard创建得到的应用仅仅是一个程序框架,可以看到包含一个全局成员:CComModule_Module;
•一个包含着最初的类型库的说明的.idl文件
•一个.def文件。
•一个.rc资源文件
•一个包含着资源ID定义的头文件
•stdafx.h和stdafx.cpp文件
•其它的.cpp文件,用来实现全局功能应用的源文件。
可以看出与MFC应用程序不同的一点是多了一个后缀为.idl的文件。每一个ATL工程都有一个与工程同名的IDL文件,此IDL文件记录了该工程中所用到COM接口或COM对象的定义。ATLAppWizard或ATLObjectWizard会自动维护此IDL文件,也可以手动的修改此文件加入需要的COM接口的IDL定义。
2.3源文件说明
本节对BeepCntMod.cpp文件做简要说明,从而了解主文件的程序结构。
#include"stdafx.h"
#include"resource.h"
#include
#include"BeepCntMod.h"
#include"BeepCntMod_i.c"
CComModule_Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
Initguid.h文件是一个标准OLE系统文件,需要在工程中引用,用来定义GUID(thegloballyuniqueidentifier)结构。
如果现在编译和连接一下应用程序,会发现多了一个与工程的IDL文件同名的.h头文件(BeepCntMod.h)和一个._i.c(BeepCntMod_i.c)文件。这是集成环境调用MIDL编译器对工程的IDL文件进行编译生成的。在进行第一次编译之前,看不到这两个文件,必须编译以后才可以看这两个文件。
BeepCntMod.h包含了接口和组件的定义。BeepCntMod_i.c包含着GUID的定义。
_Module是一个全局变量,定义了类厂以及类厂所有的初始化代码功能,如类厂的注册等,如果不用ATL进行开发,而是采用MFC开发,类厂的注册等都需要手动加入。可以通过MSDN来查看CComModule的详细文档。
最后一部分,是一个空的对象映射,对象向导会将需要的映射加入进去。每一个元素的对象映射都有一个CLSID和一个类名。_Module对象读取这些映射根据CLSID来创建对象。接下来看一下DllMain函数,它简单的调用了_Module对象的Init和Term函数。
//DLLEntryPoint
extern"C"
BOOLWINAPIDllMain(HINSTANCEhInstance,DWORDdwReason,LPVOID/*lpReserved*/)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap,hInstance,
&LIBID_BEEPCNTMODLib);
DisableThreadLibraryCalls(hInstance);
}
elseif(dwReason==DLL_PROCESS_DETACH)
_Module.Term();
returnTRUE;
}
最后的四个函数如下://ok
STDAPIDllCanUnloadNow(void)
{
return(_Module.GetLockCount()==0)?S_OK:S_FAL
SE;
}
STDAPIDllGetClassObject(REFCLSIDrclsid,REFIIDriid,LPVOID*ppv)
{
return_Module.GetClassObject(rclsid,riid,ppv);
}
STDAPIDllRegisterServer(void)
{
//registersobject,typelibandallinterfacesin
typelib
return_Module.RegisterServer(TRUE);
}
STDAPIDllUnregisterServer(void)
{
return_Module.UnregisterServer(TRUE);
}
这些函数除非调用_Module对象的合适的函数,什么也不做。有关这四个函数的详细说明请参考有关COM书籍和MSDN。
2.4添加组件对象
通过上面的介绍,开始对ATL有了一定的认识,下面为上面的应用程序添加一个COM组件,本节的主要目的是熟悉如何通过ATL添加组件对象。
最简单的添加组件的方法是通过ATLObjectWizard。从Insert菜单下选择NewATLObject...,你将看到图2.4
所示的对话框。
图2.4ATLObjectWizard
这里选择SimpleObject,单击Next,图2.5所示的属性页显示出来,在这个属性页可以输入组件的名称和组件的属性。在ShortName编辑框里输入“BeepCnt”,也可以输入其它名字。输入“BeepCnt”
其它七个编辑框的内容会自动生成。
图2.5ATLObjectWizardProperties
单击属性页上的Attributes,出现图2.6所示的属性框,在这个框中可以选择组件相应的属性,本例中选择默认。单击OK。
图2.6ATLObjectWizardProperties
ApartmentThreadingModel的意思是指这个对象可以被一个或者多个线程所调用。实际上每个COM对象的实例都是单线程的,需要注意的是假设COM对象有多个线程在调用它,如果在程序的方法中引用了全局变量,必须保证程序多个线程访问的安全问题。不管如何,只要引用了全局变量,需要考虑程序多线程访问时的安全问题,或者在此选择SingleThreadingModel.
在这里,选择DualInterface,双重接口的意思是指创建的组件支持自定义接口,也支持自动化接口。自动化接口是用来支持脚本语言访问的接口,考虑到客户程序自动化的实现比自定义的实现简单的多,所以选择了双接口的方式。
Aggregation,汉语意思为聚合。包容和聚合是COM的两种重要类型。聚合是指组件对象一种特殊的包容方法,在一个组件对象里面可以拥有另外的组件对象。一般而言,采用COM自己实现聚合的的方式是复杂的,而ATL可以帮助我们操作聚合的实现。如果组件支持聚合,那么组件将是更加灵活的,因此我们选择支持聚合。
ISupportErrorInfo,创建ISupportErrorInfo接口支持,以便对象可将错误信息返回到客户端。我们不选择它。
ConnectionPoints,连接点用来支持事件,通过使对象的类从IConnectionPointContainerImpl导出来启用对象的连接点,连接点用来支持事件,所谓事件是指COM对象中当某个属性发生改变时,对象产生一个事件,通知到客户程序,客户程序可以处理这些事件。本例中,不选择连接点。
Free-ThreadedMarshaller,自由线程封送拆收器,创建自由线程封送拆收器对象,以有效地在同一进程中的两个线程之间封送接口指针。对指定“两者”或“自由”作为线程模型的对象可用。用于多线程控制的特定类型。本例中,不选择它。
创建完成后的组件对象由于支持双接口(自动化接口,自定义接口),聚合,多线程访问,因此它是一个简单但是易于扩展的组件。
现在工程中又多了两个ATL对象向导创建的文件:beepcnt.cpp和beepcnt.h。beepcnt.cpp基本上是空的,因为没有为组件对象添加任何的属性。所有将来添加的对象方法和属性都将在这个文件中实现。
beepcnt.h包含了对象的类的定义。
classATL_NO_VTABLECBeepCnt:
publicCComObjectRootEx,
publicCComCoClass,
publicIDispatchImpl
&LIBID_BEEPCNTMODLib>
{
public:
CBeepCnt()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_BEEPCNT)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CBeepCnt)
COM_INTERFACE_ENTRY(IBeepCnt)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
//IBeepCnt
public:
};
可以看出,CBeepCnt类基于三个模板类。CComObjectRootEx操作了组件的引用计数;CComCoClass用来定义该对象的默认类工厂和聚合模型;IDispatchImpl,提供了一个双重接口IBeepCnt(接口ID是IID_IbeepCnt,支持自动化接口和自定义接口)。
CBeepCnt类同样提供了一组宏,ATL_NO_VTABLE会告诉编译器不要再为CBeepCnt类生成虚函数,因为ATL_NO_VTABLE必须只能与一个无法直接创建的基类一起使用。在项目中一定不能将declspec(novtable)与基本上是导出的类一起使用,因为该类(通常是CComObject、CComAggObject或CComPolyObject)为项目初始化vtable指针。一定不能从任何使用declspec(novtable)的对象的构造函数调用虚函数。应该将那些调用移动到FinalConstruct(大家要记着这个方法)方法。需要我们切记的是,我们可以尝试着添加一个虚函数,但是编译器会编译错误,切记ATL_NO_VTABLE意味着没有虚函数,不要为基于ATL_NO_VTABLE的类添加虚函数。
当对象创建时,对象实际上是CComObject。换言之,用的是把对象类型作为一个参数的CComObject
模板类。一个派生的图如下图所示。
图2.7CcomObject派生图
CComObject的主要工作是提供IUnknown方法的实现。这些必须由最低层的派生类提供,以便所有由IUnknown派生的类可以共享IUnknown方法的实现。CComObject的方法仅仅调用CComObjectRootEx中的实现。
另外需要注意的是COM接口映射,COM接口映射包含着我们实现自定义接口和IDispatch的宏。COM接口映射中包含的接口是可以通过QueryInterface返回的接口指针的接口。这里还有一对宏。一个用来根据资源ID定义注册入口,一个用来改变对象创建的方法来保证对象不会被意外的删除。
与非采用ATL方式的COM相比,可以看到采用ATL开发的组件中没有引用计数的代码,是因为引用计数的工作由CComObjectRootEx类操作,而不需要我们来操作了。
接着讨论的新文件,beepcnt.rgs,包含了为ATL处理的代码的源脚本。大部分直接反映了为了COM运行时能够找到组件的注册入口。文件如下:
HKCR
{
BeepCntMod.BeepCnt.1=s'BeepCntClass'
{
CLSID=s'{AE73F2F8-4E95-11D2-A2E1-00C04F8
EE2AF}'
}
BeepCntMod.BeepCnt=s'BeepCntClass'
{
CLSID=s'{AE73F2F8-4E95-11D2-A2E1-00C04F8
EE2AF}'
CurVer=s'BeepCntMod.BeepCnt.1'
}
NoRemoveCLSID
{
ForceRemove{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}=s'BeepCntClass'
{
ProgID=s'BeepCntMod.BeepCnt.1'
VersionIndependentProgID=s'BeepCntMod.BeepC
nt'
ForceRemove'Programmable'
InprocServer32=s'%MODULE%'
{
valThreadingModel=s'Apartment'
}
'TypeLib'=s'{170BBD8D-4DE8-11D2-A2E0-00C04F8E
E2AF}'
}
}
}
这段脚本用来注册和注销组件。默认注册时,所有的键都会添加到注册表,不管这些键是否在注册表中。当注销时,默认的方式是删除所有在脚本中列出的键。一般情况下,你不需要编辑这个文件,当你添加对象和接口时,向导会自动的实现它。
最后讨论的文件是“BeepCntMod_i.c”,在此文件中,定义了组件的CLSID。CLSID是唯一地标识类或组件的GUID,传统地,CLSID的一般形式为CLSID_。TypeLibID是标识系统上的类型库。按照惯例,TypeLibID的一般形式为LIBID_Lib。
#ifdef__cplusplus
extern"C"{
#endif
#ifndef__IID_DEFINED__
#define__IID_DEFINED__
typedefstruct_IID
{
unsignedlongx;
unsignedshorts1;
unsignedshorts2;
unsignedcharc[8];
}IID;
#endif//__IID_DEFINED__
#ifndefCLSID_DEFINED
#defineCLSID_DEFINED
typedefIIDCLSID;
#endif//CLSID_DEFINED
constIIDLIBID_BEEPCNTMODLib=
{0x170BBD8D,0x4DE8,0x11D2,{0xA2,0xE0,0x00,0xC0,0x4F,0x8E,0xE2,0xAF}};
#ifdef__cplusplus
}
#endif
与添加组件对象前相比,已经存在的文件也有一些小的变化。
•BeepCntMod.cpp现在添加了#includesBeepCnt.h,并且为CbeepCnt添加了一个对象映射入口,每一个COM对象都有一个对象映射入口:
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_BeepCnt,CBeepCnt)
END_OBJECT_MAP()
•.idl文件里现在有了IDispatch派生的IBeepCnt接口入口,一个为BeepCnt类的在类型库的Coclass入口。
当重新编译程序时,MIDL生成了一个新的反映新建组件对象的BeepCntMod.h文件。编译后就生成了组件的DLL文件,在编译的同时完成组件的注册。
2.5添加组件对象的属性和方法(函数)
上面已经介绍了如何用ATL组件。然而生成的对象是个空的对象,属于这个对象的属性和方法都是空的。如何添加组件对象的属性和方法是本节要讨论的内容。
从ClassView中右击IbeepCnt,选择“AddMethod…”如图2所示的对话框出现。在MethodName编辑框中,选择返回值类型为HRESULT,方法名输入Beep,参数一项为空,单击OK,完成Beep方法的添加,向导会把所有Beep的定义的代码自动添加到BeepCnt.cpp和BeepCnt.h
文件中。
图2.8向接口添加方法
添加一个属性也是类似的。右击接口,选择“AddProperty…”,如图2.9所示的对话框出现。选择合适的类型,输入属性的名称,单击OK完成添加。
图2.9向接口添加属性
完成了方法和属性的添加后,向导向.idl文件添加了方法和属性的定义。
interfaceIBeepCnt:IDispatch
{
[id(1),helpstring("methodBeep")]HRESULTBee
p();
[propget,id(0),helpstring("propertyCount")]
HRESULTCount([out,retval]long*pVal);
[propput,id(0),helpstring("propertyCount")]
HRESULTCount([in]longnewVal);
};
BeepCnt.h包含了三个新函数的定义,BeepCnt.cpp文件包含了这些函数的框架。
STDMETHODIMPCBeepCnt::Beep()
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
STDMETHODIMPCBeepCnt::get_Count(long*pVal)
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
STDMETHODIMPCBeepCnt::put_Count(longnewVal)
{
//TODO:Addyourimplementationcodehere
returnS_OK;
}
为了让创建的组件可以做一些我们能感觉到的事情,需要添加一些代码,首先,为CBeepCnt类添加一个计数器,并且在CBeepCnt()构造函数中把它初始化为1。
longcBeeps;
CBeepCnt():CBeeps(1){}
接着编写一部分代码在CBeepCnt的方法里。注意属性有两个功能,一个是设置计数器(put_Count),一个是获得计数器(get_Count)。
STDMETHODIMPCBeepCnt::Beep()
{
for(inti=0;i
{
MessageBeep((UINT)-1);
Sleep(1000);
}
returnS_OK;
}
STDMETHODIMPCBeepCnt::get_Count(long*pVal)
{
*pVal=cBeeps;
returnS_OK;
}
STDMETHODIMPCBeepCnt::put_Count(longnewVal)
{
cBeeps=newVal;
returnS_OK;
}
现在一个简单功能的组件诞生了,它可以用来发出嘟嘟声。
2.6测试组件
如何来测试这个组件呢?一般而言,采用VisualBasic是个不错的选择,可以快速的编写客户程序来实现测试。可以编写一个图2.10
所示的对话框程序来测试它。
图2.10测试界面
采用VisualBasic6.0生成的代码如下:
DimBeeperCntAsBeepCnt
PrivateSubBeep_Click()
Text1=BeeperCnt
BeeperCnt.Beep
EndSub
PrivateSubSet_Click()
BeeperCnt=Val(Text1)
Text1=BeeperCnt
EndSub
PrivateSubForm_Load()
SetBeeperCnt=NewBeepCnt
Text1=BeeperCnt
EndSub
需要注意的是需要引用BeepCntMod1.0TypeLibrary。
图2.11引用库
运行这个程序,是不是听到了嘟嘟声,而且可以设置嘟嘟声的次数,类似的,你可以添加更多的属性和方法,来增强这个组件的功能,是不是很简单呢。
重点:何为ATL?何为类厂?COM对象如何创建?如何测试COM组件功能?
第4章OPC客户程序实例
关键字:同步异步VCVB
4.1OPC客户程序开发环境
无论开发者还是最终使用者都必须安装OPC代理/存根(Proxy/Stub)DLL,并进行环境设置。这些文件(opc_aeps.dll,opccomn_ps.dll,
opchda_ps.dll,opcproxy.dll,aprxdist.exe,opcenum.exe)可以从OPC基金会网站下载,也可以在http://www.opc-china.com下载。所有文件必须安装在客户端机器和服务器端机器上。
安装步骤如下:
1.复制所有的文件到你的Windows系统路径,如:
copyopcproxy.dllc:\winnt\system32
copyopccomn_ps.dllc:\winnt\system32
copyopc_aeps.dllc:\winnt\system32
copyopchda_ps.dllc:\winnt\system32
copyaprxdist.exec:\winnt\system32
copyopcenum.exec:\winnt\system32
2.安装代理DLL。
REGSVR32opcproxy.dll
REGSVR32opccomn_ps.dll
REGSVR32opc_aeps.dll
REGSVR32opchda_ps.dll
3.如果aprxdist.dll不存在,可以运行aprxdist.exe生成aprxdist.dll。
4.安装opcenum.exe
opcenum/regserver
在本书的VB客户程序中引用了OPCAutomation2.0库,库文件为
opcdaauto.dll。可以在本书附送的源码中找到,注册方式为:REGSVR32opcdaauto.dll。
4.3OPC客户程序(VB篇——同步)
正文:建立如下窗体:
引用如下:
代码如下:
OptionExplicit
DimWithEventsServerObjAsOPCServer
DimWithEventsGroupObjAsOPCGroup
DimItemObjAsOPCItem
PrivateSubCommand_Start_Click()
DimOutTextAsString
OnErrorGoToErrorHandler
Command_Start.Enabled=False
Command_Read.Enabled=True
Command_Write.Enabled=True
Command_Exit.Enabled=True
OutText="连接OPC服务器"
SetServerObj=NewOPCServer
ServerObj.Connect("XXXSERVER")@#XXXSERVER为某OPC服务器名称
OutText="添加组"
SetGroupObj=ServerObj.OPCGroups.Add("Group")
OutText="AddinganItemtothegroup"
SetItemObj=GroupObj.OPCItems.AddItem("XXXITEM",1)@#XXXITEM为添加的ITEM名称
ExitSub
ErrorHandler:@#如果出现异常,则报出错误。
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Read_Click()@#同步读
DimOutTextAsString
DimmyValueAsVariant
DimmyQualityAsVariant
DimmyTimeStampAsVariant
OnErrorGoToErrorHandler
OutText="读ITEM值"
ItemObj.ReadOPCDevice,myValue,myQuality,myTimeStampEdit_ReadVal=myValue
Edit_ReadQu=GetQualityText(myQuality)
Edit_ReadTS=myTimeStamp
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Write_Click()@#同步写
DimOutTextAsString
DimServerhandles(1)AsLong
DimMyValues(1)AsVariant
DimMyErrors()AsLong
OutText="写值"
OnErrorGoToErrorHandler
Serverhandles(1)=ItemObj.ServerHandle
MyValues(1)=Edit_WriteVal
GroupObj.SyncWrite1,Serverhandles,MyValues,MyErrors
Edit_WriteRes=ServerObj.GetErrorString(MyErrors(1))
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateSubCommand_Exit_Click()@#停止,删除ITEM,删除GROUP,删除SERVER。DimOutTextAsString
OnErrorGoToErrorHandler
Command_Start.Enabled=True
Command_Read.Enabled=False
Command_Write.Enabled=False
Command_Exit.Enabled=False
OutText="删除对象"
SetItemObj=Nothing
ServerObj.OPCGroups.RemoveAll
SetGroupObj=Nothing
ServerObj.Disconnect
SetServerObj=Nothing
ExitSub
ErrorHandler:
MsgBoxErr.Description+Chr(13)+_
OutText,vbCritical,"ERROR"
EndSub
PrivateFunctionGetQualityText(Quality)AsString
SelectCaseQuality
Case0:GetQualityText="BAD"
Case64:GetQualityText="UNCERTAIN"Case192:GetQualityText="GOOD"
Case8:GetQualityText="NOT_CONNECTED"Case13:GetQualityText="DEVICE_FAILURE"Case16:GetQualityText="SENSOR_FAILURE"Case20:GetQualityText="LAST_KNOWN"Case24:GetQualityText="COMM_FAILURE"Case28:GetQualityText="OUT_OF_SERVICE"Case132:GetQualityText="LAST_USABLE"Case144:GetQualityText="SENSOR_CAL"Case148:GetQualityText="EGU_EXCEEDED"Case152:GetQualityText="SUB_NORMAL"Case216:GetQualityText="LOCAL_OVERRIDE"CaseElse:GetQualityText="UNKNOWNERROR"EndSelect
EndFunction