劉永慶等
摘要:該文針對利用動態(tài)庫技術(shù)進(jìn)行通信協(xié)議模塊化設(shè)計進(jìn)行研究,首先簡要地介紹了動態(tài)庫基本理論,然后給通信協(xié)議動態(tài)庫設(shè)計方法和設(shè)計要點,最后給出了基于UDP的通信協(xié)議動態(tài)庫開發(fā)實例。
關(guān)鍵詞:動態(tài)庫
中圖分類號:TP393 文獻(xiàn)標(biāo)識碼:A 文章編號:1009-3044(2015)09-0058-03
在設(shè)計通信程序時,在其程序的實現(xiàn)形式上主要分為可執(zhí)行應(yīng)用程序和動態(tài)鏈接庫。前者能夠獨立運行,通常針對某一特定需求而使用,功能完備但可移植性不強;后者不能獨立運行,只是以庫的形式提供相關(guān)功能的函數(shù)、類及其他數(shù)據(jù),動態(tài)庫可以為某一特定需求而定制。
利用動態(tài)庫技術(shù)進(jìn)行通信協(xié)議設(shè)計,按照從核心到外圍的層次關(guān)系進(jìn)行模塊化組合設(shè)計,各模塊動態(tài)加載,可擴展,獨立編譯,軟件系統(tǒng)層次明確、內(nèi)外松散耦合,便于功能組合和升級改造,提升軟件質(zhì)量。
1 動態(tài)庫基本理論
1.1動態(tài)庫分類
VC支持三種DLL,它們是:
1)Non-MFC DLL:指的是不用MFC的類庫結(jié)構(gòu),直接用C語言寫的DLL,其輸出函數(shù)一般用的是標(biāo)準(zhǔn)C接口,并能被非MFC或MFC編寫的應(yīng)用程序所調(diào)用。
2)Regular DLL:和下述Extension DLL一樣,是用MFC類庫編寫的,能夠被所有支持DLL技術(shù)的語言所編寫的應(yīng)用程序調(diào)用。在這種動態(tài)鏈接庫中,它必需有一個從CWinAPP繼承下來的類,DLLMain函數(shù)被MFC提供,不用自己顯式的寫出來。
3)Extension DLL:只被用MFC類庫所編寫的應(yīng)用程序所調(diào)用。在這種動態(tài)鏈接庫中,用戶可以從MFC繼承想要的、更適于自己用的類,并把它提供給自己的應(yīng)用程序。與Regular DLL不一樣,它沒有一個從CWinAPP繼承下來的類的對象,用戶必需為自己的DLLMain函數(shù)添加初始化代碼和結(jié)束代碼。
1.2 DLL調(diào)用方法
DLL的創(chuàng)建是供可執(zhí)行應(yīng)用程序調(diào)用的。使用了外部DLL的應(yīng)用程序的創(chuàng)建與普通應(yīng)用程序的創(chuàng)建完全一樣。在此基礎(chǔ)上可以對外部DLL進(jìn)行顯式或隱式調(diào)用。對DLL的調(diào)用分為兩種,一種是顯式的調(diào)用,一種是隱式的調(diào)用。所謂顯示的調(diào)用,是指在應(yīng)用程序中用LoadLibrary或MFC提供的AfxLoadLibrary顯示地將自己所做的動態(tài)庫調(diào)進(jìn)來,動態(tài)鏈接庫的文件名即是上面函數(shù)的參數(shù),再用GetProcAddress獲取想要引入的函數(shù)。自此,就可以像使用應(yīng)用程序自定義的函數(shù)一樣來調(diào)用此引入函數(shù)了。在應(yīng)用程序退出之前,應(yīng)該用FreeLibrary或MFC提供的AfxFreeLibrary釋放動態(tài)鏈接庫。
隱式的調(diào)用則需要把產(chǎn)生動態(tài)鏈接庫時產(chǎn)生的.LIB文件加入到應(yīng)用程序的工程中,想使用DLL中的函數(shù)時,只需聲明一下即可,而無需調(diào)用LoadLibrary和FreeLibrary對DLL進(jìn)行顯示加載、卸載。
隱式調(diào)用的方法比較簡單,但隱式調(diào)用的DLL在應(yīng)用程序加載的同時被加載到內(nèi)存中,當(dāng)應(yīng)用程序調(diào)用的DLL比較多時,裝入的過程十分緩慢。通過延遲加載技術(shù)可以很好地解決該問題。但除了必須的.dll文件外還需要DLL的.h文件和.lib文件。這在那些只提供.dll文件的場合就無法使用了,而只能采用顯式調(diào)用方式。
1.3 輸入函數(shù)和輸出函數(shù)
模塊是Windows的基本構(gòu)成單元,主要由應(yīng)用程序模塊和DLL模塊組成。這兩類模塊的結(jié)構(gòu)是一樣的,都可以“輸出”(export)函數(shù)供其他模塊使用,也可以“輸入”(import)其他模塊的函數(shù)。輸入一個函數(shù)就是在代碼中創(chuàng)建指向該函數(shù)的動態(tài)鏈接,而非像在靜態(tài)鏈接中那樣實際裝配該函數(shù)的代碼。與DLL不同,由應(yīng)用程序模塊輸出的函數(shù)是無法為其他應(yīng)用程序模塊所輸入的。
MFC提供的用于輸出的函數(shù)的關(guān)鍵字是__declspec和dllexport。在要輸出的函數(shù)、類或數(shù)據(jù)的聲明前使用__declspec(dllexport)表示輸出。若要輸出動態(tài)庫中的函數(shù)mimafuwu(HWND hWnd)供應(yīng)用程序輸入使用則在動態(tài)庫中聲明該函數(shù)如下:
#define REGULARMFCDLLLIB __declspec(dllexport)
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd);
在應(yīng)用程序輸入聲明如下,_cdecl為調(diào)用約定:
unsigned short (_cdecl *Func)(HWND);
2 通信協(xié)議動態(tài)庫設(shè)計
2.1 動態(tài)庫結(jié)構(gòu)
通信協(xié)議動態(tài)庫一般只包含一個輸出函數(shù)和由該輸出函數(shù)創(chuàng)建的三個UI線程(用戶界面線程)即主控線程、數(shù)據(jù)接收線程和數(shù)據(jù)發(fā)送線程組成。三個線程分別對應(yīng)三個模塊:DLL主控模塊,DLL數(shù)據(jù)接收模塊和DLL數(shù)據(jù)發(fā)送模塊。DLL主控模塊負(fù)責(zé)與調(diào)用DLL的應(yīng)用程序及DLL數(shù)據(jù)收發(fā)模塊交互數(shù)據(jù)和消息,同時負(fù)責(zé)按接口協(xié)議進(jìn)行解析、分包、組包、超時重傳等數(shù)據(jù)處理操作,DLL數(shù)據(jù)收發(fā)模塊負(fù)責(zé)與外部通信端進(jìn)行物理層接口(如網(wǎng)口、串口等)的數(shù)據(jù)收發(fā),DLL數(shù)據(jù)收發(fā)模塊相互獨立不涉及信息交互。通信協(xié)議動態(tài)庫結(jié)構(gòu)示意圖見圖1。
2.2 動態(tài)庫接口及協(xié)議
通信協(xié)議動態(tài)庫接口設(shè)計為內(nèi)部接口和外部接口。如圖2所示,內(nèi)部接口為動態(tài)庫內(nèi)部模塊之間的接口,外部接口有兩種,分為動態(tài)庫與調(diào)用其的應(yīng)用程序之間的接口和動態(tài)庫與外部通信端之間的接口。
2.2.1內(nèi)部接口及協(xié)議
動態(tài)庫內(nèi)部接口為DLL主控模塊與DLL數(shù)據(jù)發(fā)送模塊之間和DLL主控模塊與DLL數(shù)據(jù)接收模塊之間的接口。內(nèi)部模塊之間主要通過自定義消息方式構(gòu)造協(xié)議進(jìn)行數(shù)據(jù)通信。
2.2.2外部接口及協(xié)議
2.2.2.1 動態(tài)庫和調(diào)用DLL的應(yīng)用程序之間接口及協(xié)議
動態(tài)庫和調(diào)用DLL的應(yīng)用程序之間接口為DLL輸出函數(shù)。兩者之間主要通過自定義消息方式構(gòu)造協(xié)議進(jìn)行數(shù)據(jù)通信。
2.2.2.2 動態(tài)庫和外部通信端之間接口及協(xié)議
動態(tài)庫和外部通信端之間的接口主要為以太網(wǎng)口和串口、并口等通信端口等。使用的接口協(xié)議主要有:基于TCP的網(wǎng)絡(luò)通信協(xié)議、基于UDP的網(wǎng)絡(luò)通信協(xié)議和基于串口/并口的端口通信協(xié)議等。
2.3 動態(tài)庫信息處理流程
調(diào)用DLL的A端應(yīng)用程序擬制一份數(shù)據(jù)按動態(tài)庫和調(diào)用DLL的應(yīng)用程序之間接口協(xié)議將其提交DLL主控模塊,DLL主控模塊按動態(tài)庫和外部通信端之間接口協(xié)議進(jìn)行數(shù)據(jù)處理后再按內(nèi)部接口協(xié)議將數(shù)據(jù)提交DLL發(fā)送模塊,DLL發(fā)送模塊將數(shù)據(jù)發(fā)送到B端。DLL接收模塊接收B端數(shù)據(jù)后按內(nèi)部接口協(xié)議將其提交DLL主控模塊,DLL主控模塊按動態(tài)庫和外部通信端之間接口協(xié)議收齊數(shù)據(jù)后,再按動態(tài)庫和調(diào)用DLL的應(yīng)用程序之間接口協(xié)議將數(shù)據(jù)提交A端應(yīng)用程序。即:
1)A端調(diào)用DLL的應(yīng)用程序->DDL主控模塊->DLL發(fā)送模塊- >B端
2)B端 - >DLL接收模塊->DLL主控模塊->A端調(diào)用DLL的應(yīng)用程序
3 通信協(xié)議動態(tài)庫設(shè)計要點
3.1動態(tài)庫中的輸出函數(shù)
應(yīng)用程序一啟動就應(yīng)加載動態(tài)庫,調(diào)用動態(tài)庫輸出函數(shù)。動態(tài)庫中一般只有一個輸出函數(shù),該函數(shù)只負(fù)責(zé)創(chuàng)建UI線程。輸出函數(shù)參數(shù)須包含應(yīng)用程序某窗口句柄,一般為主框架窗口句柄,同時輸出函數(shù)將必要的變量信息如動態(tài)庫創(chuàng)建的某個線程的線程號回傳至應(yīng)用程序。通過窗口句柄和線程號作為參數(shù),以便于應(yīng)用程序和動態(tài)庫之間以自定義消息的方式進(jìn)行通信。
3.2動態(tài)庫中的超時時鐘設(shè)置
動態(tài)庫中超時時鐘的設(shè)置與應(yīng)用程序有別,不能使用ON_WM_TIMER()消息機制,需采用自定義消息方式。具體方法如下。
自定義超時消息:
ON_MESSAGE(WM_TIMER, OnTimer)
設(shè)置超時時鐘:
UNIT m_iTimer=::SetTimer(0,0,3000,NULL);//3000表示定時3秒
超時消息處理函數(shù):
void OnTimer(WPARAM wparam,LPARAM lparam)
{
UINT nIDEvent =(UINT)wparam;
if(nIDEvent==m_iTimer)
{
//超時處理
}
}
關(guān)閉超時時鐘:
KillTimer(0,m_iTimer);
3.3動態(tài)庫與調(diào)用DLL的應(yīng)用程序之間的消息傳遞
如前所述,動態(tài)庫與調(diào)用DLL的應(yīng)用程序之間消息傳遞時首先需要知道應(yīng)用程序窗口句柄和動態(tài)庫某線程的線程號,使用的MFC消息函數(shù)如下。
動態(tài)庫往應(yīng)用程序發(fā)消息:
::PostMessage(
ApphWnd,
WM_DLL_TO_APP_MSG,
WPARAM wparam,
LPARAM lparam);
其中,參數(shù)ApphWnd為應(yīng)用程序主框架窗口句柄,WM_DLL_TO_APP_MSG為自定義消息標(biāo)識,wparam為消息中攜帶的參數(shù)一(如數(shù)據(jù)指針等),lparam為消息中攜帶的參數(shù)二(如數(shù)據(jù)長度等)。
應(yīng)用程序往動態(tài)庫發(fā)消息:
PostThreadMessage(
m_Threadid,
WM_APP_TO_DLL_MSG,
WPARAM wparam,
LPARAM lparam);
其中,參數(shù)m_Threadid為動態(tài)庫中某個線程的線程號,應(yīng)用程序?qū)⑾l(fā)往該線程,WM_APP_TO_DLL_MSG為自定義的消息標(biāo)識,wparam為消息中攜帶的參數(shù)一(如數(shù)據(jù)指針等),lparam為消息中攜帶的參數(shù)二(如數(shù)據(jù)長度等)。
3.4 通信參數(shù)的設(shè)置和使用
動態(tài)庫對通信參數(shù)(諸如IP地址、端口號、串口配置,動態(tài)庫路徑、分包長度、固定包頭、超時時鐘值和重傳次數(shù)等)的設(shè)置和使用一般有兩種方式。一種為,讀取第三方軟件形成的通信參數(shù)配置文件的方式。另一種為,應(yīng)用程序調(diào)用輸出函數(shù)時將通信參數(shù)傳遞給動態(tài)庫,動態(tài)庫再進(jìn)行通信參數(shù)的設(shè)置和使用。兩種方式以前者為優(yōu)。
4 基于UDP的通信協(xié)議動態(tài)庫開發(fā)實例
結(jié)合第3節(jié)和第4節(jié)內(nèi)容,本節(jié)以創(chuàng)建Regular DLL和顯式調(diào)用DLL為例,設(shè)計一個基于UDP的通信協(xié)議動態(tài)庫。為了使用該動態(tài)庫,首先創(chuàng)建一個調(diào)用該DLL的簡單應(yīng)用程序。
第一步:創(chuàng)建應(yīng)用程序
啟動VC++,單擊[File]->[New]菜單項,在project頁中選擇MFC AppWizard(exe),新建一個名為MyApp的基于單文檔界面的工程。
第二步:創(chuàng)建DLL
1)啟動VC++,單擊[File]->[New]菜單項,在project頁中選擇MFC AppWizard(dll),新建一個名為MyLib的工程,在第一步的時候選擇,創(chuàng)建一個動態(tài)鏈接MFC的規(guī)則DLL。
2)構(gòu)造輸出函數(shù)mimafuwu():
① 在MyLib工程中填加空白源文件mimafuwu.cpp和mimafuwu.h;
② 在mimafuwu.cpp文件中輸入如下代碼:
#include "StdAfx.h"
#include "mimafuwu.h"
//輸出函數(shù)根據(jù)具體應(yīng)用而定制。
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd)
{
AfxMessageBox("裝載DLL模塊成功!");
return 0;
}
③ 在mimafuwu.h文件中輸入如下代碼:
#define REGULARMFCDLLLIB __declspec(dllexport)
//輸出函數(shù)聲明,輸出函數(shù)根據(jù)具體應(yīng)用而定制。
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd);
3)編譯后會生成庫文件MyLib.dll。
第三步:應(yīng)用程序加載和使用DLL
1)在創(chuàng)建的MyApp工程的MainFrm.cpp文件的函數(shù)
CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
return語句前添加如下代碼,完成對MyLib.dll的動態(tài)鏈接,并完成對輸出函數(shù)mimafuwu()的調(diào)用:
//選擇好MyLib.dll文件路徑,裝載DLL模塊
HINSTANCE hDLL = ::LoadLibrary("MyLib.dll");
//輸入函數(shù)聲明
unsigned short (_cdecl *Func)(HWND);
// 獲取函數(shù)指針
Func = (unsigned short(_cdecl *)(HWND))::GetProcAddress(hDLL, "mimafuwu");
//調(diào)用DLL中的函數(shù)mimafuwu(HWND)
//同時將應(yīng)用程序主框架窗口句柄傳至動態(tài)庫
unsigned short nResult = Func(GetSafeHwnd());
在上述代碼中,首先由LoadLibrary()將DLL模塊映射到進(jìn)程的內(nèi)存空間,對DLL模塊進(jìn)行動態(tài)加載。其函數(shù)原型為:
LoadLibrary(LPCTSTR lpLibFileName);
其中,參數(shù)lpLibFileName為待加載的模塊名,如不特殊指定擴展名,Windows將指定默認(rèn)的擴展名為“.dll”。如果成功加載則返回HINSTANCE值,標(biāo)識了文件映像映射到進(jìn)程地址空間的虛擬內(nèi)存地址;如果加載失敗則返回NULL,可通過GetLastError()了解進(jìn)一步的信息。
接下來的GetProcAddress()函數(shù)將在DLL模塊中找到要輸入符號的地址。其函數(shù)原型為:
FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);
其中,參數(shù)hModule為通過LoadLibrary()等函數(shù)而得到的DLL模塊句柄,lpProcName為要查找的輸入符號名。GetProcAddress()在成功調(diào)用后將返回DLL的輸出符號地址,否則返回空指針NULL。通過其返回得到的內(nèi)存地址即可完成對輸出函數(shù)的調(diào)用。
當(dāng)進(jìn)程中的線程不再需要DLL中的輸出符號時,可以通過AfxFreeLibrary()函數(shù)從進(jìn)程的地址空間顯式卸載DLL。其函數(shù)原型如下:
BOOL FreeLibrary(HMODULE hLibModule);
其中參數(shù)hLibModule標(biāo)識了要卸載的DLL模塊。
2) 編譯后會生成可執(zhí)行文件MyApp.exe,確保文件MyLib.dll路徑正確。運行后若彈出提示框,則應(yīng)用程序加載和使用DLL成功。
第四步:根據(jù)具體應(yīng)用定制應(yīng)用程序和DLL
在前面生成的MyApp和MyLib工程的基礎(chǔ)上進(jìn)行修改。應(yīng)用程序一啟動就加載一個開了三個UI線程(用戶界面線程)即數(shù)據(jù)接收線程、數(shù)據(jù)發(fā)送線程和主控線程的動態(tài)庫,應(yīng)用程序與動態(tài)庫主控線程、動態(tài)庫收發(fā)線程與主控線程之間通過自定義消息方式進(jìn)行數(shù)據(jù)交互。在動態(tài)庫庫數(shù)據(jù)接收線程中創(chuàng)建UDP套接字,通過將IP地址設(shè)置為127.0.0.l實現(xiàn)應(yīng)用程序?qū)?shù)據(jù)的自發(fā)自收。
整個信息流程為:應(yīng)用程序擬制一份數(shù)據(jù)提交動態(tài)庫主控線程,動態(tài)庫主控線程將收到到的數(shù)據(jù)提交動態(tài)庫發(fā)送線程發(fā)送,動態(tài)庫接收線程收到數(shù)據(jù)后提交動態(tài)庫主控線程,動態(tài)庫主控線程將數(shù)據(jù)提交應(yīng)用程序,即:應(yīng)用程序->DLL主控->DLL發(fā)送- >DLL接收->DLL主控->應(yīng)用程序。數(shù)據(jù)在各提交過程中不做任何處理,應(yīng)用程序發(fā)出的數(shù)據(jù)和收到的數(shù)據(jù)內(nèi)容一致。
5 結(jié)束語
編寫通信協(xié)議動態(tài)鏈接庫DLL設(shè)計說明,目的是作為規(guī)范和指導(dǎo)DLL形式的通信協(xié)議程序模塊設(shè)計工作的技術(shù)文件。同時對DLL基本程序設(shè)計、實現(xiàn)DLL功能擴展和對第三方提供的DLL功能模塊調(diào)用等提供編程基礎(chǔ)。利用動態(tài)庫技術(shù),遵循從核心到外圍的層次關(guān)系進(jìn)行模塊化組合設(shè)計理念,使軟件系統(tǒng)層次明確,各模塊松散耦合、獨立開發(fā)、獨立驗證、獨立升級改造,便于整個軟件系統(tǒng)維護(hù)與功能擴展,提升軟件質(zhì)量。
參考文獻(xiàn):
[1] Roberts J W. Traffic control in the BISDN[J]. Computer Networks and ISDN Systems, 1993(25): 1055-1064.
[2] 郎銳, 孫方. Visual C++ 網(wǎng)絡(luò)通信程序開發(fā)基礎(chǔ)及實例解析[M]. 2版. 北京: 機械工業(yè)出版社, 2006.
[3] Kruglinski D J. Visual C++ 技術(shù)內(nèi)幕[M]. 4版. 北京: 清華大學(xué)出版社, 2009.
[4] Ian Sommervill. 軟件工程[M]. 9版. 北京: 機械工業(yè)出版社, 2011.