• 基于第三方开源库的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()函数执行失败!
    ");
     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
    ", 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
    ", hr);
     79                 break;
     80             }
     81 
     82             //* 如果没有获取到DCOM组件的查询接口
     83             if(FAILED(stMultiQI.hr))
     84             {
     85                 printf("获取组件的查询接口失败,错误码:0x%08X
    ", 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
    ", nResult);
    111                     break;
    112 
    113                 case 2:
    114                     printf("请输入相减的两个整型数字(空格分开):");
    115                     scanf("%d%d", &nNum1, &nNum2);
    116                     piobjArithmetic->sub(nNum1, nNum2, &nResult);
    117                     printf("[减]操作结果为:%d
    ", 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:windowsSysWOW64 egsvr32.exe iDCOMTestSrvPS.dll

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

    c:windowsSystem32 egsvr32.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版本,已在两台机器上按照上述步骤测试通过。

  • 相关阅读:
    正则表达式贪婪与非贪婪模式
    GPGPU OpenCL 获取设备信息
    GPGPU OpenCL编程步骤与简单实例
    去掉linux 系统vi中出现^M字符的方法
    OpenMP 线程互斥锁
    CUDA使用Event进行程序计时
    Python日志库logging总结-可能是目前为止将logging库总结的最好的一篇文章
    Python-文件读写及修改
    有关Python的import...和from...import...的区别
    Python中调用其他程序的方式
  • 原文地址:https://www.cnblogs.com/neo-T/p/OPCSrvExample-1.html
Copyright © 2020-2023  润新知