平心而論,我們從樣例服務器的代碼可以看出,利用LightOPC庫開發OPC服務器還是比較啰嗦的,網上有人提出opc workshop庫就簡單很多,我千辛萬苦終於找到一個05年版本的workshop庫源碼,忘了出處是在哪里了,依稀記得是Codeforge網站。相較於LightOPC,用這個庫開發OPC服務器確實簡單了很多,其對核心業務邏輯做了高度封裝,使得服務器的開發流程非常清晰,這一點值得贊揚。但遺憾的是,完美的事情在這個世界上根本就不存在,經過實測,我手頭上擁有的版本存在三個嚴重問題:
1、利用該庫開發的OPC服務器無法由OPC客戶端遠程啟動;
2、通過標准接口ValidateItems()無法獲取指定變量的數據類型;
3、提供的樣例服務器主處理邏輯存在重復注冊的BUG,沒有把服務器注冊和處理邏輯分開;
好在已經有了LightOPC這碗酒墊底,這幾個問題都不是問題。我的方法簡單粗暴——直接上手改源碼。對於第一個問題,通過分析源碼發現,導致該問題的原因是注冊函數在獲取模塊文件工作路徑時,接收緩沖區的首地址錯誤導致的:
1 int COPCServerObject::RegisterServer() 2 { 3 char np[FILENAME_MAX + 32]; 4 printf("Registering"); 5 GetModuleFileName(NULL, np + 1, sizeof(np) - 8); 6 7 return ServerRegister(&CLSID_OPCServerEXE, 8 OPCServerProgID, 9 "OPCServer (c) Alexey Obukhov", np, 0); 10 }
出問題的這個注冊函數在OPCServerObject.cpp文件中,不知道是什么原因讓作者在獲取進程工作路徑時將緩沖區首地址后移了一個字節,即:
1 GetModuleFileName(NULL, np + 1, sizeof(np) - 8);
至今我沒參透為何要“np + 1”。事實證明,把后面加的那個“1”去掉后,服后務器不僅可以遠程啟動了且工作也完全正常。看來這件事需要作者本人親自解釋這到底是為什么了,咱們只要能用就行了。
第2個問題更加匪夷所思,作者提供的“ValidateItems()”接口函數竟然缺少了關鍵的對變量類型的賦值語句:
1 STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount, 2 /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray, 3 /*[in]*/ BOOL bBlobUpdate, 4 /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults, 5 /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors ) 6 { 7 DWORD i; 8 HRESULT res = S_OK; 9 OPC_GROUP_CHECK_DELETED(); 10 11 VALIDATE_ARGUMENT(pItemArray); 12 VALIDATE_ARGUMENT(ppValidationResults); 13 VALIDATE_ARGUMENT(ppErrors); 14 15 *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount ); 16 *ppErrors = allocate_buffer<HRESULT> ( dwCount ); 17 18 // TODO 19 for( i=0;i<dwCount; ++i) { 20 OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ]; 21 CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer ); 22 if( browseIT == g_BrowseItems.end() ) { 23 (*ppErrors)[i] = OPC_E_UNKNOWNITEMID; 24 res = S_FALSE; 25 } 26 } 27 // TODO 28 29 return res; 30 }
上述函數在IOPCItemMgtImpl.h源文件中可以找到。其中入口參數“ppValidationResults”即被用於獲取指定變量的相關信息。但奇怪的是,在這個函數里作者只是對這個變量分配了一塊內存,接下來的代碼並沒有對其賦值。如果說我到手的源碼並不完整的話,那么為何解決上述幾個問題后,OPC服務器竟然工作正常,沒有任何問題?要不說這個問題很是匪夷所思呢。既然咱們有源碼,這個事完全可以自己解決,在這個函數增加幾行代碼:
1 STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount, 2 /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray, 3 /*[in]*/ BOOL bBlobUpdate, 4 /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults, 5 /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors ) 6 { 7 DWORD i; 8 HRESULT res = S_OK; 9 OPC_GROUP_CHECK_DELETED(); 10 11 VALIDATE_ARGUMENT(pItemArray); 12 VALIDATE_ARGUMENT(ppValidationResults); 13 VALIDATE_ARGUMENT(ppErrors); 14 15 *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount ); 16 *ppErrors = allocate_buffer<HRESULT> ( dwCount ); 17 18 /// TODO 19 for( i=0;i<dwCount; ++i) { 20 OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ]; 21 CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer ); 22 if( browseIT == g_BrowseItems.end() ) { 23 (*ppErrors)[i] = OPC_E_UNKNOWNITEMID; 24 res = S_FALSE; 25 } 26 else 27 { 28 (*ppValidationResults)->vtCanonicalDataType = browseIT->type; 29 break; 30 } 31 } 32 // TODO 33 34 return res; 35 }
連花括號都算着其實就增加了4行代碼。只是對參數“ppValidationResults”的數據類型成員“vtCanonicalDataType”進行了賦值。如此一來,“ValidateItems()”接口即可滿足我們的要求了。
第3個問題就簡單多了,直接修改樣例服務器的“main()”函數把注冊和主處理邏輯分開就可以了:
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 FILE *pfFile; 4 5 AllocConsole(); 6 freopen_s(&pfFile,"conout$","w+",stdout); //打䨰開a控?制?台¬¡§ 7 8 if(argc > 2) 9 { 10 printf("Usage:%s", argv[0]); 11 printf(" %s /r", argv[0]); 12 printf(" %s /u", argv[0]); 13 printf(" : start opc server\r\n"); 14 printf("/r: regist opc server\r\n"); 15 printf("/u: unregist opc server\r\n"); 16 17 fclose(pfFile); 18 FreeConsole(); 19 20 return -1; 21 } 22 23 char str[1024] = {0}; 24 25 HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); 26 27 // define server object 28 COPCServerObject server; 29 // define data event receiver 30 dataReceiver receiver; 31 32 // set server name and clsid 33 server.setServerProgID( _T("OPC.myTestServer") ); 34 server.setServerCLSID( CLSID_OPCServerEXE ); 35 36 // set delimeter for params name 37 server.SetDelimeter( "." ); 38 39 if(argc == 2) 40 { 41 if(strstr(argv[1], "/r")) 42 { 43 // register server as COM/DCOM object 44 server.RegisterServer(); 45 46 fclose(pfFile); 47 FreeConsole(); 48 49 return 0; 50 } 51 else if(strstr(argv[1], "/u")) 52 { 53 server.UnregisterServer(); 54 55 getchar(); 56 57 fclose(pfFile); 58 FreeConsole(); 59 60 return 0; 61 } 62 } 63 64 65 // define server values tree 66 server.AddTag("Values.int1", VT_I4 ); 67 server.AddTag("Values.int2", VT_I4 ); 68 server.AddTag("Values.fltArray2", VT_ARRAY|VT_R4 ); 69 server.AddTag("Values.fltArray2.In", VT_I4, false ); 70 71 { 72 CAG_Clocker cl("Create 10000 tags",false); 73 74 for(int i=0;i<10000;++i) { 75 sprintf(str,"RandomValues.int%d",i+1); 76 server.AddTag( str ,VT_I4 ); 77 } 78 } 79 80 // setup object will be received add values change 81 server.setDataReceiver( &receiver ); 82 83 // create COM class factory and register it 84 server.StartServer(); 85 86 printf("\t waiting return\n"); 87 gets(str); // 等待用戶任意輸入,比如按個回車鍵,服務器才會繼續執行 88 89 // write initial values to OPC params 90 for( double x =0.; x< 50.;x+=.1 ) { 91 server.WriteValue( "Values.int1", FILETIME_NULL, 192, CComVariant( sin(x) ) ); 92 server.WriteValue( "Values.int2", FILETIME_NULL, 192, CComVariant( cos(x) ) ); 93 Sleep(100); 94 } 95 96 srand( (unsigned)time( NULL ) ); 97 98 for(int i=0;i<10000;++i) { 99 sprintf(str,"RandomValues.int%d",i+1); 100 server.WriteValue( str , FILETIME_NULL, 192, CComVariant( rand() ) ); 101 } 102 103 printf("\t waiting return for close server \n"); 104 gets(str); // 同樣是等待用戶在控制台的任意輸入,服務器結束服務 105 106 server.StopServer(); 107 108 109 CoUninitialize(); 110 111 fclose(pfFile); 112 FreeConsole(); 113 114 return 0; 115 } 116 117
其實解決方案就是通過控制台輸入參數來區分進程啟動后進入注冊流程還是處理流程,同時為了調試方便,並能夠讓我看到客戶端遠程啟動服務器的實際效果,我還為服務器分配了一個輸出控制台(缺省情況下OPC后台啟動是看不到交互窗口的),這樣服務器一旦被客戶端啟動,輸出控制台將在遠程機器上彈出,我們就可以看到服務器輸出的調試信息了,是不是很酷!至此三個問題解決,workshop庫的樣例服務器可以正常工作了。
最后,已經調整完且測試通過的workshop庫VS2010的源碼工程還是在我的github倉庫獲取: