3.4 使用WM_COPYDATA消息通信
对于少量数据可以用WM_COPYDATA方便地实现通信。由于SendMessage()是阻塞的,只有接收方响应了消息,SendMessage()才能返回,否则一直阻塞。所以,对于大量数据来说,用SendMessage()就容易造成窗口假死。
3.4.1 通过WM_COPYDATA消息实现进程间通信的方法
在Win32中,WM_COPYDATA消息主要目的是允许在进程间传递只读数据。SDK文档推荐用户使用SendMessage()函数,接收方在数据复制完成前不返回,这样发送方就不可能删除和修改数据。这个函数的原型如下:
SendMessage(WM_COPYDATA,wParam,lParam)
其中wParam设置为包含数据的窗口句柄,lParam指向一个COPYDATASTRUCT的结构,其定义为:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;
DWORD cbData;
PVOID lpData;
}COPYDATASTRUCT;
其中dwData为自定义数据, cbData为数据大小, lpData为指向数据的指针。需要注意的是,WM_COPYDATA消息保证发送的数据从原进程复制到目标进程。但是,WM_COPYDATA消息不能发送HDC、HBITMAP之类的东西,它们对于目标进程来说是无效的。目标进程得到这些数据不能在原进程作任何事情,因为它们属于不同的进程。
与其他进程通信方法一样,要实现进程间的数据通信,在发送数据的程序中,首先要找到接收数据进程的窗口句柄pWnd,可以用CWnd::FindWindow(NULL,_ T("DataRecv"))函数来得到,其中字符串"DataRecv"为接收数据的程序名。然后用SendMessage()函数发送数据,其具体的做法见后面的实例。
在接收数据的程序中,首先在消息映射表中增加WM_COPYDATA消息映射,然后定义消息映射函数,其函数的格式为:
BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
// 增加用户自定义程序代码
…
}
3.4.2 通过WM_COPYDATA消息实现进程间通信的实例
与前面所说的自定义消息不一样,WM_COPYDATA消息是Win32提供的消息。与自定义消息相比较,WM_COPYDATA消息可以传递一个较大的数据块。这里仍然用两个对话框程序来实现WM_COPYDATA消息的通信。
以下分别给出发送数据程序的发送函数和接收数据程序的接收函数。在发送数据的对话框类CDataSendDlg中,用MFC ClassWizard工具或者手工的方法增加函数void CDataSendDlg::OnSendCopydata(),其具体代码如下:
void CDataSendDlg::OnSendCopydata()
{
UpdateData(); // 更新数据
CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox("Unable to find DataRecv.");
return;
}
COPYDATASTRUCT cpd; // 给COPYDATASTRUCT结构赋值
cpd.dwData = 0;
cpd.cbData = m_strCopyData.GetLength();
cpd.lpData = (void*)m_strCopyData.GetBuffer(cpd.cbData);
pWnd->SendMessage(WM_COPYDATA,NULL,(LPARAM)&cpd); // 发送
}
在用MFC AppWizard(exe)创建接收数据的对话框程序后,生成对话框类CDataRecvDlg。在这个类中,首先要定义接收WM_COPYDATA消息的映射,可以用ClassWizard工具来增加,也可以手动增加,但手动增加需要修改三个地方:①在消息映射表中增加ON_WM_COPYDATA();②增加成员函数BOOL CDataRecvDlg::OnCopyData();③在CDataRecvDlg类中增加WM_COPYDATA消息映射函数的定义。
WM_COPYDATA消息的映射如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog)
//{{AFX_MSG_MAP(CDataRecvDlg)
ON_WM_COPYDATA()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
CDataRecvDlg::OnCopyData()函数的定义如下:
BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
m_strCopyData=(LPSTR)pCopyDataStruct->lpData;
// 获得实际长度的字符串
m_strCopyData=m_strCopyData.Left(pCopyDataStruct->cbData);
// 更新数据
UpdateData(FALSE);
return CDialog::OnCopyData(pWnd, pCopyDataStruct);
}
其中m_strCopyData为接收到的字符串,pCopyDataStruct为COPYDATASTRUCT结构指针。注意由pCopyDataStruct直接得到的m_strCopyData字符串长度可能不是实际发送的字符串长度,需要用发送字符串时所给定的字符串长度来进一步确定,其长度由pCopyDataStruct ->cbData来得到。
3.5 使用内存读写函数和内存映射文件通信
对于ReadProcessMemory()和WriteProcessMemory()函数的通信方法,在第1章已做介绍。并用它说明了C指针的意义,但有两点需要改进:①接收程序在接收数据时所用的指针代码值不需要事先给定;②内存大小是可以变化的。这里将对内存读写函数的通信方法做一点改进。
3.5.1 使用内存映射文件通信的方法
采用内存映射(File Mapping)机制可以将整个文件映射为进程虚拟地址空间的一部分来加以访问。这种方法和实例前面已做了详细介绍,这里不再重复。
3.5.2 使用内存读写函数实现进程间通信的方法
要使接收程序获得发送程序的数据指针,可以通过发送消息方法来进行,即通过消息把数据指针从发送程序传递到接收程序。也可以用第1章所介绍的方法:先获得发送程序中的被发送数据指针,然后把这个指针直接赋值给接收数据的程序。但这种方法在实际操作中较困难,使用起来不方便。要使用发送消息的方法来传递指针,就需要定义一个用户消息。可用如下的自定义消息来传递指针,即
const UINT wm_nMemMsg=RegisterWindowMessage("mem_data");
要通过内存来传递数据,还必须要在内存中申请一定的内存空间,这一点很重要。用变量定义的方法只能申请有限的固定的内存空间,例如,定义一个char变量只能在内存里申请到一个字节的内存空间,定义一个int 变量只能在内存里申请到4个字节的内存空间。如果要分配一块内存空间存放数据,可以调用GlobalAlloc()或者VirtualAllocEx()等来实现。
3.5.3 使用内存读写函数实现进程间通信的实例
自定义消息和内存读写函数(ReadProcessMemory()和WriteProcessMemory())相结合,利用它们各自的长处进行通信。自定义消息通信只能传递一个长整型数值,而内存读写函数却需要一个内存读写地址,并且缺少一个传递数据指针的方法。这样它们正好可以“合作”,来进行大批量的数据传递工作。
要进行这种方式的通信,同样需要编写两个对话框程序,并且在这两个程序中分别定义一个相同的用于传递指针的消息wm_nMemMsg。这里借用前面所使用的发送数据对话框类CDataSendDlg和接收数据对话框类CDataRecvDlg。在CDataSendDlg中,用MFC ClassWizard工具或手动增加成员函数void CDataSendDlg::OnSendMem(),其源代码如下:
void CDataSendDlg::OnSendMem()
{
UpdateData(); // 更新数据
CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox("Unable to find DataRecv.");
return;
}
// 获取进程号
DWORD PID;
GetWindowThreadProcessId(pWnd->m_hWnd, (DWORD*)&PID );
HANDLE hProcess = OpenProcess (PROCESS_ALL_ACCESS,FALSE,PID);
// 分配虚拟内存
LPVOID lpBaseAddress;
lpBaseAddress = VirtualAllocEx(hProcess, 0, BUFFER_SIZE,
MEM_COMMIT, PAGE_READWRITE);
char data[BUFFER_SIZE];
strcpy(data,m_strMem);
// 把字符串写入hProcess进程的内存
WriteProcessMemory(hProcess, lpBaseAddress, data, BUFFER_SIZE, NULL);
// 发送基址给DataRecv进程
pWnd->SendMessage(wm_nMemMsg,NULL,(LPARAM)lpBaseAddress);
// 等待接收程序接收数据
Sleep(100);
// 释放虚拟内存
VirtualFreeEx(hProcess,lpBaseAddress, 0, MEM_RELEASE);
}
从以上程序中可以看出如何使用WriteProcessMemory()和wm_nMemMsg消息来发送字符串m_strMem。这段程序中,首先,寻找接收数据的程序DataRecv的窗口指针pWnd和进程句柄hProcess,再用VirtualAllocEx()函数在这个进程中申请虚拟内存空间。然后,用WriteProcessMemory()把字符串m_strMem存放入虚拟内存,并且通过消息wm_nMemMsg把所申请的内存空间起始地址发送给数据接收程序。最后,当数据接收程序接收到数据后,用VirtualFreeEx()释放所申请的虚拟内存。
在数据接收程序的对话框类CDataRecvDlg中,需要定义wm_nMemMsg消息映射,它在消息映射表中的表示方法如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog)
//{{AFX_MSG_MAP(CDataRecvDlg)
ON_REGISTERED_MESSAGE(wm_nMemMsg,OnRegMemMsg)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
在数据接收对话框类CDataRecvDlg中,用MFC ClassWizard工具或手动增加消息映射函数void CDataRecvDlg::OnRegMemMsg(),其定义如下:
void CDataRecvDlg::OnRegMemMsg(WPARAM wParam,LPARAM lParam)
{
LPVOID lpBaseAddress=(LPVOID)lParam;
// 把字符串写入hProcess进程的内存
HANDLE hProcess=GetCurrentProcess();
char data[BUFFER_SIZE];
ReadProcessMemory(hProcess, lpBaseAddress, data,BUFFER_SIZE, NULL);
m_strMem=data;
// 更新数据
UpdateData(FALSE);
}
3.6 使用动态链接库通信
动态链接库(Dynamic Link Library,DLL)不仅可以用来共享程序代码,而且可以用来共享数据。用DLL共享程序代码的方法以后再做介绍,这里只说明如何利用它共享数据。
3.6.1 DLL概述
目前,动态链接库有Win32 DLL 和Win16 DLL之分,它们的特性不相同。但是,对于同一个DLL,不管Win32 DLL还是Win16 DLL的代码装入内存实际只有一次。这里必须强调的是对同一个DLL的调用才是这样。如果同一个DLL有多个副本,且每个DLL副本分别被不同的程序调用时,那么每个副本都会被装入内存一次。装入内存后,虽然说它们名字相同,但它们是多份副本,因此不是对同一个DLL的调用。在这种情况下就不能用这个DLL实现进程之间的通信。例如在c:\DLL_A目录下有动态库文件shared. dll和应用程序user1.exe,而在另外一个c:\DLL_B目录下有动态库文件shared.dll和应用程序user2.exe,那么就不能通过shared.dll来实现user1.exe和user2.exe之间的通信。
Win32把DLL装入全局内存并把DLL映射到每个程序的地址空间,并且不需要把DLL映射到每个进程相同的地址空间上。DLL成为载入它的进程的一部分,而不像在Win16中,成为系统的一部分。
对于Win16来说,在DLL中共享数据是很容易的,因为访问DLL的每个应用程序都可以得到它的全局静态变量,如图3.3所示。在Win32中,对每个载入DLL的进程,DLL获取一个该进程的唯一的全局静态变量的副本,如图3.4所示。
3.6.2 使用DLL通信的方法
从上面可以看出,对于Win32的DLL,所有载入DLL的应用程序只能共享程序代码,不能共享数据,必须要采取一种方法才能使这些程序之间共享数据。当然访问数据时要注意同步问题。
要想使Win32 DLL的数据区能设计成共享的存储区,可以通过#pragma data_seg指令建立一个新段来做到这一点,实际上是告诉编译器包含段中的特定变量。然而,仅此不足以做到数据共享,还必须把段中将要共享的变量告诉连接器。可通过如下命令来 实现:
(1)指定DEF文件在SECTIONS段下的名字,如下所示:
SECTIONS
共享段名 READ WRITE SHARED
(2)#pragma comment(linker,"/SECTION: 共享段名,RWS")
下面的例子示范了如何初始化一个全局变量:
#pragma data_seg("MyShared")
UINT m_glnData=0;
#pragma data_seg()
#pragma comment(linker,"/SECTION:MyShared,RWS")
注意要初始化变量。初始化变量很重要,因为编译器将把所有未初始化数据存放在.bss段。把变量安排在与用户预期不同的段中,则它们就不能被共享,除非明确地指出要共享的是.bss段。
关于共享数据段名称MyShared,完全可以用其他的名称,如MYDATA、.MYSC等,但建议不要与PE文件的固定的段名相同,以免程序运行时出错。
最后一点要强调的是,进行通信的程序要使用同一个DLL文件。如果使用的是相同DLL文件的不同副本,则不能实现进程之间的通信。
3.6.3 使用DLL通信的实例
与前面所讲的通信方法不同,本实例使用DLL实现进程间的通信。以下是一个用于生成DLL文件的头文件DllObj.h。其具体代码如下:
// DllObj.h:头文件
//
#ifndef _DLLOBJ_H_INCLUDED
#define _DLLOBJ_H_INCLUDED
#include <afxmt.h>
#ifdef _cplusplus
#define _DLLCOM_ extern "C" _declspec (dllexport)
#else
#define _DLLCOM_ _declspec (dllexport)
#endif
_DLLCOM_LPSTR GetValueString();
_DLLCOM_void SetValueString(LPCSTR str);
#endif
其中SetValueString()和GetValueString()函数分别用于向所指定的共享存储区里写入和读取字符串。_DLLCOM_ 用于定义DLL中函数的输出。可以看出,SetValueString()和GetValueString()函数的写法与其他DLL文件的写法没有什么不同。但是,这两个函数所用到的共用字符串变量m_strString的表示方法则是用DLL实现内存数据共享的关键。
现在来看看如何制作一个这样的DLL文件。先用MFC AppWizard(dll)生成一个dllcom 模板(可以取其他的名称),然后把以下的代码包含在一个dllcom.cpp文件中,再用VC++编译器进行编译和连接,就可以生成dllcom.dll和dllcom.lib文件。生成这个DLL文件的核心代码如下:
#pragma data_seg("MyShared")
char m_strString[256]=TEXT("");
volatile bool bInCriticalSection=FALSE;
#pragma data_seg()
#pragma comment(linker,"/SECTION:MyShared,RWS")
CCriticalSection cs;
// 从内存中读取字符串
_DLLCOM_ LPSTR GetValueString()
{
while(bInCriticalSection) // 等待
Sleep(1);
return m_strString;
}
// 把字符串存储到共享内存中
_DLLCOM_ void SetValueString(LPCSTR str)
{
while(bInCriticalSection) // 等待
Sleep(1);
cs.Lock();
bInCriticalSection = TRUE;
strcpy(m_strString,str);
bInCriticalSection = FALSE;
cs.Unlock();
}
其中bInCriticalSection为进程访问数据时的同步标识。
在制作了一个用于进程间通信的DLL文件后,就可以利用它实现进程之间的通信。可以设计两个应用程序,dlluser1和dlluser2,在这两个程序中把动态库文件头DllObj.h和动态库dllcom.lib包含其中,即
#include "DllObj.h"
#pragma comment(lib,"dllcom.lib")
然后,用MFC VC++编译器进行编译和连接,这样就可以用SetValueString()和GetValue- String()函数进行通信了。
3.7 使用Windows剪贴板通信
Windows剪贴板是一种比较简单同时也是开销比较小的IPC(进程间通信)机制。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存,用来暂存各个进程间进行交换的数据。提供数据的进程创建一个全局内存块,并将要传送的数据移到或复制到该内存块;而接受数据的进程(也可以是提供数据的进程本身)获取此内存块的句柄,并完成对该内存块数据的读取。
在Windows系统和其他工具软件中有自带的使用剪贴板的命令。例如,在Microsoft Word 中,组合键Ctrl+C用于文字的复制、组合键Ctrl+X用于对文字的剪切、组合键Ctrl+V用于对文字的粘贴。使用这些命令可以很方便地对所选择字符串进行复制和移动。然而,这里关心的是如何在编写应用程序时使用剪贴板实现进程间的通信。
3.7.1 使用剪贴板实现进程间通信的方法
可以使用剪贴板函数实现进程间的数据传输。常用的剪贴板函数有:
// 打开剪贴板
BOOL OpenClipboard();
// 关闭剪贴板
BOOL CloseClipboard();
// 清空剪贴板,并将所有权分配给打开剪贴板的进程
BOOL EmptyClipboard( );
// 按指定数据格式放置剪贴板数据,用之前必须使用OpenClipboard函数
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem);
// 检测是否已经包含了所需要的数据
BOOL IsClipboardFormatAvailable(UINT uFormat);
// 获取指定剪贴板数据
HANDLE GetClipboardData( UINT uFormat);
其中uFormat 为剪贴板格式,见MSDN(微软开发者网络)描述,hMem为所申请的内存控制句柄。
文本剪贴板和位图剪贴板是比较常用的。其中,文本剪贴板是包含具有格式CF_TEXT的字符串的剪贴板,是最经常使用的剪贴板之一。在文本剪贴板中传递的数据是不带任何格式信息的ASCII字符。若要将文本传送到剪贴板,可以先分配一个可移动全局内存块,然后将要复制的文本内容写入到此内存区域,最后调用剪贴板函数如OpenClipboard()、SetClipboardData()将数据放置到剪贴板。从剪贴板获取文本的过程与之类似,首先用OpenClipboard()函数打开剪贴板并获取剪贴板的数据句柄,如果数据存在就复制其数据到程序变量。由于GetClipboardData()获取的数据句柄属于剪贴板,因此用户程序必须在调用CloseClipboard()函数之前使用它。
大多数应用程序对图形数据采取是位图剪贴板数据格式。位图剪贴板的使用与文本剪贴板的使用类似,只是数据格式要指明为CF_BITMAP,而且在使用SetClipboardData()或GetClipboardData()函数时交给剪贴板或从剪贴板返回的是设备相关位图句柄。
3.7.2 使用剪贴板实现进程间通信的实例
剪贴板中可以存放许多类型的数据,其中包括标准文本格式、位图格式、RTF格式等,由于类型比较多,这里只给出经常使用的文本格式的实例,其他的数据类型的操作方法基本类似。同样,用 VC++ 编写两个对话框应用程序。为了方便,仍然借用前面所使用的对话框类CDataSendDlg和CDataRecvDlg。
为了把文本放置到剪贴板上,在CDataSendDlg中,用MFC ClassWizard工具或者用手工的方法增加函数void CDataSendDlg::OnSendClipboard(),其源代码如下:
void CDataSendDlg::OnSendClipboard()
{
UpdateData(); // 更新数据
CString strData=m_strClipBoard; // 获得数据
// 打开系统剪贴板
if (!OpenClipboard()) return;
// 使用之前,清空系统剪贴板
EmptyClipboard();
// 分配一内存,大小等于要复制的字符串的大小,返回到内存控制句柄
HGLOBAL hClipboardData;
hClipboardData = GlobalAlloc(GMEM_DDESHARE, strData.GetLength()+1);
// 内存控制句柄加锁,返回值为指向那内存控制句柄所在的特定数据格式的指针
char * pchData;
pchData = (char*)GlobalLock(hClipboardData);
// 将本地变量的值赋给全局内存
strcpy(pchData, LPCSTR(strData));
// 给加锁的全局内存控制句柄解锁
GlobalUnlock(hClipboardData);
// 通过全局内存句柄将要复制的数据放到剪贴板上
SetClipboardData(CF_TEXT,hClipboardData);
// 使用完后关闭剪贴板
CloseClipboard();
}
在数据接收程序的CDataRecvDlg类中,用与前面所用的同样的方法,增加从剪贴板上获取文本的函数,即void CDataRecvDlg::OnRecvClipboard(),其源代码如下:
void CDataRecvDlg::OnRecvClipboard()
{
// 打开系统剪贴板
if (!OpenClipboard()) return;
// 判断剪贴板上的数据是否是指定的数据格式
if (IsClipboardFormatAvailable(CF_TEXT)|| IsClipboardFormatAvaila- ble(CF_OEMTEXT))
{
// 从剪贴板上获得数据
HANDLE hClipboardData = GetClipboardData(CF_TEXT);
// 通过给内存句柄加锁,获得指向指定格式数据的指针
char *pchData = (char*)GlobalLock(hClipboardData);
// 本地变量获得数据
m_strClipBoard = pchData;
// 给内存句柄解锁
GlobalUnlock(hClipboardData);
}
else
{
AfxMessageBox("There is no text (ANSI) data on the Clipboard.");
}
// 使用完后关闭剪贴板
CloseClipboard();
// 更新数据
UpdateData(FALSE);
}
使用剪贴板通信的方法与使用发送消息通信的方法所经历的过程是不一样的,前者是把所共享的数据先放在剪贴板上,然后由接收数据的程序去获取,而后者是直接把共享数据发送到接收数据的程序。因此,在使用剪贴板通信方法时,接收数据程序的对话框上需要增加一个获取数据的命令按钮,而使用消息通信方法则不需要这个按钮。
3.8 使用动态数据交换(DDE)通信
动态数据交换(Dynamic Data Exchange,DDE)也是一种进程间通信形式。它最早是随着Windows 3.1由美国微软公司提出的。当前大部分软件仍就支持DDE,但近10年间微软公司已经停止发展DDE技术,只保持对DDE技术给予兼容和支持。但我们仍然可以利用DDE技术编写自己的数据交换程序。
3.8.1 使用DDE技术通信原理
两个同时运行的程序间通过DDE方式交换数据时是客户/服务器关系,一旦客户和服务器建立起来连接关系,则当服务器中的数据发生变化后就会马上通知客户。通过DDE方式建立的数据连接通道是双向的,即客户不但能够读取服务器中的数据,而且可以对其进行修改。
DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答,如从菜单中选择粘贴命令。尽管DDE也可以由用户启动,但它继续发挥作用,一般不必用户进一步干预。
DDE有三种数据交换方式,即
(1)冷连接(Cool Link):数据交换是一次性数据传输,与剪贴板相同。当服务器中的数据发生变化后不通知客户,但客户可以随时从服务器读写数据;
(2)温连接(Warm Link):当服务器中的数据发生变化后马上通知客户,客户得到通知后将数据取回;
(3)热连接(Hot Link):当服务器中的数据发生变化后马上通知客户,同时将变化的数据直接送给客户。
DDE 客户程序向DDE 服务器程序请求数据时,它必须首先知道服务器的名称(即DDE Service名)、DDE主题名称(Topics名),还要知道请求哪一个数据项的项目名称(Items名)。DDE Service名应该具有唯一性,否则容易产生混乱。通常DDE Service就是服务器的程序名称,但不是绝对的,它是由程序设计人员在程序内部设定好的,并不是通过修改程序名称就可以改变的。Topics名和Items名也是由DDE Service在其内部设定好的,所有服务程序的Service名、Topics名都是注册在系统中,当一个客户向一个服务器请求数据时,客户必须向系统报告服务器的Service名和Topics名。只有当Service名、Topics名与服务器内部设定的名称一致时,系统才将客户的请求传达给服务器。
当服务名和Topics名相符时,服务器马上判断Items名是否合法。如果请求的Item名是服务器中的合法数据项,服务器即建立此项连接,建立连接的数据发生数值变化后,服务器会及时通知客户。一个服务器可以有多个Topics名,Items名的数量也不受限制。
DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式,进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。但DDE有个明显的缺点就是,通信效率低下,当通信量较大时数据刷新速度慢,在数据较少时DDE较实用。
3.8.2 如何使用DDEML编写程序
早期的DDE基于消息机制,应用程序间的消息传递需程序员调度。由于DDE消息通信牵涉的操作细节颇多,实现完全的DDE协议不是非常容易的事情,而且不同的开发者对协议的解释也略有不同。为了使用方便起见,微软提供DDE管理库(The DDE Management Library, 简称DDEML)。DDEML专门协调DDE通信,给DDE应用程序提供句柄字符串和数据交换的服务,消除了早期由于DDE协议不一致所引起的问题。
使用DDEML开发的应用程序(客户/服务器)无论在运行一致性方面,还是在程序相互通信方面,性能均优于没有使用DDEML的应用程序。而且DDEML的应用使得开发支持DDE的应用程序容易了许多,因为 DDEML(这是个 DLL)担起了内务府总管的工作。使用DDEML后,实际上客户和服务器之间的多数会话并不是直达对方的,而是经由DDEML中转,即用Callback函数处理DDE交易(Transaction),而早期的消息通信是直接的。
在调用其他DDEML函数前,客户/服务器必须调用DdeInitialize()函数,以获取实例标识符,注册DDE Callback函数,并为Callback函数指定事务过滤。对于服务器,在使用DdeInitialize()初始化后,调用DdeCreateStringHandle()建立Service名、Topics名和Items名等标识的句柄,再通过DdeNameService()在操作系统中注册服务器的名字。根据这些句柄,客户就可以使用它提供的DDE服务了。
为了执行某个DDE任务,许多DDEML函数需要获得字符串的访问权。例如:一个客户在调用DdeConnect()函数来请求同服务器建立会话时,必须指定Service名和Topics名。可以通过调用DdeCreateStringHandle()函数来获取特定字符串句柄。例如:
HSZ hszServName = DdeCreateStringHandle(idInst,"MyServer",CP_WINANSI);
HSZ hszSysTopic = DdeCreateStringHandle(idInst,SZDDESYS_TOPIC,CP_WINANSI);
一个应用程序的DDE回调函数在大多DDE事务中接收多个字符串句柄。比如:在XTYP_REQUEST事务处理期间,一个DDE 服务器接收两个字符串句柄:一个标识Topics名字符串,另一个标识Items名字符串。可以通过调用DdeQueryString()函数来获取相应于字符串句柄的字符串长度,并且复制字符串到应用程序定义的buffer中。例如:
DWORD idInst;
DWORD cb;
HSZ hszServ;
PSTR pszServName;
cb = DdeQueryString(idInst, hszServ, (LPSTR) NULL, 0, CP_WINANSI) + 1;
pszServName = (PSTR) LocalAlloc(LPTR, (UINT) cb);
DdeQueryString(idInst, hszServ, pszServName, cb, CP_WINANSI);
根据微软MSDN,现有的基于消息DDE协议的应用程序与DDEML应用程序是相容的,也就是说,基于消息通信的DDE应用程序可以与DDEML应用程序对话和交易。在使用DDEML时,必须在源程序文件中包括ddeml.h头文件,连接user32.lib文件,并保证ddeml.dll文件正确的系统路径。
3.8.3 使用DDE通信的实例
由上面的介绍可知,可以编写基于消息DDE应用程序,也可以编写应用DDEML的应用程序。对于前者,实现的方法较复杂,这里不做介绍。这里介绍一个应用DDEML编写的DDE通信实例。
为了便于管理,这里把这个程序封装成一个CMyDde类,下面介绍这个类。CMyDde类头文件如下:
// DDE.h: 定义CMyDde类
//
#ifndef _DDE_H_INCLUDED
#define _DDE_H_INCLUDED
#include <ddeml.h>
class CMyDde
{
public:
CMyDde();
~CMyDde();
// 静态回调成员函数
static HDDEDATA CALLBACK DdeCallback(UINT iType,UINT iFmt,
HCONV hConv,HSZ hsz1,HSZ hsz2,
HDDEDATA hData,DWORD dwData1,DWORD data2);
void DdeCall(UINT iType, LPCSTR szSvr,LPCSTR szTopic,LPCSTR szAtom);
void DdeServer(CString strReply);
void DdeClient(CString strRequest);
CString GetReply() { return m_strReply;}
CString GetRequest() { return m_strRequest;}
private:
static CMyDde* fakeThis;
DWORD idInst;
CString AppName;
CString m_strReply;
CString m_strRequest;
};
#endif
其中包含了ddeml.h头文件,DdeCallback()为static回调函数。之所以使用static,是因为DdeInitialize()函数的需要,否则编译会出错。
对于服务程序,使用类中的DdeServer()函数。在这个函数中用DdeInitialize()调用回调函数DdeCallback(),注册服务名MyDDEService,以便客户程序与服务程序取得联系。在DdeInitialize()中设置事务过滤,例如以下的DdeServer()函数中,在DdeInitialize()中设置CBF_FAIL_POKES,表示XTYP_ POKES事件将被过滤掉。DdeServer()函数的代码 如下:
void CMyDde::DdeServer(CString strReply)
{
m_strReply=strReply;
fakeThis=this;
// 建立DDE
DdeInitialize(&idInst,DdeCallback,APPCLASS_STANDARD|
CBF_FAIL_ADVISES|
CBF_FAIL_POKES|
CBF_SKIP_REGISTRATIONS|
CBF_SKIP_UNREGISTRATIONS,0L);
// 注册服务名MyDDEService,使该程序作为DDE服务器
AppName="MyDDEService";
HSZ hszService=DdeCreateStringHandle(idInst,AppName,0);
DdeNameService(idInst,hszService,NULL,DNS_REGISTER);
}
回调函数(Callback function)大量用于Windows的系统服务,通过它,程序员可以安装设备驱动程序和消息过滤系统,以控制Windows的有效使用。以下是DDE服务程序的回调函数源代码:
HDDEDATA CALLBACK CMyDde::DdeCallback(UINT iType,
UINT iFmt,HCONV hConv,
HSZ hsz1, // Topic.
HSZ hsz2, // atom.
HDDEDATA hData,DWORD dwData1,DWORD data2)
{
char szBuffer[100];
switch(iType)
{
// 建立交易连接
case XTYP_CONNECT:
// 获得应用名
DdeQueryString(fakeThis->idInst,hsz2,
szBuffer,sizeof(szBuffer),0);
// 如果此应用不能被此服务器支持,返回NULL
if(strcmp(szBuffer,fakeThis->AppName)) return NULL;
// 获得topic名
DdeQueryString(fakeThis->idInst,hsz1,
szBuffer,sizeof(szBuffer),0);
// 如果连接成功,返回1
return (HDDEDATA)1;
case XTYP_REQUEST:
// 获得topic名
DdeQueryString(fakeThis->idInst,hsz1,
szBuffer,sizeof(szBuffer),0);
if(strcmp(szBuffer,"query")==0)
{
// 获得Item 名
DdeQueryString(fakeThis->idInst,hsz2,
szBuffer,sizeof(szBuffer),0);
strcpy(szBuffer,fakeThis->m_strReply);
return DdeCreateDataHandle(fakeThis->idInst,
(LPBYTE)szBuffer,sizeof(szBuffer),0,hsz2,CF_TEXT,0);
}
break;
case XTYP_EXECUTE:
// 获得topic名
DdeQueryString(fakeThis->idInst,hsz1,
szBuffer,sizeof(szBuffer),0);
if(strcmp(szBuffer,"data")==0)
{
// 获得数据
DdeGetData(hData, (LPBYTE)szBuffer, 40L, 0L);
fakeThis->m_strRequest=szBuffer;
return (HDDEDATA)1;
}
break;
}
return NULL;
}
其中只使用了三个选项,即XTYP_CONNECT、XTYP_REQUEST和XTYP_ EXECUTE,还有其他的一些选项,见微软的MSDN说明。XTYP_CONNECT响应于客户程序使用的DdeConnect()函数。XTYP_REQUEST和XTYP_EXECUTE分别响应于客户程序中使用DdeClientTransaction()函数的XTYP_REQUEST和XTYP_ EXECUTE选项。在服务程序中,对于XTYP_REQUEST选项,可以用DdeCreateDataHandle函数向客户程序发送数据,而XTYP_EXECUTE则不能。而对于XTYP_EXECUTE选项,可以用DdeGetData()函数从客户获取数据,而XTYP_REQUEST则不能。
在服务程序中用DdeQueryString()函数从客户程序中获得Topics名和Items名,先得到Topics名,然后得到Items名。在本实例中XTYP_REQUEST选项的Topics名是“query”,Items名为“1”,而XTYP_EXECUTE选项的Topics名是“data”,Items名为“1”,但Items名都没有被利用。
以下是用于客户程序的主函数,也需要用DdeInitialize()函数初始化,并设置过滤类型。其中使用了类型调用函数DdeCall()。DdeClient()函数的代码如下:
void CMyDde::DdeClient(CString strRequest)
{
m_strRequest=strRequest;
idInst=0;
DdeInitialize(&idInst,NULL,APPCLASS_STANDARD|
CBF_FAIL_ADVISES|
CBF_FAIL_POKES|
CBF_SKIP_REGISTRATIONS|
CBF_SKIP_UNREGISTRATIONS,0L);
DdeCall(XTYP_EXECUTE,TEXT("MyDDEService"),TEXT("data"),TEXT("1"));
DdeCall(XTYP_REQUEST,TEXT("MyDDEService"),TEXT("query"),TEXT("1"));
}
在类型调用的DdeCall()函数中,首先获得Service名、Topics名和Items名的字符串句柄,然后用DdeConnect()函数与服务程序连接。如果连接成功,就可以用DdeClientTransaction() 函数和用XTYP_REQUEST和XTYP_EXECUTE类型向服务程序发送数据。其中,对于XTYP_REQUEST,可以用DdeGetData()函数从服务程序获得数据。最后用DdeDisconnect()函数断开与服务程序的连接,并且用DdeFreeStringHandle()函数释放Service名、Topics名和Items名的字符串句柄。DdeCall()函数的源代码如下:
void CMyDde::DdeCall(UINT iType,LPCSTR szSvr,LPCSTR szTopic,LPCSTR szItem)
{
HSZ hszServName = DdeCreateStringHandle(idInst,szSvr,CP_WINANSI);
HSZ hszTopic = DdeCreateStringHandle(idInst,szTopic,CP_WINANSI);
HSZ hszItem = DdeCreateStringHandle(idInst,szItem,CP_WINANSI);
HCONV hConv= DdeConnect(idInst,hszServName,hszTopic,NULL);
HDDEDATA hData;
DWORD dwResult;
char szBuffer[100];
DWORD dwLength;
switch(iType)
{
case XTYP_REQUEST:
// 向服务器发送请求
hData = DdeClientTransaction(NULL,0,hConv,
hszItem, CF_TEXT, iType, 5000, &dwResult);
// 从服务器取得返回值
dwLength = DdeGetData(hData, (LPBYTE)szBuffer,sizeof(szBuffer), 0);
if (dwLength > 0)
m_strReply=szBuffer;
break;
case XTYP_EXECUTE:
strcpy(szBuffer,m_strRequest);
// 向服务器发送执行命令
hData = DdeClientTransaction((LPBYTE)szBuffer,
sizeof(szBuffer), hConv,
hszItem, CF_TEXT, iType, 5000, &dwResult);
break;
}
DdeDisconnect(hConv);
DdeFreeStringHandle(idInst,hszServName);
DdeFreeStringHandle(idInst,hszTopic);
DdeFreeStringHandle(idInst,hszItem);
}
3.9 本章小结
本章首先介绍了一些有关进程间通信的背景知识,便于读者弄清一些名词和概念,对进程间的通信有更深入的了解。
本章介绍了简单的本地进程之间的通信技术,它们也是较为实用的通信技术。还有另外一些简单的通信技术没有介绍,例如,使用一个临时文件、使用Windows注册表等,这些技术较容易,可以自己研究。一些高级的进程间通信技术将在以后章节介绍。
还把自定义消息、WM_COPYDATA消息、内存读写函数、FileMapping和剪贴板通信技术用一个服务程序和一个客户程序实现,如图3.1和图3.2所示。其中的数据发送类型如这两张图所示,并且发送字符串的方法可以用来发送较大的数据。