Windows管道系统 - 命名管道

命名管道

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

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

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

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

管道命名

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

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

//ServerName/pipe/PipeName

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

Windows Me9895: 管道名字不能包含冒号

管道服务端不能在另外一台计算机上创建一个管道(也就是说对于管道服务端而言,它只能创建本机命名管道,远程命名管道是对管道客户端而言的),因此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          等待模式
> 管道句柄的等待模式确定了ReadFileWriteFileConnectNamedPipe函数如何处理那些耗时的操作。在阻塞等待模式中,这些函数会无限等待管道另一端的进程完成某个操作,而在非阻塞等待模式下,这些函数可以立即返回,否则需要无限期等待
> 在管道为空的情况下(即:管道里面没有任何数据)管道句柄的等待模式会对ReadFile操作产生影响。使用阻塞等待(模式的)句柄时,(ReadFile)操作只有在线程写到管道对端的数据变得可用时才会成功完成。如果使用非阻塞等待(模式的)句柄时,(ReadFile)函数立即返回0,并且GetLastError函数返回ERROR_NO_DATA
> 在管道缓冲区不足的情况下(即:管道被塞满)管道句柄的等待模式会对WriteFile操作产生影响。使用阻塞等待(模式的)句柄时,只有线程从管道的另一端读取数据以腾出足够空间后写操作才会成功完成。如果使用非阻塞等待(模式的)句柄时,要么没有写进任何字节(对消息类型管道而言),要么在写入所持缓冲区的所有字节后(对字节类型管道而言),写操作都会立即返回一个*非*0值。
> 在没有客户端连接或等待连接到管道实例的情况下管道句柄的等待模式会对ConnectNamedPipe操作产生影响。使用阻塞等待(模式的)句柄时,只有当某个管道客户端调用CreateFileCallNamedPipe函数连接到管道实例上,连接操作才会成功返回。如果使用非阻塞等待(模式的)句柄时,连接操作立即返回0,并且GetLastError函数返回ERROR_PIPE_LISTENING
> CreateNamedPipe CreateFile函数创建的命名管道句柄默认都是阻塞等待模式。要想用*非*阻塞等待模式创建管道,管道服务端在调用CreateNamedPipe函数时需要指定PIPE_NOWAIT标志。
> 管道客户端和管道服务端都可以通过调用SetNamedPipeHandleState函数指定PIPE_WAITPIPE_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

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

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

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

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

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

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

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

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

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

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

l         打开异步操作
ReadFileWriteFileTransactNamedPipeConnectNamedPipe函数仅仅在你为管道句柄打开重叠模式、并指定一个有效的OVERLAPPED结构指针的情况下才可以完成异步操作。如果OVERLAPPED指针为空(NULL),函数返回值会错误的指出操作已经完成。因此,强烈建议:如果你使用FILE_FLAG_OVERLAPPED标志创建了管道句柄并想实现异步操作,那么,你应该总是指定一个合法有效的OVERLAPPED结构指针!
OVERLAPPED结构中的hEvent成员必须包含一个手动复位(manual-reset)事件对象的句柄。这是一个由CreateEvent函数创建的同步对象。初始化重叠操作的那个线程使用这个事件对象来确定操作何时完成。当在相同的管道句柄上完成并发操作时,你不应使用管道句柄来进行同步(也就是说不应该在等待函数中使用管道句柄),这是因为没有办法知道到底是哪个操作的完成导致管道句柄被设置成信号态。在相同的管道句柄完成并发操作的唯一可靠技术就是为每一个操作都使用一个单独的OVERLAPPED结构和它自己的事件对象。关于事件对象的更多内容请查看MSDN文章:Synchronization
ReadFile,WriteFileTransactNamedPipeConnectNamedPipe操作以异步方式完成时则会发生下面某种情况:
> 当函数返回时如果操作完成,返回值指示着操作的成功或失败。如果发生了某个错误,返回值是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_READGENERIC_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_DATAFILE_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)(模拟级别共有四级:SecurityAnonymousSecurityIdentificationSecurityImpersonationSecurityDelegation)确定了服务端可以完成的操作。默认情况下,服务端在SecurityImpersonation级别进行模拟。然而,当客户端调用CreateFile函数打开到管道客户端的句柄时,客户端可以使用SECURITY_SQOS_PRESENT标志来控制服务端的模拟级别。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值