win32数据通讯技术
一、 前言 众所周知,Windows是一个多任务操作系统。所谓多任务,就是在同一时间可以运行多个应用程序,如当我使用WORD程序书写本篇文章的同时运行MP3播放器欣赏阿杜的忧郁情歌。多任务机制确实使PC的世界变得更加丰富多彩,但是同时也带给我们程序员许多技术难题,如本文将来探讨的主题――Win32应用进程间数据通讯问题,就是这些技术难题中的比较典型的一种。 二、 问题的提出 Win32为提高系统的安全性,在内存管理方面采用安全的独立编址机制。在其中运行的每个进程都有自己的独立的地址空间,一个进程不能访问另外进程的地址空间。每个进程都拥有4GB的虚拟地址空间,多个进程可能具有相同的虚拟地址指针,但每个进程的指针所指向的物理内存则不同。 Win32这样处理进程地址空间,却是提高了系统的安全性,使进程数据得到有效的保护,确保只有本进程才能访问到进程数据。这种机制很好地防止非法程序对进程数据的读取和修改,但同时也阻止了合法程序对进行数据的存取,这也是造成了Win32应用进程间数据通讯难题的主要原因。 本文我们将重点探讨解决Win32应用进程间数据通讯问题的有效方法,并同时编写进程短信服务程序实例,以帮助大家理解本文介绍的相关技术。 三、 解决的方法 任何情况下的数据通讯问题,都需要解决两个问题,即数据传输和通告机制。数据传输解决如何将进程A的数据通过一些中间环节正确无误的传送到进程B的内存中。通告机制解决进程A如何将数据准备状态信息告之进程B,而进程B如何将数据需求信息告之进程A等问题。 1、 数据传输问题的解决方法 数据传输是解决应用进程间数据通讯问题的核心。我们可以采用Windows提供的多种数据通讯机制来实现数据传输的解决方案,但迄今为止没有哪一种方案是完美无缺的。因此,只有学习并了解了它们的优缺点后,才能在特定的情况下选择最佳方案,以满足最终的要求。 下面我们将对常用的数据传输方法进行一下系统的介绍,读者可根据应用的特点选择最佳的方法。 ● 使用内存映射文件 Windows中的内存映射文件的机制为我们高效地操作文件提供了一种途径,它允许我们在WIN32进程中保留一段内存区域,把目标文件映射到这段虚拟内存中。 实现内存映射文件可按以上步骤: 第一步,在发送数据的进程中使用CreateFileMapping 函数创建一个的共享内存,可以为其取一个公共名称。 HANDLE CreateFileMapping( HANDLE hFile, // 文件的句柄, 为OxFFFFFFFF为创建进程间共享的对象。 LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全属性。 DWORD flProtect, // 保护方式。 DWORD dwMaximumSizeHigh, //长度的最大值。 DWORD dwMaximumSizeLow, //长度的最小值。 LPCTSTR lpName // 映射文件名称。 ); 其中,flProtect可以是PAGE_READONLY或是PAGE_READWRITE。如果多进程都对同一共享内存进行写访问,则必须保持相互间同步。映射文件还可以指定PAGE_WRITECOPY标志,可以保证其原始数据不会遭到破坏,同时允许其他进程在必要时自由地操作数据的拷贝。 在创建文件映射对象后使用可以调用MapViewOfFile函数映射到本进程的地址空间内。 下面说明创建一个名为MySharedMem的长度为4096字节的有名映射文件: HANDLE hMySharedMapFile=CreateFileMapping((HANDLE)0xFFFFFFFF), NULL,PAGE_READWRITE,0,0x1000,“MySharedMem”); 并映射缓存区视图: LPSTR pszMySharedMapView=(LPSTR)MapViewOfFile(hMySharedMapFile, FILE_MAP_READ|FILE_MAP_WRITE,0,0,0); 其他进程访问共享对象,需要获得对象名并调用OpenFileMapping函数。 HANDLE hMySharedMapFile=OpenFileMapping(FILE_MAP_WRITE, FALSE,“MySharedMem"); 一旦其他进程获得映射对象的句柄,可以像创建进程那样调用MapViewOfFile函数来映射对象视图。用户可以使用该对象视图来进行数据读写操作,以达到数据通讯的目的。 当用户进程结束使用共享内存后,调用UnmapViewOfFile函数以取消其地址空间内的视图。 ● 使用WM_COPYDATA消息 当一个应用程序向另一个应用程序传递数据时所发出的消息时,Windows将会产生WM_COPYDATA消息。我们可以将数据附在该消息中传送给另外的进程,一般情况下,可以将源进程中的数据存在一个结构体中,再将该结构的指针做为消息的参数进行数据传递。聪明的读者一定很问,为什么偏偏只用WM_COPYDATA,如果定义一个公共的消息是否也可以进行数据传递呢?我的回答是NO,仅仅只有WM_COPYDATA才能完成这个工作。 上面我们说过,我们是通过使用消息函数的参数来传递数据结构的指针,进而实现数据的传送的。对于一般的消息处理函数而说,它们仅仅只能通过参数获得源进程的数据存放的虚拟地址,如果以此地址访问的数据是自己进程地址空间的数据,不可能访问到源进程的地址空间。所以使用一般的消息来传递数据的说,只能传递数据所在内存的地址值,而不能访问到真正的数据内容。消息WM_COPYDATA则不同,它不仅可以传递数据所在内存的地址,而且还能访问(仅仅只读,不能写)数据内容,其中真正的数据传输过程由Windows内部实现,其实Windows内部也是通过内存映射机制的实现的,不过对开发者来说是透明的。 ● 使用DDE(动态数据交换) 每个Windows程序员都不会对DDE(动态数据交换)感到陌生,它是最早的基于Windows的数据交换方法,有三种方式可供选择:冷连接、温连接和热连接。一般都是由客户端向服务器端发出连接申请,并且必须指明服务器端的名字和标题。在连接建立后,数据可以双向流动。典型的例子如抓图软件SnagIt,它提供了DDE接口,能够让其它应用程序来控制它。DDE是完全向后兼容的,从16位平台转到32位,源代码几乎不用修改。 DDE还有网络功能。使用过Win311 For Workgroup的人大概都还记得,它自带一个非常吸引人的小程序“Chat”,能使两台计算机通过网络实时交谈,这在当时几乎是一项创举。可是很少有人知道“Chat”使用的是一种特殊的DDE,即NetDDE。它的基本工作原理仍然是DDE,但它能使一台计算机向在同一个网络中的另一台终端发消息,而不像普通DDE 只能局限在同一台机器上。与其它的数据交换方式相比,DDE已不够先进,而且Microsoft也不再积极支持DDE,所以它的前景不被看好。 除以上三者方法以外,还有使用Mailslot/Pipe、共享DLL、Clipboard、DCOM等方法来实现数据传输。 2、 通告机制的解决方法 通告机制一般都采用Windows的消息机制来实现。但它与一般在进程内定义的消息有所不同,因这个消息是要传送到进程外的,当进程通过SendMessage函数将自定义的消息发送给另外进程时,该进程消息处理器必需能够识别出此消息。为此,数据通讯双方进程都必须先进行消息注册。消息注册可使用Windows API RegisterWindowsMessage()函数来实现。 一般情况下,数据通讯双方进程按以下几个步骤来实现通告: 源进程: //定义消息ID。 const UINT ugMsg; //注册消息。 ugMsg = :: RegisterWindowsMessage(“MyNotify”); //寻找目标进程中接收消息的窗口。 CWnd* pWndRecv=FindWindow(lpClassName,“Receive”); //发送消息。 PWndRecv->SendMessage(ugMsg,0,0); 目标进程: //定义消息ID。 const UINT ugMsg; //注册消息。 ugMsg = :: RegisterWindowsMessage(“MyNotify”); 另外,使用MFC消息映射机制创建一个消息处理函数,在消息处理函数中使用上面介绍的数据传输方法操作数据。 四、 实现实例 实践是检验真理的标准,下面我们将创建一个实例来检验以上所述的原理。该实例由两应用进程组成,即Send进程和Receive进程。实例模拟Windows信使服务,将Send进程中输入的字符串信息通过进程间通讯机制传送到Receive进程。 由于在这两个进程间需要进行大量的字符串数据交换,并且对通讯的实时性要求不高,所以选择使用内存映射文件(或称共享内存)方法来完成进程间的数据传输工作。内存映射文件方式的主要优点是将共享的虚拟内存空间使用类似文件的模式来管理,使其概念清晰、实现简单以及交换数据量大,但是其通讯效率较低,并且在网络环境中可靠性较差。当用户在Send进程中输入发送字符串后,Send进程首先将这些字符串拷贝到共享内存空间中,接着向Receive进程发送自定义的全局消息(或广播消息)通告Receive进程已有数据发送过来。Receive进程使用消息处理函数来监侦接收数据的状态,一旦有通告消息,则将从共享内存中拷贝字符串到进程变量中,并显示于消息框中。 实例项目创建步骤如下: 1、启动Visual C++,创建空的工程SMS。 2、插入新项目Send,选择对话框模式。 3、编辑Send项目资源IDD_SEND_DIALOG,加入文本框IDC_EDIT1和按钮IDC_SEND,并使用ClassWizard为IDC_EDIT1加入CString类型的成员变量m_szSend,为IDC_SEND按钮添加CLICK事件。 4、打开SendDlg.h文件,在CsendDlg类声明中加入如下私有变量: private: HANDLE hMySharedMapFile; LPSTR pszMySharedMapView; UINT ugMsg; 5、打开SendDlg.cpp文件,在 CSendDlg::OnInitDialog()的return TRUE语句前加入如下代码: //创建内存映射文件。 hMySharedMapFile=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL, PAGE_READWRITE,0,0x1000,"MySharedMem"); //映射到进程内存中。 pszMySharedMapView=(LPSTR)MapViewOfFile(hMySharedMapFile, FILE_MAP_READ|FILE_MAP_WRITE,0,0,0); //注册消息。 ugMsg=::RegisterWindowMessage("SMS"); 在Send按钮CLICK事件处理函数中加入如下代码: void CSendDlg::OnSend() { // TODO: Add your control notification handler code here UpdateData(TRUE); strcpy(pszMySharedMapView,(LPTSTR)(LPCTSTR)m_szSend); //寻找目标进程中接收消息的窗口,#32770为Receive窗口类名。 CWnd* pWndRecv=FindWindow("#32770","Receive"); //发送消息。 pWndRecv->SendMessage(ugMsg,0,0); //或使用广播发送消息: //::SendMessage(HWND_BROADCAST,ugMsg,0,0); } 6、编译Send项目。 7、在工程SMS中新添项目Receive,选择对话框模式。 8、编辑Send项目资源IDD_RECEIVE_DIALOG,加入文本框IDC_RECEIVE,并使用ClassWizard为IDC_RECEIVE加入CString类型的成员变量m_szReceive。 9、打开ReceiveDlg.h文件,在CreceiveDlg类声明中加入如下私有变量: private: HANDLE hMySharedMapFile; LPSTR pszMySharedMapView; 10、打开ReceiveDlg.cpp文件,首先加入全局变量声明如下: UINT ugMsg; 11、加入全局ugMsg消息处理机制代码,首先打开ReceiveDlg.h文件, //{{AFX_MSG(CReceiveDlg)与//}}AFX_MSG代码之间插入如下代码: afx_msg LRESULT OnReceiveSMS(WPARAM wParam, LPARAM lParam); 接着打开ReceiveDlg.cpp文件, //{{AFX_MSG_MAP(CReceiveDlg)与//{{AFX_MSG_MAP代码之间插入如下代码: ON_REGISTERED_MESSAGE(ugMsg,OnReceiveSMS ) 最后,在ReceiveDlg.cpp文件中加入消息处理函数,代码如下: LRESULT CReceiveDlg::OnReceiveSMS(WPARAM wParam,LPARAM lParam) { m_szReceive = (LPCTSTR)pszMySharedMapView; UpdateData(FALSE); AfxMessageBox("Receive SMS OK!"); return 0; } 12、打开ReceiveDlg.cpp文件,在CReceiveDlg::OnInitDialog()函数的return TRUE代码前插入如下代码: //打开由Send进程创建的内存映射文件。 hMySharedMapFile=OpenFileMapping(FILE_MAP_WRITE,FALSE, "MySharedMem"); //映射到进程内存中。 pszMySharedMapView=(LPSTR)MapViewOfFile(hMySharedMapFile, FILE_MAP_READ|FILE_MAP_WRITE,0,0,0); //注册消息。 ugMsg=::RegisterWindowMessage("SMS"); 13、编译Receive项目。 14、测试两个程序的运行情况。首先运行Send.exe程序,再运行Receive.exe程序,在Send中的短信输入框中输入“您好!我是中国人。”后,在Receiver中的短信接受框马上就会出现这句话。