Windows 进程通信 之 DDE技术

DDE (Dynamic Data Exchange,DDE)动态数据交换,是一种进程间通信机制,它最早是随着Windows由微软提出的。当前大部分软件仍旧支持DDE,但最近十年里微软已经停止发展DDE技术,只保持对它给予兼容和支持。但程序猿仍可以利用DDE技术来编写自己的数据交换程序。

一、使用DDE技术通信原理

  两个同时运行的程序间通过DDE方式交换数据是C/S关系(客户端/服务器),一旦客户和服务器建立连接关系,则当服务器中的数据发生变化后就会马上通知客户端。通过DDE方式建立的数据连接通道是双向的,即客户端不仅能够读取服务器中的数据,而且可以对其进行修改。

  DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自定义的数据格式.但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答,如从菜单中选择粘贴命令。尽管DDE也可以由用户启动,但它继续发挥作用,一般不必用户进一步干预。

  DDE有三种数据交换方式:

(1):冷连接(Cool Link):数据交换是一次性数据传输,当服务器中的数据发生变化后不通知客户,但是客户可以随时从服务器读写数据。

(2):温连接(Warm Link):当服务器中的数据发生变换后马上通知客户端,客户得到通知后将数据取回。

(3):热连接(Hot Link):当服务器中的数据发生变化后马上通知客户端,同事将变化的数据直接发送给客户。

  DDE客户程序向DDE服务器程序请求数据时,必须首先知道服务器的名称(DDE Service名)、DDE主题名(Topics名)、请求哪一个数据项的项目名称(Items名)。

  DDE Server名应该具有唯一性,否则容易产生混乱。通常DDE Service就是服务器的程序名,不是绝对的,它是由程序设计人员在程序内部设定好的,并不是通过修改程序名称就可以改变的。

  Topics名和Items名也是由DDE Service在其内部设定好的,所有服务程序的Service名、Topics名都是注册在系统中,当一个客户向一个服务器请求数据时,客户必须向系统报告服务器的Service名和Topics名。只有当Service名、Topics名与服务器内部设定的名称一致时,系统才将客户的请求传达给服务器。

  当服务名和Topics名相符时,服务器马上判断Items名是否合法。如果请求的Item名是服务器中的合法数据项,服务器即建立此项连接,建立连接的数据发生数值变化后,服务器会及时通知客户。一个服务器可以有多个Topics名,Items名的数量也不受限制。

  DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式,进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。但DDE有个明显的缺点就是,通信效率低下,当通信量较大时数据刷新速度慢,在数据较少时 DDE较实用。

二、如何使用DDEML编写程序
  早期的DDE基于消息机制,应用程序间的消息传递需程序员调度.由于DDE消息通信牵涉的操作细节颇多,实现完全的DDE协议不是非常容易的事情,而且不同的开发者对协议的解释也略有不同.为了使用方便起见,微软提供DDE管理库(TheDDE Management Library,简称DDEML).DDEML专门协调DDE通信,给DDE应用程序提供句柄字符串和数据交换的服务,消除了早期由于DDE协议不一致所引起的问题.

  使用DDEML开发的应用程序(客户/服务器)无论在运行一致性方面,还是在程序相互通信方面,性能均优于没有使用DDEML的应用程序.而且DDEML的应用使得开发支持DDE的应用程序容易了许多,因为DDEML(这是个DLL)担起了内务府总管的工作.使用DDEML后,实际上客户和服务器之间的多数会话并不是直达对方的,而是经由DDEML中转,即用Callback函数处理DDE交易(Transaction),而早期的消息通信是直接的.在调用其他DDEML函数前,客户/服务器必须调用DdeInitialize()函数,以获取实例标识符,注册DDE Callback函数,并为Callback函数指定事务过滤.对于服务器,在使用DdeInitialize()初始化后,调用CreateStringHandle()建立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 <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)

CBF_FAIL_POKES
回调函数(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)

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);
}