基於第三方開源庫的OPC服務器開發指南(1)——OPC與DCOM


事兒太多,好多事情並不以我的意志為轉移,原想沉下心好好研究、學習圖像識別,繼續豐富我的機器視覺庫,並繼續《機器視覺及圖像處理系列》博文的更新,但計划沒有變化快,好多項目要完成,只好耽擱下來(這一耽擱又是多半年啊,慚愧,Disappointed smile)。最近某個項目需要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”:

image

然后“確定”->”下一步”,選擇“服務(EXE)”,最后點選“完成”。

image

接下來,添加COM對象。工程名稱節點鼠標右鍵,點選“添加”->””:

image

在彈出窗口選擇“ATL簡單對象”:

image

點擊“添加”后,按下圖輸入相關信息,然后直接點擊“完成”:

image

在“類視圖”窗口,鼠標右鍵點選“IArithmeticLib”,在彈出的右鍵菜單中選擇“添加”->”添加方法”:

image

彈出窗口中按下圖所示添加add()方法:

imageimage

最后點擊“完成”按鈕,add()方法添加完畢。接着按照如上步驟再添加一個sub()方法:

image

我們的目的是熟悉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%'

增加位置如下:

image

這一句解決客戶端連接DCOM組件服務器時報0x80080005錯誤的問題。

接着編譯,如果不出意外,VS2015編譯應該能夠成功,VS2010則不一定,因為VS2010相對VS2015多做了一步:

image

如果你有系統管理員權限,那么編譯完成后這個注冊是能夠成功的,如果不是則失敗。由於我們是把DCOM部署到其它機器遠程執行,不在本機注冊,所以這里可以刪掉。不過,這一步倒是告訴我們,DCOM需要注冊才能使用。

接下來我們需要生成代理/存根文件,以用於遠程訪問DCOM組件。相對VC6,新版本的VS幫我們自動建立了代理/存根文件工程,工程需要的相關文件,是VS通過對應的IDL文件編譯生成的,我們在剛才編譯DCOM時VS已經幫我們生成了相關文件並添加到對應工程下:

image

在編譯生成代理/存根文件之前,我們需要更改一下該工程的鏈接器設置,見下圖:

image

把“注冊輸出”一項改為“”,這樣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”,這兩個文件與代理/存根工程使用的文件是同一個,所以我們沒必要再將其單獨添加到這個測試客戶端工程中來,只需在工程屬性中把這兩個文件所在的目錄包含進來即可:

image

設置完成后直接編譯、生成EXE文件。

接下來就是部署工作了,這塊工作是最麻煩的。首先我們把剛才生成的“iDCOMTestSrvPS.dll”、“iDCOMTestSrv.exe”兩個文件復制到另外一台機器的某個目錄下,然后在這個目錄下以管理員身份打開控制台,輸入如下指令:

iDCOMTestSrv.exe/RegServer/Service

如果你的權限沒問題,這一步將很順利。此時我們可以打開“服務管理器”看到我們注冊的DCOM服務已經被添加進來了:

image

服務的啟動類型為“手動”,尚未啟動,這個不用管它,一旦客戶端成功連接,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”組,如下所示:

image

用戶添加完畢,接着控制台輸入如下指令:

mmc comexp.msc

如果是32位的DCOM組件,請在上述指令后再增加“ /32”,注意別漏了前面的空格。

在打開的“組件服務”窗口找到“我的電腦”節點,然后鼠標右鍵選擇“屬性”,在打開的窗口首先設置“默認屬性”:

image

接着“默認協議”選擇“面向連接的TCP/IP”:

image

然后是“COM安全”,為剛才添加的“user”用戶分配權限:

image

訪問權限”之“編輯限制”的“user”權限:

image

訪問權限”之“編輯默認值”的“user”權限:

image

啟動和激活權限”之“編輯默認值”的“user”權限:

 

DCOM組件的缺省配置完成,我們還需要再繼續配置iDCOMTestSrv組件的權限。在下圖紅框所示位置,鼠標右鍵點選“ArithmeticLib class”:

image

右鍵菜單選擇“屬性”,按照下圖所示設置相關權限:

imageimage

imageimage

至此,所有配置完成,DCOM所在的機器重啟,無需登錄,稍等一段時間待RPC服務被OS啟動,然后在客戶端所在機器打開控制台,輸入如下指令:

iDCOMTestClient.exe

如果上面的操作完全無誤的話,你會看到如下界面:

image

至此,我們對DCOM的開發已經完全了解,接下來就是在開源庫基礎上開發OPC服務器了。

github上有該例子的完整代碼(鏈接見本文頂部開始部分),分別由VS2010和VS2015建立,VS2010是32位的DEBUG版本,編譯通過,但未實際部署測試,VS2015則是64位的Release版本,已在兩台機器上按照上述步驟測試通過。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM