刘永庆等
摘要:该文针对利用动态库技术进行通信协议模块化设计进行研究,首先简要地介绍了动态库基本理论,然后给通信协议动态库设计方法和设计要点,最后给出了基于UDP的通信协议动态库开发实例。
关键词:动态库
中图分类号:TP393 文献标识码:A 文章编号:1009-3044(2015)09-0058-03
在设计通信程序时,在其程序的实现形式上主要分为可执行应用程序和动态链接库。前者能够独立运行,通常针对某一特定需求而使用,功能完备但可移植性不强;后者不能独立运行,只是以库的形式提供相关功能的函数、类及其他数据,动态库可以为某一特定需求而定制。
利用动态库技术进行通信协议设计,按照从核心到外围的层次关系进行模块化组合设计,各模块动态加载,可扩展,独立编译,软件系统层次明确、内外松散耦合,便于功能组合和升级改造,提升软件质量。
1 动态库基本理论
1.1动态库分类
VC支持三种DLL,它们是:
1)Non-MFC DLL:指的是不用MFC的类库结构,直接用C语言写的DLL,其输出函数一般用的是标准C接口,并能被非MFC或MFC编写的应用程序所调用。
2)Regular DLL:和下述Extension DLL一样,是用MFC类库编写的,能够被所有支持DLL技术的语言所编写的应用程序调用。在这种动态链接库中,它必需有一个从CWinAPP继承下来的类,DLLMain函数被MFC提供,不用自己显式的写出来。
3)Extension DLL:只被用MFC类库所编写的应用程序所调用。在这种动态链接库中,用户可以从MFC继承想要的、更适于自己用的类,并把它提供给自己的应用程序。与Regular DLL不一样,它没有一个从CWinAPP继承下来的类的对象,用户必需为自己的DLLMain函数添加初始化代码和结束代码。
1.2 DLL调用方法
DLL的创建是供可执行应用程序调用的。使用了外部DLL的应用程序的创建与普通应用程序的创建完全一样。在此基础上可以对外部DLL进行显式或隐式调用。对DLL的调用分为两种,一种是显式的调用,一种是隐式的调用。所谓显示的调用,是指在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显示地将自己所做的动态库调进来,动态链接库的文件名即是上面函数的参数,再用GetProcAddress获取想要引入的函数。自此,就可以像使用应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。
隐式的调用则需要把产生动态链接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只需声明一下即可,而无需调用LoadLibrary和FreeLibrary对DLL进行显示加载、卸载。
隐式调用的方法比较简单,但隐式调用的DLL在应用程序加载的同时被加载到内存中,当应用程序调用的DLL比较多时,装入的过程十分缓慢。通过延迟加载技术可以很好地解决该问题。但除了必须的.dll文件外还需要DLL的.h文件和.lib文件。这在那些只提供.dll文件的场合就无法使用了,而只能采用显式调用方式。
1.3 输入函数和输出函数
模块是Windows的基本构成单元,主要由应用程序模块和DLL模块组成。这两类模块的结构是一样的,都可以“输出”(export)函数供其他模块使用,也可以“输入”(import)其他模块的函数。输入一个函数就是在代码中创建指向该函数的动态链接,而非像在静态链接中那样实际装配该函数的代码。与DLL不同,由应用程序模块输出的函数是无法为其他应用程序模块所输入的。
MFC提供的用于输出的函数的关键字是__declspec和dllexport。在要输出的函数、类或数据的声明前使用__declspec(dllexport)表示输出。若要输出动态库中的函数mimafuwu(HWND hWnd)供应用程序输入使用则在动态库中声明该函数如下:
#define REGULARMFCDLLLIB __declspec(dllexport)
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd);
在应用程序输入声明如下,_cdecl为调用约定:
unsigned short (_cdecl *Func)(HWND);
2 通信协议动态库设计
2.1 动态库结构
通信协议动态库一般只包含一个输出函数和由该输出函数创建的三个UI线程(用户界面线程)即主控线程、数据接收线程和数据发送线程组成。三个线程分别对应三个模块:DLL主控模块,DLL数据接收模块和DLL数据发送模块。DLL主控模块负责与调用DLL的应用程序及DLL数据收发模块交互数据和消息,同时负责按接口协议进行解析、分包、组包、超时重传等数据处理操作,DLL数据收发模块负责与外部通信端进行物理层接口(如网口、串口等)的数据收发,DLL数据收发模块相互独立不涉及信息交互。通信协议动态库结构示意图见图1。
2.2 动态库接口及协议
通信协议动态库接口设计为内部接口和外部接口。如图2所示,内部接口为动态库内部模块之间的接口,外部接口有两种,分为动态库与调用其的应用程序之间的接口和动态库与外部通信端之间的接口。
2.2.1内部接口及协议
动态库内部接口为DLL主控模块与DLL数据发送模块之间和DLL主控模块与DLL数据接收模块之间的接口。内部模块之间主要通过自定义消息方式构造协议进行数据通信。
2.2.2外部接口及协议
2.2.2.1 动态库和调用DLL的应用程序之间接口及协议
动态库和调用DLL的应用程序之间接口为DLL输出函数。两者之间主要通过自定义消息方式构造协议进行数据通信。
2.2.2.2 动态库和外部通信端之间接口及协议
动态库和外部通信端之间的接口主要为以太网口和串口、并口等通信端口等。使用的接口协议主要有:基于TCP的网络通信协议、基于UDP的网络通信协议和基于串口/并口的端口通信协议等。
2.3 动态库信息处理流程
调用DLL的A端应用程序拟制一份数据按动态库和调用DLL的应用程序之间接口协议将其提交DLL主控模块,DLL主控模块按动态库和外部通信端之间接口协议进行数据处理后再按内部接口协议将数据提交DLL发送模块,DLL发送模块将数据发送到B端。DLL接收模块接收B端数据后按内部接口协议将其提交DLL主控模块,DLL主控模块按动态库和外部通信端之间接口协议收齐数据后,再按动态库和调用DLL的应用程序之间接口协议将数据提交A端应用程序。即:
1)A端调用DLL的应用程序->DDL主控模块->DLL发送模块- >B端
2)B端 - >DLL接收模块->DLL主控模块->A端调用DLL的应用程序
3 通信协议动态库设计要点
3.1动态库中的输出函数
应用程序一启动就应加载动态库,调用动态库输出函数。动态库中一般只有一个输出函数,该函数只负责创建UI线程。输出函数参数须包含应用程序某窗口句柄,一般为主框架窗口句柄,同时输出函数将必要的变量信息如动态库创建的某个线程的线程号回传至应用程序。通过窗口句柄和线程号作为参数,以便于应用程序和动态库之间以自定义消息的方式进行通信。
3.2动态库中的超时时钟设置
动态库中超时时钟的设置与应用程序有别,不能使用ON_WM_TIMER()消息机制,需采用自定义消息方式。具体方法如下。
自定义超时消息:
ON_MESSAGE(WM_TIMER, OnTimer)
设置超时时钟:
UNIT m_iTimer=::SetTimer(0,0,3000,NULL);//3000表示定时3秒
超时消息处理函数:
void OnTimer(WPARAM wparam,LPARAM lparam)
{
UINT nIDEvent =(UINT)wparam;
if(nIDEvent==m_iTimer)
{
//超时处理
}
}
关闭超时时钟:
KillTimer(0,m_iTimer);
3.3动态库与调用DLL的应用程序之间的消息传递
如前所述,动态库与调用DLL的应用程序之间消息传递时首先需要知道应用程序窗口句柄和动态库某线程的线程号,使用的MFC消息函数如下。
动态库往应用程序发消息:
::PostMessage(
ApphWnd,
WM_DLL_TO_APP_MSG,
WPARAM wparam,
LPARAM lparam);
其中,参数ApphWnd为应用程序主框架窗口句柄,WM_DLL_TO_APP_MSG为自定义消息标识,wparam为消息中携带的参数一(如数据指针等),lparam为消息中携带的参数二(如数据长度等)。
应用程序往动态库发消息:
PostThreadMessage(
m_Threadid,
WM_APP_TO_DLL_MSG,
WPARAM wparam,
LPARAM lparam);
其中,参数m_Threadid为动态库中某个线程的线程号,应用程序将消息发往该线程,WM_APP_TO_DLL_MSG为自定义的消息标识,wparam为消息中携带的参数一(如数据指针等),lparam为消息中携带的参数二(如数据长度等)。
3.4 通信参数的设置和使用
动态库对通信参数(诸如IP地址、端口号、串口配置,动态库路径、分包长度、固定包头、超时时钟值和重传次数等)的设置和使用一般有两种方式。一种为,读取第三方软件形成的通信参数配置文件的方式。另一种为,应用程序调用输出函数时将通信参数传递给动态库,动态库再进行通信参数的设置和使用。两种方式以前者为优。
4 基于UDP的通信协议动态库开发实例
结合第3节和第4节内容,本节以创建Regular DLL和显式调用DLL为例,设计一个基于UDP的通信协议动态库。为了使用该动态库,首先创建一个调用该DLL的简单应用程序。
第一步:创建应用程序
启动VC++,单击[File]->[New]菜单项,在project页中选择MFC AppWizard(exe),新建一个名为MyApp的基于单文档界面的工程。
第二步:创建DLL
1)启动VC++,单击[File]->[New]菜单项,在project页中选择MFC AppWizard(dll),新建一个名为MyLib的工程,在第一步的时候选择,创建一个动态链接MFC的规则DLL。
2)构造输出函数mimafuwu():
① 在MyLib工程中填加空白源文件mimafuwu.cpp和mimafuwu.h;
② 在mimafuwu.cpp文件中输入如下代码:
#include "StdAfx.h"
#include "mimafuwu.h"
//输出函数根据具体应用而定制。
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd)
{
AfxMessageBox("装载DLL模块成功!");
return 0;
}
③ 在mimafuwu.h文件中输入如下代码:
#define REGULARMFCDLLLIB __declspec(dllexport)
//输出函数声明,输出函数根据具体应用而定制。
extern "C" REGULARMFCDLLLIB unsigned short mimafuwu(HWND hWnd);
3)编译后会生成库文件MyLib.dll。
第三步:应用程序加载和使用DLL
1)在创建的MyApp工程的MainFrm.cpp文件的函数
CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
return语句前添加如下代码,完成对MyLib.dll的动态链接,并完成对输出函数mimafuwu()的调用:
//选择好MyLib.dll文件路径,装载DLL模块
HINSTANCE hDLL = ::LoadLibrary("MyLib.dll");
//输入函数声明
unsigned short (_cdecl *Func)(HWND);
// 获取函数指针
Func = (unsigned short(_cdecl *)(HWND))::GetProcAddress(hDLL, "mimafuwu");
//调用DLL中的函数mimafuwu(HWND)
//同时将应用程序主框架窗口句柄传至动态库
unsigned short nResult = Func(GetSafeHwnd());
在上述代码中,首先由LoadLibrary()将DLL模块映射到进程的内存空间,对DLL模块进行动态加载。其函数原型为:
LoadLibrary(LPCTSTR lpLibFileName);
其中,参数lpLibFileName为待加载的模块名,如不特殊指定扩展名,Windows将指定默认的扩展名为“.dll”。如果成功加载则返回HINSTANCE值,标识了文件映像映射到进程地址空间的虚拟内存地址;如果加载失败则返回NULL,可通过GetLastError()了解进一步的信息。
接下来的GetProcAddress()函数将在DLL模块中找到要输入符号的地址。其函数原型为:
FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);
其中,参数hModule为通过LoadLibrary()等函数而得到的DLL模块句柄,lpProcName为要查找的输入符号名。GetProcAddress()在成功调用后将返回DLL的输出符号地址,否则返回空指针NULL。通过其返回得到的内存地址即可完成对输出函数的调用。
当进程中的线程不再需要DLL中的输出符号时,可以通过AfxFreeLibrary()函数从进程的地址空间显式卸载DLL。其函数原型如下:
BOOL FreeLibrary(HMODULE hLibModule);
其中参数hLibModule标识了要卸载的DLL模块。
2) 编译后会生成可执行文件MyApp.exe,确保文件MyLib.dll路径正确。运行后若弹出提示框,则应用程序加载和使用DLL成功。
第四步:根据具体应用定制应用程序和DLL
在前面生成的MyApp和MyLib工程的基础上进行修改。应用程序一启动就加载一个开了三个UI线程(用户界面线程)即数据接收线程、数据发送线程和主控线程的动态库,应用程序与动态库主控线程、动态库收发线程与主控线程之间通过自定义消息方式进行数据交互。在动态库库数据接收线程中创建UDP套接字,通过将IP地址设置为127.0.0.l实现应用程序对数据的自发自收。
整个信息流程为:应用程序拟制一份数据提交动态库主控线程,动态库主控线程将收到到的数据提交动态库发送线程发送,动态库接收线程收到数据后提交动态库主控线程,动态库主控线程将数据提交应用程序,即:应用程序->DLL主控->DLL发送- >DLL接收->DLL主控->应用程序。数据在各提交过程中不做任何处理,应用程序发出的数据和收到的数据内容一致。
5 结束语
编写通信协议动态链接库DLL设计说明,目的是作为规范和指导DLL形式的通信协议程序模块设计工作的技术文件。同时对DLL基本程序设计、实现DLL功能扩展和对第三方提供的DLL功能模块调用等提供编程基础。利用动态库技术,遵循从核心到外围的层次关系进行模块化组合设计理念,使软件系统层次明确,各模块松散耦合、独立开发、独立验证、独立升级改造,便于整个软件系统维护与功能扩展,提升软件质量。
参考文献:
[1] Roberts J W. Traffic control in the BISDN[J]. Computer Networks and ISDN Systems, 1993(25): 1055-1064.
[2] 郎锐, 孙方. Visual C++ 网络通信程序开发基础及实例解析[M]. 2版. 北京: 机械工业出版社, 2006.
[3] Kruglinski D J. Visual C++ 技术内幕[M]. 4版. 北京: 清华大学出版社, 2009.
[4] Ian Sommervill. 软件工程[M]. 9版. 北京: 机械工业出版社, 2011.