zoukankan      html  css  js  c++  java
  • 基于第三方开源库的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版本,已在两台机器上按照上述步骤测试通过。

  • 相关阅读:
    可爱的中国电信 请问我们的电脑还属于我们自己吗?
    了解客户的需求,写出的代码或许才是最优秀的............
    DELPHI DATASNAP 入门操作(3)简单的主从表的简单更新【含简单事务处理】
    用数组公式获取字符在字符串中最后出现的位置
    在ehlib的DBGridEh控件中使用过滤功能(可以不用 MemTableEh 控件 适用ehlib 5.2 ehlib 5.3)
    格式化json返回的时间
    ExtJs中使用Ajax赋值给全局变量异常解决方案
    java compiler level does not match the version of the installed java project facet (转)
    收集的资料(六)ASP.NET编程中的十大技巧
    收集的资料共享出来(五)Asp.Net 权限解决办法
  • 原文地址:https://www.cnblogs.com/neo-T/p/OPCSrvExample-1.html
Copyright © 2011-2022 走看看