基於第三方開源庫的OPC伺服器開發指南(4)——後記:與另一個開源庫opc workshop庫相關的問題
平心而論,我們從樣例伺服器的程式碼可以看出利用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倉庫獲取:
https://github.com/Neo-T/OPCDASrvBasedOnLigh