您的位置:首页 > 其它

Windows API 进程间通信,管道(Pipe)

2015-05-07 12:39 260 查看


Windows API 进程间通信,管道(Pipe)

管道是一种用于在进程间共享数据的机制,其实质是一段共享内存。Windows系统为这段共享的内存设计采用数据流I/0的方式来访问。由一个进程读、另一个进程写,类似于一个管道两端,因此这种进程间的通信方式称作“管道”。
管道分为匿名管道和命名管道。
匿名管道只能在父子进程间进行通信,不能在网络间通信,而且数据传输是单向的,只能一端写,另一端读。
命令管道可以在任意进程间通信,通信是双向的,任意一端都可读可写,但是在同一时间只能有一端读、一端写。
一、注意点
1、常用API
Pipes[2]
在[3,4]中也对这一部分进行了介绍。
2、示例
1)服务器端
创建管道 >> 监听 >> 读写 >> 关闭
CreateNamedPipe
ConnectNamedPipe
ReadFile/WriteFile
DisconnectNamedPipe
示例代码



通过pipe进程间通信
**************************************/
/* 头文件 */
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
/* 常量 */
#define PIPE_TIMEOUT 5000
#define BUFSIZE 4096
/* 结构定义 */
typedef struct
{
OVERLAPPED oOverlap;
HANDLE hPipeInst;
TCHAR chRequest[BUFSIZE];
DWORD cbRead;
TCHAR chReply[BUFSIZE];
DWORD cbToWrite;
} PIPEINST, *LPPIPEINST;
/* 函数声明 */
VOID DisconnectAndClose(LPPIPEINST);
BOOL CreateAndConnectInstance(LPOVERLAPPED);
BOOL ConnectToNewClient(HANDLE, LPOVERLAPPED);
VOID GetAnswerToRequest(LPPIPEINST);
VOID WINAPI CompletedWriteRoutine(DWORD, DWORD, LPOVERLAPPED);
VOID WINAPI CompletedReadRoutine(DWORD, DWORD, LPOVERLAPPED);
/* 全局变量 */
HANDLE hPipe;
/* ************************************
* int main(VOID)
* 功能    pipe 通信服务端主函数
**************************************/
int main(VOID)
{
HANDLE hConnectEvent;
OVERLAPPED oConnect;
LPPIPEINST lpPipeInst;
DWORD dwWait, cbRet;
BOOL fSuccess, fPendingIO;

// 用于连接操作的事件对象
hConnectEvent = CreateEvent(
NULL,    // 默认属性
TRUE,    // 手工reset
TRUE,    // 初始状态 signaled
NULL);   // 未命名

if (hConnectEvent == NULL)
{
printf("CreateEvent failed with %d.\n", GetLastError());
return 0;
}
// OVERLAPPED 事件
oConnect.hEvent = hConnectEvent;

// 创建连接实例,等待连接
fPendingIO = CreateAndConnectInstance(&oConnect);

while (1)
{
// 等待客户端连接或读写操作完成
dwWait = WaitForSingleObjectEx(
hConnectEvent,  // 等待的事件
INFINITE,       // 无限等待
TRUE);

switch (dwWait)
{
case 0:
// pending
if (fPendingIO)
{
// 获取 Overlapped I/O 的结果
fSuccess = GetOverlappedResult(
hPipe,     // pipe 句柄
&oConnect, // OVERLAPPED 结构
&cbRet,    // 已经传送的数据量
FALSE);    // 不等待
if (!fSuccess)
{
printf("ConnectNamedPipe (%d)\n", GetLastError());
return 0;
}
}

// 分配内存
lpPipeInst = (LPPIPEINST) HeapAlloc(GetProcessHeap(),0,sizeof(PIPEINST));
if (lpPipeInst == NULL)
{
printf("GlobalAlloc failed (%d)\n", GetLastError());
return 0;
}
lpPipeInst->hPipeInst = hPipe;

// 读和写,注意CompletedWriteRoutine和CompletedReadRoutine的相互调用
lpPipeInst->cbToWrite = 0;
CompletedWriteRoutine(0, 0, (LPOVERLAPPED) lpPipeInst);

// 再创建一个连接实例,以响应下一个客户端的连接
fPendingIO = CreateAndConnectInstance(
&oConnect);
break;

// 读写完成
case WAIT_IO_COMPLETION:
break;

default:
{
printf("WaitForSingleObjectEx (%d)\n", GetLastError());
return 0;
}
}
}
return 0;
}

/* ************************************
* CompletedWriteRoutine
*     写入pipe操作的完成函数
*    接口参见FileIOCompletionRoutine回调函数定义
*
*    当写操作完成时被调用,开始读另外一个客户端的请求
**************************************/
VOID WINAPI CompletedWriteRoutine(
DWORD dwErr,
DWORD cbWritten,
LPOVERLAPPED lpOverLap)
{
LPPIPEINST lpPipeInst;
BOOL fRead = FALSE;
// 保存overlap实例
lpPipeInst = (LPPIPEINST) lpOverLap;

// 如果没有错误
if ((dwErr == 0) && (cbWritten == lpPipeInst->cbToWrite))
{
fRead = ReadFileEx(
lpPipeInst->hPipeInst,
lpPipeInst->chRequest,
BUFSIZE*sizeof(TCHAR),
(LPOVERLAPPED) lpPipeInst,
// 写读操作完成后,调用CompletedReadRoutine
(LPOVERLAPPED_COMPLETION_ROUTINE) CompletedReadRoutine);
}
if (! fRead)
// 出错,断开连接
DisconnectAndClose(lpPipeInst);
}

/* ************************************
* CompletedReadRoutine
*     读取pipe操作的完成函数
*    接口参见FileIOCompletionRoutine回调函数定义
*
*    当读操作完成时被调用,写入回复
**************************************/
VOID WINAPI CompletedReadRoutine(
DWORD dwErr,
DWORD cbBytesRead,
LPOVERLAPPED lpOverLap)
{
LPPIPEINST lpPipeInst;
BOOL fWrite = FALSE;

// 保存overlap实例
lpPipeInst = (LPPIPEINST) lpOverLap;

// 如果没有错误
if ((dwErr == 0) && (cbBytesRead != 0))
{
// 根据客户端的请求,生成回复
GetAnswerToRequest(lpPipeInst);
// 将回复写入到pipe
fWrite = WriteFileEx(
lpPipeInst->hPipeInst,
lpPipeInst->chReply,    //将响应写入pipe
lpPipeInst->cbToWrite,
(LPOVERLAPPED) lpPipeInst,
// 写入完成后,调用CompletedWriteRoutine
(LPOVERLAPPED_COMPLETION_ROUTINE) CompletedWriteRoutine);
}

if (! fWrite)
// 出错,断开连接
DisconnectAndClose(lpPipeInst);
}

/* ************************************
* VOID DisconnectAndClose(LPPIPEINST lpPipeInst)
* 功能    断开一个连接的实例
* 参数    lpPipeInst,断开并关闭的实例句柄
**************************************/
VOID DisconnectAndClose(LPPIPEINST lpPipeInst)
{
// 关闭连接实例
if (! DisconnectNamedPipe(lpPipeInst->hPipeInst) )
{
printf("DisconnectNamedPipe failed with %d.\n", GetLastError());
}
// 关闭 pipe 实例的句柄
CloseHandle(lpPipeInst->hPipeInst);
// 释放
if (lpPipeInst != NULL)
HeapFree(GetProcessHeap(),0, lpPipeInst);
}

/* ************************************
* BOOL CreateAndConnectInstance(LPOVERLAPPED lpoOverlap)
* 功能    建立连接实例
* 参数    lpoOverlap,用于overlapped IO的结构
* 返回值    是否成功
**************************************/
BOOL CreateAndConnectInstance(LPOVERLAPPED lpoOverlap)
{
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\samplenamedpipe");
// 创建named pipe
hPipe = CreateNamedPipe(
lpszPipename,             // pipe 名
PIPE_ACCESS_DUPLEX |      // 可读可写
FILE_FLAG_OVERLAPPED,     // overlapped 模式
// pipe模式
PIPE_TYPE_MESSAGE |       // 消息类型 pipe
PIPE_READMODE_MESSAGE |   // 消息读模式
PIPE_WAIT,                // 阻塞模式
PIPE_UNLIMITED_INSTANCES, // 无限制实例
BUFSIZE*sizeof(TCHAR),    // 输出缓存大小
BUFSIZE*sizeof(TCHAR),    // 输入缓存大小
PIPE_TIMEOUT,             // 客户端超时
NULL);                    // 默认安全属性
if (hPipe == INVALID_HANDLE_VALUE)
{
printf("CreateNamedPipe failed with %d.\n", GetLastError());
return 0;
}

// 连接到新的客户端
return ConnectToNewClient(hPipe, lpoOverlap);
}

/* ************************************
* BOOL ConnectToNewClient(HANDLE hPipe, LPOVERLAPPED lpo)
* 功能    建立连接实例
* 参数    lpoOverlap,用于overlapped IO的结构
* 返回值    是否成功
**************************************/
BOOL ConnectToNewClient(HANDLE hPipe, LPOVERLAPPED lpo)
{
BOOL fConnected, fPendingIO = FALSE;

// 开始一个 overlapped 连接
fConnected = ConnectNamedPipe(hPipe, lpo);

if (fConnected)
{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());
return 0;
}
switch (GetLastError())
{
// overlapped连接进行中.
case ERROR_IO_PENDING:
fPendingIO = TRUE;
break;
// 已经连接,因此Event未置位
case ERROR_PIPE_CONNECTED:
if (SetEvent(lpo->hEvent))
break;
// error
default:
{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());
return 0;
}
}
return fPendingIO;
}

// TODO根据客户端的请求,给出响应
VOID GetAnswerToRequest(LPPIPEINST pipe)
{
_tprintf( TEXT("[%d] %s\n"), pipe->hPipeInst, pipe->chRequest);
lstrcpyn( pipe->chReply,  TEXT("Default answer from server") ,BUFSIZE);
pipe->cbToWrite = (lstrlen(pipe->chReply)+1)*sizeof(TCHAR);
}


2)客户端

打开命令管道,获得句柄 >> 写入数据 >> 等待回复
WaitNamedPipe
SetNamedPipeHandleState
示例代码



通过pipe进程间通信
**************************************/
/* 头文件 */
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
/* 常量 */
#define BUFSIZE 512
/* ************************************
* int main(VOID)
* 功能    pipe 通信服务端主函数
**************************************/
int main(int argc, TCHAR *argv[])
{
HANDLE hPipe;
LPTSTR lpvMessage=TEXT("Default message from client");
TCHAR chBuf[BUFSIZE];
BOOL fSuccess;
DWORD cbRead, cbWritten, dwMode;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\samplenamedpipe");

if( argc > 1 )    // 如果输入了参数,则使用输入的参数
lpvMessage = argv[1];
while (1)
{
// 打开一个命名pipe
hPipe = CreateFile(
lpszPipename,   // pipe 名
GENERIC_READ |   GENERIC_WRITE,        //  可读可写
0,              // 不共享
NULL,           // 默认安全属性
OPEN_EXISTING,  // 已经存在(由服务端创建)
0,              // 默认属性
NULL);
if (hPipe != INVALID_HANDLE_VALUE)
break;

// 如果不是 ERROR_PIPE_BUSY 错误,直接退出
if (GetLastError() != ERROR_PIPE_BUSY)
{
printf("Could not open pipe");
return 0;
}

// 如果所有pipe实例都处于繁忙状态,等待2秒。
if (!WaitNamedPipe(lpszPipename, 2000))
{
printf("Could not open pipe");
return 0;
}
}

// pipe已经连接,设置为消息读状态
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
hPipe,    // 句柄
&dwMode,  // 新状态
NULL,     // 不设置最大缓存
NULL);    // 不设置最长时间
if (!fSuccess)
{
printf("SetNamedPipeHandleState failed");
return 0;
}

// 写入pipe
fSuccess = WriteFile(
hPipe,                  // 句柄
lpvMessage,             // 写入的内容
(lstrlen(lpvMessage)+1)*sizeof(TCHAR), // 写入内容的长度
&cbWritten,             // 实际写的内容
NULL);                  // 非 overlapped
if (!fSuccess)
{
printf("WriteFile failed");
return 0;
}

do
{
// 读回复
fSuccess = ReadFile(
hPipe,    // 句柄
chBuf,    // 读取内容的缓存
BUFSIZE*sizeof(TCHAR),  // 缓存大小
&cbRead,  // 实际读的字节
NULL);    // 非 overlapped

if (! fSuccess && GetLastError() != ERROR_MORE_DATA)
break; //失败,退出

_tprintf( TEXT("%s\n"), chBuf ); // 打印读的结果
} while (!fSuccess);  //  ERROR_MORE_DATA 或者成功则循环

getch();//任意键退出
// 关闭句柄
CloseHandle(hPipe);
return 0;
}


3、I/O简介

I/O模式不仅在进程间通信时使用,任何具有数据流形式的输入输出(包括文件输入输出、内核通信、网络输入输出等)都涉及I/O模式。
异步(
Asynchronous)和同步(Synchronous) I/O是两种基本的I/O模式。
同步I/O

所谓同步I/O是指在调用ReadFile、WriteFile等函数进行输入输出操作时,系统完成了输入输出ReadFile、WriteFile才返回。在操作系统进行I/O操作的过程上,用户态线程不能执行,因此在同步I/O时,如果需要在I/O时进行其他操作就只能再开启线程。
异步I/O
异步I/O是在调用ReadFile、WriteFile等函数后,函数立即返回,线程可以进行其他操作。剩下的I/O操作在系统内核中自动完成。那么在系统内核完成输入输出后,程序如何知道I/O是否已完成?
一种方法,称作完成函数(Completion
Routines),如果使用ReadFileEx、WriteFileEx等进行I/O,可以指定完成函数,所谓完成函数是指内核在完成I/O后,内核会回调这个函数。当完成函数被调用时,就指明内核已经完成了I/O,程序可以在这个函数中进行一个I/O完成后需要的操作(例如释放内存)。
参考
[1] 精通Windows
API 函数、接口、编程实例
[2] http://msdn.microsoft.com/en-us/library/aa365137%28VS.85%29.aspx
[3] /article/4743262.html
[4] /article/4743261.html

管道(pipe)是进程用来通讯的共享内存区域。一个进程往管道中写入信息,而其它的进程可以从管道中读出信息。如其名,管道是进程间数据交流的通道。邮路(Mailslots)的功能与管道类似,也是进程间通讯(interprocess communications,IPC)的媒介,只不过其具体实现方式与管道有些差别。一个基于Win32的应用程序可以在邮路中储存消息,这些消息通常通过网络发往一个指定的计算机或某域名(域是共享一个组名的一组工作站或服务器。)下的所有计算机。你也可以使用命名管道代替邮路来进行进程间通信。命名管道最适合用来两个进程间的消息传递,邮路则更适合一个进程向多个进程广播消息。邮路具有一个重要的特点,它使用数据包广播消息。广播(broadcast)是网络传输中使用的术语,它意味着接收方收到数据后不发送确认消息通知发送方。而管道(这里的管道指命名管道,有关命名管道以下详解。)则不同,它更类似于打电话,你只对一个当事人说话,但是你却非常清楚你的话都被对方听到。邮路和管道一样,也是一个虚拟文件,它保存在内存中,但是你却必须使用普通的Win32文件函数访问它,比如CreateFile、ReadFile、WriteFile等。邮路中储存的数据可以是任何形式的,唯一的要求是不得超过64K。与磁盘文件不同的是,邮路是一个临时的对象,当某个邮路所有的句柄都关闭的时候,该邮路及其中的数据就被删除。

管道的类型有两种:匿名管道和命名管道。匿名管道是不命名的,它最初用于在本地系统中父进程与它启动的子进程之间的通信。命名管道更高级,它由一个名字来标识,以使客户端和服务端应用程序可以通过它进行彼此通信。而且,Win32命名管道甚至可以在不同系统的进程间使用,这使它成为许多客户/服务器应用程序的理想之选。

就像水管连接两个地方并输送水一样,软件的管道连接两个进程并输送数据。一个一个管道一旦被建立,它就可以象文件一样被访问,并且可以使用许多与文件操作同样的函数。可以使用CreateFile函数获取一个已打开的管道的句柄,或者由另一个进程提供一个句柄。使用WriteFile函数向管道写入数据,之后这些数据可以被另外的进程用ReadFile函数读取。管道是系统对象,因此管道的句柄在不需要时必须使用CloseHandle函数关闭。

匿名管道只能单向传送数据,而命名管道可以双向传送。管道可以以比特流形式传送任意数量的数据。命名管道还可以将数据集合到称为消息的数据块中。命名管道甚至具有通过网络连接多进程的能力。但遗憾的是Windows9X不支持创建命名管道,它只能在WindowsNT系列(如Windows NT,Windows 2000,Windows XP)的操作系统上创建。

示例代码:

进程1:

#include<windows.h>

#include<iostream>

using namespace std;

void main()

{

HANDLE hPipe=CreateNamedPipe("\\\\.\\pipe\\MyPipe",

PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,

0,1,1024,1024,0,NULL);

if(INVALID_HANDLE_VALUE==hPipe)

{

cout<<"创建命名管道失败!"<<endl;

hPipe=NULL;

return;

}

HANDLE hEvent;

hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);

if(!hEvent)

{

cout<<"创建事件对象失败!"<<endl;

CloseHandle(hPipe);

hPipe=NULL;

return;

}

OVERLAPPED ovlap;

ZeroMemory(&ovlap,sizeof(OVERLAPPED));

ovlap.hEvent=hEvent;

if(!ConnectNamedPipe(hPipe,&ovlap))

{

if(ERROR_IO_PENDING!=GetLastError())

{

cout<<"等待客户端连接失败!"<<endl;

CloseHandle(hPipe);

CloseHandle(hEvent);

hPipe=NULL;

return;

}

}

if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))

{

cout<<"等待对象失败!"<<endl;

CloseHandle(hPipe);

CloseHandle(hEvent);

hPipe=NULL;

return;

}

CloseHandle(hEvent);

/////////////////////////////////////////读取内容

char buf1[100];

DWORD dwRead;

if(!ReadFile(hPipe,buf1,100,&dwRead,NULL))

{

cout<<"ReadFile failed"<<endl;

return;

}

else

{

cout<<"读取成功"<<endl;

cout<<buf1<<endl;

}

}

进程2:

#include<windows.h>

#include<iostream>

using namespace std;

void main()

{

HANDLE hPipe;

if(!WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER))

{

cout<<"当前没有可利用的命名管道实例!"<<endl;

return;

}

hPipe=CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ | GENERIC_WRITE,

0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(INVALID_HANDLE_VALUE==hPipe)

{

cout<<"打开命名管道失败!"<<endl;

hPipe=NULL;

return;

}

else

{

cout<<"打开管道成功"<<endl;

}

//////////////////////////写入内容

char buf[100]="howareyou";

DWORD dwWrite;

if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))

{

cout<<"WriteFile failed"<<endl;

return;

}

else

{

cout<<"写入成功"<<endl;

}

}

命名管道

一个命名管道是一个有名的、用于管道服务端与一个或多个管道客户端进行单路(“One-way”)或双向通讯的管道。一个命名管道的所有实例共享同一个管道名字,但是每一个实例都有它自己的管道句柄和缓冲区,并为客户/服务端的通讯提供独立的通讯渠道。管道实例的使用可以让多个管道客户端同时使用同一个命名管道。

任何进程都可以访问命名管道,并接受安全权限的检查,通过命名管道使相关的或不相关的进程之间的通讯变得异常简单!

任何进程都可以扮演服务端和客户端双重角色,这一点使点对点双向通讯成为可能。在这里,管道服务端进程指的是创建命名管道的一端,而管道客户端指的是连接到命名管道某个实例的一端。

命名管道可以用在为本机或不同计算机(跨网络)的进程之间提供通讯的场合,。如果服务端的服务正在运行,那么所有的命名管道都可以通过网络进行远程访问。如果你只关注命名管道的本机应用场景,那么你完全可以禁止NT AUTHORITY/NETWORK用户访问该命名管道(通过设置管道的安全描述符)或者用本机RPC通讯机制来作为替代方案。

管道命名

每个命名管道都有一个全局惟一的名字以示和其它命名管道的区别。管道服务端在调用CreateNamedPipe函数创建一个或多个命名管道实例时会为管道指定一个名字,而管道客户端当调用CreateFileCallNamedPipe函数连接命名管道某个实例时会指定要连接的命名管道的名字。

当在CreateFile、WaitNamedPipeCallNamedPipe函数中指定管道的名字时请使用如下命名格式:

//ServerName/pipe/PipeName

其中ServerName域是远程或本地计算机的名字,而管道的名字字符串则是由PipeName域来指定的,它可以包含除了反斜杠以外的所有字符,包括数字和特殊字符,整个管道的名字字符串最长可以有256个字符。注意:管道名字不区分大小写!

Windows Me、98、95: 管道名字不能包含冒号

管道服务端不能在另外一台计算机上创建一个管道(也就是说对于管道服务端而言,它只能创建本机命名管道,远程命名管道是对管道客户端而言的),因此CreateNamedPipe函数必须使用一个用句号代替的ServerName域名,格式如下:

//./pipe/PipeName

管道服务端经常要传递管道的名字给它的客户端,这样管道客户端才能连接到管道。否则,管道客户端必须在编译时就得知道管道的名字。

命名管道打开模式

管道服务端在调用CreateNamedPipe函数创建命名管道时在dwOpenMode参数中指定管道的访问方式、异步(重叠)以及直写(Write-through)模式等信息。管道客户端可以在调用CreateFile函数时指定管道的打开模式。

l 访问模式

设置管道的访问方式相当于指定管道服务端句柄的读写访问,下表列出了可以在CreateNamedPipe函数中指定的访问模式掩码,并列出了与CreateFile函数相对应的访问模式掩码:

访问模式
CreateFile的等价物
PIPE_ACCESS_INBOUND
GENERIC_READ (服务端只读,客户端只写)
PIPE_ACCESS_OUTBOUND
GENERIC_WRITE (服务端只写,客户端只读)
PIPE_ACCESS_DUPLEX
GENERIC_READ | GENERIC_WRITE (服务端和客户端可读、可写)
管道客户端使用CreateFile函数连接到命名管道时必须在dwDesiredAccess参数中指定一个和管道服务端(创建管道时指定的访问模式)相兼容的访问模式。例如,当管道服务端创建管道时指定了PIPE_ACCESS_OUTBOUND访问模式,那么,管道客户端就必须指定GENERIC_READ访问模式。注意:对于所有的管道实例访问模式必须一样!

l 异步(重叠)模式

在异步(重叠)模式中,函数执行漫长的读、写和连接操作时可以立即返回而不会阻塞。这使得线程在后台执行耗时操作的同时可以继续执行其它操作。要指定异步(重叠)模式,请使用FILE_FLAG_OVERLAPPED标志。

CreateFile函数允许管道客户端在dwFlagsAndAttributes参数中使用(FILE_FLAG_OVERLAPPED)标志把管道句柄设置为异步(重叠)模式。

l 直写模式

我们通过FILE_FLAG_WRITE_THROUGH标志来指定直写模式。这个模式仅仅对字节类型管道(也仅限于管道客户端和管道服务端在不同计算机上的情况)的写操作产生影响。在直写模式下,那些对命名管道进行写操作的函数不会返回,直到网络上的数据传输完毕,并完整地保存到在远程计算机上的管道缓冲区中。直写模式对那些要求每一个写操作都要求同步的应用非常有用。

如果直写模式没有打开,系统通过缓冲机制来提高网络操作的效率,缓冲机制使系统将多个写操作合并到单个网络传输任务中,这意味着对于用户的某个写操作可以在系统将数据放入输出缓冲区后就成功返回,而不必等系统真正将数据传输完毕再成功返回。

CreateFile函数允许管道客户端在dwFlagsAndAttributes参数中使用 (FILE_FLAG_WRITE_THROUGH)标志把管道句柄设置为直写模式。但要记住,直写模式的管道句柄一旦创建就不能再更改,并且直写模式对于同一个管道实例,服务端和客户端的句柄可以不同。

客户端管道可以使用SetNamedPipeHandleState函数控制那些直写模式被禁用的管道在传输任务开始之前的字节数和超时时间。

命名管道的输入模式、读模式和等待模式

管道服务端在CreateNamedPipe函数中通过dwPipeMode参数来指定管道的类型、读和等待模式,管道客户端也可以对CreateFile函数返回的管道句柄指定这些管道模式。

l 输入模式

> 管道的输入模式确定了数据是如何被写进管道的。数据可以字节流或消息流的方式在管道中传输,管道服务端在调用CreateNamedPipe函数创建管道实例时指定管道输入模式,并且这个输入模式对所有的管道实例都必须相同。

> 要创建一个字节类型管道,请指定PIPE_TYPE_BYTE标志或使用默认值即可。在这种输入模式下,所有数据都是以字节流方式被写进管道的,并且系统不区分不同写操作所写入字节之间的差异(就是所写数据一视同仁,一律都按字节对待)。

> 要创建一个消息类型管道,请指定PIPE_TYPE_MESSAGE标志。系统把每次写操作所写入的字节都当作一个消息单元来处理。在直写模式打开的情况下,系统总是在消息类型管道上完成写操作

l 读模式

> 管道的读模式确定了数据是如何从管道读出的。管道服务端在调用CreateNamedPipe函数时为管道句柄指定初始的读模式,我们有两种模式可以读取数据:字节读模式和消息读模式。一个指向字节类型管道的句柄只能用字节读模式,而一个指向消息类型管道的句柄即可以用字节读模式又可以用消息读模式。对于同一个管道实例,服务端和客户端句柄的读模式可以不同。

> 要创建字节读模式的管道句柄,请指定PIPE_READMODE_BYTE标志。数据作为字节流从管道读出。当读取了管道中所有有效字节或者读到了用户指定数量的字节内容,一次读操作就会成功完成。

> 要创建消息模式的管道句柄,请指定PIPE_READMODE_MESSAGE标志。数据作为消息流从管道读出。*只有*在整个消息被读出的情况下一次读操作才会成功完成!如果指定读取的字节长度小于下一条消息的大小,函数在返回0之前会读取尽可能多的消息内容(GetLastError函数此时返回ERROR_MORE_DATA)。而剩余的消息你可以用另外的读操作取出。

> 对于管道客户端,CreateFile函数返回的管道句柄初始总是字节读模式工作,管道客户端和管道服务端都可以使用SetNamedPipeHandleState函数来改变管道句柄的读模式。

l 等待模式

> 管道句柄的等待模式确定了ReadFile、WriteFileConnectNamedPipe函数如何处理那些耗时的操作。在阻塞等待模式中,这些函数会无限等待管道另一端的进程完成某个操作,而在非阻塞等待模式下,这些函数可以立即返回,否则需要无限期等待。

> 在管道为空的情况下(即:管道里面没有任何数据)管道句柄的等待模式会对ReadFile操作产生影响。使用阻塞等待(模式的)句柄时,(ReadFile)操作只有在线程写到管道对端的数据变得可用时才会成功完成。如果使用非阻塞等待(模式的)句柄时,(ReadFile)函数立即返回0,并且GetLastError函数返回ERROR_NO_DATA。

> 在管道缓冲区不足的情况下(即:管道被塞满)管道句柄的等待模式会对WriteFile操作产生影响。使用阻塞等待(模式的)句柄时,只有线程从管道的另一端读取数据以腾出足够空间后写操作才会成功完成。如果使用非阻塞等待(模式的)句柄时,要么没有写进任何字节(对消息类型管道而言),要么在写入所持缓冲区的所有字节后(对字节类型管道而言),写操作都会立即返回一个*非*0值。

> 在没有客户端连接或等待连接到管道实例的情况下管道句柄的等待模式会对ConnectNamedPipe操作产生影响。使用阻塞等待(模式的)句柄时,只有当某个管道客户端调用CreateFileCallNamedPipe函数连接到管道实例上,连接操作才会成功返回。如果使用非阻塞等待(模式的)句柄时,连接操作立即返回0,并且GetLastError函数返回ERROR_PIPE_LISTENING。

> CreateNamedPipeCreateFile函数创建的命名管道句柄默认都是阻塞等待模式。要想用*非*阻塞等待模式创建管道,管道服务端在调用CreateNamedPipe函数时需要指定PIPE_NOWAIT标志。

> 管道客户端和管道服务端都可以通过调用SetNamedPipeHandleState函数指定PIPE_WAIT或PIPE_NOWAIT标志来改变管道句柄的等待模式。

注意:非阻塞等待模式是为了和“Microsoft® LAN Manager version 2.0”保持兼容才被支 持的。这种模式不应该被用来实现重叠I/O的命名管道,而应该使用真正的重叠I/O技术,因为它(重叠I/O技术)使函数在返回后可以在后台运行耗时操作。

命名管道实例

> 我能想得到的最简单的管道服务器模型应该是:管道服务端创建一个管道实例,并连接到一个客户端,之后和客户端进行正常通讯,通讯结束后断开与客户端的连接、关闭管道句柄并终止服务。它虽然简单可是在单服务器和多客户端通讯的场合这种模型却非常常见。一个管道服务器通过“与每个管道客户端先建立连接,然后再断开连接”的操作序列可以实现用单一的服务端管道实例来和多个管道客户端进行连接的目的,但是这样操作效率会非常低。管道服务端必须创建多个管道实例来有效地处理多个客户端的并发连接。

> 下面有三个基本模型可以用于多管道实例的服务:

l 为每个管道实例创建一个独立的处理线程。例如MSDN中“多线程管道服务器“例子:Multithreaded Pipe Server

l 在ReadFile、 WriteFileConnectNamedPipe函数中指定一个OVERLAPPED结构来使用重叠操作。例如MSDN中“使用重叠I/O的命名管道服务器
“例子:Named Pipe Server Using Overlapped I/O

l 为ReadFileExWriteFileEx函数指定一个(当操作完成时被系统执行的)完成例程来使用重叠操作。例如MSDN中“使用完成例程的命名管道服务器 “例子:Named
Pipe Server Using Completion Routines

多线程管道服务器模型相比上面三个模型是最容易编写的一个,因为每个管道实例线程处理与一个单一客户端的通讯,线程之间相互独立,互不干扰。系统为每个线程分配所需的处理器时间。但是每个线程使用系统资源对于一个需要处理大量客户端请求的服务器是不利的。

对一个单线程服务器而言,它更容易协调对多个客户端都产生影响的操作,并且它在保护多个客户端对共享资源的并发访问上面相对容易一些。对一个单线程服务器的挑战就是需要重叠操作的协调为处理客户端的并发需要分配相应的处理器时间。

同步和异步(重叠)输入和输出

ReadFile、WriteFile、TransactNamedPipeConnectNamedPipe函数可以在命名管道上以同步或异步的方式完成输入和输出操作。当函数以同步方式运行时,函数不会返回直到操作完成为止,这意味着调用线程在完成一个耗时操作时其执行过程可以无限期阻塞。而当函数以异步方式运行时,即使操作没有完成函数也会立即返回,这使得当调用线程释放出来去完成其它任务的同时可以在后台执行耗时的操作。

使用异步I/O可以使管道服务端用一个循环来完成如下步骤:

1. 在等待函数(例如:WaitForMultipleObjects)中指定多个事件对象,并等待其中一个事件被设置成信号态。

2. 使用等待函数的返回值来确定是哪个重叠操作完成了。

3. 完成任务,必要时清除完成操作并为管道句柄初始化下次操作,比如:为同一个管道句柄开启另一个重叠操作。

重叠操作使管道并发读写数据、用一个单线程在多个管道句柄上完成并发I/O操作成为可能。它使一个单线程管道服务器处理和多个管道客户端的通讯更加有效。例如,MSDN中的例子:Named Pipe Server
Using Overlapped I/O

对于一个管道服务端使用同步操作来和多个(多于一个)客户端进行通讯的情况,管道服务端必须为每个管道客户端创建一个独立的处理线程,例如,MSDN中的例子:Multithreaded Pipe Server

l 打开异步操作

ReadFile、WriteFile、TransactNamedPipeConnectNamedPipe函数仅仅在你为管道句柄打开重叠模式、并指定一个有效的OVERLAPPED结构指针的情况下才可以完成异步操作。如果OVERLAPPED指针为空(NULL),函数返回值会错误的指出操作已经完成。因此,强烈建议:如果你使用FILE_FLAG_OVERLAPPED标志创建了管道句柄并想实现异步操作,那么,你应该总是指定一个合法有效的OVERLAPPED结构指针!

OVERLAPPED结构中的hEvent成员必须包含一个手动复位(manual-reset)事件对象的句柄。这是一个由CreateEvent函数创建的同步对象。初始化重叠操作的那个线程使用这个事件对象来确定操作何时完成。当在相同的管道句柄上完成并发操作时,你不应使用管道句柄来进行同步(也就是说不应该在等待函数中使用管道句柄),这是因为没有办法知道到底是哪个操作的完成导致管道句柄被设置成信号态。在相同的管道句柄完成并发操作的唯一可靠技术就是为每一个操作都使用一个单独的OVERLAPPED结构和它自己的事件对象。关于事件对象的更多内容请查看MSDN文章:Synchronization

ReadFile,、WriteFile、TransactNamedPipeConnectNamedPipe操作以异步方式完成时则会发生下面某种情况:

> 当函数返回时如果操作完成,返回值指示着操作的成功或失败。如果发生了某个错误,返回值是0并且GetLastError函数返回除ERROR_IO_PENDING以外的错误代码。

> 当函数返回时如果操作还未完成,返回值是0并且GetLastError返回ERROR_IO_PENDING。在这种情况下,调用线程必须等到操作完成,调用线程必须接着调用GetOverlappedResult函数来确定结果。

l 使用完成例程

ReadFileExWriteFileEx函数提供了另一种重叠I/O的模型。不象重叠的ReadFileWriteFile函数使用一个事件对象来指示I/O操作完成,这些扩展函数指定一个完成例程,一个完成例程是一个普通函数,当读、写操作完成时这些函数被排队执行。完成例程是不会被执行的,当调用ReadFileExWriteFileEx的线程通过调用alertable等待操作函数并指定fAlertable参数为TRUE来开始alertable等待操作时,完成例程才会被执行,否则,完成例程是不会被执行的!在alertable等待操作中,当ReadFileExWriteFileEx完成例程被排队执行时,函数总是返回。管道服务端可以使用扩展函数为每个连接到它的客户端完成一系列的读、写操作。序列中的每个读、写操作都指定一个完成例程,并且每个完成例程负责初始化序列中的下一步操作。例如MSDN例子代码:Named
Pipe Server Using Completion Routines

命名管道安全和访问权限

Windows安全机制使你可以控制对命名管道的访问。关于安全方面的更多信息,请参照MSDN文章:Access-Control Model

当你调用CreateNamedPipe函数时可以为命名管道指定一个安全描述符(security descriptor),这个安全描述符控制着对命名管道两端(客户端和服务端)的访问权限。如果你指定一个空的安全描述符(NULL),那么命名管道将获得一个默认的安全描述符,默认的安全描述符内部的ACLs赋予了LocalSystem账户、administrators和所有者对命名管道完全控制的权限,同时也赋予了Everyone组和匿名(anonymous)账户的读权限。

要获取命名管道的安全描述符可以调用GetSecurityInfo函数。而要改变命名管道的安全描述符,可以调用SetSecurityInfo函数。

当一个线程调用CreateNamedPipe函数打开指向一个已存在的命名管道服务端的句柄时,系统在返回这个句柄之前会执行一个访问检查,访问检查过程会比较线程的访问令牌和访问权限是否和命名管道安全描述符里的DACL冲突。除了用户请求的访问权限,DACL必须允许调用线程对命名管道拥有FILE_CREATE_PIPE_INSTANCE访问权限。

同样当客户端调用CreateFileCallNamedPipe函数连接到命名管道客户端时,系统在返回句柄之前也会执行一个访问检查。

CreateNamedPipe函数返回的管道句柄总是有SYNCHRONIZE访问权限,它还有GENERIC_READ(读)、GENERIC_WRITE(写)或GENERIC_READ和GENERIC_WRITE(读和写),但这要取决于管道的打开模式。下表是每种打开模式所对应的访问权限:

打开模式
访问权限
PIPE_ACCESS_DUPLEX (0x00000003)
FILE_GENERIC_READ、FILE_GENERIC_WRITE和SYNCHRONIZE
PIPE_ACCESS_INBOUND (0x00000001)
FILE_GENERIC_READ和SYNCHRONIZE
PIPE_ACCESS_OUTBOUND (0x00000002)
FILE_GENERIC_WRITE和SYNCHRONIZE
FILE_GENERIC_READ访问权限融合了:从管道读取数据、读取管道属性、读取扩展属性以及读取管道的DACL这几种访问权限。

FILE_GENERIC_WRITE访问权限融合了:向管道写入数据、向管道追加数据、写管道属性、写扩展属性以及读取管道的DACL这几种访问权限。因为FILE_APPEND_DATA和FILE_CREATE_PIPE_INSTANCE有着相同的定义,所以,FILE_GENERIC_WRITE也有创建管道的权限。为了避免定义上混淆,建议使用单一的、权限明确的权限位来替代FILE_GENERIC_WRITE。

如果想读或写对象的SACL信息,你可以请求管道的ACCESS_SYSTEM_SECURITY访问权限。要了解关于ACLs和SACL访问权限的更多信息请查看MSDN文章:Access-Control Lists (ACLs)和SACL Access Right

为了避免远程用户或处在不同终端服务会话中的用户访问命名管道,请在管道的DACL中使用登录用户的SID。登录用户的SID 一样被用于” run-as”登录(以某个用户身份来运行程序)。这个SID用来保护每个会话对象命名空间。如何在C++中获取登陆用的SID请查看MSDN文章:Getting the Logon SID in C++

模拟一个命名管道客户端

模拟(Impersonation)可以让线程运行在与进程所不同的安全上下文中。模拟可以让服务端线程代表客户端来完成操作,但在客户端的安全上下文限制下来完成,而客户端通常情况只有一些较低级别的访问权限。关于模拟方面的更多信息请查看MSDN文章:Impersonation

命名管道服务端线程可以调用ImpersonateNamedPipeClient函数来模拟一个命名管道客户端应用,例如:命名管道服务端可以提供对数据库或文件系统的访问特权,当管道客户端发送请求到服务端时,服务端模拟这个客户端并试图访问受保护的数据库,基于客户端的安全级别,系统随后授权或禁止服务端的访问。当服务端完成模拟时,可以调用RevertToSelf函数来恢复服务端原有的安全令牌。

当模拟客户端时,模拟级别(impersonation level)(模拟级别共有四级:SecurityAnonymous、SecurityIdentification、SecurityImpersonation、SecurityDelegation)确定了服务端可以完成的操作。默认情况下,服务端在SecurityImpersonation级别进行模拟。然而,当客户端调用CreateFile函数打开到管道客户端的句柄时,客户端可以使用SECURITY_SQOS_PRESENT标志来控制服务端的模拟级别。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: