简介
命名管道是一种命名的可以在管道服务器进程与管道客户进程进行单向或双向通信的一种机制。命名管道即可以是基于消息的也可以是基于字节流的。命名管道既可以实现同一台电脑上的进程间的通信,也可以实现基于网络的不同电脑上的进程间的通信,当然同一个进程即可以是服务端也可以是客户端。一个命名管道的多个实例可以共享同一个管道名称,但每个实例具有独立的缓冲区和句柄,因此每个实例可以同时与不同管道客户端进行通信而不互相影响,但是同一时刻,同一个实例只能与一个客户端进行通信。
命名管道服务端
服务端通过CreateNamePipe创建命名管道,以下是其函数声明
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_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
- lpName表示管道名称,必须是如下形式\.\pipe\pipename,小数点表示本地机器,因此不能在远程机器上创建管道,管道名称大小写不敏感,最长不能超过256个字符。
- dwOpenMode表示打开模式,该参数必须设置如下三个互斥标志中的一个:PIPE_ACCESS_DUPLEX(管道既可以读又可以写),PIPE_ACCESS_INBOUND(管道只能读不能写),PIPE_ACCESS_OUTBOUND(管道只能写不能读)。该参数还可以包含如下标志中的一个或多个:FILE_FLAG_FIRST_PIPE_INSTANCE (表示只有第一个管道实例可以创建成功,即使nMaxInstances参数设置为多个),FILE_FLAG_WRITE_THROUGH(此标志只对基于网络的字节流模式的管道起作用,当设置此标志时,当程序向管道写数据时,只有当所有数据写入到远端缓存区时才返回),FILE_FLAG_OVERLAPPED (是否启用重叠IO又叫异步IO,当此标志设置时,程序可以同一个线程并发访问管道的多个实例,也可以在一个管道实例上同时执行读和写操作)。多个标志用”|”分割。
- dwPipeMode用来设置管道模式,该参数必须包含如下互斥标志中的一个:PIPE_TYPE_BYTE (表示管道是基于字节流的),PIPE_TYPE_MESSAGE (表示管道是基于消息的)。该参数还可以设置如下两组互斥标志中的一个或多个:PIPE_READMODE_BYTE ,PIPE_READMODE_MESSAGE ,表示管道读取模式是基于消息还是基于字节流的,PIPE_READMODE_MESSAGE只能与PIPE_TYPE_MESSAGE一起使用; PIPE_WAIT,PIPE_NOWAIT,表示管道读取,写入与等待连接是是否阻塞,PIPE_NOWAIT表示不阻塞,因为实现管道异步有更好的方法(重叠IO),所以这个标志总是设置为PIPE_WAIT。
- nMaxInstances表示最多可以创建同名管道的多少个实例,不能超过255,也可以设置为PIPE_UNLIMITED_INSTANCES,由系统根据自身资源来决定最多能创建多少个实例。一般有三种方式实现同名管道的多个实例的并发操作:1、为每一个连接上的实例创建一个线程执行读写,2、采用重叠结构来实现,3、采用完成端口来实现。
- nOutBufferSize输入缓冲区的大小
- nInBufferSize输入缓冲区的大小
- nDefaultTimeOut客户端连接时的默认超时时间,只有客户端调用WaitNamePipe函数并将nTimeOut参数设置为NMPWAIT_USE_DEFAULT_WAIT起作用,当设置为0时,表示为50ms
lpSecurityAttributes设置基于ACL访问控制属性。
返回值为管道实例句柄。
服务端通过ConnectNamedPipe函数等待客户端连接,函数声明如下:
BOOL WINAPI ConnectNamedPipe(
_In_ HANDLE hNamedPipe,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
- hNamePipe管道实例句柄
lpOverlapped 重叠IO结构,当打开模式设置为FILE_FLAG_OVERLAPPED 有效。
如果成功返回值为True,如果启用重叠IO结构当返回为FALSE,但是调用GetLastError返回值为ERROR_IO_PENDING也表示成功。
通过ReadFile,ReadFileEx函数来读取管道,通过WriteFile,WirteFileEx函数来写入管道。
以下为一个不使用重叠结构的单实例的管道服务端代码:
#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
const int nBufferSize = 1024;
int main()
{
// 创建命名管道
HANDLE hPipe = CreateNamedPipe(
lpszPipename, PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES, nBufferSize, nBufferSize, 0, NULL);
if (hPipe == INVALID_HANDLE_VALUE)
{
cout << "CreateNamedPipe failed:" << GetLastError() << endl;
return -1;
}
// 等待客户端连接
BOOL bRet = ConnectNamedPipe(hPipe, NULL);
if (!bRet)
{
cout << "ConnectNamedPipe failed:" << GetLastError() << endl;
CloseHandle(hPipe);
return -2;
}
// 读取管道
DWORD dwBytesRead = 0;
TCHAR *pszRecvBuf = new TCHAR[nBufferSize];
bRet = ReadFile(hPipe,
pszRecvBuf, // 接收缓存区
nBufferSize * sizeof(TCHAR), // 接收缓冲区大小
&dwBytesRead, // 读取到的字节数
NULL); // 不用重叠结构
if (!bRet || dwBytesRead == 0)
{
cout << "ReadFile failed:" << GetLastError() << endl;
CloseHandle(hPipe);
return -3;
}
delete [] pszRecvBuf;
DWORD dwBytesWirte = 0;
TCHAR szWriteBuf[] = L"OK";
// 写入管道
bRet = WriteFile(hPipe,
szWriteBuf, // 写入数据缓冲区
sizeof(szWriteBuf), // 要写入的字节数
&dwBytesWirte, // 写入的数据
NULL); // 不用重叠结构
if (!bRet || dwBytesWirte == 0)
{
cout << "WriteFile failed:" << GetLastError() << endl;
CloseHandle(hPipe);
return -4;
}
FlushFileBuffers(hPipe); // 保证客户端完全读取管道中的内容
DisconnectNamedPipe(hPipe); // 断开管道连接
CloseHandle(hPipe); // 关闭管道
return 0;
}
命名管道客户端
客户端使用CreateFile函数来创建管道连接,如果管道是在本地机器上管道名称与服务端管道名称一致,如果是在远端机器上管道名称需要设置为\server\pipe\pipename(server为远端IP地址)。
客户端使用WaitNamedPipe函数检测服务端管道是否有实例可以连接,函数声明如下
BOOL WINAPI WaitNamedPipe(
_In_ LPCTSTR lpNamedPipeName, // 管道名称
_In_ DWORD nTimeOut
);
- lpNamedPipeName管道名称
- nTimeOut检测超时时间,当设置为NMPWAIT_USE_DEFAULT_WAIT 是,则会使用服务端管道创建时的设置默认超时时间(nDefaultTimeOut参数),当设置为NMPWAIT_WAIT_FOREVER 表示永久等待,知道服务端有可用实例为止,这里需要注意如果服务端管道尚未创建(也就是说至少存在一个实例),该函数会立即返回,而不管nTimeOut设置。
客户端通过SetNamedPipeHandleState函数设置管道句柄的模式,函数声明如下:
BOOL WINAPI SetNamedPipeHandleState(
_In_ HANDLE hNamedPipe,
_In_opt_ LPDWORD lpMode,
_In_opt_ LPDWORD lpMaxCollectionCount,
_In_opt_ LPDWORD lpCollectDataTimeout
);
- hNamedPipe表示管道句柄
- lpMode管道的模式可以设置为PIPE_TYPE_BYTE (表示管道是基于字节流的),PIPE_TYPE_MESSAGE (表示管道是基于消息的),要与服务端保持一致
- lpMaxCollectionCount,只有服务端在远程机器上起作用,表示客户端缓冲区最多积攒多少个字节,就必须向外发送一次,一般都设置为NULL
- lpCollectDataTimeout,只有服务端在远程机器上才起作用,表示向远端机器穿输信息时,必须在多长时间内完成,一般都设置为NULL
客户端也通过ReadFile,ReadFileEx函数来读取管道,通过WriteFile,WirteFileEx函数来写入管道。
以下为一个不使用重叠结构的客户端管道例子:
#include <windows.h>
#include <tchar.h>
#include <iostream>
using namespace std;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
const int nBufferSize = 1024;
int main()
{
HANDLE hPipe = INVALID_HANDLE_VALUE;
while (true)
{
// 创建连接管道的句柄
HANDLE hPipe = CreateFile(lpszPipename,
GENERIC_READ | GENERIC_WRITE, // 管道可读可写
0, // 共享模式,0表示不共享
NULL, // 默认的访问控制权限
OPEN_EXISTING, // 打开已经存在的管道
0, // 默认的文件属性
NULL); // 不设置临时模板文件,只有创建新文件时有用
if (hPipe != INVALID_HANDLE_VALUE)
break;
// 如果返回错误不是ERROR_PIPE_BUSY
if (GetLastError() != ERROR_PIPE_BUSY)
{
cout << "open pipe faild:" << GetLastError() << endl;
return -1;
}
WaitNamedPipe(lpszPipename, NMPWAIT_WAIT_FOREVER);
}
DWORD dwMode = PIPE_READMODE_MESSAGE;
BOOL bRet = SetNamedPipeHandleState(hPipe, &dwMode, NULL, NULL);
if (!bRet)
{
cout << "SetNamedPipeHandleState faild:" << GetLastError() << endl;
return -1;
}
// 写入管道
DWORD dwBytesWirte = 0;
TCHAR szWriteBuf[] = L"Hello";
bRet = WriteFile(hPipe,
szWriteBuf, // 写入数据缓冲区
sizeof(szWriteBuf), // 要写入的字节数
&dwBytesWirte, // 写入的数据
NULL); // 不用重叠结构
if (!bRet || dwBytesWirte == 0)
{
cout << "WriteFile failed:" << GetLastError() << endl;
CloseHandle(hPipe);
return -3;
}
// 读取管道
DWORD dwBytesRead = 0;
TCHAR *pszRecvBuf = new TCHAR[nBufferSize];
bRet = ReadFile(hPipe,
pszRecvBuf, // 接收缓存区
nBufferSize * sizeof(TCHAR), // 接收缓冲区大小
&dwBytesRead, // 读取到的字节数
NULL); // 不用重叠结构
if (!bRet || dwBytesRead == 0)
{
cout << "ReadFile failed:" << GetLastError() << endl;
CloseHandle(hPipe);
return -4;
}
_tprintf(TEXT("%s\n"), pszRecvBuf);
delete[] pszRecvBuf;
CloseHandle(hPipe);
return 0;
}
为了方便使用windows还提供了两个简化函数来完成一次管道读取:
TransactNamedPipe函数将WriteFile与ReadFile当成是一次客户事务,一次完成写入数据和读取数据,函数声明如下:
BOOL WINAPI TransactNamedPipe(
_In_ HANDLE hNamedPipe, // 管道句柄
_In_ LPVOID lpInBuffer, // 要写入的数据缓存
_In_ DWORD nInBufferSize, // 要写入的数据大小,以字节为单位
_Out_ LPVOID lpOutBuffer, // 读取数据缓存
_In_ DWORD nOutBufferSize, // 读取数据缓存大小
_Out_ LPDWORD lpBytesRead, // 实际读取的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped // 重叠IO结构
);
CallNamedPipe将CreateFile,WaitNamedPipe,WriteFile,ReadFile,CloseHandle当成是一次客户事务,一次完成管道打开,写入,读取,和关闭,函数声明如下:
BOOL WINAPI CallNamedPipe(
_In_ LPCTSTR lpNamedPipeName, // 管道名称
_In_ LPVOID lpInBuffer, // 吸入数据缓存
_In_ DWORD nInBufferSize, // 写入数据缓存大小
_Out_ LPVOID lpOutBuffer, // 读取数据缓存
_In_ DWORD nOutBufferSize, // 读取数据缓存大小
_Out_ LPDWORD lpBytesRead, // 实际读取的字节数
_In_ DWORD nTimeOut // 超时设置
);
注意CallNamePipe是同步的,不支持重叠结构。
其他的一些辅助函数
- GetNamedPipeClientComputerName:获取客户端机器名称
- GetNamedPipeClientProcessId:获取客户进程ID
- GetNamedPipeClientSessionId:获取客户进程所属会话
- GetNamedPipeHandleState:获取管道状态信息,例如管道是基于消息的还是基于字节流的,管道是阻塞的还是非阻塞的,管道实例的数量
- GetNamedPipeInfo:获取管道信息,例如输入输出缓冲区大小,最大实例数量
- GetNamedPipeServerProcessId:获取服务端进程ID
- GetNamedPipeServerSessionId:获取服务端回话ID
查看当前机器上都有哪些命名管道
windows在SysinternalsSuite工具包中提供了pipelist.exe工具来查看当前电脑上有哪些命名管道:
其中Pipe Name列显示管道名称,Instances显示此管道当前存在多少个实例,Max Instances表示此管道最多可以创建多少个实例,-1表示系统根据自身资源来决定,也就是在调用CreateNamedPipe函数中的nMaxInstances设置为PIPE_UNLIMITED_INSTANCES。