进程间通信——命名管道和邮槽

时间:2024-03-01 19:42:21

    前面介绍了进程间通信的两种方法:剪贴板和匿名管道。这两种进程间通信的方法只能在本地主机的进程之间通信。而匿名管道还限制通信的两进程之间必须有父子关系。在开发网络间不同进程之间相互通信的应用程序时,我们可以用命名管道和邮槽。这两种方法不仅支持本地主机通信也支持网间进程通信。下面详细介绍这两种方法:

一、命名管道

    将命名管道作为网络通信的方案时,他实际上建立了一个客户机/服务器通信体系,并在其中可靠的传输数据。命名管道式围绕Windows文件系统设计的一种机制,采用“命名管道文件系统借口”,因此客户机和服务器可利用标准的Win32文件系统函数来进行数据的收发。在利用命名管道进行通信时,服务器是唯一一个有权建立命名管道的进程,可以接收客户机的连接请求,客户机只能同一个现有的命名管道客户机建立连接。命名管道提供了两种基本的通信模式:字节模式和消息模式。在实际编程过程中可以根据实际需要选择不同的通信模式。利用命名管道进行通信可以按照下面的步骤进行:

(一)服务器端:

(1)创建命名管道

    在利用命名管道进行通信之前,肯定要先创建命名管道。创建命名管道需调用的函数为CreateNamedPipe(),该函数的创建一个命名管道的实例并返回该实例的句柄。该函数的第一个参数用指定的格式保存创建的管道的名字。该函数的饿第二个参数指定管道的进入模式、重叠模式、读写模式、安全属性等信息,在下面的示例中我用的是双向的进入模式(PIPE_ACCESS_DUPLEX),客户机和服务器都可以从管道读写数据,重叠方式为可重叠的方式(FILE_FLAG_OVERLAPPED),指定重叠模式后,需要花费时间的线程会立即返回执行其他操作,耗费时间的线程会在后台完成。该函数的第四个参数指定该管道可创建的最大实例个数。指定该参数的原因是,一个命名管道的示例只能和一个客户端通信,如果有多个客户端要通过该命名管道和服务器通信,需要创建多个该命名管道的实例。创建命名管道的示例代码如下:

 /***************************************************
                创建命名管道
 *******************************************************/
 hPipe=CreateNamedPipe("\\\\.\\pipe\\MyPipe",PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,
  0,1,1024,1024,0,NULL);
 if (INVALID_HANDLE_VALUE==hPipe)
 {
  MessageBox("创建命名管道失败!");
  hPipe=NULL;
  //CloseHandle(hPipe);
  return;
 }

(2)等待客户端连接请求的到来

    命名管道创建完成之后,服务器就可以等待客户端的连接请求。等待客户端的连接请求调用的函数为ConnectNamedPipe()该函数的的第二个参数是一个指向OVERLAPPED结构体的指针,该结构体包含用于异步执行I/O操作的信息。该结构体中我们感兴趣的参数是一个指向事件对象的句柄,我们可以利用该事件对象来执行异步I/O操作。所以我们要先调用CreateEvent()创建一个人工重置的事件对象。当等待客户端连接请求之后,我们就可以调用WaitForSingleObject()函数将该事件对象置为无信号状态,以供下一次连接请求使用。实现代码如下:

/*********************************************************
                创建人工重置的匿名事件对象
 *********************************************************/
 HANDLE hEvent;
 hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
 if (!hEvent)
 {
  MessageBox(" 创建人工重置的匿名事件对象失败!");
  CloseHandle(hEvent);
  hPipe=NULL;
  return;
 }
   /*********************************************************
                等待客户端连接请求的到来
 *********************************************************/
 OVERLAPPED olp;
 ZeroMemory(&olp,sizeof(OVERLAPPED));
 olp.hEvent=hEvent;
 if(!ConnectNamedPipe(hPipe,&olp))
 {
  if (ERROR_IO_PENDING!=GetLastError())
  {
   MessageBox("等待客户端连接请求失败!");
   CloseHandle(hPipe);
   CloseHandle(hEvent);
   hPipe=NULL;
   
   return;
  }
 }
 if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
 {
  MessageBox("等待对象失败!");
  CloseHandle(hPipe);
  CloseHandle(hEvent);
  hPipe=NULL;
  return;
 }
 CloseHandle(hEvent);

(3)往管道中执行读写操作

    由于命名管道式围绕文件系统设计的,所以我们可以利用标准的Win32文件操作函数执行读写操作。示例代码如下:

void CNamedPipeSrvView::OnPipRead()
{
 // TODO: Add your command handler code here
 char buf[200];
 DWORD dwRead;
 if(!ReadFile(hPipe,buf,200,&dwRead,NULL))
 {
  MessageBox("读取匿名管道失败!");
  return;
 }
 MessageBox(buf);
}


void CNamedPipeSrvView::OnPipWrite()
{
 // TODO: Add your command handler code here
 char buf[]="匿名管道测试程序!";
 DWORD dwWrite;
 if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
 {
  MessageBox("写匿名管道失败!");
  return;
 }
}
(二)客户端

    命名管道的实现比较简单了,客户端首先判断是否有可利用的命名管道实例,如果有,然后打开管道就可以进行读写操作了。

(1)连接管道实例并打开管道

    WaitNamedPipe()函数可用来连接一个命名管实例,连接成功后,就可以调用CreateFile()函数打开管道,该函数可以指定管道进入的模式、文件属性等。示例代码如下:

**********************************************************
                判断是否有可利用的命名管道实例
 ************************************************************/
 if(!WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER))
 {
  MessageBox("当前没有可利用的命名管道实例");
  return;
 }
 /**********************************************************
                打开命名管道
 ************************************************************/
 hPipe=CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ|GENERIC_WRITE,
      0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
 if (INVALID_HANDLE_VALUE==hPipe)
 {
  MessageBox("打开管道失败!");
  hPipe=NULL;
  return;
 }

(2)往管道进行读写操作

void CNamedPipeCltView::OnPipRead()
{
 // TODO: Add your command handler code here
 char buf[200];
 DWORD dwRead;
 if(!ReadFile(hPipe,buf,200,&dwRead,NULL))
 {
  MessageBox("读取匿名管道失败!");
  return;
 }
 MessageBox(buf);
}


void CNamedPipeCltView::OnPipWrite()
{
 // TODO: Add your command handler code here
 char buf[]="乘风736博客园 http://www.cnblogs.com/chengfeng736/";
 DWORD dwWrite;
 if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
 {
  MessageBox("写匿名管道失败!");
  return;
 }
}

运行结果如下:

这样,一个通过命名管道进行通信的服务器和客户端程序就设计好了。两个进程就可以实现通信了。

二 、邮槽

    邮槽是基于广播模式的单向通信方式,服务器只能从邮槽读取数据,客户端只能往邮槽写入数据,而且利用邮槽通信的信息量不能太大。下面介绍利用邮槽进行进程通信的过程:

(一)服务器

(1)创建油槽

    创建油槽可调用CreateMailslot()函数实现,该函数的第一个参数按照指定格式指定油槽的名字,第三个参数指定读操作等待的时间。下面的示例程序我设定读操作一直等待(MAILSLOT_WAIT_FOREVER),只到接收到数据为止。示例代码如下:

HANDLE hMailslot;
 hMailslot=CreateMailslot("\\\\.\\mailslot\\MyMailslot",0,MAILSLOT_WAIT_FOREVER,NULL);
 if (INVALID_HANDLE_VALUE==hMailslot)
 {
  MessageBox("创建邮槽失败!");
  CloseHandle(hMailslot);
  return;
 }

(2)读取数据

DWORD dwRead;
 char buf[200];
 if(!ReadFile(hMailslot,buf,200,&dwRead,NULL))
 {
  MessageBox("读取数据失败!");
  CloseHandle(hMailslot);
  return;
 }
 MessageBox(buf);
 CloseHandle(hMailslot);

(二)客户端

(1)打开油槽

CreateFile()函数不仅可以打开文件、管道还可以打开油槽。示例代码如下:

HANDLE hMailslot;
 hMailslot=CreateFile("\\\\.\\mailslot\\MyMailslot",GENERIC_WRITE,FILE_SHARE_READ,
  NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
 if (INVALID_HANDLE_VALUE==hMailslot)
 {
  MessageBox("打开邮槽失败!");
  CloseHandle(hMailslot);
  return;
 }

(2)写入数据

char buf[]="使用邮槽进行进程间通信\r\n乘风736博客园 http://www.cnblogs.com/chengfeng736/";
  DWORD dwWrite;
 if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,0))
 {
  MessageBox("写入数据失败!");
  CloseHandle(hMailslot);
  return;
 }
 CloseHandle(hMailslot);

运行结果如下:

使用油槽通信的实现是很简单的。油槽只能单向通信,如果要实现双向通信,可以在客户端和服务器分别实现读写操作就可以了。