基于第三方开源库的OPC服务器开发指南(1)——OPC与DCOM

事儿太多,好多事情并不以我的意志为转移,原想沉下心好好研究、学习图像识别,继续丰富我的机器视觉库,并继续《机器视觉及图像处理系列》博文的更新,但计划没有变化快,好多项目要完成,只好耽搁下来(这一耽搁又是多半年啊,惭愧,Disappointed smile)。最近某个项目需要OPC服务器支持,于是又转战OPC战场。说实话这之前对于OPC我只是粗浅了解,知道这是基于微软的DCOM技术制定的用于工控领域的技术标准,制定并持续维护这一标准的组织被称作OPC基金会。我不知道基金会对OPC的应用推广做了多少工作,做出了多大贡献,但至少可以确定的是,这个基金会对普通开发人员相当不友好,我即使成功注册了用户,依然在它的网站上没找到OPC的支持组件“OPC Core Components Redistributable 3.0”,我估计这是他们的商业考虑,为的是多发展企业用户,好多赚钱。这么LaJi的组织,实在是让人鄙视,OPC的未来最终会被这满是铜臭味的GouPi基金会葬送。要不是目前的工控市场还被OPC把持(没人和小钱钱有仇啊),说实话我是不会把精力浪费在这么封闭的系统上。所以,我对OPC的定位就是不求甚解,只求一用。有了清晰的目标,我开始在网上满世界的找OPC开发相关的资料和源码,度娘、bing、github、gitee、CodeForge以及搭梯子google之,反正能想到的方法都想到了,但结果相当不理想。CSDN上倒是有不少OPC服务器源码可供下载,但需要积分,我没有积分,我也不想挣或者花钱买积分,因为我一直主张并坚持技术共享精神,CSDN和众多CSDNers严重违背了这一精神,特别是那些拿着别人开源的源码赚积分的WuChi小“人”们。这无疑增加了我的难度,好在我们有github,有众多的具备分享精神的程序猿们,当然还有强大的搜索引擎,最终我找齐了所有资料和源码,搞明白了OPC服务器的整个开发流程。再次鄙视OPC基金会、CSDN,感谢如下网址的主人们:

开源OPC服务器库 http://www.ipi.ac.ru/lab43/lopc-en.html

DCOM开发样例 https://blog.csdn.net/u011402642/article/details/46516559

上面的DCOM开发样例出现0x80080005错误时的解决方案 https://blog.csdn.net/oshuangyue12/article/details/88424114

OPC头文件 https://www.cnblogs.com/opcconnect/archive/2010/12/20/1911604.html

 

本指南的样例代码请直接从github上拉取:https://github.com/Neo-T/OPCDASrvBasedOnLightOPC

 

吐槽完毕,言归正传。前面我们已经说过,OPC基于微软的DCOM技术,所以要想明白如何开发OPC服务器,首先就得知道如何开发DCOM。否则,你会摔得遍体鳞伤,浪费大把时间后依然是——不得其门而入。因为这个DCOM啊,实在是太过啰嗦,开发啰嗦、部署和使用更啰嗦,个人感觉它早晚会被淘汰。关于DCOM开发样例,上面给出的链接虽然是作者2015年写的,时间不算老,但开发环境竟然是1998年发布的VC6,这个鸿沟就有点大了。现在常用的VS2010和VS2015在DCOM开发上均做了不少改进,因此有必要在这里再开一篇,简明扼要地介绍VS2010和VS2015下的DCOM开发流程,以备后查。

首先,打开VS2010或VS2015(下简称VS),”新建项目”->”Visual C++”->”ATL项目”,输入名称“iDCOMTestSrv”:

image

然后“确定”->”下一步”,选择“服务(EXE)”,最后点选“完成”。

image

接下来,添加COM对象。工程名称节点鼠标右键,点选“添加”->””:

image

在弹出窗口选择“ATL简单对象”:

image

点击“添加”后,按下图输入相关信息,然后直接点击“完成”:

image

在“类视图”窗口,鼠标右键点选“IArithmeticLib”,在弹出的右键菜单中选择“添加”->”添加方法”:

image

弹出窗口中按下图所示添加add()方法:

imageimage

最后点击“完成”按钮,add()方法添加完毕。接着按照如上步骤再添加一个sub()方法:

image

我们的目的是熟悉DCOM的开发流程,所以不用编写复杂的函数,添加两个add()和sub()方法就行了。

转到VS的解决方案资源管理器,“源文件”节点双击“ArithmeticLib.cpp”,添加两个方法的处理代码:

  1 STDMETHODIMP CArithmeticLib::add(int nNum1, int nNum2, int * pnResult)
  2 {
  3 	*pnResult = nNum1 + nNum2;
  4 	return S_OK;
  5 }
  6 
  7 
  8 STDMETHODIMP CArithmeticLib::sub(int nNum1, int nNum2, int * pnResult)
  9 {
 10 	*pnResult = nNum1 - nNum2;
 11 	return S_OK;
 12 }

接下来我们还需要调整一个地方,在左侧的“解决方案资源管理器”窗口,找到“iDCOMTestSrv”工程的“资源文件”->“ArithmeticLib.rgs”,双击打开,然后在这个文件增加如下一句:

val AppID = s '%APPID%'

增加位置如下:

image

这一句解决客户端连接DCOM组件服务器时报0x80080005错误的问题。

接着编译,如果不出意外,VS2015编译应该能够成功,VS2010则不一定,因为VS2010相对VS2015多做了一步:

image

如果你有系统管理员权限,那么编译完成后这个注册是能够成功的,如果不是则失败。由于我们是把DCOM部署到其它机器远程执行,不在本机注册,所以这里可以删掉。不过,这一步倒是告诉我们,DCOM需要注册才能使用。

接下来我们需要生成代理/存根文件,以用于远程访问DCOM组件。相对VC6,新版本的VS帮我们自动建立了代理/存根文件工程,工程需要的相关文件,是VS通过对应的IDL文件编译生成的,我们在刚才编译DCOM时VS已经帮我们生成了相关文件并添加到对应工程下:

image

在编译生成代理/存根文件之前,我们需要更改一下该工程的链接器设置,见下图:

image

把“注册输出”一项改为“”,这样VS就不会在编译完成后顺带手帮我们在本机上注册该动态库了。虽然,VS这样设计对直接调试来说比较省事,但这样也掩盖了技术细节,所以还是禁止VS这种越俎代庖的行为更好。接下来鼠标右键点选“解决方案资源管理器”中的“iDCOMTestSrvPS”,点击“生成”,如无意外,我们将生成iDCOMTestSrv的代理/存根文件——“iDCOMTestSrvPS.dll”。

接下来我们还需要编写使用这个DCOM组件的客户端,看看效果如何。继续在VS中新建一个“Win32控制台应用程序”,在打开的源文件“iDCOMTestClient.cpp”中添加如下代码:

  1 // iDCOMTestClient.cpp : 定义控制台应用程序的入口点。
  2 //
  3 
  4 #include "stdafx.h"
  5 #include <windows.h>
  6 #include "iDCOMTestSrv_i.h"
  7 #include "iDCOMTestSrv_i.c"
  8 
  9 int _tmain(int argc, _TCHAR* argv[])
 10 {
 11     CoInitialize(NULL);
 12     {
 13         do{
 14             HRESULT hr;
 15 
 16             COSERVERINFO stCoServerInfo;
 17             COAUTHINFO stCoAuthInfo;
 18             COAUTHIDENTITY stCoAuthID;
 19             INT nSize = strlen("192.168.xxx.xxx") * sizeof(WCHAR);
 20             memset(&stCoServerInfo, 0, sizeof(stCoServerInfo));
 21             stCoServerInfo.pwszName = (WCHAR *)CoTaskMemAlloc(nSize * sizeof(WCHAR));
 22             if(!stCoServerInfo.pwszName)
 23             {
 24                 printf("CoTaskMemAlloc()函数执行失败!\r\n");
 25                 break;
 26             }
 27 
 28             ZeroMemory(&stCoAuthID, sizeof(COAUTHIDENTITY));
 29             stCoAuthID.User =  reinterpret_cast<USHORT *>("user");
 30             stCoAuthID.UserLength = strlen("user");
 31             stCoAuthID.Domain = reinterpret_cast<USHORT *>("");
 32             stCoAuthID.DomainLength = 0;
 33             stCoAuthID.Password = reinterpret_cast<USHORT *>("user_password");
 34             stCoAuthID.PasswordLength = strlen("user_password");
 35             stCoAuthID.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
 36 
 37             ZeroMemory(&stCoAuthInfo, sizeof(COAUTHINFO));
 38             stCoAuthInfo.dwAuthnSvc =  RPC_C_AUTHN_WINNT;
 39             stCoAuthInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
 40             stCoAuthInfo.pwszServerPrincName = NULL;
 41             stCoAuthInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CONNECT;
 42             stCoAuthInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;    //* 必须是模拟登陆
 43             stCoAuthInfo.pAuthIdentityData = &stCoAuthID;
 44             stCoAuthInfo.dwCapabilities = EOAC_NONE;
 45 
 46             mbstowcs(stCoServerInfo.pwszName, "192.168.xxx.xxx", nSize);
 47             stCoServerInfo.pAuthInfo = &stCoAuthInfo;
 48             stCoServerInfo.dwReserved1 = 0;
 49             stCoServerInfo.dwReserved2 = 0;
 50 
 51             MULTI_QI stMultiQI;
 52             ZeroMemory(&stMultiQI, sizeof(stMultiQI));
 53             stMultiQI.pIID = &IID_IArithmeticLib;    //* 参见iDCOMTestSrv_i.c
 54             stMultiQI.pItf = NULL;
 55 
 56             //* 初始化安全结构,模拟登录远程机器
 57             hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
 58             if(!(SUCCEEDED(hr) || RPC_E_TOO_LATE == hr))
 59             {
 60                 printf("CoInitializeSecurity()函数执行失败,错误码:0x%08X\r\n", hr);
 61                 break;
 62             }
 63 
 64             //* 建立COM组件实例并按照需求获取查询接口
 65             hr = CoCreateInstanceEx(CLSID_ArithmeticLib,    //* 参见iDCOMTestSrv_i.c
 66                                     NULL,
 67                                     CLSCTX_REMOTE_SERVER,    //* 显式的指定要连接远程机器
 68                                     &stCoServerInfo,
 69                                     sizeof(stMultiQI)/sizeof(MULTI_QI),
 70                                     &stMultiQI);
 71 
 72             //* 无论成功与否,先释放刚才申请的内存
 73             CoTaskMemFree(stCoServerInfo.pwszName);
 74 
 75             //* 如果CoCreateInstanceEx()执行失败
 76             if(FAILED(hr))
 77             {
 78                 printf("CoCreateInstanceEx()函数执行失败,错误码:0x%08X\r\n", hr);
 79                 break;
 80             }
 81 
 82             //* 如果没有获取到DCOM组件的查询接口
 83             if(FAILED(stMultiQI.hr))
 84             {
 85                 printf("获取组件的查询接口失败,错误码:0x%08X\r\n", stMultiQI.hr);
 86                 break;
 87             }
 88 
 89             //* 查询并获取组件的调用接口,获取完毕后直接释放即可
 90             IArithmeticLib *piobjArithmetic = NULL;
 91             stMultiQI.pItf->QueryInterface(&piobjArithmetic);
 92             stMultiQI.pItf->Release();
 93 
 94             //* 接收用户输入并调用远程组件获得计算结果
 95             INT blIsRunning = TRUE;
 96             while(blIsRunning)
 97             {
 98                 INT nEnterFunCode;
 99                 INT nNum1, nNum2, nResult;
100 
101                 printf("1: 加; 2: 减; 0: 退出");
102                 scanf("%d", &nEnterFunCode);
103 
104                 switch(nEnterFunCode)
105                 {
106                 case 1:
107                     printf("请输入相加的两个整型数字(空格分开):");
108                     scanf("%d%d", &nNum1, &nNum2);
109                     piobjArithmetic->add(nNum1, nNum2, &nResult);
110                     printf("[加]操作结果为:%d\r\n", nResult);
111                     break;
112 
113                 case 2:
114                     printf("请输入相减的两个整型数字(空格分开):");
115                     scanf("%d%d", &nNum1, &nNum2);
116                     piobjArithmetic->sub(nNum1, nNum2, &nResult);
117                     printf("[减]操作结果为:%d\r\n", nResult);
118                     break;
119 
120                 case 0:
121                 default:
122                     blIsRunning = FALSE;
123                     break;
124                 }
125             }
126         }while(FALSE);
127     }
128     CoUninitialize();
129 
130     return 0;
131 }

代码很简单,关键地方都添加了注释,这里不再作过多说明。重点说一下“#include”进来的两个文件“iDCOMTestSrv_i.h”和“iDCOMTestSrv_i.c”,这两个文件与代理/存根工程使用的文件是同一个,所以我们没必要再将其单独添加到这个测试客户端工程中来,只需在工程属性中把这两个文件所在的目录包含进来即可:

image

设置完成后直接编译、生成EXE文件。

接下来就是部署工作了,这块工作是最麻烦的。首先我们把刚才生成的“iDCOMTestSrvPS.dll”、“iDCOMTestSrv.exe”两个文件复制到另外一台机器的某个目录下,然后在这个目录下以管理员身份打开控制台,输入如下指令:

iDCOMTestSrv.exe/RegServer/Service

如果你的权限没问题,这一步将很顺利。此时我们可以打开“服务管理器”看到我们注册的DCOM服务已经被添加进来了:

image

服务的启动类型为“手动”,尚未启动,这个不用管它,一旦客户端成功连接,OS会为我们启动它的。

接着我们注册代理/存根文件,如果你编译的是32位的DCOM,请使用如下指令注册:

c:\windows\SysWOW64\regsvr32.exe iDCOMTestSrvPS.dll

如果是是64位DCOM则输入如下指令:

c:\windows\System32\regsvr32.exe iDCOMTestSrvPS.dll

只有如此才能正确注册32位和64位DCOM。

接着我们在DCOM客户端所在的机器注册代理/存根文件,注册指令与上同。

注册完毕,我们再把工作焦点转移到DCOM服务器。首先我们添加一个DCOM用户“user”,设定一个密码(不能是空密码),然后让其隶属于“Distributed COM Users”组,如下所示:

image

用户添加完毕,接着控制台输入如下指令:

mmc comexp.msc

如果是32位的DCOM组件,请在上述指令后再增加“ /32”,注意别漏了前面的空格。

在打开的“组件服务”窗口找到“我的电脑”节点,然后鼠标右键选择“属性”,在打开的窗口首先设置“默认属性”:

image

接着“默认协议”选择“面向连接的TCP/IP”:

image

然后是“COM安全”,为刚才添加的“user”用户分配权限:

image

访问权限”之“编辑限制”的“user”权限:

image

访问权限”之“编辑默认值”的“user”权限:

image

启动和激活权限”之“编辑默认值”的“user”权限:

 

DCOM组件的缺省配置完成,我们还需要再继续配置iDCOMTestSrv组件的权限。在下图红框所示位置,鼠标右键点选“ArithmeticLib class”:

image

右键菜单选择“属性”,按照下图所示设置相关权限:

imageimage

imageimage

至此,所有配置完成,DCOM所在的机器重启,无需登录,稍等一段时间待RPC服务被OS启动,然后在客户端所在机器打开控制台,输入如下指令:

iDCOMTestClient.exe

如果上面的操作完全无误的话,你会看到如下界面:

image

至此,我们对DCOM的开发已经完全了解,接下来就是在开源库基础上开发OPC服务器了。

github上有该例子的完整代码(链接见本文顶部开始部分),分别由VS2010和VS2015建立,VS2010是32位的DEBUG版本,编译通过,但未实际部署测试,VS2015则是64位的Release版本,已在两台机器上按照上述步骤测试通过。

posted @ 2019-05-06 19:42  Neo-T  阅读(12100)  评论(12编辑  收藏  举报