平心而论,我们从样例服务器的代码可以看出,利用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文件中,不知道是什么原因让作者在获取进程工作路径时将缓冲区首地址后移了一个字节,即:
GetModuleFileName(NULL, np + 1, sizeof(np) - 8);
至今我没参透为何要“np + 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仓库获取: