动态数据交换(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文件正确的系统 路径。
使用DDE通信的实例
由上面的介绍可知,可以编写基于消息DDE应用程序,也可以编写应用DDEML的应用程序。对于前者,实现的方法较复杂,这里不做介绍。这里介绍一个应用DDEML编写的DDE通信实例。
为了便于管理,这里把这个程序封装成一个CMyDde类,下面介绍这个类。CMyDde类头文件如下:
// DDE.h: 定义CMyDde类
//
#ifndef _DDE_H_INCLUDED
#define _DDE_H_INCLUDED
#include
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);
}