引子
好,到这里呢,就需要介绍实现进程间通信的第四种方式了,
也就是通过命名管道来实现,前面介绍的那三种方式呢,都是有缺陷或者说局限性太强,
而这里介绍的命名管道相对来说,在这方面就做得好很多了,
比如,剪贴板的话只能实现本机上进程之间的通信,
而邮槽的话虽然是可以实现跨网络之间的进程的通信,
但麻烦的是邮槽的服务端只能接收数据,邮槽的客户端只能发送数据,太悲剧了,
而对于匿名管道的话,其也只能实现本机上进程之间的通信,
你要是能够实现本机进程间的通信也就算了,
关键是它还只用来实现本地的父子进程之间的通信,也太局限了吧?
而这里介绍的这个命名管道的话,就和他们有些不同了,在功能上也就显得强大很多了,
至少其可以实现跨网络之间的进程的通信,同时其客户端既可以接收数据也可以发送数据,
服务端也是既可以接收数据,又可以发送数据的。
命名管道概述
命名管道是通过网络来完成进程之间的通信的,命名管道依赖于底层网络接口,
其中包括有 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 程序)
项目结构:
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 程序)
项目结构:
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;
}
效果展示:
首先启动服务端进程(可以看到服务端进程正在等待客户端进程来连接命名管道):
然后启动客户端进程,可以看到客户端进程已经读取到了来自服务端进程发送到命名管道中的数据,
同时客户端进程也成功将数据写入到了命名管道中,从而这些数据可以被服务端进程获取到:
此时再来看服务端进程,可以发现服务端进程已经结束了等待,也就是已经成功和客户端进程建立了连接,
同时,服务端进程也成功将数据写入到了命名管道中,并且也成功获取到了客户端写入到命名管道中的数据。
结束语
对于命名管道来说的话,简单理解的话,其实是可以将其看做是一种 Socket 的,
而对于命名管道也就是那几个 API 在使用,对于一些不常用的 API ,
感兴趣的也可以从 MSDN 中获取到这部分信息。
对于进程间的通信的话,其实也就可以利用介绍的这四种方式来实现了,
第一种是利用剪贴板实现本机进程间的通信。
第二种是利用邮槽实现本机或跨网络进程间的通信。
第三种是利用匿名管道实现本机父子进程之间的通信。
第四种是利用命名管道实现本机或跨网络进程间的通信。
然后的话,我还打算介绍一种比较偏门的实现进程间通信的手段,
当然,这要到下一篇博文中才会作出介绍。
最后的话,就是在前面的一篇博文中有一位朋友说可以利用 WCF 来实现进程之间的通信,
这个呢理论上是可以实现的,但是本人也没有做过这方面的 Demo ,
所以估计得看以后有时间的话,也可以拿过来写写文章的。 转载自 Zachary.XiaoZhen - 梦想的天空