一、管道实现进程间通讯
主要的理论知识
1.什么是管道以及分类
管道是两个头的东西,每一个头各连接一个进程或者同一个进程的不同代码,依照管道的类别分有两种管道,匿名的和命名的;依照管道的传输方向分也能够分成两种,单向的双向的。依据管道的特点,命名管道通经常使用在网络环境下不同计算机上执行的进程之间的通信(当然也能够用在同一台机的不同进程中)它能够是单向或双向的;而匿名管道仅仅能用在同一台计算机中,它仅仅能是单向的。匿名管道事实上是通过用给了一个指定名字的有名管道来实现的。
使用管道的优点在于:读写它使用的是对文件操作的
api,结果操作管道就和操作文件一样。即使你在不同的计算机之间用命名管道来通信,你也不必了解和自己去实现网络间通信的详细细节。
2.管道的使用
A.命名管道
命名管道是由server端的进程建立的,管道的命名必须遵循特定的命名方法,就是"//./pipe/管道名",当作为client的进程要使用时,使用"//计算机名//pipe/管道名"来打开使用,详细过程例如以下:
服务端通过函数CreateNamedPipe创建一个命名管道的实例并返回用于今后操作的句柄,或为已存在的管道创建新的实例。服务端侦听来自client的连接请求,该功能通过ConnectNamedPipe函数实现。
client通过函数WaitNamedPipe来等待管道的出现,假设在超时值变为零曾经,有一个管道能够使用,则WaitNamedPipe将返回
True,并通过调用CreateFile或CallNamedPipe来呼叫对服务端的连接。
此时服务端将接受client的连接请求,成功建立连接,服务端ConnectNamedPipe返回
True 建立连接之后,client与server端就可以通过ReadFile和WriteFile,利用得到的管道文件句柄,彼此间进行信息交换。当client与服务端的通信结束,client调用CloseFile,服务端接着调用DisconnectNamedPipe。最后调用函数CloseHandle来关闭该管道。
B.匿名管道
因为命名管道使用时作为client的程序必须知道管道的名称,所以很多其它的用在同一“作者”编写的server/工作站程序中,你不可能随便找出一个程序来要求它和你写的程序来通过命名管道通信。而匿名管道的使用则全然不同,它同意你和全然不相干的进程通信,条件是这个进程通过控制台“console”来输入输出,典型的样例是老的
Dos 应用程序,它们在执行时 Windows为它们开了个
Dos窗体,它们的输入输出就是 console方式的。另一些标准的 Win32程序也使用控制台输入输出,假设在
Win32编程中不想使用图形界面,你照样能够使用AllocConsole得到一个控制台,然后通过GetStdHandle得到输入或输出句柄,再通过WriteConsole或WriteFile把结果输出到控制台(一般是一个象
Dos窗体)的屏幕上。尽管这些程序看起来象 Dos程序,但它们是不折不扣的 Win32程序,假设你在纯
Dos下使用,就会显示“The program must run under Windows!”。
一个控制台有三个句柄:标准输入、标准输出和和标准错误句柄,标准输入、标准输出句柄是能够又一次定向的,你能够用匿名管道来取代它,这样一来,你能够在管道的还有一端用别的进程来接收或输入,而控制台一方并没有感到什么不同,就象
Dos下的 >或者 <能够又一次定向输出或输入一样。通常控制台程序的输入输出例如以下:
(控制台进程output)
write ----> 标准输出设备(通常是屏幕)
(控制台进程input)
read <---- 标准输入设备(通常是键盘)
而用管道取代后: (作为子进程的控制台进程output)
write ----> 管道1 ----> read (父进程)
(作为子进程的控制台进程input)
read <----> 管道2 <---- write (父进程)
使用匿名管道的过程例如以下:
使用CreatePipe建立两个管道,得到管道句柄,一个用来输入,一个用来输出
准备运行控制台子进程,首先使用GetStartupInfo得到StartupInfo
使用第一个管道句柄取代 StartupInfo中的
hStdInput,第二个取代 hStdOutput、hStdError,即标准输入、输出、错误句柄
使用CreateProcess运行子进程,这样建立的子进程输入和输出就被定向到管道中
父进程通过 ReadFile读第二个管道来获得子进程的输出,通过
WriteFile写第一个管道来将输入写到子进程
父进程能够通过PeekNamedPipe来查询子进程有没有输出
子进程结束后,要通过CloseHandle来关闭两个管道。
管道使用的API函数集
CallNamedPipe函数
|
函数原型:BOOL CallNamedPipe( LPCTSTR lpNamedPipeName, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize,
LPDWORD lpBytesRead, DWORD nTimeOut );
|
说明:这个函数由一个希望通过管道通信的一个客户进程调用。如有可能,它就同一个管道连接(在必要的情况下等候管道可用)。随后,它对指定的数据进行读写,然后将管道关闭
|
|
參数表 :
lpNamedPipeName:LPCTSTR,指定管道名,採用的形式是://./管道/管道名。最多可达256个字符的长度,并且不用区分大写和小写。假设存在指定名字的一个管道,则创建那个管道的一个新实例
lpInBuffer:LPVOID,包括了要写入管道的数据的一个内存缓冲区
nInBufferSize:DWORD,lpInBuffer缓冲区中的字符数量
lpOutBuffer:LPVOID,指定一个内存缓冲区,用于装载从管道中读出的数据
nOutBufferSize:DWORD,指定一个长整数变量,用于装载来自管道的数据
lpBytesRead:LPDWORD,指定从管道中读出的字节数。会阅读单条消息。如lpOutBuffer的容量不够大,不能容下整条消息,则函数会返回FALSE,并且GetLastError会设为ERROR_MORE_DATA(消息中留下的不论什么字节都会丢失)
nTimeOut:DWORD,下列常量之中的一个:
1. NMPWAIT_NOWAIT: 如管道不可用,则马上返回一个错误
2. NMPWAIT_WAIT_FOREVER:永远等候管道可用.
3. NMPWAIT_USE_DEFAULT_WAIT:使用管道的默认超时设置,这个设置是用CreateNamedPipe函数指定的
|
|
ConnectNamedPipe函数
|
函数原型:BOOL ConnectNamedPipe( HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped );
|
说明:指示一台server等待下去,直至客户机同一个命名管道连接
|
返回值:BOOL,如lpOverlapped为NULL,那么:
1.
如管道已连接,就返回Ture(非零);
2. lpOverlapped有效,就返回零;
如重叠操作成功完毕,就返回ERROR_IO_PENDING。
在这两种情况下,倘若一个客户已关闭了管道,且server尚未用DisconnectNamedPipe函数同客户断开连接,那么GetLastError都会返回ERROR_NO_DATA
|
參数:
hNamedPipe:HANDLE,管道的句柄
lpOverlapped:LPOVERLAPPED,
如设为NULL(传递ByVal
As Long),表示将线程挂起,直到一个客户同管道连接为止。否则就马上返回;
此时,如管道尚未连接,客户同管道连接时就会触发lpOverlapped结构中的事件对象。随后,可用一个等待函数来监视连接
|
适用平台:Windows NT
|
凝视:可用这个函数将一个管道换成同还有一个客户连接,但首先必须用DisconnectNamedPipe函数断开同当前进程的连接
|
|
CreateNamedPipe函数
|
函数原型:HANDLE CreateNamedPipe( LPCTSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD
nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes);
|
说明:创建一个命名管道。返回的句柄由管道的server端使用
|
返回值:HANDLE,如运行成功,返回管道的句柄。INVALID_HANDLE_VALUE表示失败.会设置GetLastError
|
參数:
lpName:LPCTSTR,指定管道名,採用的形式是://./管道/管道名。最多可达256个字符的长度,并且不用区分大写和小写。假设存在指定名字的一个管道,则创建那个管道的一个新实例
dwOpenMode:DWORD,下述常数组的一个组合
下述常数之中的一个(对于管道的全部实例都要一样):
1. PIPE_ACCESS_DUPLEX管道是双向的
2. PIPE_ACCESS_INBOUND数据从client流到server端
3. PIPE_ACCESS_OUTBOUND数据从server端流到client
下述常数的随意组合
1. FILE_FLAG_WRITE_THROUGH在网络中建立的字节型管道内,强迫数据在每次读写操作的时候通过网络传输。否则传输就可能延迟
2. FILE_FLAG_OVERLAPPED同意(但不要求)用这个管道进行异步(重叠式)操作
常数WRITE_DAC, WRITE_OWNER和
ACCESS_ SYSTEM_SECURITY提供了附加的安全选项
dwPipeMode:DWORD,下述常数组的一个组合
下述常数之中的一个(管道的全部实例都必须指定同样的常数)
1. PIPE_TYPE_BYTE数据作为一个连续的字节数据流写入管道
2. PIPE_TYPE_MESSAGE数据用数据块(名为“消息”或“报文”)的形式写入管道
下述常数之中的一个:
1. PIPE_READMODE_PIPE数据以单独字节的形式从管道中读出
2. PIPE_READMODE_MESSAGE数据以名为“消息”的数据块形式从管道中读出(要求指定PIPE_TYPE_MESSAGE)
下述常数之中的一个:
1. PIPE_WAIT同步操作在等待的时候挂起线程
2. PIPE_NOWAIT(不推荐!)同步操作马上返回。这样可为异步传输提供一种落后的实现方法,已由Win32的重叠式传输机制代替了
nMaxInstances:DWORD,这个管道可以创建的最大实例数量。必须是1到常数PIPE_UNLIMITED_INSTANCES间的一个值。它对于管道的全部实例来说都应是同样的
nOutBufferSize:DWORD,建议的输出缓冲区长度;零表示用默认设置
nInBufferSize:DWORD,建议的输入缓冲区长度;零表示用默认设置
nDefaultTimeOut:DWORD,管道的默认等待超时。对一个管道的全部实例来说都应同样
lpSecurityAttributes:LPSECURITY_ATTRIBUTES,指定一个SECURITY_ATTRIBUTES结构,或者传递零值(将參数声明为ByVal
As Long,并传递零值),以便使用不同意继承的一个默认描写叙述符
|
适用平台:Windows NT
|
|
CreatePipe函数
|
函数原型:BOOL CreatePipe(PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize
);
|
说明:创建一个匿名管道
|
|
參数:
phReadPipe:PHANDLE,指定一个变量,设为管道读入(输出)端的一个句柄
phWritePipe:PHANDLE,指定一个变量,设为管道写入(输入)端的一个句柄
lpPipeAttributes:LPSECURITY _ATTRIBUTES,指定一个SECURITY_ATTRIBUTES结构,或者传递零值,以便使用不同意继承的一个默认描写叙述符
nSize:DWORD,管道缓冲区的建议大小。零表示用默认值
|
注解:匿名管道不同意异步操作,所以如在一个管道中写入数据,且缓冲区已满,那么除非还有一个进程从管道中读出数据,从而腾出了缓冲区的空间,否则写入函数不会返回
|
|
DisconnectNamedPipe函数
|
函数原型:BOOL DisconnectNamedPipe( HANDLE hNamedPipe );
|
说明:断开一个客户与一个命名管道的连接(server端与client都可调用)
|
|
參数:hNamedPipe Long,管道的句柄
|
适用平台:Windows NT
|
注解:如客户尚未在它自己那端关闭管道句柄,下次试图訪问管道的时候就会错误发生
|
|
GetNamedPipeHandleState函数
|
函数原型:BOOL GetNamedPipeHandleState(HANDLE hNamedPipe, LPDWORD lpState, LPDWORD lpCurInstances, LPDWORD lpMaxCollectionCount,
LPDWORD lpCollectDataTimeout, LPTSTR lpUserName, DWORD nMaxUserNameSize);
|
说明:获取一个命名管道当前的状态信息
|
|
參数:
hNamedPipe Long,指定一个命名管道的句柄
lpState Long,用于装载下述一个或多个常数的长整数变量
PIPE_NOWAIT管道设置成永不阻塞,这样的模式非常少使用
PIPE_READMODE_MESSAGE管道设置成读取消息
lpCurInstancesLong,装载这个管道眼下存在的实例数量
lpMaxCollectionCount Long,如管道设置成通过一个网络数据传输,就用这个变量装载通过管道发送之前可排队等候的最大数据量
lpCollectDataTimeout Long,如管道设置成通过一个网络数据传输,就在这里指定一个长整数变量,用它装载进行一次网络数据传输前须要等候的最长时间
lpUserName String,如这是个server句柄,就在这里指定一个字串缓冲区,在当中加载客户应用程序的username。可设为vbNullString,表示不取回信息
nMaxUserNameSize Long,指定lpUserName缓冲区的长度,能够为零
|
|
GetNamedPipeInfo函数
|
函数原型:BOOL GetNamedPipeInfo(HANDLE hNamedPipe, LPDWORD lpFlags, LPDWORD
lpOutBufferSize, LPDWORD lpInBufferSize, LPDWORD lpMaxInstances);
|
说明:获得指定命名管道的信息
|
返回值:假设函数运行成功,返回值非零,否则,返回值为零,此时调用GetLastError函数获得扩展错误信息
|
參数:
hNamedPipe:命名管道句柄。这个句柄具有命名管道的GENERIC_READ訪问权限。
lpFlags:指定一个识别命名管道类型的32位变量。假设这个信息不需获得,能够给此參数置NULL。否则使用下面的值:
1. PIPE_CLIENT_END这个句柄是关于一个命名管道的client,此值被默认
2. PIPE_SERVER_END 这个句柄是关于命名管道的server端。假设这个值没有被指定,这个命名管道句柄是关于client的
3. PIPE_TYPE_BYTE 命名管道是一个字节管道型,此值被默认
4.
PIPE_TYPE_MESSAGE 命名管道是一个消息管道。假设这个值没有被指定,则默觉得字节管道型
lpOutBufferSize:一个32变量地址。用来按字节,返回输出数据缓冲的尺寸。假设缓冲值为零,则这个缓冲区没有按要求分配。假设镇魂歌信息不须要的,能够被置NULL.
lpInBufferSize:一个32变量地址。用来按字节,返回输入数据缓冲的尺寸。假设缓冲值为零,则这个缓冲区没有按要求分配。假设这个信息不须要的,能够被置NULL.
lpMaxInstances:一个32变量地址。用来获得被创建的管道实例的最大尺寸。假设此位被设置为PIPE_UNLIMITED_INSTANCES,被创建的管道实例的最大尺寸被依照系统的可容量所限制。假设这个信息不须要的,能够被置NULL.
|
|
PeekNamedPipe函数
|
函数原型:BOOL PeekNamedPipe(HANDLE hNamedPipe, LPVOID lpBuffer,DWORD nBufferSize,
LPDWORD lpBytesRead, LPDWORD lpTotalBytesAvail, LPDWORD lpBytesLeftThisMessage );
|
说明: 预览一个管道中的数据,或取得与管道中的数据有关的信息
|
|
參数:
hNamedPipe HANDLE,指定一个管道的句柄。这并不一定是某个命名管道的句柄——匿名管道相同适用
lpBuffer LPVOID,指定要装载数据的一个缓冲区的头一个字符。能够为零
nBufferSize DWORD,lpBuffer缓冲区长度
lpBytesRead LPDWORD,保存装载到缓冲区的字符数量
lpTotalBytesAvail LPDWORD,保存管道中可用的字符数量
lpBytesLeftThisMessage Long,保存这次读操作后仍然保留在消息中的字符数。仅仅能为那些基于消息的命名管道设置
|
注解:由这个函数读入的数据实际并不能从管道中删除。假设要对一个管道进行轮询,了解是否有可能数据,那么使用这个函数特别理想
|
|
SetNamedPipeHandleState函数
|
函数原型: BOOL SetNamedPipeHandleState(HANDLE
hNamedPipe, LPDWORD lpMode, LPDWORD lpMaxCollectionCount, LPDWORD lpCollectDataTimeout );
|
说明:设置与一个命名管道的运作有关的信息
|
|
參数:
hNamedPipe HANDLE,指定一个命名管道的句柄
lpMode LPDWORD,下列常数的一个或多个:PIPE_WAIT,
PIPE_NOWAIT, PIPE_READMODE_BYTE以及 PIPE_READMODE_MESSAGE。请參考CreateNamedPipe函数,了解有关这些标志的进一步情况
pMaxCollectionCount LPDWORD,如管道设为通过一个网络数据传输,则在这里指定通过管道发送之前可排除等候的最大数据量
lpCollectDataTimeout LPDWORD,如管道设为通过一个网络数据传输,则在这里指定网络数据传输前可以忍受的最长等候时间(超时)
|
|
TransactNamedPipe函数
|
函数原型: BOOL TransactNamedPipe( HANDLE
hNamedPipe, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesRead, LPOVERLAPPED lpOverlapped );
|
说明: 该函数在单独一个函数中同一时候合并了对管道的读、写操作。客户和server进程都可用它
|
返回值: BOOL,如操作已结束,则返回TRUE(非零);否则返回零。在异步模式中,GetLastError会设置成ERROR_IO_PENDING,并且操作会继续在后台进行。可測试lpOverlapped结构中的事件对象,了解操作是否结束
|
參数:
hNamedPipe HANDLE,指定一个消息类型的命名管道的句柄
lpInBuffer LPVOID,指定一个内存缓冲区,在当中包括要写入管道的数据
nInBufferSize DWORD,指定lpInBuffer缓冲区中的字节数量
lpOutBuffer LPVOID,指定一个内存缓冲区,用于装载从管道中读入的数据
nOutBufferSize DWORD,用于装载来自管道的数据
lpBytesRead LPDWORD,指定要从管道读入的字节数量。会读入单条消息。如因为lpOutBuffer不够大,不能容下完整的消息,那么函数会返回FALSE,并且GetLastError会设为ERROR_MORE_DATA(消息中剩下的全部字节都会丢失)
lpOverlapped LPOVERLAPPED,能够为NULL(变成ByVal
As Long,并传递零值),或指定包括了一个事件对象的OVERLAPPED结构
|
注解:如lpOverlapped设为NULL,或者句柄没有创建成FILE_FLAG_OVERLAPPED样式,那么除非读和写操作都完毕,否则函数不会返回
|
|
WaitNamedPipe函数
|
函数原型: BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut );
|
|
|
參数:
lpNamedPipeName LPCTSTR,指定要连接的管道名称
nTimeOut DWORD,以毫秒数表示的等待时间,或者下述常数之中的一个:
1.
NMPWAIT_USE_DEFAULT_WAIT使用管道创建时的默认超时设定
2. NMPWAIT_WAIT_FOREVER永远等待
|
|
|
二、
BOOL WaitNamedPipe( LPCTSTR lpNamedPipeName, DWORD nTimeOut );
lpNamedPipeName 要打开的管道名,格式\\servername\pipe\pipename,假设是本地管道则servername能够使用点“.”。
nTimeOut 等待命名管道的一个实例有效的超时时间,单位毫秒,也能够使用以下两个值中的一个:
NMPWAIT_USE_DEFAULT_WAIT 0x00000000,使用服务端CreateNamedPipe 创建管道时设置的超时时间。
NMPWAIT_WAIT_FOREVER 0xffffffff,一直等到一个命名管道的实例有效才返回。
返回值:
假设在超时时间前管道的一个实例有效,返回非0。
假设超时时间内没有一个有效的实例,返回0。
注意:
假设指定的命名管道没有实例存在,即没有服务端创建该命名管道(所以在确定server端创建了该命名管道时能够不调用此API),函数无视超时等待时间直接返回0。
假设函数运行成功返回TRUE,表示至少有一个命名管道的实例有效,接下来应该使用CreateFile函数打开命名管道的一个句柄,可是CreateFile可能会打开管道失败,由于该实例有可能被服务端关闭或被已经被其它client打开。
三、管道使用的伤痛所在
平时都使用得好好的管道突然出发问题了
static inline BOOL
CTPipe_WriteNBytes(HANDLE hPipe, BYTE *buf, DWORD size, DWORD timeout, HANDLE stopEvent)
{
BOOL ret = FALSE;
BOOL writeRet;
OVERLAPPED ol;
BYTE *pos;
DWORD cbBytesWrite;
HANDLE hWriteEvent;
memset( &ol, 0, sizeof(ol) );
ol.hEvent = hWriteEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if ( ol.hEvent == NULL )
{
return FALSE;
}
pos = buf;
while ( size > 0 )
{
writeRet = WriteFile(hPipe,
pos,
size,
&cbBytesWrite,
&ol);
if ( writeRet )
{
pos += cbBytesWrite;
size -= cbBytesWrite;
ResetEvent(hWriteEvent);
}
else
{
if ( GetLastError() == ERROR_IO_PENDING )
{
DWORD waitRet;
HANDLE handles[2];
int numOfHandles = 1;
handles[0] = hWriteEvent;
if ( stopEvent )
{
handles[1] = stopEvent;
numOfHandles++;
}
waitRet = WaitForMultipleObjects(numOfHandles, handles, FALSE, timeout);
if ( waitRet == WAIT_OBJECT_0 )
{
writeRet = GetOverlappedResult(hPipe, &ol, &cbBytesWrite, TRUE );
if ( writeRet == FALSE )
{
goto EXIT;
}
pos += cbBytesWrite;
size -= cbBytesWrite;
ResetEvent(hWriteEvent);
}
else /* timeout, notify event or other */
{
/*
* Cancel request
*/
if ( CancelIo(hPipe) == FALSE )
{
L_ERROR(_T("IoCancel fail 0x%x, force close pipe\n"), GetLastError());
CloseHandle(hPipe);
}
else
{
/*
* Wait cannel finish
*/
GetOverlappedResult(hPipe, &ol, &cbBytesWrite, TRUE);
}
goto EXIT;
}
}
else
{
goto EXIT;
}
}
}
ret = TRUE;
EXIT:
if ( hWriteEvent )
{
CloseHandle(hWriteEvent);
}
return ret;
}
在
waitRet = WaitForMultipleObjects(numOfHandles, handles, FALSE, timeout);
发生了堵塞,此管理是採用异步的,这里写的管道是作为服务端,问题是服务端的代码根本就没有变,倒是管道client的代码变了。
在client是起一个线程不断地读管道的数据,可是读到数据后是有一个处理数据的过程。
而在曾经的版本号呻这个过程仅仅是PostMessage一个消息,就继续去ReadFile了;而在新的版本号中读到数据却是花了非常多时间去处理数据,然后才去ReadFile。
导致服务端管道写堵塞的原因:
我们设置的模式是PIPE_WAIT,依据MSDN的要求是:当一端在写时,还有一要发生读的动作,才干写成功;否则堵塞。
hPipe = CreateNamedPipe("Name",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
0,
0,
0, /*XENHANCE_PIPE_TIMEOUT*/
&sa);
解决方法 :
法一、在服务端的管道写时设置超时时间,这尽管能使用服务端管道的线程不卡死,但也可能该写的数据没有写进去。
法二、依据上图所看到的,去掉“管道Client端“中的”等待RetEvent事件。
缺点:“管道Client端“不知道自己要求”管道Service端“运行的动作的结果。
1、
2、
3、
4、
5、
6、
7、