Windows和Linux下多个进程之间的一种通信手段,和其他通信手段相比,管道有它自己的限制和特点,管道实际上是一段
共享内存区,进程把共享消息放在那里。并通过一些 API 提供信息交换。
基本信息
管道是两个头的东西,每个头各连接一个进程或者同一个进程的不同代码,按照管道的类别分有两种管道,匿名的和命名的;按照管道的传输方向分也可以分成两种,单向的双向的。根据管道的特点,
命名管道通常用在网络环境下不同计算机上运行的进程之间的通信(当然也可以用在同一台机的不同进程中)它可以是单向或双向的;而匿名管道只能用在同一台计算机中,它只能是单向的。匿名管道其实是通过用给了一个指定名字的有名管道来实现的。
使用管道的好处在于:读写它使用的是对文件操作的 api,结果操作管道就和操作文件一样。即使在不同的计算机之间用
命名管道来通信,也不必了解和自己去实现网络间通信的具体细节。
使用说明
服务端通过函数 CreateNamedPipe 创建一个
命名管道的实例并返回用于今后操作的句柄,或为已存在的管道创建新的实例。
服务端侦听来自
客户端的连接请求,该功能通过 ConnectNamedPipe 函数实现。
客户端通过函数
WaitNamedPipe 来等待管道的出现,如果在超时值变为零以前,有一个管道可以使用,则 WaitNamedPipe 将返回 True,并通过调用 CreateFile 或
CallNamedPipe 来呼叫对
服务端的连接。 此时
服务端将接受
客户端的连接请求,成功建立连接,
服务端 ConnectNamedPipe 返回 True 建立连接之后,客户端与服务器端即可通过 ReadFile 和 WriteFile,利用得到的管道
文件句柄,彼此间进行信息交换。 当
客户端与服务的通信结束,客户端调用 CloseFile,服务端接着调用 DisconnectNamedPipe。最后调用函数CloseHandle来关闭该管道。 由于
命名管道使用时作为
客户端的程序必须知道管道的名称,所以更多的用在同一“作者”编写的服务器/工作站程序中,不可能随便找出一个程序来要求它和写的程序来通过命名管道通信。而匿名管道的使用则完全不同,它允许完全不相干地
进程通信,条件是这个进程通过控制台“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 来关闭两个管道。 下面是具体的说明和定义:
1. 建立匿名管道使用 CreatePipe 原形如下:
BOOL CreatePipe
PHANDLE hReadPipe, // address of variable for read handle
PHANDLE hWritePipe, // address of variable for write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // number of bytes reserved for pipe
;
当管道建立后,结构中指向的 hReadPipe 和 hWritePipe 可用来读写管道,当然由于匿名管道是单向的,只能使用其中的一个句柄,参数中的 SECURITY_ATTRIBUTES 的结构必须填写,定义如下:
typedef struct_SECURITY_ATTRIBUTES
DWORD nLength: //定义以
字节为单位的此结构的长度
LPVOID lpSecurityDescriptor; //指向控制这个对象共享的
安全描述符,如果为NULL这个对象将被分配一个缺省的安全描述
BOOL bInheritHandle; //当一个新过程被创建时,定义其返回是否是继承的.供系统
API函数使用.
SECURITY_ATTRIBUTES;
2. 填写创建子进程用的 STARTUPINFO 结构,一般可以先用
GetStartupInfo 来填写一个缺省的结构,然后改动用得到的地方,它们是:
hStdInput -- 用其中一个管道的 hWritePipe 代替 hStdOutput、hStdError -- 用另一个管道的 hReadPipe 代替 dwFlags -- 设置为 STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW 表示输入输出句柄及 wShowWindow 字段有效 wShowWindow -- 设置为 SW_HIDE,这样子进程执行时不显示窗口。 填写好以后,就可以用 CreateProcess 来执行子进程了,具体有关执行子进程的操作可以参考上一篇教程《进程控制》
3. 在程序中可以用
PeekNamedPipe 查询子进程有没有输出,原形如下:
HANDLE hNamedPipe, // handle to pipe to copy from
LPVOID lpBuffer, // pointer to data buffer
DWORD nBufferSize, // size, in bytes, of data buffer
LPDWORD lpBytesRead, // pointer to number of bytes read
LPDWORD lpTotalBytesAvail, // pointer to total number of bytes available
LPDWORD lpBytesLeftThisMessage // pointer to unread bytes in this message
;
可以将尝试读取 nBuffersize 大小的数据,然后可以通过返回的 BytesRead 得到管道中有多少数据,如果不等于零,则表示有数据可以读取。
4. 用 ReadFile 和 WriteFile 来读写管道,它们的参数是完全一样的,原形如下:
ReadFile or WriteFile
HANDLE hFile, // handle of file to read 在这里使用管道句柄
LPVOID lpBuffer, // address of buffer that receives data 缓冲区地址
DWORD nNumberOfBytesToRead, // number of bytes to read 准备读写的字节数
LPDWORD lpNumberOfBytesRead, // address of number of bytes read,实际读到的或写入的字节数
LPOVERLAPPED lpOverlapped // address of structure for data 在这里用 NULL
;
5. 用 CloseHandle 关闭管道一和管道二的 hReadPipe和 hWritePipe 这四个句柄。
下面给出了一个例
子程序,这个程序是上篇教程《进程控制》的例子的扩充,如果对有的 api 感到陌生的话,请先阅读上一篇教程。