进程间通信

来源:互联网 发布:淘宝 知乎精选 编辑:程序博客网 时间:2024/06/05 16:43

进程间通信 - 剪贴板实现

进程间通信 - 剪贴板实现

引子

由于在启动一个进程后,操作系统会给这个进程分配 4GB 的私有地址空间,至于为何有 4GB 这么大,

那得考虑进程的私有地址空间和实际物理内存地址空间之间的映射以及页交换等等细节问题了,这里不予讨论,

从名字就可以知道,既然操作系统给每一个进程分配的是私有地址空间,

自然,这段地址空间也只有这个进程自己才能访问了,不然还称为私有干吗呢?

既然这段私有地址空间只能由进程本身访问,那也就说明别的进程是不能够随意的访问这个进程的地址空间的,

而本篇博文介绍的是进程间的通信,而上面又说任意两个进程之间是并能够互相访问对方的私有地址空间的,

都不能访问了,那还通信个屁啊 ?

自然上面的访问对方进程的私有地址空间是行不通了,那应该还有其他办法的 !!!

解决方法:

如果我在物理内存中划分出一块内存,这一块内存不为任何的进程所私有,但是任何的进程又都可以访问这块内存,

那么 进程 A 就可以往这块内存中存放数据 Data ,然后 进程 B 也是可以访问这块内存的,从而 进程 B 就可以访问到数据 Data 了,

这样不就实现了 进程 A 进程 B 之间的通信了 !!!

而上面的这种思路就是剪贴板了。

当然解决进程间通信还有好几种思路,这将会在后续博文中介绍,本篇博文暂只介绍利用剪贴板来实现进程间的通信。

            

            

剪贴板定义

剪贴板是由操作系统维护的一块内存区域,这块内存区域不属于任何单独的进程,但是每一个进程又都可以访问这块内存区域,

而实质上当在一个进程中复制数据时,就是将数据放到该内存区域中,

而当在另一个进程中粘贴数据时,则是从该块内存区域中取出数据。           

            

             

剪贴板操作

其实在剪贴板中也就那么几个 API 在使用,所以在这里的还是本着 API 介绍为主,

不管三七二十一,先列出常用的 API 再说(到后面结合 Demo 的使用即可)。

                     

剪贴板的打开 – OpenClipboard

要想把数据放置到剪贴板中,则必须先打开剪贴板,而这是通过 OpenClipboard 成员函数实现:

BOOL  OpenClipboard(HWND  hWndNewOwner );

第一个参数 hWndNewOwner 指向一个与之关联的窗口句柄,即代表是这个窗口打开剪贴板,

如果这个参数设置为 NULL 的话,则以当前的任务或者说是进程来打开剪贴板。

如果打开剪贴板成功,则该函数返回非 0 值,如果其他程序已经打开了剪贴板,

那么当前这个程序就无法再打开剪贴板了,所以会致使打开剪贴板失败,从而该函数返回 0 值。

其实这也好理解,你想啊,剪贴板总共才那么一块内存区域,你 进程 A 要往里面写数据,你 进程 B 又要往里面写数据,那不乱套去,

解决这个乱套的办法就是,如果我 进程 A 正在往剪贴板里面写数据(可以理解为 进程 A 打开剪贴板了),那么 进程 B 就不能往剪贴板里头写数据了,

既然要让 进程 B 不能往剪贴板中写数据了,那我就让 进程 B 打开剪贴板失败不就得了。

所以如果某个程序已经打开了剪贴板,那么其他应用程序将不能修改剪贴板,

直到打开了剪贴板的这个程序调用了 CloseClipboard 函数,

并且只有在调用了 EmptyClipboard 函数之后,打开剪贴板的当前窗口才能拥有剪贴板,

注意是必须要在调用了 EmptyClipboard 函数之后才能拥有剪贴板。

                             

剪贴板的清空 - EmptyClipboard

这个函数将清空剪贴板,并释放剪贴板中数据的句柄,然后将剪贴板的所有权分配给当前打开剪贴板的窗口,

因为剪贴板是所有进程都可以访问的,

所以应用程序在使用这个剪贴板时,有可能已经有其他的应用程序把数据放置到了剪贴板上,

因此该进程打开剪贴板之后,就需要调用 EmptyClipboard 函数来清空剪贴板,

释放剪贴板中存放的数据的句柄,并将剪贴板的所有权分配给当前的进程,

这样做之后当前打开这个剪贴板的程序就拥有了剪贴板的所有权,因此这个程序就可以往剪贴板上放置数据了。

BOOL EmptyClipboard(void);

                              

剪贴板的关闭 - CloseClipboard

如果某个进程打开了剪贴板,则在这个进程没有调用 CloseClipboard 函数关闭剪贴板句柄之前,

其他进程都是无法打开剪贴板的,所以我们每次使用完剪贴板之后都应该关闭剪贴板。

注意,这里的关闭剪贴板并不代表当前打开剪贴板的这个程序失去了对剪贴板的所有权,

只有在别的程序调用了 EmptyClipboard 函数之后,当前的这个程序才会失去对剪贴板的所有权,

而那个调用 EmptyClipboard 函数的程序才能拥有剪贴板。

BOOL CloseClipboard(void);

                           

数据发送到剪贴板 - SetClipboardData

可以通过 SetClipboardData 函数来实现往剪贴板中放置数据,这个函数以指定的剪贴板格式向剪贴板中放置数据。

HANDLE  SetClipboardData(UINT uFormat,  HANDLE hMem ); 

第一个参数 uFormat 用来指定要放到剪贴板上的数据的格式,

比如常见的有 CF_BITMAP CF_TEXTCF_DIB 等等(其他格式可以参考 MSDN)。

第二个参数 hMem 用来指定具有指定格式的数据的句柄,该参数可以是 NULL

如果该参数为 NULL 则表明直到有程序对剪贴板中的数据进行请求时,

该程序(也就是拥有剪贴板所有权的进程)才会将数据复制到剪贴板中,也就是提供指定剪贴板格式的数据,

上面提及的就是延迟提交技术,这个延迟提交技术将会在后面做详细的介绍。

              

剪贴板中数据格式判断 – IsClipboardFormatAvaliable

BOOL  IsClipboardFormatAvailable( UINT format ); 

该函数用来判断剪贴板上的数据格式是否为 format 指定的格式。

             

剪贴板中数据接收 - GetClipboardData

HANDLE  GetClipboardData( UINT uFormat ); 

该函数根据 uFormat 指定的格式,返回一个以指定格式存在于剪贴板中的剪贴板对象的句柄。

             

             

全局内存分配 – HGLOBAL

剪贴板中的内存从何而来

从上面的介绍中可以知道剪贴板其实就是一块内存,那么这块内存是什么时候分配的呢?

难不成说一开机,操作系统就给剪贴板分配个几 M 的内存的吧?

这种方式也太逊色了,你想啊,我的程序要往剪贴板中放置的数据,我事先又不晓得数据长度,

所以,一开机操作系统究竟要给剪贴板分配多少内存呢?很明显,太不动态了,不可取。

要想动态的话,那有一种方案,就是当我的程序要往剪贴板中放置数据的时候来确定要分配给剪贴板的内存的大小,

很明显,既然我都知道要往剪贴板中放置那些数据了,自然我也就知道了这些数据的长度,

那么我就可以以这个数据长度来给剪贴板分配内存了,这是很动态的了吧,所以这种方案是可取的,

但关键是,当我们以前在程序中分配内存的时候,都是使用的标准 C 运行库中的 malloc 或者是 C++ 中的 new 关键字,

(当然分配内存还有很多其他的函数,比如就有内核中的执行体中就有很多分配内存的函数,这里不讨论),

而使用 malloc 或者 new 有一个问题,那就是,用这个两个东西来分配的内存空间都是在当前进程的私有地址空间上分配内存,

也就是它们两个东东所分配的内存空间为进程私有地址空间所有,并不为所有进程所共享,

上面提到了,任何进程之间都是不能访问对方的私有地址空间的,你把剪贴板中的内存分配到了你当前进程的私有地址空间上,

而其他进程又不能访问你这个进程的私有地址空间,那怎么能够访问剪贴板呢?

很明显,不能使用 malloc new 关键字来分配内存给剪贴板。

我们应该要使用另外一个特殊一点的函数来分配内存给剪贴板,

这个特殊函数所分配的内存不能够是在进程的私有地址空间上分配,而是要在全局地址空间上分配内存,

这样这个函数所分配的内存才能够被所有的进程所共享,这样,剪贴板中的数据就可以被其他的进程所访问了。

                     

GlobalAlloc 函数

GlobalAlloc 函数是从堆上分配指定数目的字节,

与其他的内存管理函数相比,全局内存函数的运行速度会稍微慢一些(等下会解释为什么会慢),

但是全局函数支持动态数据交换,同时,其分配的内存也不为任何一个进程所私有,而是由操作系统来管理这块内存,

所以用在给剪贴板分配内存空间是很适合的。

这里有读者可能会问:

为什么我们在自己的应用程序中不使用 GlobalAlloc 函数来分配内存,而是要使用 malloc 或者 new  来实现?

其实,这个也只用稍微想想就知道了,你想啊,使用 malloc 或者 new 分配的内存是在进程的私有地址空间上分配的,

这片私有地址空间都是归这个进程所拥有,所管理的,自然,在以后对这块内存的读写会快很多的,

而全局内存不属于这个进程,你下次要去访问全局内存的时候,还得通过映射转换,这样肯定是运行效率低下一些了,

简单点就可以这样理解,你使用 malloc 或者 new 分配的内存和你的进程隔得很近,程序要过去拿数据 - 得,很近吧,

而是用 GlobalAlloc 函数分配的内存和你的进程隔得很远,程序要过去拿数据 - 太远了,耗时。

应用程序在调用了 SetClipboardData 函数之后,

系统就拥有了 hMem 参数所标识的数据对象,该应用程序可以读取这个数据对象,

但是在应用程序调用 CloseClipboard 函数之前,它都是不能释放该对象的句柄的,或者锁定这个句柄,

如果 hMem 标识一个内存对象,那么这个对象必须是利用 GMEM_MOVEABLE 标识调用GlobalAlloc函数为其分配内存的。

HGLOBAL  WINAPI  GlobalAlloc( UINT  uFlags,   SIZE_T  dwBytes );

第一个参数 uFlags 用来指定分配内存的方式。其取值如下列表所示

(但是在剪贴板的使用中,由于要实现动态数据交换,所以必须得使用 GHND 或者 GMEM_MOVEABLE):

描述

GHND

GMEM_MOVEABLE GMEM_ZEROINIT 的组合。

GMEM_FIXED

分配一块固定内存,返回值是一个指针。

GMEM_MOVEABLE

分配一块可移动内存。

GMEM_ZEROINIT

初始化内存的内容为 0

GPTR

GMEM_FIXED GMEM_ZEROINIT 的组合。

第二个参数 dwBytes 用来指定分配的字节数。

                   

GlobalReAlloc 函数

HGLOBAL  WINAPI  GlobalReAlloc(HGLOBAL hMem,  SIZE_T dwBytes,  UINT uFlags);

该函数为再分配函数,即在原有的数据对象 hMem 上,为其扩大内存空间。

第一个参数 hMem 代表由 GlobalAlloc 函数返回的数据对象句柄。

第二个参数 dwBytes 指定需要重新分配的内存的大小。

第三个参数 uFlags 指定分配的方式(可以参考 GlobalAlloc 函数)。

                   

GlobalSize 函数

SIZE_T  WINAPI  GlobalSize( HGLOBAL  hMem );

该函数用来返回内存块的大小。

第一个参数 hMem 代表由 GlobalAlloc 函数返回的数据对象句柄。

               

GlobalLock 函数

LPVOID  WINAPI  GlobalLock( HGLOBAL  hMem );

该函数的作用是对全局内存对象加锁,然后返回该对象内存块第一个字节的指针

第一个参数 hMem 代表由 GlobalAlloc 函数返回的数据对象句柄。

           

GlobalUnLock 函数

BOOL  WINAPI  GlobalUnlock( HGLOBAL  hMem );

你通过上面的 GlobalLock 函数可以获得这块全局内存的访问权,

加锁的意思就是你已经在使用这块全局内存了,别的程序就不能再使用这块全局内存了,

而如果你一直不解锁,那也不是个事啊,别的程序将会一直都使用不了这块全局内存,

那还叫全局内存干吗啊?所以这个函数就是用来对全局内存对象解锁。

第一个参数 hMem 代表由 GlobalAlloc 函数返回的数据对象句柄。

                 

GlobalFree 函数

HGLOBAL  WINAPI  GlobalFree( HGLOBAL  hMem );

该函数释放全局内存块。

第一个参数 hMem 代表由 GlobalAlloc 函数返回的数据对象句柄。

               

             

Demo1 – ConsoleClipboard(剪贴板常用手法)

整个项目结构很简单:

image

ConsoleClipboard.h

#ifndef CONSOLE_CLIP_BOARD_H
#define CONSOLE_CLIP_BOARD_H
 
#include <Windows.h>
#include <iostream>
 
using namespace std;
 
const char * pStrData = "Zachary";
 
void SetClipBoardData();
 
void GetClipBoardData();
 
 
#endif
ConsoleClipboard.cpp 
#include "ConsoleClipboard.h"
 
int main(int argc, char * argv)
{
    SetClipBoardData();
    GetClipBoardData();
 
    system("pause");
}
 
void SetClipBoardData()
{
    //将 OpenClipboard 函数的参数指定为 NULL,表明为当前进程打开剪贴板
    if(OpenClipboard(NULL))
    {
        char * pDataBuf;
 
        //全局内存对象
        HGLOBAL hGlobalClip;
 
        //给全局内存对象分配全局内存
        hGlobalClip = GlobalAlloc(GHND, strlen(pStrData) + 1);
        //通过给全局内存对象加锁获得对全局内存块的引用
        pDataBuf = (char *)GlobalLock(hGlobalClip);
        strcpy(pDataBuf, pStrData);
        //使用完全局内存块后需要对全局内存块解锁
        GlobalUnlock(hGlobalClip);
 
        //清空剪贴板
        EmptyClipboard();
        //设置剪贴板数据,这里直接将数据放到了剪贴板中,而没有使用延迟提交技术
        SetClipboardData(CF_TEXT, hGlobalClip);
        //关闭剪贴板
        CloseClipboard();
 
        cout<<"设置剪贴板为:    "<<pStrData<<endl<<endl;
    }
}
 
void GetClipBoardData()
{
    if(OpenClipboard(NULL))
    {
        //判断剪贴板中的数据格式是否为 CF_TEXT
        if(IsClipboardFormatAvailable(CF_TEXT))
        {
            char *            pDataBuf;
            HGLOBAL            hGlobalClip;
 
            //从剪贴板中获取格式为 CF_TEXT 的数据
            hGlobalClip = GetClipboardData(CF_TEXT);
            pDataBuf = (char *)GlobalLock(hGlobalClip);
            GlobalUnlock(hGlobalClip);
 
            cout<<"从剪贴板中获取到数据:    "<<pDataBuf<<endl<<endl;
        }
        CloseClipboard();
    }
}

效果展示:

程序运行效果:

image

打开记事本进行粘贴操作:

image

           

            

延迟提交技术

什么是延迟提交技术?

当把数据放入剪贴板中时,一般来说要制作一份数据的副本,

也就是要分配全局内存,然后将数据再复制一份,然后再将包含这份副本的内存块句柄传递给剪贴板,

对于小数据量来说,这个没什么,但是对于大数据量的话,就有问题了,

你一使用剪贴板,就往里面复制个什么几百 MB 的数据,

那这个数据在剪贴板中的数据被其他数据取代之前都是存放在内存中的啊,

这个方法也太龌龊了,你想啊,要是我就复制了一个 500MB 的数据,然后我一直不再复制其他的东西,

那么这个 500MB 的数据就会一直驻留在内存中,咦 . . . 太可怕了 !!!太浪费内存的使用效率了 !!!

为了解决上面这个问题,就需要通过使用延迟提交技术来避免内存的浪费,

当使用延迟提交技术时,实际上,直到另一个程序需要数据时,程序才会提供这份数据,

也就是,其实我一开始 程序 A 并不往剪贴板中存放真实的数据,

而只是告诉剪贴板,我往里面放了数据(其实数据还没有放进去),

而后,如果有其他的 程序 B 访问了剪贴板中的数据,也就是执行了“粘贴”操作,

那么此时操作系统就会去检查数据是不是真正的存放在了剪贴板中,

如果剪贴板中存放了数据,那么直接把数据送出去就可以了(这就没有使用延迟提交技术了),

而如果剪贴板中没有数据,那么 Windows 就会给上次往剪贴板中存放数据(尽管没有存放实际的数据)的程序,

也就是 程序 A发送消息,

而后,我们的 程序 A 就可以再次调用 SetClipboardData 来将真实的数据放入到剪贴板中了,这样就是延迟提交技术了。

要实现延迟提交技术,则在 程序 A 中不应该将数据句柄传送给 Windows

而是在 SetClipboardData 调用中使用 NULL

然后当另外一个 程序 B 调用 GetClipboardData 函数时,

Windows 就会检查这种格式的数据在剪贴板中的句柄是否为 NULL

如果为 NULL ,则 Windows 会给程序 A发送一个消息,从而请求到数据的实际句柄,

这个数据的实际句柄是 程序 A 在响应消息的处理函数中重新调用 SetClipboardData 来提供的。

              

延迟提交技术中涉及的三个消息:

下面提及的 程序 A 代表剪贴板当前拥有者,也就是 程序 A 负责往剪贴板中写入数据,

而 程序 B 则代表从剪贴板中读取出数据,其没有对剪贴板的所有权。

            

WM_RENDERFORMAT :

程序 B 调用 GetClipboardData 时,Windows将会给程序 A 的窗口过程发送这个消息,

其中 wParam 参数的值是所要求的格式。

在处理这个消息时,程序 A 就不再需要打开或者清空剪贴板了,

也就是不需要再次调用 OpenClipboard EmptyClipboard 函数了,

为什么不需要再次调用这两个函数?

这是因为,我们一开始的时候已经调用了这两个函数(如果一开始没有调用的话,窗口根本就不会接受到这个消息),

而此举已经告诉操作系统剪贴板已经归我所有了,而且里面的数据已经被清空了,

剪贴板所有权都归我了,那还去打开个鬼啊,不是浪费嘛?

在处理这个消息时,应该为 wParam 所指定的格式创建一个全局内存块,

然后再把数据传递到这个全局内存块中,并要正确的格式和数据句柄再一次调用 SetClipboardData 函数。

也就是需要将数据真实的复制到剪贴板中了。

WM_RENDERALLFORAMTS :

如果 程序 A 在它自己仍然是剪贴板所有者的时候就要终止运行,

并且剪贴板上仍然包含着该 程序 A SetClipboardData 所设置的 NULL 数据句柄(延迟提交技术),

也就是 程序 A 当前还是剪贴板的所有者,但是用户又单击了关闭窗口,

而剪贴板中还没有真实的数据存在(因为使用了延迟提交技术),

即数据还没有被提交给剪贴板,程序 A 就要死了,则此时 程 序 A 的窗口过程将接收到这个消息,

这个消息的一般处理为打开剪贴板,并且清空剪贴板,然后把数据加载到内存中,

并为每种格式调用 SetClipboardData ,然后再关闭剪贴板即可。

WM_DESTROYCLIPBOARD :

当在 程序 B 中调用 EmptyClipboard 时,Windows将会给程序 A 的窗口过程发送这个消息。

即通知 程序 A 其已不再是剪贴板的拥有者了。

               

             

Demo2 – MFCClipboard(延迟提交技术的使用)

整个项目结构很简单:

image

主界面:

image

添加 3 个消息处理:

image

消息映射函数声明:
protected:
    HICON m_hIcon;
 
    // 生成的消息映射函数
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnDestroyClipboard();
    afx_msg void OnRenderAllFormats();
    afx_msg void OnRenderFormat(UINT nFormat);
    afx_msg void OnBnClickedBtnWrite();
    afx_msg void OnBnClickedBtnRead();
    CString m_CStrWrite;
    CString m_CStrRead;
};

消息映射实现:

void CMFCClipboardDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_WRITE, m_CStrWrite);
    DDX_Text(pDX, IDC_EDIT_READ, m_CStrRead);
}
 
BEGIN_MESSAGE_MAP(CMFCClipboardDlg, CDialogEx)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_DESTROYCLIPBOARD()
    ON_WM_RENDERALLFORMATS()
    ON_WM_RENDERFORMAT()
    ON_BN_CLICKED(ID_BTN_WRITE, &CMFCClipboardDlg::OnBnClickedBtnWrite)
    ON_BN_CLICKED(ID_BTN_READ, &CMFCClipboardDlg::OnBnClickedBtnRead)
END_MESSAGE_MAP()

消息映射函数实现

 
//WM_DESTROYCLIPBOARD 消息处理函数
void CMFCClipboardDlg::OnDestroyClipboard()
{    
    //当有另外的程序调用 EmptyClipboard 时,
    //Windows 将向当前窗口过程发送 WM_DESTROYCLIPBOARD 消息
    MessageBox(TEXT("很抱歉 , 您已失去对剪贴板的拥有权 ..."), 
        TEXT("提示"), MB_ICONINFORMATION);
 
    CDialogEx::OnDestroyClipboard();
}
 
 
//WM_RENDERALLFORMATS 消息处理函数
void CMFCClipboardDlg::OnRenderAllFormats()
{
    //当剪贴板中的数据句柄为当前程序所拥有,而当前程序又将被退出时,
    //Windows 给该程序窗口发送 WM_RENDERALLFORMATS 消息
 
    OpenClipboard();
    EmptyClipboard();
    CloseClipboard();
 
    CDialogEx::OnRenderAllFormats();
}
 
//WM_RENDERFORMAT 消息处理函数
void CMFCClipboardDlg::OnRenderFormat(UINT nFormat)
{
    //当有另外的程序访问剪贴板时
    //Windows 给该程序窗口过程发送 WM_RENDERFORMAT 消息
    int                dataNum;
    int                dataIndex;
    char *            pDataBuf;
    HGLOBAL            hGlobalClip;
 
    dataNum = this->m_CStrWrite.GetLength();
 
    hGlobalClip = GlobalAlloc(GHND, dataNum + 1);
    pDataBuf = (char *)GlobalLock(hGlobalClip);
    for(dataIndex=0;dataIndex<dataNum;dataIndex++)
    {
        pDataBuf[dataIndex] = this->m_CStrWrite.GetAt(dataIndex);
    }
    GlobalUnlock(hGlobalClip);
 
    //此时需要将有效数据写入到剪贴板中
    SetClipboardData(CF_TEXT, hGlobalClip);
 
    CDialogEx::OnRenderFormat(nFormat);
}
 
 
void CMFCClipboardDlg::OnBnClickedBtnWrite()
{
    UpdateData();
    if(this->m_CStrWrite.GetLength() > 0)
    {
        if(OpenClipboard())
        {
            EmptyClipboard();
            SetClipboardData(CF_TEXT, NULL);
            CloseClipboard();
            MessageBox(TEXT("  恭喜您 , 设置剪贴板成功 ..."), 
                TEXT("提示"), MB_ICONINFORMATION);
        }
    }
}
 
 
void CMFCClipboardDlg::OnBnClickedBtnRead()
{
    if(OpenClipboard())
    {
        //判断剪贴板中的数据格式是否为 CF_TEXT
        if(IsClipboardFormatAvailable(CF_TEXT))
        {
            char *            pDataBuf;
            HGLOBAL            hGlobalClip;
 
            //从剪贴板中获取到指定格式的数据
              hGlobalClip = GetClipboardData(CF_TEXT);
            pDataBuf = (char *)GlobalLock(hGlobalClip);
            this->m_CStrRead = pDataBuf;
            GlobalUnlock(hGlobalClip);
 
            UpdateData(FALSE);
        }
        CloseClipboard();
    }
}

效果展示:

设置剪贴板中数据:

image

当前程序读取剪贴板中数据:

image

记事本程序读取剪贴板中数据:

image

测试当前进程失去剪贴板所有权:

首先单击当前程序设置好剪贴板中的数据,

然后打开一个记事本文件,在在其中输入一些数据,然后选择这部分数据,按下复制:

image

            

             

结束语

对于剪贴板的使用呢,也就是那么几个 API 在使用而已,熟悉一下就可以了,

关键是延迟提交技术的使用,同时还有对于全局内存对象的理解还是有点难度的,

不过,我相信我解释的还是比较明白了,大家可以通过我的解释再对照 Demo 来理解,

这样理解起来容易快速一些。

上面介绍的是通过剪贴板来实现进程之间的通信,其实这还是有问题的,

因为我们的剪贴板是位于本地机器上,所以,利用剪贴板还是无法实现本地进程与远程进程通信,

当然要想实现本地进程和远程进程的通信,那也还是有办法的,这会在后续博文中引出的。

然后的话,今天圣诞节嘛,祝诸位节日快乐,也不是我崇洋媚外,说个节日快乐还是可以的。


进程间通信 - 邮槽实现

引子

前面的一篇博文介绍了进程之间通信的一种最为简单的方式,

也就是在本地进程之间通过剪贴板来实现进程间通信,而剪贴板自有其缺陷,

很显然的是,剪贴板只能在本地机器上实现,

无法实现本地进程与远程服务器上的进程之间的通信,

那么有没有办法实现本地进程和远程进程的通信呢?

办法自然是有的,要是实在搞不出,

我拿 Socket 来实现本地进程和远程进程的通信来实现也是可以的,

但是你想啊,要用 Socket 来实现本地进程和远程进程之间的通信,

那不仅我要在本地进程中加一堆的 Socket 代码,

并且服务器上的进程中也是需要加一堆的 Socket 代码的,

那不搞死人去,也太麻烦了吧,所以不行不行,得换一种方案。

下面就来介绍一种超级无敌简单的方案,其可以用来实现本地进程与远程进程之间的通信,

那就是通过邮槽来实现。

           

            

邮槽定义

邮槽(Mailslot)也称为邮件槽,其是 Windows 提供的一种用来实现进程间通信的手段,

其提供的是基于不可靠的,并且是单向数据传输的服务。

邮件槽只支持单向数据传输,也就是服务器只能接收数据,而客户端只能发送数据,

何为服务端?何为客户端?

服务端就是创建邮槽的那一端,而客户端就是已存在的邮件槽的那一端。

还有需要提及的一点是,客户端在使用邮槽发送数据的时候只有当数据的长度 < 425 字节时,

才可以被广播给多个服务器,如果消息的长度 > 425 字节的话,那么在这种情形下,

邮槽是不支持广播通信的。

              

          

邮槽的实现

首先是服务端调用 CreateMailslot 函数,这个函数会将创建邮件槽的请求传递给内核的系统服务,

也就是 NtCreateMailslot 函数,而 NtCreateMailslotFile 这个函数会到达底层的邮槽驱动程序,

也就是 msfs.sys ,然后一些创建邮槽的工作就交给邮槽驱动程序来完成了,对于底层驱动,这里不作介绍,

而在高层,我们也就只需要调用 CreateMailslot 函数就可以实现创建邮槽了。

            

             

邮槽的创建

下面我们就来看看这个 CreateMailslot 函数了:

该函数利用指定的名称来创建一个邮槽,然后返回所创建的邮槽的句柄。

HANDLE    WINAPI   CreateMailslot(
        __in          LPCTSTR lpName,
        __in          DWORD nMaxMessageSize,
        __in          DWORD lReadTimeout,
        __in_opt      LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

参数 lpName 指定了将要创建的邮槽的名称,该名称的格式必须为 \\.\mailslot\MailslotName

在这里需要注意的是两个斜杠后的那个 “.”,在这里使用圆点代表的是本地机器,

参数 nMaxMessageSize 用来指定可以被写入到邮槽的单一消息的最大尺寸,

为了可以发送任意大小的消息,需要将该参数设置为 0 。

参数 lReadTimeOut 指定读取操作的超时时间间隔,以毫秒作为单位。

读取操作在超时之前可以等待一个消息被写入到邮槽中,如果将这个值设置为 0 ,那么若没有消息可用的话,该函数将立即返回。

如果将该值设置为 MAILSLOT_WAIT_FOREVER,则该函数会一直等待,直到有消息可用。

参数 lpSecurityAttributes 一般设置为 NULL 即可,即采用 Windows 默认的针对于邮槽的安全性。

                       

              

示例:邮槽实现进程间通信

服务端实现:(简单 MFC 程序)

项目结构:

image

消息以及成员函数和成员变量的声明:

// 实现
protected:
    HICON m_hIcon;
 
    // 生成的消息映射函数
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnBnClickedBtnExit();
    afx_msg void OnBnClickedBtnRecv();
    afx_msg void OnBnClickedBtnCreate();
 
    //定义一个用来创建线程的成员函数
    HANDLE    CreateRecvThread(LPVOID lpParameter, DWORD threadFlag, LPDWORD lpThreadID);
 
    //控件变量:用来接收用户输入的数据
    CEdit m_RecvEdit;
 
    //成员变量:用来保存创建的邮件槽句柄
    HANDLE m_hMailslot;

消息映射表定义:

//用来定义邮槽发送和接收的最大数据字节数
const int        maxDataLen = 424;
 
//用来接收由客户端发送过来的数据
char *            pStrRecvData;
 
CMailSlotServerDlg::CMailSlotServerDlg(CWnd* pParent /*=NULL*/)
    : CDialogEx(CMailSlotServerDlg::IDD, pParent)
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
    
 
    m_hMailslot = NULL;
 
    //给用来接收数据的指针变量分配内存并清为 0
    pStrRecvData = new char[maxDataLen];
    memset(pStrRecvData, 0, maxDataLen);
}
 
void CMailSlotServerDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_EDIT_MAILSLOT, m_RecvEdit);
}
 
BEGIN_MESSAGE_MAP(CMailSlotServerDlg, CDialogEx)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(ID_BTN_EXIT, &CMailSlotServerDlg::OnBnClickedBtnExit)
    ON_BN_CLICKED(ID_BTN_RECV, &CMailSlotServerDlg::OnBnClickedBtnRecv)
    ON_BN_CLICKED(ID_BTN_CREATE, &CMailSlotServerDlg::OnBnClickedBtnCreate)
END_MESSAGE_MAP()

消息处理函数:

//退出按钮的消息处理例程
void CMailSlotServerDlg::OnBnClickedBtnExit()
{
    CDialogEx::OnOK();
}
 
//创建按钮的消息处理
void CMailSlotServerDlg::OnBnClickedBtnCreate()
{
    //创建名为 ZacharyMailSlot 的邮槽
    this->m_hMailslot = CreateMailslot(TEXT("\\\\.\\mailslot\\ZacharyMailSlot"), 0, 
        MAILSLOT_WAIT_FOREVER, NULL);
 
    if(INVALID_HANDLE_VALUE == this->m_hMailslot)
    {
        MessageBox(TEXT("创建邮槽失败 ..."), TEXT("提示"), MB_ICONERROR);
        return;
    }
}
 
//接收按钮的消息处理
void CMailSlotServerDlg::OnBnClickedBtnRecv()
{
    CString                cStrRecvData;
    DWORD                dwRead;
 
    //创建接收数据的线程,将邮槽句柄传递给线程
    CreateRecvThread((LPVOID)this->m_hMailslot, 0, NULL);
 
    cStrRecvData = pStrRecvData;
 
    this->m_RecvEdit.SetWindowText(cStrRecvData);
 
    UpdateData(FALSE);
}
 
//线程处理函数
DWORD WINAPI RecvThreadProc(LPVOID lpPrameter)
{
    HANDLE                hRecvMailSlot;
    DWORD                dwRead;
 
    hRecvMailSlot = (HANDLE)lpPrameter;
 
    //利用传进来的邮槽句柄接收收据,并将数据存放到 pStrRecvData 中
    if(!ReadFile(hRecvMailSlot, pStrRecvData, maxDataLen, &dwRead, NULL))
    {
        return NULL;
    }
 
    //关闭邮槽
    CloseHandle(hRecvMailSlot);
 
    return NULL;
}
 
HANDLE CMailSlotServerDlg::CreateRecvThread(LPVOID lpParameter, DWORD threadFlag, LPDWORD lpThreadID)
{
    //创建一个线程
    return CreateThread(NULL, 0, RecvThreadProc, lpParameter, threadFlag, lpThreadID);
}

客户端实现:(简单 MFC 程序)

项目结构:

image

消息以及成员函数和成员变量的声明:

// 实现
protected:
    HICON m_hIcon;
 
    // 生成的消息映射函数
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnBnClickedBtnExit();
    afx_msg void OnBnClickedBtnSend();
    CEdit m_SendEdit;

消息映射表定义:

const int maxDataLen = 424;CMailSlotClientDlg::CMailSlotClientDlg(CWnd* pParent /*=NULL*/)    : CDialogEx(CMailSlotClientDlg::IDD, pParent){    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);}void CMailSlotClientDlg::DoDataExchange(CDataExchange* pDX){    CDialogEx::DoDataExchange(pDX);    DDX_Control(pDX, IDC_EDIT_SEND, m_SendEdit);}BEGIN_MESSAGE_MAP(CMailSlotClientDlg, CDialogEx)    ON_WM_PAINT()    ON_WM_QUERYDRAGICON()    ON_BN_CLICKED(ID_BTN_EXIT, &CMailSlotClientDlg::OnBnClickedBtnExit)    ON_BN_CLICKED(ID_BTN_SEND, &CMailSlotClientDlg::OnBnClickedBtnSend)END_MESSAGE_MAP()

消息处理函数:

//退出按钮的消息处理例程
void CMailSlotClientDlg::OnBnClickedBtnExit()
{
    CDialogEx::OnOK();
}
 
 
//发送数据的消息处理例程
void CMailSlotClientDlg::OnBnClickedBtnSend()
{
    UpdateData();
 
    if(this->m_SendEdit.GetWindowTextLength() > 0 && 
       this->m_SendEdit.GetWindowTextLength() < maxDataLen)
    {
        HANDLE                hSendMailSlot;
        CString                cStrSendData;
        DWORD                dwWrite;
        char *                pSendBuf;
 
        //打开由服务端创建的邮件槽
        hSendMailSlot = CreateFile(TEXT("\\\\.\\mailslot\\ZacharyMailSlot"), 
            GENERIC_WRITE, FILE_SHARE_READ, NULL, 
            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
        if(INVALID_HANDLE_VALUE == hSendMailSlot)
        {
            MessageBox(TEXT("打开邮槽失败 ..."), TEXT("提示"), MB_ICONERROR);
            return;
        }
 
        this->m_SendEdit.GetWindowText(cStrSendData);
 
        //需要将 Unicode 字符转换为 ASCII 字符发送
        pSendBuf = new char[cStrSendData.GetLength() + 1];
        memset(pSendBuf, 0, sizeof(cStrSendData.GetLength() + 1));
        for(int i=0;i<cStrSendData.GetLength();i++)
        {
            pSendBuf[i] = cStrSendData.GetAt(i);
        }
 
        //通过邮件槽向服务端发送数据
        if(!WriteFile(hSendMailSlot, pSendBuf, cStrSendData.GetLength(), &dwWrite, NULL))
        {
            MessageBox(TEXT("写入数据失败 ..."), TEXT("提示"), MB_ICONERROR);
 
            CloseHandle(hSendMailSlot);
            return;
        }
        MessageBox(TEXT("写入数据成功 ..."), TEXT("提示"), MB_ICONINFORMATION);
    }
}

效果展示:

首先启动服务端进程并单击创建按钮:

image

然后启动客户端进程,并在客户端程序文本框中输入数据,然后单击发送按钮:

image

然后回到服务端程序中,并且单击接收按钮:

image

从上面的截图中可以看出,通过邮槽确实实现了从客户端进程向服务端进程发送数据。

当然上面的 Demo 中的服务端和客户端都是在本地机器上实现的,

如果想要实现本地进程和远程进程通信的话,

只需在客户端调用 CreateFile 打开邮槽时,将下面截图中标记的圆点置换为远程服务器的名称即可以实现了。

image

           

          

结束语

对于邮槽呢,其实还是蛮简单的,

在服务端的话,也就只需要在服务端调用 CreateMailslot 创建一个邮槽,

然后再在服务端调用 ReadFile 来等待读取数据即可以了,

而在客户端的话,也就只需要调用 CreateFile 来打开一个已经在服务端创建好的邮槽,

然后再调用 WriteFile 往这个邮槽中写入数据就可以了。

也就是说,对于邮槽的话,也就那么点东西需要介绍,

但是通过前面的介绍我们也很容易知道,对于通过利用邮槽来实现本地进程和远程进程的通信还是有缺陷的,

缺陷就是对于邮槽来说,服务端只能接收来自客户端的数据,而不能给客户端发送数据,

而客户端的话,则只能给服务端发送数据,而不能接收服务端发送过来的数据(事实上,服务端也发送不了)。

如果要实现客户端可以发送数据给服务端,同时也能接收来自服务端的数据,

而服务端也可以发送数据给客户端,并且服务端也可以接收到来自客户端的数据的话,

那需要利用另外的进程间通信的手段了,对于这点,留到下一篇博文介绍。

最后的话,那就是今天是 2010  年的最后一天了,在这里祝诸位节日快乐,2011 会更好 !!!



进程间通信 - 匿名管道实现

引子

前面的两篇博文分别介绍了通过剪贴板和通过邮槽来实现进程之间的通信,

其中剪贴板呢,是用来实现本地两个进程之间的通信,

而邮槽则既可以实现本地两个进程之间的通信,也可以实现本地进程和服务器进程的通信,

当然,上面这两种方式都是有局限性的,比如剪贴板就只能实现本地进程之间的通信,

而在邮槽中,虽然是既可以实现本地进程之间的通信,又能实现本地进程和远程服务器进程的通信,

但是使用邮槽的话,服务端只能接收数据,不能发送数据,而客户端只能发送数据而不能接收数据。

而本篇博文介绍的这个通过匿名管道来实现进程之间的通信的话,

局限性就显得更加严重了,

第一:匿名管道只能实现本地进程之间的通信,不能实现跨网络之间的进程间的通信。

第二:匿名管道只能实现父进程和子进程之间的通信,而不能实现任意两个本地进程之间的通信。

           

            

匿名管道概述

既然是匿名管道的话,自然,就是没有名字的管道了,还有一种管道呢,叫做命名管道,

命名管道的功能是很强大的,匿名管道在命名管道面前,功能那是简陋的不行的,

至于命名管道的话,会留到下一篇博文中介绍的,

匿名管道正因为提供的功能很单一,所以它所需要的系统的开销也就比命名管道小很多,

在本地机器上可以使用匿名管道来实现父进程和子进程之间的通信,

这里需要注意两点,第一就是在本地机器上,这是因为匿名管道不支持跨网络之间的两个进程之间的通信,

第二就是实现的是父进程和子进程之间的通信,而不是任意的两个进程。

然后得话还顺便介绍匿名管道的另外一种功能,其通过匿名管道可以实现子进程输出的重定向,

何为输出重定向呢?还请听下面详解:

比如我现在建立一个 Win32 的 Console 程序,然后在其中使用如下代码来输出一些信息:

#include <iostream>
using namespace std;
 
int main(int argc, char * argv)
{
    cout<<"Zachary  XiaoZhen "<<endl<<endl;
    cout<<"Happy  New   Year"<<endl<<endl;
    
    system("pause");
}

那么在默认下,编译运行上面的代码时,Windows 会弹出一个黑框框,并且在这个黑框框中显示一些信息,

image

为什么一定要将输出的信息显示在这个黑框框中呢?有没有办法让其显示在我们自己定义的文本框中呢?

而后我们再看一幅截图:

QQ截图未命名

上面画了很多红线的这个区域中的信息来自那里呢?为什么会在这个文本框中输出呢?

其实这就可以通过匿名管道来实现,

在卸载 QQ 游戏这幅截图中呢,其实运行了两个进程,

一个就是我们看到的这个输出了图形界面的进程,我们称之为卸载表象进程(父进程),

而另外一个用来执行真正意义上的卸载的进程我们称之为卸载实质进程(子进程)。

其实该卸载表象进程在其执行过程中创建了卸载实质进程来执行真正的卸载操作,

而后,卸载实质进程会输出上面用红色矩形标记的区域中的信息,

如果我们使用默认的输出的话,卸载实质进程会将上面红色区域标记中的信息输出到默认的黑框框中,

但是我们可以使用匿名管道来更改卸载实质进程的输出,

让其将输出数据输入到匿名管道中,而后卸载表象进程从匿名管道中读取到这些输出数据,

然后再将这些数据显示到卸载表象进程的文本框中就可以了。

而上面的这种用来更改卸载实质进程的输出的技术就称之为输出重定向。

当然与之相对的还有输入重定向的。

我们可以让一个进程的输入来自于匿名管道,而不是我们在黑框框中输入数据。

话说到这份上呢,也可以点出一点东东了,

上面的这个重定向不就是利用匿名管道实现的父进程和子进程之间的通信嘛。

             

          

匿名管道的使用

匿名管道主要用于本地父进程和子进程之间的通信,

在父进程中的话,首先是要创建一个匿名管道,

在创建匿名管道成功后,可以获取到对这个匿名管道的读写句柄,

然后父进程就可以向这个匿名管道中写入数据和读取数据了,

但是如果要实现的是父子进程通信的话,那么还必须在父进程中创建一个子进程,

同时,这个子进程必须能够继承和使用父进程的一些公开的句柄,

为什么呢?

因为在子进程中必须要使用父进程创建的匿名管道的读写句柄,

通过这个匿名管道才能实现父子进程的通信,所以必须继承父进程的公开句柄。

同时在创建子进程的时候,

必须将子进程的标准输入句柄设置为父进程中创建匿名管道时得到的读管道句柄,

将子进程的标准输出句柄设置为父进程中创建匿名管道时得到的写管道句柄。

然后在子进程就可以读写匿名管道了。

               

             

匿名管道的创建

BOOL WINAPI CreatePipe(
          __out   PHANDLE hReadPipe,
          __out   PHANDLE hWritePipe,
          __in    LPSECURITY_ATTRIBUTES lpPipeAttributes,
          __in    DWORD nSize );

参数 hReadPipe 为输出参数,该句柄代表管道的读取句柄。

参数 hWritePipe 为输出参数,该句柄代表管道的写入句柄。

参数 lpPipeAttributes 为一个输入参数,指向一个SECURITY_ATTRIBUTES 的结构体指针,

其检测返回的句柄是否能够被子进程继承,如果此参数为 NULL ,则表明句柄不能被继承,

在匿名管道中,由于匿名管道要在父子进程之间进行通信,

而子进程如果想要获得匿名管道的读写句柄,则其只能通过从父进程继承获得,

当一个子进程从其父进程处继承了匿名管道的读写句柄以后,

子进程和父进程之间就可以通过这个匿名管道的读写句柄进行通信了。

所以在这里必须构建一个 SECURITY_ATTRIBUTES 的结构体,

并且该结构体的第三个结构成员变量 bInheritHandle 参数必须设置为 TRUE

从而让子进程可以继承父进程所创建的匿名管道的读写句柄。

typedef struct _SECURITY_ATTRIBUTES {
 
    DWORD nLength;
 
    LPVOID lpSecurityDescriptor;
 
    BOOL bInheritHandle;
 
} SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

参数 nSize 用来指定缓冲区的大小,

如果此参数设置为 0 ,则表明系统将使用默认的缓冲区大小。一般将该参数设置为 0 即可。

         

              

子进程的创建

BOOL  CreateProcess( 
        LPCWSTR pszImageName,  LPCWSTR pszCmdLine, 
        LPSECURITY_ATTRIBUTES psaProcess, 
        LPSECURITY_ATTRIBUTES psaThread, 
        BOOL fInheritHandles,  DWORD fdwCreate, 
        LPVOID pvEnvironment,  LPWSTR pszCurDir, 
        LPSTARTUPINFOW psiStartInfo, 
        LPPROCESS_INFORMATION pProcInfo );

参数 pszImageName 是一个指向 NULL 终止的字符串,用来指定可执行程序的名称。

参数 pszCmdLine 用来指定传递给新进程的命令行字符串,一般做法是在pszImageName 中传递可执行文件的名称,

pszCmdLine 中传递命令行参数。

参数 psaProcess 即代表当 CreateProcess 函数创建进程时,需要给进程对象设置一个安全性。

参数 psaThread 代表当 CreateProcess函数创建新进程后,需要给该进程的主线程对象设置一个安全性。

参数 fInheritHandles 用来指定父进程随后创建的子进程是否能够继承父进程的对象句柄,

如果该参数设置为 TRUE ,则父进程的每一个可继承的打开句柄都将被子进程所继承,

继承的句柄与原始的句柄拥有同样的访问权。

在匿名管道的使用中,因为子进程需要使用父进程中创建的匿名管道的读写句柄,

所以应该将这个参数设置为 TRUE ,从而可以让子进程继承父进程创建的匿名管道的读写句柄。

参数 fdwCreate 用来指定控件优先级类和进程创建的附加标记。

如果只是为了启动子进程,则并不需要设置它创建的标记,可以将此参数设置为 0

对于这个参数的具体取值列表可以参考 MSDN 。

参数 pvEnvironment 代表指向环境块的指针,

如果该参数设置为 NULL ,则默认将使用父进程的环境。通常给该参数传递 NULL

参数 pszCurDir 用来指定子进程当前的路径,

这个字符串必须是一个完整的路径名,其包括驱动器的标识符,

如果此参数设置为 NULL ,那么新的子进程将与父进程拥有相同的驱动器和目录。

参数 psiStartInfo 指向一个 StartUpInfo 的结构体的指针,用来指定新进程的主窗口如何显示。

typedef struct _STARTUPINFOA {
 
    DWORD cb;
 
    LPSTR lpReserved;
 
    LPSTR lpDesktop;
 
    LPSTR lpTitle;
 
    DWORD dwX;
 
    DWORD dwY;
 
    DWORD dwXSize;
 
    DWORD dwYSize;
 
    DWORD dwXCountChars;
 
    DWORD dwYCountChars;
 
    DWORD dwFillAttribute;
 
    DWORD dwFlags;
 
    WORD wShowWindow;
 
    WORD cbReserved2;
 
    LPBYTE lpReserved2;
 
    HANDLE hStdInput;
 
    HANDLE hStdOutput;
 
    HANDLE hStdError;
 
} STARTUPINFOA, *LPSTARTUPINFOA;

对于 dwFlags 参数来说,如果其设置为 STARTF_USESTDHANDLES

则将会使用该 STARTUPINFO 结构体中的 hStdInput hStdOutput hStdError 成员,

来设置新创建的进程的标准输入,标准输出,标准错误句柄。

参数 pProcInfo 为一个输出参数,

指向一个 PROCESS_INFORMATION 结构体的指针,用来接收关于新进程的标识信息。

typedef struct _PROCESS_INFORMATION 
{             
    HANDLE hProcess;             
    HANDLE hThread;             
    DWORD dwProcessId;              
    DWORD dwThreadId; 
 
}PROCESS_INFORMATION;

其中 hProcess hThread 分别用来标识新创建的进程句柄和新创建的进程的主线程句柄。

dwProcessId dwThreadId 分别是全局进程标识符和全局线程标识符。

前者可以用来标识一个进程,后者用来标识一个线程。

        

               

示例:匿名管道实现父子进程间通信

父进程实现:(简单 MFC 程序)

项目结构:

image

消息以及成员函数和成员变量的声明:

public:
    //创建匿名管道
    afx_msg void OnBnClickedBtnCreatePipe();
    //写匿名管道
    afx_msg void OnBnClickedBtnWritePipe();
    //读匿名管道
    afx_msg void OnBnClickedBtnReadPipe();
 
    //定义父进程读匿名管道的成员函数
    void ParentReadPipe(void);
    //定义父进程写匿名管道的成员函数
    void ParentWritePipe(void);
    //创建 SECURITY_ATTRIBUTES 结构的成员函数
    void CreateSecurityAttributes(PSECURITY_ATTRIBUTES pSa);
    //创建 STARTUPINFO 结构的成员函数
    void CreateStartUpInfo(LPSTARTUPINFO lpStartUpInfo);
    //创建匿名管道的成员函数
    void CreateNoNamedPipe(void);
 
    //分别代表要从匿名管道中读的数据和要写到匿名管道中的数据
    CString m_CStrReadPipe;
    CString m_CStrWritePipe;
 
    //保存创建匿名管道后所得到的对匿名管道的读写句柄
    HANDLE hPipeRead;
    HANDLE hPipeWrite;
    
    //保证匿名管道只创建一次
    BOOL m_PipeIsCreated;

消息映射表定义:

const int        dataLength    = 100;
 
CNoNamedPipeParentDlg::CNoNamedPipeParentDlg(CWnd* pParent /*=NULL*/)
    : CDialogEx(CNoNamedPipeParentDlg::IDD, pParent)
    , m_CStrReadPipe(_T(""))
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
 
    m_PipeIsCreated = FALSE;
}
 
void CNoNamedPipeParentDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_WRITE_PIPE, m_CStrWritePipe);
    DDX_Text(pDX, IDC_EDIT_READ_PIPE, m_CStrReadPipe);
}
 
BEGIN_MESSAGE_MAP(CNoNamedPipeParentDlg, CDialogEx)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDC_BTN_CREATE_PIPE, 
                &CNoNamedPipeParentDlg::OnBnClickedBtnCreatePipe)
    ON_BN_CLICKED(IDC_BTN_WRITE_PIPE, 
                &CNoNamedPipeParentDlg::OnBnClickedBtnWritePipe)
    ON_BN_CLICKED(IDC_BTN_READ_PIPE, 
                &CNoNamedPipeParentDlg::OnBnClickedBtnReadPipe)
END_MESSAGE_MAP()

消息处理函数:

//创建匿名管道按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnCreatePipe()
{
    if(m_PipeIsCreated == FALSE)
    {
        this->CreateNoNamedPipe();
    }
}
 
 
//写入数据到匿名管道中按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnWritePipe()
{
    this->ParentWritePipe();
}
 
 
//从匿名管道中读取数据按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnReadPipe()
{
    this->ParentReadPipe();
}
 
 
//接收数据
void CNoNamedPipeParentDlg::ParentReadPipe(void)
{
    DWORD            dwRead;
    char *            pReadBuf;
    CString            cStrRecvData;
 
    pReadBuf = new char[dataLength];
    memset(pReadBuf, 0, dataLength);
 
    if(!ReadFile(hPipeRead, pReadBuf, dataLength, &dwRead, NULL))
    {
        MessageBox(TEXT("   从匿名管道接收数据失败 ..."), 
            TEXT("提示"), MB_ICONERROR);
        return;
    }
 
    cStrRecvData = "   从匿名管道接收数据成功:    ";
    cStrRecvData += pReadBuf;
 
    this->m_CStrReadPipe.Empty();
 
    this->m_CStrReadPipe = pReadBuf;
    UpdateData(FALSE);
 
    MessageBox(cStrRecvData, TEXT("提示"), MB_ICONINFORMATION);
}
 
 
//发送数据
void CNoNamedPipeParentDlg::ParentWritePipe(void)
{
    UpdateData();
 
    if(!this->m_CStrWritePipe.IsEmpty())
    {
        char *            pSendData;
        DWORD            dwWrite;
        CString            cStrSendData;
 
        //在这里需要将 Unicode 字符集转换为 ASCII 字符集
        pSendData = new char[this->m_CStrWritePipe.GetLength() + 1];
        memset(pSendData, 0, this->m_CStrWritePipe.GetLength() + 1);
        for(int i=0;i<this->m_CStrWritePipe.GetLength();i++)
        {
            pSendData[i] = (char)this->m_CStrWritePipe.GetAt(i);
        }
 
        if(!WriteFile(hPipeWrite, pSendData, 
            this->m_CStrWritePipe.GetLength() + 1, &dwWrite, NULL))
        {
            MessageBox(TEXT("   给匿名管道发送数据失败 ..."), 
                TEXT("提示"), MB_ICONERROR);
            return;
        }
 
        cStrSendData = "   给匿名管道发送数据成功:    ";
        cStrSendData += this->m_CStrWritePipe;
 
        this->m_CStrWritePipe.Empty();
        UpdateData(FALSE);
 
        MessageBox(cStrSendData, TEXT("提示"), MB_ICONINFORMATION);
    }
    else
    {
        MessageBox(TEXT("   请先输入要发送给匿名管道的数据 ..."), 
            TEXT("提示"), MB_ICONERROR);
    }
}
 
 
//创建 SECURITY_ATTRIBUTES 结构
void CNoNamedPipeParentDlg::CreateSecurityAttributes(PSECURITY_ATTRIBUTES pSa)
{
    //这里必须将 bInheritHandle 设置为 TRUE,
    //从而使得子进程可以继承父进程创建的匿名管道的句柄
    pSa->bInheritHandle = TRUE;
    pSa->lpSecurityDescriptor = NULL;
    pSa->nLength = sizeof(SECURITY_ATTRIBUTES);
}
 
 
//用来初始化新进程的 STARTUPINFO 成员
void CNoNamedPipeParentDlg::CreateStartUpInfo(LPSTARTUPINFO lpStartUpInfo)
{
    memset(lpStartUpInfo, 0, sizeof(STARTUPINFO));
 
    lpStartUpInfo->cb = sizeof(STARTUPINFO);
    lpStartUpInfo->dwFlags = STARTF_USESTDHANDLES;
 
    //子进程的标准输入句柄为父进程管道的读数据句柄
    lpStartUpInfo->hStdInput = hPipeRead;
 
    //子进程的标准输出句柄为父进程管道的写数据句柄
    lpStartUpInfo->hStdOutput = hPipeWrite;
 
    //子进程的标准错误处理句柄和父进程的标准错误处理句柄一致
    lpStartUpInfo->hStdError = GetStdHandle(STD_ERROR_HANDLE);
}
 
 
//创建匿名管道
void CNoNamedPipeParentDlg::CreateNoNamedPipe(void)
{
    SECURITY_ATTRIBUTES                sa;
    PROCESS_INFORMATION                processInfo;
    STARTUPINFO                        startUpInfo;
 
    CreateSecurityAttributes(&sa);
    if(!CreatePipe(&hPipeRead, &hPipeWrite, &sa, 0))
    {
        MessageBox(TEXT("   创建匿名管道失败 ..."), 
            TEXT("提示"), MB_ICONERROR);
        return;
    }
 
    CreateStartUpInfo(&startUpInfo);
    if(!CreateProcess(TEXT("NoNamedPipeChild.exe"), 
        NULL, NULL, NULL, TRUE, 
        CREATE_NEW_CONSOLE, NULL, NULL, 
        &startUpInfo, &processInfo))
    {
        CloseHandle(hPipeRead);
        CloseHandle(hPipeWrite);
 
        hPipeWrite = NULL;
        hPipeRead = NULL;
 
        MessageBox(TEXT("   创建子进程失败 ..."), 
            TEXT("提示"), MB_ICONERROR);
        return;
    }
    else
    {
        m_PipeIsCreated = TRUE;
 
        //对于 processInfo.hProcess 和 processInfo.hThread 
        //这两个句柄不需要使用,所以释放资源
        CloseHandle(processInfo.hProcess);
        CloseHandle(processInfo.hThread);
    }
}

子进程实现:(简单 MFC 程序)

项目结构:

image

消息以及成员函数和成员变量的声明:

// 实现
protected:
    HICON m_hIcon;
 
    // 生成的消息映射函数
    virtual BOOL OnInitDialog();
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    DECLARE_MESSAGE_MAP()
public:
    afx_msg void OnBnClickedBtnWritePipe();
    afx_msg void OnBnClickedBtnReadPipe();
 
    //保存从父进程得到针对于匿名管道的读写句柄
    HANDLE hPipeRead;
    HANDLE hPipeWrite;
 
    //分别代表要从匿名管道中读的数据和要写到匿名管道中的数据
    CString m_CStrWritePipe;
    CString m_CStrReadPipe;
 
    //子进程读取匿名管道
    void ChildReadPipe(void);
    //子进程写匿名管道
    void ChildWritePipe(void);
    //子进程获取从父进程处继承得到的关于匿名管道的读写句柄
    void GetReadWriteHandleFromParent(void);
 
    //只需要获取一次匿名管道的读写句柄即可
    BOOL m_IsGettedParentHandle;

消息映射表定义:

const int        dataLength    = 100;
 
CNoNamedPipeChildDlg::CNoNamedPipeChildDlg(CWnd* pParent /*=NULL*/)
    : CDialogEx(CNoNamedPipeChildDlg::IDD, pParent)
    , m_CStrWritePipe(_T(""))
    , m_CStrReadPipe(_T(""))
{
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
 
    this->m_IsGettedParentHandle = FALSE;
}
 
void CNoNamedPipeChildDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_WRITE_PIPE, m_CStrWritePipe);
    DDX_Text(pDX, IDC_EDIT_READ_PIPE, m_CStrReadPipe);
}
 
BEGIN_MESSAGE_MAP(CNoNamedPipeChildDlg, CDialogEx)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(ID_BTN_WRITE_PIPE, 
            &CNoNamedPipeChildDlg::OnBnClickedBtnWritePipe)
    ON_BN_CLICKED(ID_BTN_READ_PIPE, 
            &CNoNamedPipeChildDlg::OnBnClickedBtnReadPipe)
END_MESSAGE_MAP()

消息处理函数:

//往匿名管道中写入数据按钮的消息处理函数
void CNoNamedPipeChildDlg::OnBnClickedBtnWritePipe()
{
    //如果子进程还没有获得对匿名管道的读写句柄的话需要先获取句柄
    this->GetReadWriteHandleFromParent();
 
    ChildWritePipe();
}
 
 
//从匿名管道中读取数据按钮的消息处理函数
void CNoNamedPipeChildDlg::OnBnClickedBtnReadPipe()
{
    //如果子进程还没有获得对匿名管道的读写句柄的话需要先获取句柄
    this->GetReadWriteHandleFromParent();
 
    ChildReadPipe();
}
 
//从匿名管道读取数据成员函数
void CNoNamedPipeChildDlg::ChildReadPipe(void)
{
    DWORD            dwRead;
    char *            pReadBuf;
    CString            cStrRecvData;
 
    pReadBuf = new char[dataLength];
    memset(pReadBuf, 0, dataLength);
 
    //读取数据
    if(!ReadFile(hPipeRead, pReadBuf, dataLength, &dwRead, NULL))
    {
        MessageBox(TEXT("   从匿名管道接收数据失败 ..."), 
            TEXT("提示"), MB_ICONERROR);
        return;
    }
 
    cStrRecvData = "   从匿名管道接收数据成功:    ";
    cStrRecvData += pReadBuf;
 
    this->m_CStrReadPipe.Empty();
    this->m_CStrReadPipe = pReadBuf;
    UpdateData(FALSE);
 
    MessageBox(cStrRecvData, TEXT("提示"), MB_ICONINFORMATION);
}
 
//往匿名管道中写入数据
void CNoNamedPipeChildDlg::ChildWritePipe(void)
{
    UpdateData();
 
    if(!this->m_CStrWritePipe.IsEmpty())
    {
        char *                pSendData;
        DWORD                dwWrite;
        CString                cStrSendData;
 
        //在这里需要将 Unicode 字符集转换为 ASCII 字符集
        pSendData = new char[this->m_CStrWritePipe.GetLength() + 1];
        memset(pSendData, 0, this->m_CStrWritePipe.GetLength() + 1);
        for(int i=0;i<this->m_CStrWritePipe.GetLength();i++)
        {
            pSendData[i] = (char)this->m_CStrWritePipe.GetAt(i);
        }
 
        //写入数据
        if(!WriteFile(hPipeWrite, pSendData, 
            this->m_CStrWritePipe.GetLength(), &dwWrite, NULL))
        {
            MessageBox(TEXT("   给匿名管道发送数据失败 ..."), 
                TEXT("提示"), MB_ICONERROR);
            return;
        }
 
        cStrSendData = "给匿名管道发送数据成功:    ";
        cStrSendData += this->m_CStrWritePipe;
 
        this->m_CStrWritePipe.Empty();
        UpdateData(FALSE);
 
        MessageBox(cStrSendData, TEXT("提示"), MB_ICONINFORMATION);
    }
    else
    {
        MessageBox(TEXT("   请先输入要发送给匿名管道的数据 ..."), 
            TEXT("提示"), MB_ICONERROR);
    }
}
 
//需要获取继承自父进程的匿名管道读写句柄
void CNoNamedPipeChildDlg::GetReadWriteHandleFromParent(void)
{
    if(this->m_IsGettedParentHandle == FALSE)
    {
        hPipeRead = GetStdHandle(STD_INPUT_HANDLE);
        hPipeWrite = GetStdHandle(STD_OUTPUT_HANDLE);
 
        this->m_IsGettedParentHandle = TRUE;
    }
}

效果展示:

首先需要将子进程的可执行文件拷贝到父进程所在目录下,否则创建进程时会找不到子进程的可执行文件。 

image

启动父进程可执行文件,并单击创建匿名管道按钮,此时会弹出子进程窗口(新建了进程):

image

再在父进程的左边文本框中输入数据,单击写入数据按钮:

image

再在子进程窗口中单击读取数据按钮:

image

再在子进程窗口左边的文本框中输入数据,单击写入数据按钮:

image

再在父进程窗口中单击读取数据按钮:

image

             

                  

结束语

从上面的效果展示中就可以看出我们确实是实现了父子进程之间通过匿名管道来进行通信,

最后再来总结一下对于匿名管道的使用,

匿名管道一般用于实现本地父子进程之间的通信,其不能实现跨网络进程之间的通信,

同时其也一般只用于实现父进程和子进程之间的通信。

像匿名管道的话,其和邮槽不同,

其可以实现父进程即可以向子进程发送数据,同时父进程又可以从子进程接收到数据。

而且子进程可以接收来自父进程的数据,并且也可以给父进程发送数据。

最后的话,那就是今天是 2011  年的第一天,说实话,太冷了,实在不想写博客的,

不过因为想写完这个进程之间通信的几篇博客,同时也对 2011 有个好的开始,

所以硬着头皮写下了这篇博客,在这里希望自己 2011 会更好,人会更成熟,更上进 !!!

同时也祝各位新年快乐,2011 会更好 !!!


进程间通信 - 命名管道实现

引子

好,到这里呢,就需要介绍实现进程间通信的第四种方式了,

也就是通过命名管道来实现,前面介绍的那三种方式呢,都是有缺陷或者说局限性太强,

而这里介绍的命名管道相对来说,在这方面就做得好很多了,

比如,剪贴板的话只能实现本机上进程之间的通信,

而邮槽的话虽然是可以实现跨网络之间的进程的通信,

但麻烦的是邮槽的服务端只能接收数据,邮槽的客户端只能发送数据,太悲剧了,

而对于匿名管道的话,其也只能实现本机上进程之间的通信,

你要是能够实现本机进程间的通信也就算了,

关键是它还只用来实现本地的父子进程之间的通信,也太局限了吧?

而这里介绍的这个命名管道的话,就和他们有些不同了,在功能上也就显得强大很多了,

至少其可以实现跨网络之间的进程的通信,同时其客户端既可以接收数据也可以发送数据,

服务端也是既可以接收数据,又可以发送数据的。

             

             

命名管道概述

命名管道是通过网络来完成进程之间的通信的,命名管道依赖于底层网络接口,

其中包括有 DNS 服务,TCP/IP 协议等等机制,但是其屏蔽了底层的网络协议细节,

对于匿名管道而言,其只能实现在父进程和子进程之间进行通信,而对于命名管道而言,

其不仅可以在本地机器上实现两个进程之间的通信,还可以跨越网络实现两个进程之间的通信。

命名管道使用了 Windows 安全机制,因而命名管道的服务端可以控制哪些客户有权与其建立连接,

而哪些客户端是不能够与这个命名管道建立连接的。

利用命名管道机制实现不同机器上的进程之间相互进行通信时,

可以将命名管道作为一种网络编程方案时,也就是看做是 Socket 就可以了,

它实际上是建立了一个客户机/服务器通信体系,并在其中可靠的传输数据。

命名管道的通信是以连接的方式来进行的,

服务器创建一个命名管道对象,然后在此对象上等待连接请求,

一旦客户连接过来,则两者都可以通过命名管道读或者写数据。

                

命名管道提供了两种通信模式:字节模式和消息模式。

在字节模式下,数据以一个连续的字节流的形式在客户机和服务器之间流动,

而在消息模式下,客户机和服务器则通过一系列的不连续的数据单位,进行数据的收发,

每次在管道上发出一个消息后,它必须作为一个完整的消息读入。

               

            

命名管道使用流程

服务端:

服务端进程调用 CreateNamedPipe 函数来创建一个有名称的命名管道,

在创建命名管道的时候必须指定一个本地的命名管道名称(不然就不叫命名管道了),

Windows 允许同一个本地的命名管道名称有多个命名管道实例,

所以,服务器进程在调用 CreateNamedPipe 函数时必须指定最大允许的实例数(0 -255),

如果 CreateNamedPipe 函数成功返回后,服务器进程得到一个指向一个命名管道实例的句柄,

然后,服务器进程就可以调用 ConnectNamedPipe 来等待客户的连接请求,

这个 ConnectNamedPipe 既支持同步形式,又支持异步形式,

若服务器进程以同步形式调用 ConnectNamedPipe 函数,

(同步方式也就是如果没有得到客户端的连接请求,则会一直等到)

那么,当该函数返回时,客户端与服务器之间的命名管道连接也就已经建立起来了。

在已经建立了连接的命名管道实例中,

服务端进程就会得到一个指向该管道实例的句柄,这个句柄称之为服务端句柄。

同时,服务端进程可以调用 DisconnectNamedPipe 函数,

将一个管道实例与当前建立连接的客户端进程断开,从而可以重新连接到新的客户进程。

当然在服务端也是可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。

客户端

客户端进程调用 CreateFile 函数连接到一个正在等待连接的命名管道上,

在这里客户端需要指定将要连接的命名管道的名称,

CreateFile 成功返回后,客户进程就得到了一个指向已经建立连接的命名管道实例的句柄,

到这里,服务器进程的 ConnectNamedPipe 也就完成了其建立连接的任务。

客户端进程除了调用 CreateFile 函数来建立管道连接以外,

还可以调用 WaitNamedPipe 函数来测试指定名称的管道实例是否可用。

在已经建立了连接的命名管道实例中,客户端进程就会得到一个指向该管道实例的句柄,

这个句柄称之为客户端句柄。

在客户端可以调用 CloseHandle 来关闭一个已经建立连接的命名管道实例。

          

             

服务端创建命名管道

HANDLE  WINAPI   CreateNamedPipe(
   __in    LPCTSTR lpName,
  __in    DWORD dwOpenMode,
  __in    DWORD dwPipeMode,
  __in    DWORD nMaxInstances,
   __in    DWORD nOutBufferSize,
  __in    DWORD nInBufferSize,
   __in    DWORD nDefaultTimeOut,
  __in    LPSECURITY_ATTRIBUTES lpSecurityAttributes
               );

该函数用来创建一个命名管道的实例,并返回这个命名管道的句柄,

一个命名管道的服务器进程使用该函数创建命名管道的第一个实例,

并建立它的基本属性,或者创建一个现有的命名管道的新实例。

如果需要创建一个命名管道的多个实例,就需要多次调用 CreateNamedPipe 函数。

参数 lpName 为一个字符串,其格式必须为 \\.\pipe\pipeName,其中圆点 ”.” 表示的是本地机器,

如果想要与远程的服务器建立连接,那么这个圆点位置处应指定这个远程服务器的名称,

而其中的 “pipe” 这个是个固定的字符串,也就是说不能进行改变的,

最后的 “pipename” 则代表的是我将要创建的命名管道的名称了。

参数 dwOpenMode 用来指定管道的访问方式,重叠方式,写直通方式,还有管道句柄的安全访问方式。

同一个命名管道的每一个实例都必须具有相同的类型。

如果该参数设置为 0 ,则默认将使用字节类型方式,即通过这个参数可指定创建的是字节模式还是消息流模式。

对于管道句柄的读取方式来说,同一个管道的不同实例可以指定不同的读取方式。

如果该值为 0 ,则默认将使用字节读方式。

而对于管道句柄的等待方式,则同一个管道的不同实例可以取不同的等待方式。

如果该值设置为 0 ,则默认为阻塞方式。

命名管道的访问方式如下表所列:

解释

PIPE_ACCESS_DUPLEX

双向模式。

服务器进程和客户端进程都可以从管道读取数据和向管道中写入数据。

PIPE_ACCESS_INBOUND

服务器端就只能读取数据,而客户端就只能向管道中写入数据。

PIPE_ACCESS_OUTBOUND

服务器端就只能写入数据,而客户端就只能从管道中读取数据。

命名管道的写直通方式和重叠方式:

解释

FILE_FLAG_WRITE_THROUGH

写直通方式(可以简单的看做是同步操作)。

该方式只影响对字节类型管道的写入操作。

且只有当客户端与服务端进程位于不同的计算机上时才有效。

该方式只有等到欲写入命名管道的数据通过网络传送出去,

并且放置到了远程计算机的管道缓冲区以后,

写数据的函数才会成功返回。

FILE_FLAG_OVERLAPPED

重叠模式(可以简单的看做是异步操作)。

实现前台线程执行其他操作,而耗时操作可在后台进行。

命名管道的安全访问方式:

解释

WRITE_DAC

调用者对命名管道的任意访问控制列表都可以进行写入。

WRITE_OWNER

调用者对命名管道的所有者可以进行写入访问。

ACCESS_SYSTEM_SECURITY

调用者对命名管道的安全访问控制列表都可以写入。

参数 dwPipeMode 用来指定管道句柄的类型,读取和等待方式。

命名管道句柄的类型:

解释

PIPE_TYPE_BYTE

数据以字节流的形式写入管道。

该方式不能在 PIPE_READMODE_MESSAGE 读方式下使用。

PIPE_TYPE_MESSAGE

数据以消息流的形式写入管道。

命名管道句柄的读取方式:

解释

PIPE_READMODE_BYTE

以字节流的方式从管道中发读取数据。

PIPE_READMODE_MESSAGE

以消息流的方式从管道读取数据。

该方式只有在 PIPE_TYPE_MESSAGE 类型下才可以使用。

命名管道句柄的等待方式:

解释

PIPE_WAIT

允许阻塞方式也就是同步方式。

ReadFile,WriteFile,ConnectNamedPipe 函数,

必须等到读取到数据或写入新数据或有一个客户连接来才能返回。

PIPE_NOWAIT

允许非阻塞方式也就是异步方式。

ReadFile,WriteFile,ConnectNamedPipe 函数总是立即返回。

参数 nMaxInstance 指定命名管道能够创建的实例的最大数目。

该参数的取值可以从 0 – 255 ,这里说的最大实例数目是指对同一个命名管道最多能创建的实例数目,

如果希望同时连接 5 个客户端,那么则必须调用 5 次 CreateNamedPipe 函数创建 5 个命名管道实例,

然后才能同时接收到 5 个客户端连接请求的到来,

对于同一个命名管道的实例来说,在某一个时刻,它只能和一个客户端进行通信。

参数 nOutBufferSize用来指定将要为输出缓冲区所保留的字节数。

参数 nInBufferSize用来指定将要为输入缓冲区所保留的字节数。

参数 nDefaultTimeOut用来指定默认的超时值,以毫秒为单位,同一个管道的不同实例必须指定同样的超时值。

参数 lpSecurityAttributes 用来设置该命名管道的安全性,

一般设置为 NULL ,也就是采用 Windows 提供的默认安全性。

         

           

服务端等待客户端连接请求

BOOL  WINAPI  ConnectNamedPipe(
          __in    HANDLE hNamedPipe,
          __in    LPOVERLAPPED lpOverlapped
          );

该函数的作用是让服务器等待客户端的连接请求的到来。

参数  hNamedPipe 指向一个命名管道实例的服务器的句柄。

该句柄由 CreateNamedPipe 函数返回。

参数 lpOverlapped 指向一个 OVERLAPPED 结构的指针,

如果 hNamedPipe 所标识的命名管道是用 FILE_FLAG_OVERLAPPED ,

(也就是重叠模式或者说异步方式)标记打开的,则这个参数不能为 NULL

必须是一个有效的指向一个 OVERLAPPED 结构的指针,否则该函数可能会错误的执行。

              

                

客户端连接命名管道

BOOL  WINAPI  WaitNamedPipe(
        __in    LPCTSTR lpNamedPipeName,
         __in    DWORD nTimeOut
          );

客户端在连接服务端程序创建的命名管道之前,

首先应该判断一下,是否有可以利用的命名管道,

通过调用该函数可以用来实现这一点,该函数会一直等到,

直到等待的时间间隔已过,或者指定的命名管道的实例可以用来连接了,

也就是说该管道的服务器进程有正在等待被连接的的 ConnectNamedPipe 操作。

参数 lpNamedPipeName 用来指定命名管道的名称,

这个名称必须包括创建该命名管道的服务器进程所在的机器的名称,

格式为:\\.\pipe\pipeName,如果是在同一个机器上编写的命名管道的服务器端程序和客户端程序,

则当指定这个名称时,在开始的两个反斜杠后可以设置一个圆点来表示服务器进程在本地机器上运行,

如果是跨网络通信,则在这个圆点位置处应该设置为服务器端所在的主机的名称。

参数 nTimeOut 用来指定超时间隔。

解释

NMPWAIT_USE_DEFAULT_WAIT

超时间隔即为服务器端创建该命名管道时指定的超时间隔。

NMPWAIT_USE_DEFAULT_WAIT

一直等待,直到出现一个可用的命名管道的实例。

           

               

示例:命名管道实现进程间通信

服务端实现:(简单 Console 程序)

项目结构:

image

NamedPipeServer.h

#ifndef NAMED_PIPE_SERVER_H
#define NAMED_PIPE_SERVER_H
 
#include <Windows.h>
#include <iostream>
 
using namespace std;
 
//服务端用来保存创建的命名管道句柄
HANDLE            hNamedPipe;
 
const char *    pStr        = "Zachary";
const char *    pPipeName    = "\\\\.\\pipe\\ZacharyPipe";
 
//创建命名管道
void CreateNamedPipeInServer();
 
//从命名管道中读取数据
void NamedPipeReadInServer();
 
//往命名管道中写入数据
void NamedPipeWriteInServer();
 
#endif
         
NamedPipeServer.cpp
#include "NamedPipeServer.h"
 
int main(int argc, char * argv)
{
    CreateNamedPipeInServer();
 
    //在服务端往管道中写入数据
    NamedPipeWriteInServer();
 
    //接收客户端发来的数据
    NamedPipeReadInServer();
 
    system("pause");
}
 
 
void CreateNamedPipeInServer()
{
    HANDLE                    hEvent;
    OVERLAPPED                ovlpd;
 
    //首先需要创建命名管道
    //这里创建的是双向模式且使用重叠模式的命名管道
    hNamedPipe = CreateNamedPipe(pPipeName, 
        PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 
        0, 1, 1024, 1024, 0, NULL);
 
    if(INVALID_HANDLE_VALUE == hNamedPipe)
    {
        hNamedPipe = NULL;
        cout<<"创建命名管道失败 ..."<<endl<<endl;
        return;
    }
 
    //添加事件以等待客户端连接命名管道
    //该事件为手动重置事件,且初始化状态为无信号状态
    hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if(!hEvent)
    {
        cout<<"创建事件失败 ..."<<endl<<endl;
        return;
    }
 
    memset(&ovlpd, 0, sizeof(OVERLAPPED));
 
    //将手动重置事件传递给 ovlap 参数
    ovlpd.hEvent = hEvent;
 
    //等待客户端连接
    if(!ConnectNamedPipe(hNamedPipe, &ovlpd))
    {
        if(ERROR_IO_PENDING != GetLastError())
        {
            CloseHandle(hNamedPipe);
            CloseHandle(hEvent);
 
            cout<<"等待客户端连接失败 ..."<<endl<<endl;
            return;
        }
    }
 
    //等待事件 hEvent 失败
    if(WAIT_FAILED == WaitForSingleObject(hEvent, INFINITE))
    {
        CloseHandle(hNamedPipe);
        CloseHandle(hEvent);
 
        cout<<"等待对象失败 ..."<<endl<<endl;
        return;
    }
 
    CloseHandle(hEvent);
}
 
 
void NamedPipeReadInServer()
{
    char *            pReadBuf;
    DWORD            dwRead;
 
    pReadBuf = new char[strlen(pStr) + 1];
    memset(pReadBuf, 0, strlen(pStr) + 1);
 
    //从命名管道中读取数据
    if(!ReadFile(hNamedPipe, pReadBuf, strlen(pStr), &dwRead, NULL))
    {
        delete []pReadBuf;
 
        cout<<"读取数据失败 ..."<<endl<<endl;
        return;
    }
    cout<<"读取数据成功:    "<<pReadBuf<<endl<<endl;
}
 
 
void NamedPipeWriteInServer()
{
    DWORD            dwWrite;
 
    //向命名管道中写入数据
    if(!WriteFile(hNamedPipe, pStr, strlen(pStr), &dwWrite, NULL))
    {
        cout<<"写入数据失败 ..."<<endl<<endl;
        return;
    }
    cout<<"写入数据成功:    "<<pStr<<endl<<endl;
}

客户端实现:(简单 Console 程序)

项目结构:

image

NamedPipeClient.h

#ifndef NAMED_PIPE_CLIENT_H
#define NAMED_PIPE_CLIENT_H
 
#include <Windows.h>
#include <iostream>
 
using namespace std;
 
//用来保存在客户端通过 CreateFile 打开的命名管道句柄
HANDLE            hNamedPipe;
 
const char * pStr        = "Zachary";
const char * pPipeName    = "\\\\.\\pipe\\ZacharyPipe";
 
//打开命名管道
void OpenNamedPipeInClient();
 
//客户端从命名管道中读取数据
void NamedPipeReadInClient();
 
//客户端往命名管道中写入数据
void NamedPipeWriteInClient();
 
#endif
              

NamedPipeClient.cpp

#include "NamedPipeClient.h"
 
int main(int argc, char * argv)
{
    OpenNamedPipeInClient();
 
    //接收服务端发来的数据
    NamedPipeReadInClient();
 
    //往命名管道中写入数据
    NamedPipeWriteInClient();
 
    system("pause");
}
 
 
void OpenNamedPipeInClient()
{
    //等待连接命名管道
    if(!WaitNamedPipe(pPipeName, NMPWAIT_WAIT_FOREVER))
    {
        cout<<"命名管道实例不存在 ..."<<endl<<endl;
        return;
    }
 
    //打开命名管道
    hNamedPipe = CreateFile(pPipeName, GENERIC_READ | GENERIC_WRITE, 
        0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if(INVALID_HANDLE_VALUE == hNamedPipe)
    {
        cout<<"打开命名管道失败 ..."<<endl<<endl;
        return;
    }
}
 
 
void NamedPipeReadInClient()
{
    char *                pReadBuf;
    DWORD                dwRead;
 
    pReadBuf = new char[strlen(pStr) + 1];
    memset(pReadBuf, 0, strlen(pStr) + 1);
 
    //从命名管道中读取数据
    if(!ReadFile(hNamedPipe, pReadBuf, strlen(pStr), &dwRead, NULL))
    {
        delete []pReadBuf;
 
        cout<<"读取数据失败 ..."<<endl<<endl;
        return;
    }
    cout<<"读取数据成功:    "<<pReadBuf<<endl<<endl;
}
 
 
void NamedPipeWriteInClient()
{
    DWORD                dwWrite;
 
    //向命名管道中写入数据
    if(!WriteFile(hNamedPipe, pStr, strlen(pStr), &dwWrite, NULL))
    {
        cout<<"写入数据失败 ..."<<endl<<endl;
        return;
    }
    cout<<"写入数据成功:    "<<pStr<<endl<<endl;
}

效果展示:

首先启动服务端进程(可以看到服务端进程正在等待客户端进程来连接命名管道):

image

然后启动客户端进程,可以看到客户端进程已经读取到了来自服务端进程发送到命名管道中的数据,

同时客户端进程也成功将数据写入到了命名管道中,从而这些数据可以被服务端进程获取到:

image

此时再来看服务端进程,可以发现服务端进程已经结束了等待,也就是已经成功和客户端进程建立了连接,

同时,服务端进程也成功将数据写入到了命名管道中,并且也成功获取到了客户端写入到命名管道中的数据。

image

                          

             

结束语

对于命名管道来说的话,简单理解的话,其实是可以将其看做是一种 Socket 的,

而对于命名管道也就是那几个 API 在使用,对于一些不常用的 API

感兴趣的也可以从 MSDN 中获取到这部分信息。

对于进程间的通信的话,其实也就可以利用介绍的这四种方式来实现了,

第一种是利用剪贴板实现本机进程间的通信。

第二种是利用邮槽实现本机或跨网络进程间的通信。

第三种是利用匿名管道实现本机父子进程之间的通信。

第四种是利用命名管道实现本机或跨网络进程间的通信。

然后的话,我还打算介绍一种比较偏门的实现进程间通信的手段,

当然,这要到下一篇博文中才会作出介绍。

最后的话,就是在前面的一篇博文中有一位朋友说可以利用 WCF 来实现进程之间的通信,

这个呢理论上是可以实现的,但是本人也没有做过这方面的 Demo

所以估计得看以后有时间的话,也可以拿过来写写文章的。


进程间通信 - 动态链接库实现

引子

前面介绍的几种用于实现进程之间通信的方法都是比较正统的方式,

像剪贴板,命名管道这些都还是用得比较多的,

而这里将介绍的是一种比较偏门的方法来实现进程间的通信,

所谓偏门呢,自然就是用的少,能够不用就不要使用。

其实这种方法就是通过动态链接库来实现进程间的通信。

          

                

动态链接库(DLL)概述              

既然是要通过动态链接库来实现进程间的通信,

那么这里如果不来介绍一下动态链接库的话,怎么也说不过去的。

动态链接库是 Windows 操作系统的基础,其中 Windows API 基本上都是以动态链接库的形式来提供的,

通常来说动态链接库是不能够直接运行,也不能够直接接受消息的,

它们是一些独立的文件(后缀名一般为 .dll ,当然还有其他的一些后缀名也是可以的),

其中包含能被可执行程序或其他 DLL 调用来完成某项工作的函数,

也就是说动态链接库也就是由一些函数组成而已。

并且只有在其他模块调用动态链接库中的函数时,动态链接库才发挥作用,

在实际的编程中,通常可以把完成某种功能的函数放在一个动态链接库中,然后提供给其他函数调用。

当这个访问了动态链接库的进程被加载时,系统会为这个进程分配 4GB 的私有地址空间,

然后系统就会分析这个可执行模块,找到这个可执行模块中所调用的 DLL ,然后系统就负责搜索这些 DLL

找到这些 DLL 后便将这些 DLL 加载到内存中,并为它们分配虚拟的内存空间,

最后将 DLL 的页面映射到调用进程的地址空间中,

DLL 的虚拟内存有代码页和数据页,它们被分别映射到 进程 A 的代码页面和数据页面,

如果这时 进程 B 也启动了,并且 进程 B 也需要访问该 DLL

这时,只需要将该 DLL 在虚拟内存中的代码页面和数据页面映射到第二个进程的地址空间即可。

这也表明了在内存中,只需要存在一份 DLL 的代码和数据,

多个进程共享 DLL 的同一份代码,很明显这样做可以节省内存空间的。

但是在 Windows 下,由于系统会为每一个进程分配 4GB 的私有地址空间,

DLL 中的代码和数据也只是映射到了这个私有地址空间中,所以这些应用程序之间还是不能够相互影响的,

也就是说多个应用程序虽然是可以共享同一个 DLL 中的相同的代码的,

但是 DLL 为每一个进程保存的数据都是不相同的,

并且每一个进程都为 DLL 使用的全部数据分配了自己的地址空间,

举个最简单的例子,我的 DLL 中有一个函数为 int   Add(int    num1 ,   int    num2)

这个函数的作用是实现 num1  num2 相加并返回相加后的结果。

然后我有一个 进程 A  使用了这个 DLL ,并且其调用了函数  Add(10, 20),

然后我还有一个 进程 B 其也使用了这个 DLL ,并且其调用了函数 Add(30, 40),

那么对于 进程 A 中的数据 10 20 其实是保存在进程 A 的私有地址空间中的,

而对于 进程 B 中的数据 30 40 则是保存在进程 B 的私有地址空间中的,

上面这个简单的例子表明如果单单用这种简单的使用动态链接库的方式是不能够实现进程之间的通信的。

      

          

动态链接库中共享内存的实现

如果想利用动态链接库来实现进程间的通信的话,那么有一种方案可以试一试,

即从系统为动态链接库分配的那一块内存(系统需要将动态链接库加载到内存中)下手,

由于在内存中,动态链接库其实只存在一份,

其被所有需要调用该动态链接库中的函数的模块或者简单说是可执行程序所共享,

既然是共享的话,如果我在系统给动态链接库分配的这块内存中保存数据,

那岂不是可以被所有访问该动态链接库的可执行程序所获取或者说设置。

这样的话,我就可以使用 进程 A 来设置好这个共享内存中的数据,

然后 进程 B 就可以读取这个共享内存中的数据了,这不是也可以实现进程间的通信嘛,

这样看来的话,其思路其实和使用剪贴板是一模一样的了。

也是采用一块两个进程共享的内存来作为存放数据的中介。

                  

          

示例:动态链接库实现进程间通信

共享 DLL 实现:

新建动态链接库项目步骤:

image

QQ截图未命名q

项目结构:

image

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif

         

DLL.cpp

// DLL.cpp : 定义 DLL 应用程序的导出函数。
//
 
#include "stdafx.h"
 
#define DLL_API extern "C" _declspec(dllexport)
 
#include "ShareDLL.h"
 
//使用 #pragma data_seg() 来表明这一段数据为共享数据
//一定要注意给下面的变量初始化,否则将无法实现数据在多个进程间共 享
#pragma data_seg("SharedDataInDll")
 
    //初始化为 0
    int data = 0;
 
#pragma data_seg()
 
 
//这里还需要告诉链接器表明 SharedDataInDll 数据段为可读可写可共享
#pragma comment(linker, "/SECTION:SharedDataInDll,RWS")
 
 
//返回共享数据
int GetData()
{
    return data;
}
 
//设置共享数据
void SetData(int tmpData)
{
    data = tmpData;
}

进程 A 实现:(简单 Console 程序)

项目结构:

image

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif
               

DLLProcessA.cpp

#include <iostream>
#include "ShareDLL.h"
 
using namespace std;
 
//引用 DLL.lib 引入库
#pragma comment(lib, "DLL.lib")
 
int main(int argc, char * argv)
{
    int data;
 
    cout<<"进程 A 设置数据:  ";
    cin>>data;
 
    //设置共享内存
    SetData(data);
 
    cout<<endl<<endl;
    system("pause");
 
    //读取共享内存
    cout<<"进程 A 读取数据:  "<<GetData()<<endl<<endl;
 
    system("pause");
}

进程 B 实现:(简单 Console 程序)

项目结构:

image

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif
          

DLLProcessB.cpp

#include <iostream>
#include "ShareDLL.h"
 
using namespace std;
 
//引用 DLL.lib 引入库
#pragma comment(lib, "DLL.lib")
 
int main(int argc, char * argv)
{
    int data;
 
    //读取共享数据
    cout<<"进程 B 读取数据:  "<<GetData()<<endl<<endl;
 
    cout<<"进程 B 设置数据:  ";
    cin>>data;
 
    //设置共享数据
    SetData(data);
 
    cout<<endl<<endl;
    system("pause");
}

需要将 DLL 项目中的 DLL . dll 和 DLL . lib 两个文件,

分别拷贝到项目 DLLProcessA 和 DLLProcessB 的根目录下。

然后分别编译 DLLProcessA 和 DLLProcessB 两个项目,

最后将 Dll . dll 和 DLL . lib 以及 DLLProcesA . exe 和 DLLProcessB . exe 拷贝到同一目录下面,

比如:(这样可以确保两个进程访问到的是同一个动态链接库)

image

效果展示:

首先运行 DLLProcessA . exe 文件并设置共享数据为 8 :

image

然后启动 DLLProcessB . exe 文件(可以看出其读出的值为 8 ):

image

然后再在 DLLProcessB . exe 中设置数据为 16 :

image

然后再在 DLLProcessA . exe 中按下回车键(此时可以看到进程 A 读取到的数据位 16 了):

image

           

          

结束语

从上面的这个效果展示中可以看出,我们确实通过动态链接库实现了 进程 A 进程 B 之间的通信,

前面说过使用动态链接库来实现进程之间的通信是一个偏方,

通过这个 Demo 呢,我们也是可以看出这种方式的局限性的,

第一,使用动态链接库来实现进程间的通信的话,首先必须要求通信的双方进程都访问了这个动态链接库。

第二,这种方式只适用于本地进程之间的通信,其不能实现跨网络的通信。

关于进程之间通信呢,前前后后介绍了五种方法,其中各有各的优点,

也各有各的局限性,至于具体要使用那一种的话,那还请各位看官自行斟酌,然后选用合适的方案 !!!



0 0