VC 进程间通信

我们既可以用非标准的进程间通信技术,如Windows消息、内存映射和内存共享等,也可以用标准的通信技术。微软标准进程间通信技术的发展过程如下所述。

1)进程间通信初期

自从有Windows操作系统后,剪贴板(Clipboard)首先解决了不同程序间的通信问题(由剪贴板作为数据交换中心,进行复制、粘贴的操作)。但是剪贴板传递的都是数据,应用程序开发者得自行编写、解析数据格式的代码。于是动态数据交换(Dynamic Data ExchangeDDE)的通信协定应运而生,它可以让应用程序之间自动获取彼此的最新数据。但是,解决彼此之间的数据格式转换仍然是程序员沉重的负担。对象的链接和嵌入(Object Linking and EmbeddedOLE)的诞生把原来应用程序的数据交换提高到对象交换,这样程序间不但获得数据,而且也可以获得彼此的对象,并且可以直接使用彼此数据内容。

2OLE(对象链接与嵌入)

1991年制定的OLE1.0规范主要解决多个应用程序之间的通信和消息传递问题,微软希望第三方开发商能够遵守这个规范,以便在当时的Windows平台上的应用程序能够相互协调工作,更大地提高工作效率。然而事与愿违,只有很少的软件开发商支持它。为此,微软于1993年发布了新的规范——OLE2.0,它在原有的基础上完善并增强了以下各方面的性能:OLE自动化,一个程序有计划地控制另一个程序的能力;OLE控件,小型的组件程序,可嵌入到另外的程序,提供自己的专有功能;OLE文档,完善了早期的混合文档功能,不仅支持简单的链接和嵌入,还支持在位激活、拖放等功能。

3ActiveX战略

OLE1.0相比,OLE2.0得到了很多软件厂商的支持。许多程序设计人员编写了大量的实现OLE自动化服务器功能的组件(不一定是EXE文件),这些组件一般不求功能齐全、强大,而是可以实现专门的功能,被其他程序编程控制,由此承袭OLE的名字,称为OLE控件。它们在文件名中的扩展名一般为ocxOLE Control Extension)。微软刚刚赢得广大软件厂商的支持,使OLE技术深入人心,然而天不如人愿,国际互联网的超速发展让比尔·盖茨始料未及。加上早期的OLE1.0不得人心,导致后来的人们总把在Word中插入一个图形当作OLE技术的全部,各类资料在介绍新OLE技术时命名也不统一,造成很大的混乱。针对这些情况,微软在1996年重新制订了一个关于OLE的规     ——OLE 96。这个规范扩展了 OLE控件的能力,并贯彻微软的Internet战略使它更易于在网络环境中使用,还考虑命名混淆的问题,重新给OLE控件贴上一个标签——ActiveX控件。不仅如此,以前的什么OLE文档也相应称为ActiveX 文档了。总之,为了满足Internet战略,微软把OLE换成了ActiveX,企图使人们重新看待新的OLE——ActiveX,把它看成网络上的解决软件组件问题的标准。

4OLE/ActiveXCOM/DCOM比较

OLE/ActiveX名称比COM/DCOM更为我们熟悉,其实OLEActiveX是商业名称,它们的发展过程为OLEActiveX(网络OLE)。COMDCOM是纯技术名词,它们是OLE/ActiveX基础,其发展过程为COMDCOM(网络COM),其中COMComponent Object Model,组件对象模式)是在OLE2.0中建立的规范。OLE/ActiveX不仅可以实现进程之间的通信,而且可以创建进程,它们是类厂组件对象。

3.2.2 应用程序与进程

应用程序和进程在概念上是有一定区别的,前者是静态的程序代码,而后者是动态的实体。只有应用程序加载到系统中后才能成为一个进程。Windows进程分为独立进程和共享进程两种。一般情况下,人们把独立运行的程序称为进程,其实这只是独立进程。在实际情况下也常常遇到另一种情况,即一个应用程序可能启动多个进程,一个进程空间可以运行多个程序,这就是共享进程。例如,同一个应用程序重复运行就启动了多个进程;而在一个进程中调用其他程序,或者通过程序挂钩,这就使同一个进程空间里运行了多个程序。这里着重讨论独立进程之间的通信。

3.2.3 进程之间通信的类型

根据不同的标准,进程之间通信类型有不同的划分方法。实际中也有多种划分方法,这里只给出几种划分方法如下所述。

1)低级通信和高级通信

·         低级通信:只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量和管程机制。其第一个缺点为传送信息量小、效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信;第二个缺点是编程复杂,直接实现通信的细节,容易出错。

·         高级通信:能够传送任意数量的数据,包括共享存储区、管道、消息等。

2)直接通信和间接通信

l       直接通信:信息直接传递给接收方,如管道,在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址;在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址。

l       间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如剪贴板。通常接收和发送方的数目可以是任意的。

3)本地通信和远程通信

l       本地通信方式:这种通信又称之为同机通信,它是在同一台计算机上的程序之间进行的,也就是说客户进程和服务进程位于同一台计算机上。

l       远程通信方式:这种通信又称之为网间的进程通信,要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。在这种通信中,首先要解决的是网络间的进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋予某个进程号5,在B机中也可以存在5号进程,因此,“5号进程这句话就没有意义了。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间的进程通信还要解决多重协议的识别问题。

3.3 使用自定义消息通信

Windows程序与其他类型程序的区别就是使用消息,例如键盘或鼠标消息等,在DOS系统下的程序没有定义消息。在Windows操作系统中,消息不但可以用于进程内的通信,也可以用于进程间的通信。这里重点介绍进程间的消息通信。

3.3.1 通过自定义消息实现进程间通信的方法

消息分为两种,即系统消息和用户(程序设计者)自定义消息。系统消息定义从00x3FF,可以使用0x4000x7FFF定义自己的消息。Windows0x400定义为WM_USER。如果想定义自己的一个消息,可以在WM_USER上加上一个值。

还有一种自定义窗口消息的方法是用RegisterWindowsMessage()函数来注册这个消息。与在WM_USER上加上某个数相比,它的好处是不必考虑所表示的消息标识符是否超出工程的允许范围。

要想用消息实现进程间通信,则需要在这两个程序中定义或注册相同的消息,才能保证数据通信顺利进行。用户自定义消息的方法如下:

l

#define WM_COMM      WM_USER+100

const UINT wm_nRegMsg=RegisterWindowMessage("reg_data");

有了这两种定义的消息后,可以用如下的方法来发送消息。

pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg);

pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg);

其中pWnd为接收这个消息的窗口句柄,uMsg为要通过消息发送的数据,是长整型。这两个消息的发送可以分别在一个发送函数里实现,其具体的做法见以后的实例。通过实验可知,这两种自定义消息的发送效果是一样的。

在接收消息的程序中,除与发送消息的程序需要定义相同的消息外,还需要定义相应的消息映射和消息映射函数,消息的映射方法如下:

ON_MESSAGE(WM_COMM,OnUserReceiveMsg)

ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg)

与以上消息映射对应的函数定义如下:

void CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM lParam)

{

// 增加用户自定义程序代码

}

//--------------------------------------------------------------------

void CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam)

{

// 增加用户自定义程序代码

}

其中OnUserReceiveMsg()函数为WM_COMM消息的映射函数,OnRegReceiveMsg()函数为wm_nRegMsg消息的映射函数。可以看出,这两种消息的映射函数形式是一样的。

3.3.2 通过自定义消息实现进程间通信的实例

为说明以自定义消息实现进程之间的通信,作者用VC++ 编写了这样的程序。有两个对话框程序,其中一个为发送程序,另一个为接收程序。在这两个程序中分别定义了两个消息WM_COMMwm_nRegMsg,在CDataSendDlg类中增加了用于发送数据的两个函数,即void CDataSendDlg::OnSendUsermsg()void CDataSendDlg::OnSendRegmsg()。它们的源代码如下:

void CDataSendDlg::OnSendUsermsg()

{

UpdateData();                                   // 更新数据

CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv"));   // 查找DataRecv进程

if(pWnd==NULL){

AfxMessageBox(TEXT("Unable to find DataRecv."));

return;

}

UINT uMsg;

uMsg=atoi(m_strUserMsg);

pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg);   // 发送.

}

//--------------------------------------------------------------------

void CDataSendDlg::OnSendRegmsg()

{

UpdateData();                                   // 更新数据

CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv"));   // 查找DataRecv进程

if(pWnd==NULL){

AfxMessageBox("Unable to find DataRecv.");

return;

}

UINT uMsg;

uMsg=atoi(m_strRegMsg);

pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg); // 发送

}

在接收数据的程序中要做三件事:定义自定义消息;定义消息映射表;定义消息映射函数。自定义消息的方法如前面所述。在CDataRecvDlg类中增加了两个用于接收数据的函数,即void CDataRecvDlg::OnUserReceiveMsg()void CDataRecvDlg:: OnRegReceiveMsg()。消息映射表如下:

BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog)

//{{AFX_MSG_MAP(CDataRecvDlg)

ON_MESSAGE(WM_COMM,OnUserReceiveMsg)

ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg)

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

消息映射表中的映射函数名的格式是一样的。其实它们可以用同一个函数,为了说明方便,这里把它们的名字取为不一样。接收数据程序中的消息映射函数的源代码如下:

void CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM lParam)

{

m_strUserMsg.Format("%d\n",int(lParam));

UpdateData(FALSE);   // 更新数据

}

//--------------------------------------------------------------------

void CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam)

{

m_strRegMsg.Format("%d\n",int(lParam));

UpdateData(FALSE);   // 更新数据

}

从上面的实例中可以看出,以自定义消息来进行进程之间的通信存在一定的局限性,即所发送的数据只能是长整型,而对于字符串,则不能进行通信。要进行字符串或大批量的数据的传输,则需要采用其他的通信方法。

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消息不能发送HDCHBITMAP之类的东西,它们对于目标进程来说是无效的。目标进程得到这些数据不能在原进程作任何事情,因为它们属于不同的进程。

与其他进程通信方法一样,要实现进程间的数据通信,在发送数据的程序中,首先要找到接收数据进程的窗口句柄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为接收到的字符串,pCopyDataStructCOPYDATASTRUCT结构指针。注意由pCopyDataStruct直接得到的m_strCopyData字符串长度可能不是实际发送的字符串长度,需要用发送字符串时所给定的字符串长度来进一步确定,其长度由pCopyDataStruct ->cbData来得到。

 

 

VC 进程间通信

2010-05-07 10:35

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 LibraryDLL)不仅可以用来共享程序代码,而且可以用来共享数据。用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.exeuser2.exe之间的通信。

Win32DLL装入全局内存并把DLL映射到每个程序的地址空间,并且不需要把DLL映射到每个进程相同的地址空间上。DLL成为载入它的进程的一部分,而不像在Win16中,成为系统的一部分。

对于Win16来说,在DLL中共享数据是很容易的,因为访问DLL的每个应用程序都可以得到它的全局静态变量,如图3.3所示。在Win32中,对每个载入DLL的进程,DLL获取一个该进程的唯一的全局静态变量的副本,如图3.4所示。

3.6.2 使用DLL通信的方法

从上面可以看出,对于Win32DLL,所有载入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.dlldllcom.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文件后,就可以利用它实现进程之间的通信。可以设计两个应用程序,dlluser1dlluser2,在这两个程序中把动态库文件头DllObj.h和动态库dllcom.lib包含其中,即

#include "DllObj.h"

#pragma comment(lib,"dllcom.lib")

然后,用MFC VC++编译器进行编译和连接,这样就可以用SetValueString()GetValue- String()函数进行通信了。

 

 

VC 进程间通信

2010-05-07 10:36

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++ 编写两个对话框应用程序。为了方便,仍然借用前面所使用的对话框类CDataSendDlgCDataRecvDlg

为了把文本放置到剪贴板上,在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 ExchangeDDE)也是一种进程间通信形式。它最早是随着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_CONNECTXTYP_REQUESTXTYP_ EXECUTE,还有其他的一些选项,见微软的MSDN说明。XTYP_CONNECT响应于客户程序使用的DdeConnect()函数。XTYP_REQUESTXTYP_EXECUTE分别响应于客户程序中使用DdeClientTransaction()函数的XTYP_REQUESTXTYP_ 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_REQUESTXTYP_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);

}

小结

本章首先介绍了一些有关进程间通信的背景知识,便于读者弄清一些名词和概念,对进程间的通信有更深入的了解。

本章介绍了简单的本地进程之间的通信技术,它们也是较为实用的通信技术。还有另外一些简单的通信技术没有介绍,例如,使用一个临时文件、使用Windows注册表等,这些技术较容易,可以自己研究。一些高级的进程间通信技术将在以后章节介绍。

还把自定义消息、WM_COPYDATA消息、内存读写函数、FileMapping和剪贴板通信技术用一个服务程序和一个客户程序实现,如图3.1和图3.2所示。其中的数据发送类型如这两张图所示,并且发送字符串的方法可以用来发送较大的数据。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值