基于第三方开源库的OPC服务器开发指南(1)——OPC与DCOM
事儿太多,好多事情并不以我的意志为转移,原想沉下心好好研究、学习图像识别,继续丰富我的机器视觉库,并继续《机器视觉及图像处理系列》博文的更新,但计划没有变化快,好多项目要完成,只好耽搁下来(这一耽搁又是多半年啊,惭愧,)。最近某个项目需要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”:
然后“确定”->”下一步”,选择“服务(EXE)”,最后点选“完成”。
接下来,添加COM对象。工程名称节点鼠标右键,点选“添加”->”类”:
在弹出窗口选择“ATL简单对象”:
点击“添加”后,按下图输入相关信息,然后直接点击“完成”:
在“类视图”窗口,鼠标右键点选“IArithmeticLib”,在弹出的右键菜单中选择“添加”->”添加方法”:
弹出窗口中按下图所示添加add()方法:
最后点击“完成”按钮,add()方法添加完毕。接着按照如上步骤再添加一个sub()方法:
我们的目的是熟悉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%'
增加位置如下:
这一句解决客户端连接DCOM组件服务器时报0x80080005错误的问题。
接着编译,如果不出意外,VS2015编译应该能够成功,VS2010则不一定,因为VS2010相对VS2015多做了一步:
如果你有系统管理员权限,那么编译完成后这个注册是能够成功的,如果不是则失败。由于我们是把DCOM部署到其它机器远程执行,不在本机注册,所以这里可以删掉。不过,这一步倒是告诉我们,DCOM需要注册才能使用。
接下来我们需要生成代理/存根文件,以用于远程访问DCOM组件。相对VC6,新版本的VS帮我们自动建立了代理/存根文件工程,工程需要的相关文件,是VS通过对应的IDL文件编译生成的,我们在刚才编译DCOM时VS已经帮我们生成了相关文件并添加到对应工程下:
在编译生成代理/存根文件之前,我们需要更改一下该工程的链接器设置,见下图:
把“注册输出”一项改为“否”,这样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”,这两个文件与代理/存根工程使用的文件是同一个,所以我们没必要再将其单独添加到这个测试客户端工程中来,只需在工程属性中把这两个文件所在的目录包含进来即可:
设置完成后直接编译、生成EXE文件。
接下来就是部署工作了,这块工作是最麻烦的。首先我们把刚才生成的“iDCOMTestSrvPS.dll”、“iDCOMTestSrv.exe”两个文件复制到另外一台机器的某个目录下,然后在这个目录下以管理员身份打开控制台,输入如下指令:
iDCOMTestSrv.exe/RegServer/Service
如果你的权限没问题,这一步将很顺利。此时我们可以打开“服务管理器”看到我们注册的DCOM服务已经被添加进来了:
服务的启动类型为“手动”,尚未启动,这个不用管它,一旦客户端成功连接,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”组,如下所示:
用户添加完毕,接着控制台输入如下指令:
mmc comexp.msc
如果是32位的DCOM组件,请在上述指令后再增加“ /32”,注意别漏了前面的空格。
在打开的“组件服务”窗口找到“我的电脑”节点,然后鼠标右键选择“属性”,在打开的窗口首先设置“默认属性”:
接着“默认协议”选择“面向连接的TCP/IP”:
然后是“COM安全”,为刚才添加的“user”用户分配权限:
“访问权限”之“编辑限制”的“user”权限:
“访问权限”之“编辑默认值”的“user”权限:
“启动和激活权限”之“编辑默认值”的“user”权限:
DCOM组件的缺省配置完成,我们还需要再继续配置iDCOMTestSrv组件的权限。在下图红框所示位置,鼠标右键点选“ArithmeticLib class”:
右键菜单选择“属性”,按照下图所示设置相关权限:
至此,所有配置完成,DCOM所在的机器重启,无需登录,稍等一段时间待RPC服务被OS启动,然后在客户端所在机器打开控制台,输入如下指令:
iDCOMTestClient.exe
如果上面的操作完全无误的话,你会看到如下界面:
至此,我们对DCOM的开发已经完全了解,接下来就是在开源库基础上开发OPC服务器了。
github上有该例子的完整代码(链接见本文顶部开始部分),分别由VS2010和VS2015建立,VS2010是32位的DEBUG版本,编译通过,但未实际部署测试,VS2015则是64位的Release版本,已在两台机器上按照上述步骤测试通过。