事兒太多,好多事情並不以我的意志為轉移,原想沉下心好好研究、學習圖像識別,繼續豐富我的機器視覺庫,並繼續《機器視覺及圖像處理系列》博文的更新,但計划沒有變化快,好多項目要完成,只好耽擱下來(這一耽擱又是多半年啊,慚愧,)。最近某個項目需要OPC服務器支持,於是又轉戰OPC戰場。說實話這之前對於OPC我只是粗淺了解,知道這是基於微軟的DCOM技術制定的用於工控領域的技術標准,制定並持續維護這一標准的組織被稱作OPC基金會。我不知道基金會對OPC的應用推廣做了多少工作,做出了多大貢獻,但至少可以確定的是,這個基金會對普通開發人員相當不友好,我即使成功注冊了用戶,依然在它的網站上沒找到OPC的支持組件“OPC Core Components Redistributable 3.0”,我估計這是他們的商業考慮,為的是多發展企業用戶,好多賺錢。這么LaJi的組織,實在是讓人鄙視,OPC的未來最終會被這滿是銅臭味的GouPi基金會葬送。要不是目前的工控市場還被OPC把持(沒人和小錢錢有仇啊),說實話我是不會把精力浪費在這么封閉的系統上。所以,我對OPC的定位就是不求甚解,只求一用。有了清晰的目標,我開始在網上滿世界的找OPC開發相關的資料和源碼,度娘、bing、github、gitee、CodeForge以及搭梯子google之,反正能想到的方法都想到了,但結果相當不理想。CSDN上倒是有不少OPC服務器源碼可供下載,但需要積分,我沒有積分,我也不想掙或者花錢買積分,因為我一直主張並堅持技術共享精神,CSDN和眾多CSDNers嚴重違背了這一精神,特別是那些拿着別人開源的源碼賺積分的WuChi小“人”們。這無疑增加了我的難度,好在我們有github,有眾多的具備分享精神的程序猿們,當然還有強大的搜索引擎,最終我找齊了所有資料和源碼,搞明白了OPC服務器的整個開發流程。再次鄙視OPC基金會、CSDN,感謝如下網址的主人們:
開源OPC服務器庫 http://www.ipi.ac.ru/lab43/lopc-en.html
DCOM開發樣例 https://blog.csdn.net/u011402642/article/details/46516559
上面的DCOM開發樣例出現0x80080005錯誤時的解決方案 https://blog.csdn.net/oshuangyue12/article/details/88424114
OPC頭文件 https://www.cnblogs.com/opcconnect/archive/2010/12/20/1911604.html
本指南的樣例代碼請直接從github上拉取:https://github.com/Neo-T/OPCDASrvBasedOnLightOPC
吐槽完畢,言歸正傳。前面我們已經說過,OPC基於微軟的DCOM技術,所以要想明白如何開發OPC服務器,首先就得知道如何開發DCOM。否則,你會摔得遍體鱗傷,浪費大把時間后依然是——不得其門而入。因為這個DCOM啊,實在是太過啰嗦,開發啰嗦、部署和使用更啰嗦,個人感覺它早晚會被淘汰。關於DCOM開發樣例,上面給出的鏈接雖然是作者2015年寫的,時間不算老,但開發環境竟然是1998年發布的VC6,這個鴻溝就有點大了。現在常用的VS2010和VS2015在DCOM開發上均做了不少改進,因此有必要在這里再開一篇,簡明扼要地介紹VS2010和VS2015下的DCOM開發流程,以備后查。
首先,打開VS2010或VS2015(下簡稱VS),”新建項目”->”Visual C++”->”ATL項目”,輸入名稱“iDCOMTestSrv”:
然后“確定”->”下一步”,選擇“服務(EXE)”,最后點選“完成”。
接下來,添加COM對象。工程名稱節點鼠標右鍵,點選“添加”->”類”:
在彈出窗口選擇“ATL簡單對象”:
點擊“添加”后,按下圖輸入相關信息,然后直接點擊“完成”:
在“類視圖”窗口,鼠標右鍵點選“IArithmeticLib”,在彈出的右鍵菜單中選擇“添加”->”添加方法”:
彈出窗口中按下圖所示添加add()方法:
最后點擊“完成”按鈕,add()方法添加完畢。接着按照如上步驟再添加一個sub()方法:
我們的目的是熟悉DCOM的開發流程,所以不用編寫復雜的函數,添加兩個add()和sub()方法就行了。
轉到VS的解決方案資源管理器,“源文件”節點雙擊“ArithmeticLib.cpp”,添加兩個方法的處理代碼:
1 STDMETHODIMP CArithmeticLib::add(int nNum1, int nNum2, int * pnResult) 2 { 3 *pnResult = nNum1 + nNum2; 4 return S_OK; 5 } 6 7 8 STDMETHODIMP CArithmeticLib::sub(int nNum1, int nNum2, int * pnResult) 9 { 10 *pnResult = nNum1 - nNum2; 11 return S_OK; 12 }
接下來我們還需要調整一個地方,在左側的“解決方案資源管理器”窗口,找到“iDCOMTestSrv”工程的“資源文件”->“ArithmeticLib.rgs”,雙擊打開,然后在這個文件增加如下一句:
val AppID = s '%APPID%'
增加位置如下:
這一句解決客戶端連接DCOM組件服務器時報0x80080005錯誤的問題。
接着編譯,如果不出意外,VS2015編譯應該能夠成功,VS2010則不一定,因為VS2010相對VS2015多做了一步:
如果你有系統管理員權限,那么編譯完成后這個注冊是能夠成功的,如果不是則失敗。由於我們是把DCOM部署到其它機器遠程執行,不在本機注冊,所以這里可以刪掉。不過,這一步倒是告訴我們,DCOM需要注冊才能使用。
接下來我們需要生成代理/存根文件,以用於遠程訪問DCOM組件。相對VC6,新版本的VS幫我們自動建立了代理/存根文件工程,工程需要的相關文件,是VS通過對應的IDL文件編譯生成的,我們在剛才編譯DCOM時VS已經幫我們生成了相關文件並添加到對應工程下:
在編譯生成代理/存根文件之前,我們需要更改一下該工程的鏈接器設置,見下圖:
把“注冊輸出”一項改為“否”,這樣VS就不會在編譯完成后順帶手幫我們在本機上注冊該動態庫了。雖然,VS這樣設計對直接調試來說比較省事,但這樣也掩蓋了技術細節,所以還是禁止VS這種越俎代庖的行為更好。接下來鼠標右鍵點選“解決方案資源管理器”中的“iDCOMTestSrvPS”,點擊“生成”,如無意外,我們將生成iDCOMTestSrv的代理/存根文件——“iDCOMTestSrvPS.dll”。
接下來我們還需要編寫使用這個DCOM組件的客戶端,看看效果如何。繼續在VS中新建一個“Win32控制台應用程序”,在打開的源文件“iDCOMTestClient.cpp”中添加如下代碼:
1 // iDCOMTestClient.cpp : 定義控制台應用程序的入口點。 2 // 3 4 #include "stdafx.h" 5 #include <windows.h> 6 #include "iDCOMTestSrv_i.h" 7 #include "iDCOMTestSrv_i.c" 8 9 int _tmain(int argc, _TCHAR* argv[]) 10 { 11 CoInitialize(NULL); 12 { 13 do{ 14 HRESULT hr; 15 16 COSERVERINFO stCoServerInfo; 17 COAUTHINFO stCoAuthInfo; 18 COAUTHIDENTITY stCoAuthID; 19 INT nSize = strlen("192.168.xxx.xxx") * sizeof(WCHAR); 20 memset(&stCoServerInfo, 0, sizeof(stCoServerInfo)); 21 stCoServerInfo.pwszName = (WCHAR *)CoTaskMemAlloc(nSize * sizeof(WCHAR)); 22 if(!stCoServerInfo.pwszName) 23 { 24 printf("CoTaskMemAlloc()函數執行失敗!\r\n"); 25 break; 26 } 27 28 ZeroMemory(&stCoAuthID, sizeof(COAUTHIDENTITY)); 29 stCoAuthID.User = reinterpret_cast<USHORT *>("user"); 30 stCoAuthID.UserLength = strlen("user"); 31 stCoAuthID.Domain = reinterpret_cast<USHORT *>(""); 32 stCoAuthID.DomainLength = 0; 33 stCoAuthID.Password = reinterpret_cast<USHORT *>("user_password"); 34 stCoAuthID.PasswordLength = strlen("user_password"); 35 stCoAuthID.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; 36 37 ZeroMemory(&stCoAuthInfo, sizeof(COAUTHINFO)); 38 stCoAuthInfo.dwAuthnSvc = RPC_C_AUTHN_WINNT; 39 stCoAuthInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE; 40 stCoAuthInfo.pwszServerPrincName = NULL; 41 stCoAuthInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CONNECT; 42 stCoAuthInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE; //* 必須是模擬登陸 43 stCoAuthInfo.pAuthIdentityData = &stCoAuthID; 44 stCoAuthInfo.dwCapabilities = EOAC_NONE; 45 46 mbstowcs(stCoServerInfo.pwszName, "192.168.xxx.xxx", nSize); 47 stCoServerInfo.pAuthInfo = &stCoAuthInfo; 48 stCoServerInfo.dwReserved1 = 0; 49 stCoServerInfo.dwReserved2 = 0; 50 51 MULTI_QI stMultiQI; 52 ZeroMemory(&stMultiQI, sizeof(stMultiQI)); 53 stMultiQI.pIID = &IID_IArithmeticLib; //* 參見iDCOMTestSrv_i.c 54 stMultiQI.pItf = NULL; 55 56 //* 初始化安全結構,模擬登錄遠程機器 57 hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); 58 if(!(SUCCEEDED(hr) || RPC_E_TOO_LATE == hr)) 59 { 60 printf("CoInitializeSecurity()函數執行失敗,錯誤碼:0x%08X\r\n", hr); 61 break; 62 } 63 64 //* 建立COM組件實例並按照需求獲取查詢接口 65 hr = CoCreateInstanceEx(CLSID_ArithmeticLib, //* 參見iDCOMTestSrv_i.c 66 NULL, 67 CLSCTX_REMOTE_SERVER, //* 顯式的指定要連接遠程機器 68 &stCoServerInfo, 69 sizeof(stMultiQI)/sizeof(MULTI_QI), 70 &stMultiQI); 71 72 //* 無論成功與否,先釋放剛才申請的內存 73 CoTaskMemFree(stCoServerInfo.pwszName); 74 75 //* 如果CoCreateInstanceEx()執行失敗 76 if(FAILED(hr)) 77 { 78 printf("CoCreateInstanceEx()函數執行失敗,錯誤碼:0x%08X\r\n", hr); 79 break; 80 } 81 82 //* 如果沒有獲取到DCOM組件的查詢接口 83 if(FAILED(stMultiQI.hr)) 84 { 85 printf("獲取組件的查詢接口失敗,錯誤碼:0x%08X\r\n", stMultiQI.hr); 86 break; 87 } 88 89 //* 查詢並獲取組件的調用接口,獲取完畢后直接釋放即可 90 IArithmeticLib *piobjArithmetic = NULL; 91 stMultiQI.pItf->QueryInterface(&piobjArithmetic); 92 stMultiQI.pItf->Release(); 93 94 //* 接收用戶輸入並調用遠程組件獲得計算結果 95 INT blIsRunning = TRUE; 96 while(blIsRunning) 97 { 98 INT nEnterFunCode; 99 INT nNum1, nNum2, nResult; 100 101 printf("1: 加; 2: 減; 0: 退出"); 102 scanf("%d", &nEnterFunCode); 103 104 switch(nEnterFunCode) 105 { 106 case 1: 107 printf("請輸入相加的兩個整型數字(空格分開):"); 108 scanf("%d%d", &nNum1, &nNum2); 109 piobjArithmetic->add(nNum1, nNum2, &nResult); 110 printf("[加]操作結果為:%d\r\n", nResult); 111 break; 112 113 case 2: 114 printf("請輸入相減的兩個整型數字(空格分開):"); 115 scanf("%d%d", &nNum1, &nNum2); 116 piobjArithmetic->sub(nNum1, nNum2, &nResult); 117 printf("[減]操作結果為:%d\r\n", nResult); 118 break; 119 120 case 0: 121 default: 122 blIsRunning = FALSE; 123 break; 124 } 125 } 126 }while(FALSE); 127 } 128 CoUninitialize(); 129 130 return 0; 131 }
代碼很簡單,關鍵地方都添加了注釋,這里不再作過多說明。重點說一下“#include”進來的兩個文件“iDCOMTestSrv_i.h”和“iDCOMTestSrv_i.c”,這兩個文件與代理/存根工程使用的文件是同一個,所以我們沒必要再將其單獨添加到這個測試客戶端工程中來,只需在工程屬性中把這兩個文件所在的目錄包含進來即可:
設置完成后直接編譯、生成EXE文件。
接下來就是部署工作了,這塊工作是最麻煩的。首先我們把剛才生成的“iDCOMTestSrvPS.dll”、“iDCOMTestSrv.exe”兩個文件復制到另外一台機器的某個目錄下,然后在這個目錄下以管理員身份打開控制台,輸入如下指令:
iDCOMTestSrv.exe/RegServer/Service
如果你的權限沒問題,這一步將很順利。此時我們可以打開“服務管理器”看到我們注冊的DCOM服務已經被添加進來了:
服務的啟動類型為“手動”,尚未啟動,這個不用管它,一旦客戶端成功連接,OS會為我們啟動它的。
接着我們注冊代理/存根文件,如果你編譯的是32位的DCOM,請使用如下指令注冊:
c:\windows\SysWOW64\regsvr32.exe iDCOMTestSrvPS.dll
如果是是64位DCOM則輸入如下指令:
c:\windows\System32\regsvr32.exe iDCOMTestSrvPS.dll
只有如此才能正確注冊32位和64位DCOM。
接着我們在DCOM客戶端所在的機器注冊代理/存根文件,注冊指令與上同。
注冊完畢,我們再把工作焦點轉移到DCOM服務器。首先我們添加一個DCOM用戶“user”,設定一個密碼(不能是空密碼),然后讓其隸屬於“Distributed COM Users”組,如下所示:
用戶添加完畢,接着控制台輸入如下指令:
mmc comexp.msc
如果是32位的DCOM組件,請在上述指令后再增加“ /32”,注意別漏了前面的空格。
在打開的“組件服務”窗口找到“我的電腦”節點,然后鼠標右鍵選擇“屬性”,在打開的窗口首先設置“默認屬性”:
接着“默認協議”選擇“面向連接的TCP/IP”:
然后是“COM安全”,為剛才添加的“user”用戶分配權限:
“訪問權限”之“編輯限制”的“user”權限:
“訪問權限”之“編輯默認值”的“user”權限:
“啟動和激活權限”之“編輯默認值”的“user”權限:
DCOM組件的缺省配置完成,我們還需要再繼續配置iDCOMTestSrv組件的權限。在下圖紅框所示位置,鼠標右鍵點選“ArithmeticLib class”:
右鍵菜單選擇“屬性”,按照下圖所示設置相關權限:
至此,所有配置完成,DCOM所在的機器重啟,無需登錄,稍等一段時間待RPC服務被OS啟動,然后在客戶端所在機器打開控制台,輸入如下指令:
iDCOMTestClient.exe
如果上面的操作完全無誤的話,你會看到如下界面:
至此,我們對DCOM的開發已經完全了解,接下來就是在開源庫基礎上開發OPC服務器了。
github上有該例子的完整代碼(鏈接見本文頂部開始部分),分別由VS2010和VS2015建立,VS2010是32位的DEBUG版本,編譯通過,但未實際部署測試,VS2015則是64位的Release版本,已在兩台機器上按照上述步驟測試通過。