平心而论, 我们从样例服务器的代码可以看出利用 LightOPC 库开发 OPC 服务器还是比较啰嗦的, 网上有人提出 opc workshop 库就简单很多, 我千辛万苦终于找到一个 05 年版本的 workshop 库源码, 忘了出处是在哪里了, 依稀记得是 Codeforge 网站. 相较于 LightOPC, 用这个库开发 OPC 服务器确实简单了很多, 其对核心业务逻辑做了高度封装, 使得服务器的开发流程非常清晰, 这一点值得赞扬. 但遗憾的是, 完美的事情在这个世界上根本就不存在, 经过实测, 我手头上拥有的版本存在三个严重问题:
1, 利用该库开发的 OPC 服务器无法由 OPC 客户端远程启动;
2, 通过标准接口 ValidateItems() 无法获取指定变量的数据类型;
3, 提供的样例服务器著处理逻辑存在重复注册的 BUG, 没有把服务器注册和处理逻辑分开;
好在已经有了 LightOPC 这碗酒垫底, 这几个问题都不是问题, 我的方法简单粗暴 -- 直接上手改源码. 对于第一个问题, 通过分析源码发现, 导致该问题的原因是注册函数在获取服务器程序文件所在的工作路径时, 接收缓冲区的首地址错误导致的:
- int COPCServerObject::RegisterServer()
- {
- char np[FILENAME_MAX + 32];
- printf("Registering");
- GetModuleFileName(NULL, np + 1, sizeof(np) - 8);
- return ServerRegister(&CLSID_OPCServerEXE,
- OPCServerProgID,
- "OPCServer (c) Alexey Obukhov", np, 0);
- }
该函数在 OPCServerObject.cpp 文件中, 不知道是什么原因让作者在获取进程工作路径时缓冲区首地址后移了一个字节, 即:
1 GetModuleFileName(NULL, np + 1, sizeof(np) - 8);
至今我没参透为何要 "np + 1". 事实证明, 把后面加的那个 "1" 去掉后, 服后务器不仅可以远程启动了且工作也完全正常. 看来这件事需要作者本人亲自解释这到底是为什么了, 咱们只要能用就行了.
第 2 个问题更加匪夷所思, 作者提供的 "ValidateItems()" 接口函数竟然缺少了关键的对变量类型的赋值语句:
- STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount,
- /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray,
- /*[in]*/ BOOL bBlobUpdate,
- /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults,
- /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors )
- {
- DWORD i;
- HRESULT res = S_OK;
- OPC_GROUP_CHECK_DELETED();
- VALIDATE_ARGUMENT(pItemArray);
- VALIDATE_ARGUMENT(ppValidationResults);
- VALIDATE_ARGUMENT(ppErrors);
- *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount );
- *ppErrors = allocate_buffer<HRESULT> ( dwCount );
- // TODO
- for( i=0;i<dwCount; ++i) {
- OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ];
- CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer );
- if( browseIT == g_BrowseItems.end() ) {
- (*ppErrors)[i] = OPC_E_UNKNOWNITEMID;
- res = S_FALSE;
- }
- }
- // TODO
- return res;
- }
上述函数在 IOPCItemMgtImpl.h 源文件中可以找到. 其中入口参数 "ppValidationResults" 即被用于获取指定变量的相关信息. 但奇怪的是, 在这个函数里作者只是对这个变量分配了一块内存, 接下来的代码并没有对其赋值. 如果说我到手的源码并不完整的话, 那么为何解决上述几个问题后, OPC 服务器竟然工作正常, 没有任何问题? 要不说这个问题很是匪夷所思呢. 既然咱们有源码, 这个事完全可以自己解决, 这个函数增加几行代码:
- STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount,
- /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray,
- /*[in]*/ BOOL bBlobUpdate,
- /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults,
- /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors )
- {
- DWORD i;
- HRESULT res = S_OK;
- OPC_GROUP_CHECK_DELETED();
- VALIDATE_ARGUMENT(pItemArray);
- VALIDATE_ARGUMENT(ppValidationResults);
- VALIDATE_ARGUMENT(ppErrors);
- *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount );
- *ppErrors = allocate_buffer<HRESULT> ( dwCount );
- /// TODO
- for( i=0;i<dwCount; ++i) {
- OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ];
- CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer );
- if( browseIT == g_BrowseItems.end() ) {
- (*ppErrors)[i] = OPC_E_UNKNOWNITEMID;
- res = S_FALSE;
- }
- else
- {
- (*ppValidationResults)->vtCanonicalDataType = browseIT->type;
- break;
- }
- }
- // TODO
- return res;
- }
连花括号都算着其实就增加了 4 行代码. 只是对参数 "ppValidationResults" 的数据类型成员 "vtCanonicalDataType" 进行了赋值. 如此一来,"ValidateItems()" 接口即可满足我们的要求了.
第 3 个问题就简单多了, 直接修海样例服务器的 "main()" 函数把注册和主处理逻辑分开就可以了:
- int _tmain(int argc, _TCHAR* argv[])
- {
- FILE *pfFile;
- AllocConsole();
- freopen_s(&pfFile,"conout$","w+",stdout); // 打 䨰 开 a 控? 制? 台 ¬¡§
- if(argc> 2)
- {
- printf("Usage:%s", argv[0]);
- printf("%s /r", argv[0]);
- printf("%s /u", argv[0]);
- printf(": start opc server\r\n");
- printf("/r: regist opc server\r\n");
- printf("/u: unregist opc server\r\n");
- fclose(pfFile);
- FreeConsole();
- return -1;
- }
- char str[1024] = {0};
- HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
- // define server object
- COPCServerObject server;
- // define data event receiver
- dataReceiver receiver;
- // set server name and clsid
- server.setServerProgID( _T("OPC.myTestServer") );
- server.setServerCLSID( CLSID_OPCServerEXE );
- // set delimeter for params name
- server.SetDelimeter( "." );
- if(argc == 2)
- {
- if(strstr(argv[1], "/r"))
- {
- // register server as COM/DCOM object
- server.RegisterServer();
- fclose(pfFile);
- FreeConsole();
- return 0;
- }
- else if(strstr(argv[1], "/u"))
- {
- server.UnregisterServer();
- getchar();
- fclose(pfFile);
- FreeConsole();
- return 0;
- }
- }
- // define server values tree
- server.AddTag("Values.int1", VT_I4 );
- server.AddTag("Values.int2", VT_I4 );
- server.AddTag("Values.fltArray2", VT_ARRAY|VT_R4 );
- server.AddTag("Values.fltArray2.In", VT_I4, false );
- {
- CAG_Clocker cl("Create 10000 tags",false);
- for(int i=0;i<10000;++i) {
- sprintf(str,"RandomValues.int%d",i+1);
- server.AddTag( str ,VT_I4 );
- }
- }
- // setup object will be received add values change
- server.setDataReceiver( &receiver );
- // create COM class factory and register it
- server.StartServer();
- printf("\t waiting return\n");
- gets(str);
- // write initial values to OPC params
- for( double x =0.; x< 50.;x+=.1 ) {
- server.WriteValue( "Values.int1", FILETIME_NULL, 192, CComVariant( sin(x) ) );
- server.WriteValue( "Values.int2", FILETIME_NULL, 192, CComVariant( cos(x) ) );
- Sleep(100);
- }
- srand( (unsigned)time( NULL ) );
- for(int i=0;i<10000;++i) {
- sprintf(str,"RandomValues.int%d",i+1);
- server.WriteValue( str , FILETIME_NULL, 192, CComVariant( rand() ) );
- }
- printf("\t waiting return for close server \n");
- gets(str);
- server.StopServer();
- CoUninitialize();
- fclose(pfFile);
- FreeConsole();
- return 0;
- }
其实解决方案就是通过控制台输入参数来区分进程启动后进入注册流程还是处理流程, 同时为了调试方便并能够让我看到客户端远程启动服务器的实际效果, 我还为服务器分配了一个输出控制台 (缺省情况下 OPC 后台启动是看不到交互窗口的), 这样服务器一旦被客户端启动, 输出控制台将在远程机器上弹出, 我们就可以看到服务器输出的调试信息了, 是不是很酷! 至此三个问题解决, workshop 库的样例服务器可以正常工作了.
最后, 已经调整完且测试通过的 workshop 库 VS2010 的源码工程还是在我的 GitHub 仓库获取:
https://github.com/Neo-T/OPCDASrvBasedOnLightOPC
来源: https://www.cnblogs.com/neo-T/p/OPCSrvExample-4.html