本篇将讲解如何编写一个OPC客户端程序测试我们在前文《基于第三方开源库的OPC服务器开发指南(2)——LightOPC的编译及部署》一篇建立的服务器。本指南的目的是熟悉OPC服务器的开发流程,所以客户端部分我就不做过多描述,只是简单讲解几个关键技术细节及其实现函数,完整工程源码请从如下地址获取:

https://github.com/Neo-T/OPCDASrvBasedOnLightOPC

OPC客户端的编写流程与涉及的技术细节跟本指南第一篇博文给出的DCOM客户端本质上没有什么不同,同样是模拟登录远程机器、获取操作接口然后调用就可以了。相较于普通的DCOM客户端,OPC客户端还需要枚举并读取远程机器上已经注册的OPC服务器的CLSID,我们需要根据这个CLSID来指定要使用远程机器上哪一个OPC服务器提供的远程操作接口。首先是如何枚举远程机器上所有已注册OPC服务器以及如何读取指定服务器的CLSID,有两种方法实现这个操作:

1、访问远程机器上的注册表,直接读取指定OPC服务器的CLSID;

2、使用OPC组织提供的OPCEnum服务,枚举所有已注册OPC服务并读取指定服务器的CLSID;

第一种方法不需要下载OPC组织提供的——目前看个人开发者已经无法通过OPC基金会网站免费获得的——“opc core components”支持包,只需要目标机器开通“RemoteRegistry”服务,同时我们拥有访问注册表的权限即可。可以说这种方式是最对我胃口的,不受别人限制。该方法的实现函数如下:

  1. //* 获取指定名称的OPC服务器的CLSID
  2. static INT __GetRemoteOPCSrvCLSIDByRegistry(CHAR *pszIPAddr, CHAR *pszUserName, CHAR *pszPassword, CHAR *pszOPCSrvProgID, CHAR *pszOPCSrvCLSID)
  3. {
  4. INT nRtnVal = 0;
  5.  
  6. //* 登录远程计算机
  7. HANDLE hToken;
  8. if (!LogonUser(pszUserName, pszIPAddr, pszPassword, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_DEFAULT, &hToken))
  9. {
  10. printf("无法连接IP地址为%s的计算机,错误码:%d", pszIPAddr, GetLastError());
  11. return -1;
  12. }
  13.  
  14. //* 模拟当前登录的用户
  15. ImpersonateLoggedOnUser(hToken);
  16. {
  17. do {
  18. CHAR szKey[MAX_PATH + 1];
  19. DWORD dwLen = MAX_PATH;
  20. DWORD dwIdx = 0;
  21. CHAR szCLSID[100];
  22. LONG lSize;
  23. HKEY hKey = HKEY_CLASSES_ROOT;
  24. DWORD dwRtnVal = RegConnectRegistry(pszIPAddr, HKEY_CLASSES_ROOT, &hKey);
  25. if (dwRtnVal != ERROR_SUCCESS)
  26. {
  27. printf("无法连接IP地址为%s的计算机,错误码:%d", pszIPAddr, dwRtnVal);
  28. nRtnVal = -2;
  29. break;
  30. }
  31.  
  32. printf("成功连接IP地址为%s的计算机,开始枚举该计算机系统上的注册表...\r\n", pszIPAddr);
  33.  
  34. //* 读取指定键值
  35. if (RegEnumKey(hKey, dwIdx, szKey, dwLen) == ERROR_SUCCESS)
  36. {
  37. HKEY hSubKey;
  38.  
  39. //* 打开指定名称的OPC服务器所在的键,在这里就是"OPC.LightOPC-exe"
  40. sprintf(szKey, pszOPCSrvProgID);
  41.  
  42. //* 打开指定键值并取值
  43. if (RegOpenKey(hKey, szKey, &hSubKey) == ERROR_SUCCESS)
  44. {
  45. memset(szCLSID, 0, sizeof(szCLSID));
  46. lSize = sizeof(szCLSID) - 1;
  47. if (RegQueryValue(hSubKey, "CLSID", szCLSID, &lSize) == ERROR_SUCCESS)
  48. {
  49. if (RegQueryValue(hSubKey, "OPC", NULL, NULL) == ERROR_SUCCESS)
  50. {
  51. sprintf(pszOPCSrvCLSID, "%s", szCLSID);
  52. printf("『%s』服务已找到:%s\r\n", pszOPCSrvProgID, pszOPCSrvCLSID);
  53. }
  54. else
  55. {
  56. printf("查询OPC键失败,错误码:%d\r\n", GetLastError());
  57. nRtnVal = -6;
  58. }
  59. }
  60. else
  61. {
  62. printf("查询CLSID键失败,错误码:%d\r\n", GetLastError());
  63. nRtnVal = -5;
  64. }
  65.  
  66. RegCloseKey(hSubKey);
  67. }
  68. else
  69. {
  70. printf("RegOpenKey()函数执行失败,错误码:%d\r\n", GetLastError());
  71. nRtnVal = -4;
  72. }
  73. }
  74. else
  75. {
  76. printf("RegEnumKey()函数执行失败,错误码:%d\r\n", GetLastError());
  77. nRtnVal = -3;
  78. }
  79.  
  80. } while (FALSE);
  81. }
  82. RevertToSelf(); //* 结束模拟
  83.  
  84. return nRtnVal;
  85. }

这个函数对注册表的操作没什么可说的,使用的是标准API,重点是如何获取远程注册表的访问权限。在这里我们依然使用了模拟用户登录技术,利用远程机器为我们分配的某个具有注册表访问权限的用户,通过调用LogonUser()函数获取该用户成功登录后的访问令牌,通过这个令牌获取对注册表的访问权限,这才是这个函数的得以正常执行的关键。我们可以在main()函数中输入如下代码测试一下这个函数:

  1. int main(int argc, CHAR* argv[])
  2. {
  3. CHAR szCLSID[100];
  4.  
  5. __GetRemoteOPCSrvCLSIDByRegistry(argv[1], argv[2], argv[3], argv[4], szCLSID);
  6.  
  7. return 0;
  8. }

打开控制台输入测试指令,顺利的话我们会如愿得到LightOPC样例服务器的CLSID:

控制台输入的参数依次为:远程机器的IP地址、登录账户、密码以及样例服务器的ProgID。

需要注意的一点是,如果你的程序不能正常访问远程机器,请按顺序确定以下内容无误:

1、确保lanmanserver,也就是名称为“Server”的服务已经启动,如果你的远程机器是“Server 2008 R2”,且在服务管理器中没找到“Server”服务,请在我提供的源码工程“opc_core_components”目录下找到“lanmanServer.reg”文件,直接导入你的原机器即可解决“Server”服务不存在的问题;
2、确保Workstation服务启动;
3、确保RemoteRegistry服务启动;
4、确保Remote Procedure Call (RPC)服务启动;
5、确保TCP/IP NetBIOS Helper服务启动;
6、确保网卡属性中Microsoft网络的文件和打印机共享服务已勾选;

使用第二种方法利用OPCEnum服务获取CLSID的实现函数如下:

  1. static INT __GetRemoteOPCSrvCLSIDByOPCEnum(CHAR *pszIPAddr, CHAR *pszUserName, CHAR *pszPassword, CHAR *pszOPCSrvProgID, CHAR *pszOPCSrvCLSID)
  2. {
  3. HRESULT hr;
  4. INT nRtnVal = 0;
  5.  
  6. hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
  7. if (!SUCCEEDED(hr))
  8. {
  9. printf("CoInitializeEx()初始化失败:0x%08X\r\n", hr);
  10. return -1;
  11. }
  12.  
  13. do {
  14. COSERVERINFO stCoServerInfo;
  15. COAUTHINFO stCoAuthInfo;
  16. COAUTHIDENTITY stCoAuthID;
  17. INT nSize = strlen(pszIPAddr) * sizeof(WCHAR);
  18. memset(&stCoServerInfo, 0, sizeof(stCoServerInfo));
  19. stCoServerInfo.pwszName = (WCHAR *)CoTaskMemAlloc(nSize * sizeof(WCHAR));
  20. if (!stCoServerInfo.pwszName)
  21. {
  22. printf("CoTaskMemAlloc()函数执行失败!\r\n");
  23. nRtnVal = -2;
  24. break;
  25. }
  26.  
  27. ZeroMemory(&stCoAuthID, sizeof(COAUTHIDENTITY));
  28. stCoAuthID.User = reinterpret_cast<USHORT *>(pszUserName);
  29. stCoAuthID.UserLength = strlen(pszUserName);
  30. stCoAuthID.Domain = reinterpret_cast<USHORT *>("");
  31. stCoAuthID.DomainLength = 0;
  32. stCoAuthID.Password = reinterpret_cast<USHORT *>(pszPassword);
  33. stCoAuthID.PasswordLength = strlen(pszPassword);
  34. stCoAuthID.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
  35.  
  36. ZeroMemory(&stCoAuthInfo, sizeof(COAUTHINFO));
  37. stCoAuthInfo.dwAuthnSvc = RPC_C_AUTHN_WINNT;
  38. stCoAuthInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
  39. stCoAuthInfo.pwszServerPrincName = NULL;
  40. stCoAuthInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CONNECT;
  41. stCoAuthInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE; //* 必须是模拟登陆
  42. stCoAuthInfo.pAuthIdentityData = &stCoAuthID;
  43. stCoAuthInfo.dwCapabilities = EOAC_NONE;
  44.  
  45. mbstowcs(stCoServerInfo.pwszName, pszIPAddr, nSize);
  46. stCoServerInfo.pAuthInfo = &stCoAuthInfo;
  47. stCoServerInfo.dwReserved1 = 0;
  48. stCoServerInfo.dwReserved2 = 0;
  49.  
  50. MULTI_QI stMultiQI;
  51. ZeroMemory(&stMultiQI, sizeof(stMultiQI));
  52. stMultiQI.pIID = &IID_IOPCServerList; //* 参见opccomn_i.c
  53. stMultiQI.pItf = NULL;
  54.  
  55. //* 初始化安全结构,模拟登录远程机器
  56. hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);
  57. if (!(SUCCEEDED(hr) || RPC_E_TOO_LATE == hr))
  58. {
  59. printf("CoInitializeSecurity()函数执行失败,错误码:0x%08X\r\n", hr);
  60. nRtnVal = -3;
  61. break;
  62. }
  63.  
  64. hr = CoCreateInstanceEx(CLSID_OpcServerList,
  65. NULL,
  66. CLSCTX_REMOTE_SERVER, //* 显式的指定要连接远程机器
  67. &stCoServerInfo,
  68. sizeof(stMultiQI) / sizeof(MULTI_QI),
  69. &stMultiQI);
  70.  
  71. //* 无论成功与否,先释放刚才申请的内存
  72. CoTaskMemFree(stCoServerInfo.pwszName);
  73.  
  74. //* 如果CoCreateInstanceEx()执行失败
  75. if (FAILED(hr))
  76. {
  77. printf("CoCreateInstanceEx()函数执行失败,错误码:0x%08X %s %s\r\n", hr, pszIPAddr, pszUserName);
  78. nRtnVal = -4;
  79. break;
  80. }
  81.  
  82. //* 如果没有获取到DCOM组件的查询接
  83. if (FAILED(stMultiQI.hr))
  84. {
  85. printf("获取组件的查询接口失败,错误码:0x%08X\r\n", stMultiQI.hr);
  86. nRtnVal = -5;
  87. break;
  88. }
  89.  
  90. //* 读取所有已注册的OPC服务器
  91. CComPtr<IOPCServerList> pobjOPCSrvList = (IOPCServerList *)stMultiQI.pItf;
  92. IEnumGUID *pobjEnumGUID = NULL;
  93. CLSID stCLSID;
  94. DWORD dwCeltFetchedNum;
  95. LPOLESTR wszProgID, wszUserType;
  96. CLSID stCatID = CATID_OPCDAServer20;
  97. hr = pobjOPCSrvList->EnumClassesOfCategories(1, &stCatID, 1, &stCatID, &pobjEnumGUID);
  98. if (FAILED(hr))
  99. {
  100. printf("EnumClassesOfCategories()函数执行失败,错误码:0x%08X\r\n", hr);
  101. nRtnVal = -6;
  102. break;
  103. }
  104.  
  105. //* 开始枚举服务器并获取指定ProgID的CLSID
  106. while (SUCCEEDED(pobjEnumGUID->Next(1, &stCLSID, &dwCeltFetchedNum)))
  107. {
  108. if (!dwCeltFetchedNum)
  109. break;
  110. hr = pobjOPCSrvList->GetClassDetails(stCLSID, &wszProgID, &wszUserType);
  111. if (FAILED(hr))
  112. {
  113. printf("GetClassDetails()函数执行失败,错误码:0x%08X\r\n", hr);
  114. nRtnVal = -7;
  115. break;
  116. }
  117.  
  118. CHAR szProgID[100];
  119. CString cstrProgID = wszProgID;
  120. sprintf(szProgID, "%s", cstrProgID);
  121.  
  122. if(!strcmp(pszOPCSrvProgID, szProgID))
  123. {
  124. BSTR wszCLSID;
  125. StringFromCLSID(stCLSID, &wszCLSID);
  126. CString cstrCLSID = wszCLSID;
  127.  
  128. sprintf(pszOPCSrvCLSID, "%s", cstrCLSID);
  129. printf("『%s』服务已找到:%s\r\n", pszOPCSrvProgID, pszOPCSrvCLSID);
  130.  
  131. //* 释放占用的内存
  132. CoTaskMemFree(wszProgID);
  133. CoTaskMemFree(wszUserType);
  134.  
  135. break;
  136. }
  137.  
  138. //* 释放占用的内存
  139. CoTaskMemFree(wszProgID);
  140. CoTaskMemFree(wszUserType);
  141. }
  142. } while (FALSE);
  143.  
  144. CoUninitialize();
  145.  
  146. return nRtnVal;
  147. }

这个函数处理流程与DCOM客户端基本相同,不多说了,重点是如何使用这个函数?首先我们必须下载“opc core components”支持包,64位的机器下载x64版本,32位的机器下载x86版本,可问题是在哪里下载呢?前面我不止一次说过,OPC基金会关闭了普通用户的下载通道,我们在基金会网站是找不到下载地址的。像CSDN、pudn之类的同样铜臭味十足的GoPi网站提供下载,可是要积分啊,我记得CSDN上有个64位版本支持包的下载链接竟然丧心病狂的要44积分,太WuChi了。还是要感谢github,感谢无私奉献的大神们,请去这个地址下载两个版本的支持包,顺便给该资源的主人点个星:

https://github.com/jmbeach/chocolatey-OpcClassicCoreComponents/tree/master/tools

下载下来后,远程机器和本地都要安装,远程机器安装完就不用管它了。本地安装完毕后,需要在安装路径下找到OPCEnum.h和OpcEnum_i.c两个文件将其添加到客户端工程中,同时把OPCEnum.h文件#include进来,否则会编译失败。然后修改main()函数:

  1. int main(int argc, CHAR* argv[])
  2. {
  3. CHAR szCLSID[100];
  4.  
  5. __GetRemoteOPCSrvCLSIDByOPCEnum(argv[1], argv[2], argv[3], argv[4], szCLSID);
  6. //__GetRemoteOPCSrvCLSIDByRegistry(argv[1], argv[2], argv[3], argv[4], szCLSID);
  7.  
  8. return 0;
  9. }

控制台输入指令测试该函数,结果如下:

抛开与OPC业务有关的逻辑不说,以上两个函数针对远程OPC服务器的访问提供了一种更安全的访问方法,而不像相当一部分公开资料所描述的那样把服务器权限降低到任何人都可以访问的令人发指的地步。

OPC客户端的主业务逻辑可以参见源码的main()函数,以此按图索骥理解整个处理流程:

  1. int main(int argc, CHAR* argv[])
  2. {
  3. CHAR szCLSID[100];
  4.  
  5. if (argc != 5)
  6. {
  7. printf("Usage:%s opcserver_ip username password OpcProgID\r\n", argv[0]);
  8. return -1;
  9. }
  10.  
  11. //* 设定该程序捕获控制台CTRL+C输入,以使程序能够正常退出
  12. SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleHandler, TRUE);
  13.  
  14. //__GetRemoteOPCSrvCLSIDByOPCEnum(argv[1], argv[2], argv[3], argv[4], szCLSID);
  15. if (__GetRemoteOPCSrvCLSIDByRegistry(argv[1], argv[2], argv[3], argv[4], szCLSID))
  16. {
  17. printf("__GetRemoteOPCSrvCLSIDByRegistry()函数执行失败,进程退出!\r\n");
  18. return -2;
  19. }
  20.  
  21. //* 连接成功则进行后续操作
  22. if (!__ConnOPCServer(argv[1], argv[2], argv[3], szCLSID))
  23. {
  24. do {
  25. if (__AddDefaultGroup())
  26. {
  27. printf("__AddDefaultGroup()函数执行失败,进程退出!\r\n");
  28. break;
  29. }
  30.  
  31. //* 手动添加要操作的变量
  32. //* =======================================================
  33. ST_OPC_ITEM staItem[2];
  34. sprintf(staItem[0].szItemName, "lulu");
  35. staItem[0].vtDataType = VT_I2;
  36. if (__AddItemToLocalMgmtIf(staItem[0].szItemName, staItem[0].vtDataType, &staItem[0].ohItemSrv)) //* exe_samp工程之sample.cpp文件第573行“lulu”变量为VT_I2类型
  37. {
  38. printf("__AddItemToLocalMgmtIf()函数添加变量[lulu]执行失败,进程退出!\r\n");
  39. break;
  40. }
  41.  
  42. sprintf(staItem[1].szItemName, "zuzu");
  43. staItem[1].vtDataType = VT_R8;
  44. if (__AddItemToLocalMgmtIf(staItem[1].szItemName, staItem[1].vtDataType, &staItem[1].ohItemSrv)) //* exe_samp工程之sample.cpp文件第564行“zuzu”变量为VT_R8类型
  45. {
  46. printf("__AddItemToLocalMgmtIf()函数添加变量[zuzu]执行失败,进程退出!\r\n");
  47. break;
  48. }
  49. //* =======================================================
  50.  
  51. //* 隐藏控制台光标
  52. ShowConsoleCursor(FALSE);
  53.  
  54. //* 读取控制台当前光标位置,以便循环读取时固定输出位置,而不是整屏滚动输出
  55. SHORT x, y;
  56. GetConsoleCursorPosition(&x, &y);
  57.  
  58. time_t tPrevWriteTime = time(NULL);
  59. ULONG ulWriteVal = 2009;
  60.  
  61. blIsRunning = TRUE;
  62. while (blIsRunning)
  63. {
  64. //* 每次读取均设定在控制台同一输出位置
  65. SetConsoleCursorPosition(x, y);
  66.  
  67. if (__ReadItem(staItem, sizeof(staItem) / sizeof(ST_OPC_ITEM)))
  68. break;
  69.  
  70. Sleep(100);
  71.  
  72. if (time(NULL) - tPrevWriteTime > 1)
  73. {
  74. if (__WriteItem(&staItem[0], ulWriteVal++))
  75. break;
  76.  
  77. tPrevWriteTime = time(NULL);
  78. }
  79. }
  80.  
  81. //* 恢复控制台光标
  82. ShowConsoleCursor(TRUE);
  83.  
  84. } while (FALSE);
  85.  
  86. //* 断开连接
  87. __DisconnectOPCServer();
  88. }
  89. else
  90. {
  91. printf("__ConnOPCServer()函数执行失败,进程退出!\r\n");
  92. return -3;
  93. }
  94.  
  95. return 0;
  96. }

整个流程很简单:获取CLSID,利用这个CLSID连接服务器,连接成功后调用__AddDefaultGroup()函数添加一个缺省组,添加成功则就获得了对OPC服务器指定变量进行管理、读、写等操作的具体操作接口,接着通过__AddItemToLocalMgmtIf()函数把我们要操作的样例服务器提供的两个变量添加到OPC的本地变量管理接口,然后就可以进入主循环进行读取操作了。主循环以100毫秒间隔读取“zuzu”变量的值,以1秒间隔写“lulu”变量的值。其实OPC样例服务器提供了多个变量,具体列表参见“sample.cpp”文件的第278-282行:

  1. …… ……
  2.  
  3. /* Our data tags: */
  4. /* zero is resierved for an invalid RealTag */
  5. #define TI_zuzu (1)
  6. #define TI_lulu (2)
  7. #define TI_bandwidth (3)
  8. #define TI_array (4)
  9. #define TI_enum (5)
  10. #define TI_quiet (6)
  11. #define TI_quality (7)
  12. #define TI_string (8)
  13. #define TI_MAX (8)
  14.  
  15. static loTagId ti[TI_MAX + 1]; /* their IDs */
  16. static const char *tn[TI_MAX + 1] = /* their names */
  17. { "--not--used--", "zuzu", "lulu", "bandwidth", "array", "enum-localizable",
  18. "quiet", "quality", "string" };
  19. static loTagValue tv[TI_MAX + 1]; /* their values */
  20.  
  21. …… ……

这些变量的数据类型参见driver_init()函数:

  1. int driver_init(int lflags)
  2. {
  3. …… ……
  4.  
  5. /* We needn't to VariantClear() for simple datatypes like numbers */
  6. V_R8(&var) = 214.1; /* initial value. Will be used to check types conersions */
  7. V_VT(&var) = VT_R8; //* 变量“zuzu”的数据类型
  8. ecode = loAddRealTag_a(my_service, /* actual service context */
  9. &ti[TI_zuzu], /* returned TagId */
  10. (loRealTag)TI_zuzu, /* != 0 driver's key */
  11. tn[TI_zuzu], /* tag name */
  12. 0, /* loTF_ Flags */
  13. OPC_READABLE | OPC_WRITEABLE, &var, 12., 1200.);
  14. UL_TRACE((LOGID, "%!e loAddRealTag_a(zuzu) = %u ", ecode, ti[TI_zuzu]));
  15.  
  16. V_I2(&var) = 1000;
  17. V_VT(&var) = VT_I2; //* 变量“lulu”的数据类型
  18. ecode = loAddRealTag(my_service, /* actual service context */
  19. &ti[TI_lulu], /* returned TagId */
  20. (loRealTag) TI_lulu, /* != 0 driver's key */
  21. tn[TI_lulu], /* tag name */
  22. 0, /* loTF_ Flags */
  23. OPC_READABLE | OPC_WRITEABLE, &var, 0, 0);
  24. UL_TRACE((LOGID, "%!e loAddRealTag(lulu) = %u ", ecode, ti[TI_lulu]));
  25.  
  26. …… ……
  27. }

进行客户端测试之前我们还需要对“sample.cpp”文件的代码做些调整,否则无法测试写操作。共有两个地方需要调整,其一,“simulate()”函数找到如下几句:

  1. void simulate(unsigned pause)
  2. {
  3. …… ……
  4.  
  5. double zuzu =
  6. (V_R8(&tv[TI_zuzu].tvValue) += ./.); /* main simulation */
  7. V_VT(&tv[TI_zuzu].tvValue) = VT_R8;
  8. tv[TI_zuzu].tvState.tsTime = ft;
  9.  
  10. V_I2(&tv[TI_lulu].tvValue) = (short)zuzu;
  11. V_VT(&tv[TI_lulu].tvValue) = VT_I2;
  12. tv[TI_lulu].tvState.tsTime = ft;
  13.  
  14. V_I2(&tv[TI_enum].tvValue) = (short)((ft.dwLowDateTime >> ) % );
  15. V_VT(&tv[TI_enum].tvValue) = VT_I2;
  16. tv[TI_enum].tvState.tsTime = ft;
  17.  
  18. …… ……
  19. }

红色语句全部注释掉,不让OPC服务器更新“lulu”变量。然后是“WriteTags()”函数,增加如下几句:

  1. int WriteTags(const loCaller *ca,
  2. unsigned count, loTagPair taglist[],
  3. VARIANT values[], HRESULT error[], HRESULT *master, LCID lcid)
  4. {
  5. …… ……
  6.  
  7. case TI_lulu:
  8. hr = VariantChangeType(&tv[TI_lulu].tvValue, &values[ii], 0, VT_I2);
  9. if (S_OK == hr)
  10. {
  11. lo_statperiod(V_I2(&tv[TI_lulu].tvValue)); /* VERY OPTIONAL, really */
  12.  
  13. FILETIME ft;
    GetSystemTimeAsFileTime(&ft); /* awoke */
    tv[TI_lulu].tvState.tsTime = ft;
    }
  14.  
  15. …… ……
  16. }

红色部分为要增加的语句,这几条语句的作用是当客户端写入新的“lulu”变量值时同步更新该变量的时间戳。重新编译修改后的样例服务器,并覆盖远程机器上的旧文件,然后我们就可以启动客户端看看效果了:

最后,需要注意的一点是,OPC服务器所在的机器必须已经登录,否则OPC客户端是无法连接的,会报0x8000401A错误。这一点与普通的DCOM不同,普通的DCOM客户端无须DCOM组件所在的服务器登录即可正常使用。

至此,OPC系统的完整开发流程梳理完毕。本指南的最后一篇——《基于第三方开源库的OPC服务器开发指南(4)——后记:与另一个开源库opc workshop库相关的问题》将推荐另一个更加简单的开源库opc workshop。

基于第三方开源库的OPC服务器开发指南(3)——OPC客户端的更多相关文章

  1. 基于第三方开源库的OPC服务器开发指南(2)——LightOPC的编译及部署

    前文已经说过,OPC基于微软的DCOM技术,所以开发OPC服务器我们要做的事情就是开发一个基于DCOM的EXE文件.一个代理/存根文件,然后就是写一个OPC客户端测试一下我们的服务器了.对于第一项工作 ...

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

    事儿太多,好多事情并不以我的意志为转移,原想沉下心好好研究.学习图像识别,继续丰富我的机器视觉库,并继续<机器视觉及图像处理系列>博文的更新,但计划没有变化快,好多项目要完成,只好耽搁下来 ...

  3. 基于第三方开源库的OPC服务器开发指南(4)——后记:与另一个开源库opc workshop库相关的问题

    平心而论,我们从样例服务器的代码可以看出,利用LightOPC库开发OPC服务器还是比较啰嗦的,网上有人提出opc workshop库就简单很多,我千辛万苦终于找到一个05年版本的workshop库源 ...

  4. 开源框架】Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发

    [原][开源框架]Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发,欢迎各位... 时间 2015-01-05 10:08:18 我是程序猿,我为自己代言 原文  http: ...

  5. 【开源框架】Android之史上最全最简单最有用的第三方开源库收集整理,有助于快速开发,欢迎各位网友补充完善

    链接地址:http://www.tuicool.com/articles/jyA3MrU 时间 2015-01-05 10:08:18  我是程序猿,我为自己代言 原文  http://blog.cs ...

  6. Android 第三方开源库收集整理(转)

    原文地址:http://blog.csdn.net/caoyouxing/article/details/42418591 Android开源库 自己一直很喜欢Android开发,就如博客签名一样,  ...

  7. 45.Android 第三方开源库收集整理(转)

    原文地址:http://blog.csdn.net/caoyouxing/article/details/42418591 Android开源库 自己一直很喜欢Android开发,就如博客签名一样,  ...

  8. Android之史上最全最简单最有用的第三方开源库收集整理

    Android开源库 自己一直很喜欢Android开发,就如博客签名一样, 我是程序猿,我为自己代言 . 在摸索过程中,GitHub上搜集了很多很棒的Android第三方库,推荐给在苦苦寻找的开发者, ...

  9. Android Studio 简介及导入 jar 包和第三方开源库方[转]

    原文:http://blog.sina.com.cn/s/blog_693301190102v6au.html Android Studio 简介 几天前的晚上突然又想使用 Android Studi ...

随机推荐

  1. 注释类型 XmlType

    @Retention(value=RUNTIME) @Target(value=TYPE) public @interface XmlType 将类或枚举类型映射到 XML 模式类型. 用法 @Xml ...

  2. centos7.2下快速安装zabbix4.0

    本笔记是基于CentOS 7.2下最小化安装的操作系统搭建的Zabbix4.0环境,主要用于做一些企业路由器和交换机等设备的运行状态监控. 1.安装epel源 yum -y install epel- ...

  3. 非常实用的css

    .clearfix:after {content: "";display: block;visibility: hidden;height: 0;clear: both;} .cl ...

  4. PHP中的闭包小谈

    接触PHP一段时间以来,我一直以为这是一种基于函数式编程的语言是没有闭包这种东西的,但事实上却颠覆了我的想法,PHP竟然有闭包,下面我们一起来接触一下PHP的所谓的闭包. 根据PHP官网的定义来看,闭 ...

  5. python--reflect

    一.反射 python 中用字符串的方式操作对象的相关属性,python 中一切皆对象,都可以使用反射 用eval 有安全隐患,用 反射就很安全 1.反射对象中的属性和方法 class A: a_cl ...

  6. Flyway - Version control for your database

    Flyway 是什么? Flyway是个数据库版本管理工具.在开发过程中,数据库难免发生变更,例如数据变更,表结构变更.新建表或者视图等等. 在项目进行时无法保证一旦开发环境中的数据库内容变化候会去测 ...

  7. Vue报错type check failed for prop

    在报错的'value'属性前面加:或者去掉:即可解决问题.

  8. 用IP地址访问共享文件

    一.用WIN+R 打开运行,如图输入地址: 二.输入用户名和密码就打开共享文件夹了

  9. volatile的使用及其原理

    1. volatile的作用 相比Sychronized(重量级锁,对系统性能影响较大),volatile提供了另一种解决 可见性和有序性 ???问题的方案.对于原子性,需要强调一点,也是大家容易误解 ...

  10. HashMap是不是有序的?

    不是有序的. 有没有有顺序的Map实现类? 有TreeMap和LinkedHashMap. TreeMap和LinkedHashMap是如何保证它的顺序的? LinkedHashMap 是根据元素增加 ...