命名管道实务 ── 从设计到安全,吃透 Windows 进程间通信的标准方案

· · Windows, 进程间通信, Windows 开发, C#, C++, 安全, Win32 API

更新记录(仅首版,2026年08月22日 发布)
首次发布
引用本文(DOI(已登记存档): 10.5281/zenodo.22176789)

以下 DOI 指向先前登记的存档,内容可能与当前正文不同。引用当前正文时,请使用本页网址。

Go Komura(2026)。《命名管道实务 ── 从设计到安全,吃透 Windows 进程间通信的标准方案》。小村软件有限公司。 https://comcomponent.com/zh-CN/blog/windows-named-pipes-practical-guide/

DOI(已登记存档)
10.5281/zenodo.22176789
DOI(上次登记版本)
10.5281/zenodo.22176790

「想从设置界面向常驻服务下达命令」「想只把需要管理员权限的处理拆到另一个进程」——在 Windows 上构建这类进程间通信(IPC)时,最先值得考虑的就是命名管道(named pipe)。

它在同一台 PC 内的通信中成为第一候选,理由不只是读写方便。可以用 Windows 的 ACL 收窄连接者,必要时确认对方的 Windows 账户并以该权限执行处理,这才是最大的好处。不过,光是把管道建出来并不等于安全,连接与权限的设置还是要做。123

本文按连接形态 → 消息边界 → 服务器结构 → 安全 → 故障时的行为的顺序梳理设计。读者对象是编写 Windows 业务应用和服务的开发者。进程间通信选型表的文章里给出的「同一机器 IPC 的第一候选」,这里要一路挖到实现时的判断。

1. 先说结论:设计上要定的有 5 件事

在挑 API 之前,先把通信对象、数据的单位、并发连接、权限、失败时的处理定下来。

要定的事 基本的选法 详细阅读
和谁通信 对方是同一台 PC 内的 Windows 进程,就把命名管道作为第一候选。若更看重远程部署、其他操作系统、复用既有协议,则考虑基于 TCP 的方案 第 2 章
什么算一条 想把一次写入当作一条来处理,用消息模式。已有成帧机制,用字节模式 第 3 章
怎样接纳多个客户端 用同一名称准备多个实例。新写的 .NET 实现,异步 I/O 加 async/await 最直接 第 4 章
允许谁连接和借用权限 把拒绝远程、ACL、第一个实例的保证、客户端的模拟级别配成一套来设计 第 5、6 章
通信失败了怎么办 把等待启动、重连、消息上限、响应确认写进协议 第 7 章

命名管道可以把基于 ACL 的连接控制和对客户端的模拟当作 Windows 自带的机制来用。改用 localhost 的 TCP 时,就得另行设计一套认证来确认通信对方是谁。反过来,如果将来想扩展到远程通信、想和其他操作系统通信、想复用 gRPC 等既有资产,基于 TCP 的方案更有利。23

尤其重要的一点是:模拟失败的请求不要执行。忽略失败,处理就会以服务器自身而不是客户端的权限继续跑下去。要做特权服务时,请不要只看代码示例,一定读到第 5、6 章。4

2. 连接机制:名称共用,通道按客户端各自一条

2.1 把创建、连接、读写的职责分开

命名管道是用 \\.\pipe\MyCompany.MyApp.Control 这类名称标识的单向或双向通道。服务器和客户端最先使用的 API 并不相同。1

阶段 服务器侧 客户端侧
准备通道 用 CreateNamedPipe 创建实例 使用服务器准备好的管道名称
建立连接 用 ConnectNamedPipe 等待客户端 用 CreateFile 打开同一名称
收发数据 用 ReadFile / WriteFile 读写 用 ReadFile / WriteFile 读写

连接建立之后,双方都能以和文件 I/O 相同的方式处理。除了名称的指定之外,掌握下面关于实例和方向的思路,多连接和连接错误也会更容易理解。

2.2 一个实例负责一个客户端

同一名称的管道可以创建多个,一个实例就是与一个客户端之间的通道。名称相同,并不意味着所有客户端共用一条通道。最大实例数在第一次调用 CreateNamedPipe 时指定,上限也可以写成 PIPE_UNLIMITED_INSTANCES。15

命名管道的基本结构服务器创建多个同名的管道实例并用 ConnectNamedPipe 等待连接,各客户端用 CreateFile 打开该名称,与其中一个实例建立一对一的双向通道服务器实例 1实例 2实例 3客户端 A客户端 B客户端 C

图 1:双向管道的例子。用同一名称准备多个实例,服务器就能分别与多个客户端一对一通信。

2.3 「方向」是以服务器一侧为准的说法

客户端指定的访问方式,要和服务器创建管道时的方向相符。不一致时 CreateFile 会失败。6

服务器创建的方向 服务器的动作 客户端指定的访问方式
双向 又读又写 只读、只写、读写都可以打开
出站 只写 以只读方式打开
入站 只读 以只写方式打开

所有实例都在使用中时,客户端的 CreateFile 会得到 ERROR_PIPE_BUSY。这时用 WaitNamedPipe 等待空位,再调用一次 CreateFile。它和「服务器还没创建管道」是不同的失败,将在 7.1 连同启动顺序的竞态一起梳理。6

2.4 即便只在本机使用,也要定好远程连接怎么处理

命名管道也能以 \\server\pipe\名称 的形式经 SMB 用于远程连接。不过在如今的设计里几乎没有理由主动走这条路,本机 IPC 的关键是不要让用不到的路径一直敞着。用 5.1 的 PIPE_REJECT_REMOTE_CLIENTS 明确拒绝。17

3. 模式的选法:先定「到哪里算一条」

3.1 请求与响应用消息模式,已有边界机制用字节模式

传输模式的差别,在于管道是否保存写入的边界。5

模式 接收方看到的东西 适合的用法
字节模式(PIPE_TYPE_BYTE) 和 TCP 一样,没有断点的字节流 自带长度前缀等成帧机制的数据,或流式传输
消息模式(PIPE_TYPE_MESSAGE 加消息读取) 以一次写入为一条消息的单位 一条一条的请求与响应
字节模式与消息模式的区别字节模式下三次写入会变成没有断点的字节流,接收方必须自己划分边界;消息模式下每次写入的单位会被保存,原样送达接收方字节模式:写入 AAA、BB、CCCC收到的是 AAABBCCCC 字节流边界(成帧)要自己设计消息模式:同样的三次写入收到 AAA、BB、CCCC 三条写入的单位被保存下来

图 2:字节模式下由接收方管理边界。消息模式能保存写入的单位,但一次读取未必就拿得到整条消息。

如果还没有自己的边界机制,又想把「一次写入=一条请求」来处理,消息模式很方便。如果已经在用带长度前缀的序列化格式之类,字节模式就够了。在 .NET 中,PipeTransmissionMode.Message 相当于消息型的指定。8

对消息型的双向管道,还有把请求发送与响应接收合成一次调用的 TransactNamedPipe。9

3.2 消息型和读取模式是两套设置

读取模式是按句柄设置的。即便在 CreateNamedPipe 里指定了 PIPE_READMODE_MESSAGE,那也只是服务器一侧的设置。客户端用 CreateFile 打开的句柄,默认仍是字节读取。6

实现 服务器侧 客户端侧
Win32 指定 PIPE_TYPE_MESSAGE 和 PIPE_READMODE_MESSAGE 连接后用 SetNamedPipeHandleState 指定 PIPE_READMODE_MESSAGE
.NET 指定 PipeTransmissionMode.Message 连接后把 NamedPipeClientStream.ReadMode 设为 Message

关键在于,不要以为只把服务器做成消息型,客户端就能按同样的单位读取。

3.3 即便是消息模式,也会需要续读

消息的边界得以保留,和接收缓冲区放得下整条消息,是两回事。缓冲区太小时 ReadFile 会返回 ERROR_MORE_DATA,消息会被拆开。需要一个循环:留住已经收到的部分,把剩下的续读进来并拼接。56

读取结果 处理方式
成功 当作一条完整的消息来处理
ERROR_MORE_DATA 还有剩余,续读并拼接
其他错误 不再继续处理该请求,按断开之类的通信失败来处理

出现「小请求没事,只有大请求会坏掉」时,就检查这段续读处理。另外,也不是可以无限拼接下去。单条消息的最大长度同样要作为协议定下来,超过上限就断开,以免巨大的消息把内存吃光。

4. 服务器结构:把接待和客户端处理分开

4.1 比较同步线程型和 overlapped 型

服务器的基本动作,是创建实例 → 等待连接 → 读写 → 断开后进入下一轮的循环。要同时接纳多个客户端,就用多个实例并行推进这条流程。

结构 机制 优点与注意事项
同步线程型 每个实例分配一条线程,用同步 I/O 处理 代码直接。但线程数随客户端数增长,停止时还得想办法让阻塞 I/O 退出来
overlapped(异步)型 用 FILE_FLAG_OVERLAPPED 创建,连接、读取、写入都以异步方式发出 少量线程就能处理多个实例的完成

Microsoft 的官方示例把各实例的事件放进数组,用 WaitForMultipleObjects 等待,是单线程处理多个连接的结构。这样客户端数和线程数就解耦了。规模再大一些,也可以接到 IOCP 或线程池 I/O 上。10

异步 I/O 本身的机制,请参阅同步 I/O 与异步 I/O 的文章。

4.2 新写的 .NET 实现,async/await 最直接

在 .NET 中可以使用 NamedPipeServerStream 的 WaitForConnectionAsync / ReadAsync / WriteAsync 配合 async/await。新写的实现如果没有特别理由,推荐这种异步形态。接待循环收到连接后交给单独的处理流程,然后回到下一个实例的接待。8

下面是那个接待循环的骨架。作为同一用户的进程之间使用的例子,这里指定了 PipeOptions.Asynchronous 和 PipeOptions.CurrentUserOnly。在 Windows 上,CurrentUserOnly 表示限定为同一用户且同一提升级别。它不是把不同账户下的服务和 UI 连起来的例子。那种场合需要 5.2 的 ACL 设计。11

// C#: 接纳多个客户端的服务器骨架
while (!token.IsCancellationRequested)
{
    var server = new NamedPipeServerStream(
        "MyCompany.MyApp.Control",
        PipeDirection.InOut,
        NamedPipeServerStream.MaxAllowedServerInstances,
        PipeTransmissionMode.Message,
        PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

    try
    {
        await server.WaitForConnectionAsync(token);
    }
    catch
    {
        await server.DisposeAsync();        // 在连接建立前退出时自行释放
        throw;
    }
    _ = HandleClientAsync(server, token);   // 连接建立后所有权交给处理方
}

若在连接建立前发生异常,就由接待一侧释放;连接建立之后,HandleClientAsync 一侧接过流的所有权。后者不仅包含读写,也包含结束时的释放。

这段代码略去了通信协议和处理器本体,只是骨架。实际运行时还要管理分离出去的任务的异常与结束,并把第 3 章的续读与长度上限、第 5、6 章的安全、第 7 章的断开与响应确认组合进来。CurrentUserOnly 是不用自己写 ACL 就能限定连接者的省事选项,但光靠它并不能构成面向特权服务的完整设计。

5. 安全:服务器侧三点,客户端侧一点

命名管道在安全上的好处,要正确设置之后才拿得到。特别是在「管理员权限的服务 + 低权限的 UI 应用」这种代理结构中,管道本身就是权限的边界。

5.1 服务器侧:拒绝不需要的远程连接

作为本机 IPC 使用时,要在 CreateNamedPipe 上指定 PIPE_REJECT_REMOTE_CLIENTS。远程客户端会被自动拒绝,从而堵上「本来是给同一台 PC 的应用做的,结果从网络上也打得开」这条路。7

5.2 服务器侧:用 ACL 收窄连接者和权限

把安全描述符传给 SECURITY_ATTRIBUTES,明确写出允许连接的用户和组。默认的 ACL 未必对你的用途足够严格。2

这里容易漏掉的,是给客户端的写入权限。在 ACL 里授予通用写入(GENERIC_WRITE / FILE_GENERIC_WRITE),就会把相当于 FILE_CREATE_PIPE_INSTANCE 的权限也一并带上。于是被允许的客户端自己就能创建同名的服务器实例,把后续的连接抢走。2

读写要用所需的单项权限来授予,不要把创建实例的权限交给客户端。ACL 的设计不只是「允许谁」,还包括「允许做什么」。

5.3 服务器侧:确认第一个实例没有被抢先创建

恶意进程抢先创建了同名的管道,客户端就会连到那个伪服务器上。服务器只在创建第一个实例时指定 FILE_FLAG_FIRST_PIPE_INSTANCE,以此保证自己是第一个。失败就要怀疑被抢先,停止处理。5

这个标志只用于占住名称的第一条实例。给第二条以后的实例也加上,创建就会失败。在面向多客户端的接待循环里,注意不要对所有创建都重复同样的指定。5

不过,这只是正规服务器在启动时检测异常的机制,并不是客户端对连接目标的认证。正规服务不在时,攻击者仍有先创建同名管道守在那里的余地。

5.4 客户端侧:定好让服务器借用权限到什么程度

作为对伪服务器的防备,客户端要把模拟级别压到必要的最低限。若设计上只允许确认身份,就在 CreateFile 上指定 SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION。这样服务器虽然能确认账户,却无法借用其权限去做实际的访问。3

希望服务器做的事 客户端侧的方针
只确认连接者的身份 收窄到识别级别(SECURITY_IDENTIFICATION)
以客户端权限打开文件等,进行实际访问 需要允许模拟级别(SECURITY_IMPERSONATION)。但要和「确认连到的是正规服务器」配成一套

停在识别级别上,就做不了以客户端权限进行实际访问的代理处理。反过来,不确认连接目标就允许模拟,则有被伪服务器借走权限的危险。

原则是:只在能通过服务的启动保证、或连接后的相互认证确认连接目标为正规一方时,才允许必要的模拟。不要以为有了 5.3 的抢先检测,客户端一侧就不必确认。

6. 模拟的用法:失败的请求不要执行

6.1 不是连上就模拟,而是读到请求之后再模拟

服务器确认客户端的身份并以其权限处理,用的 API 是 ImpersonateNamedPipeClient。它的对象,是从该管道最后读到的那条消息的发送方的安全上下文。所以要先读取请求,然后再调用。4

模拟作用于调用它的那条线程。在这个状态下打开文件,能否访问就以客户端的权限为准来判定。即便是特权服务,这也是「以委托者的权限执行被委托的操作」的机制。3

6.2 把读取、成功确认、恢复上下文当成一整套

必要的顺序是:读取请求 → 尝试模拟 → 确认成功 → 以客户端权限操作 → 回到原来的上下文。

使用模拟处理请求的流程服务器从管道读取请求,确认 ImpersonateNamedPipeClient 成功之后再以客户端的权限进行操作,并用 RevertToSelf 回到自己的上下文。模拟失败时不执行请求而是拒绝服务器客户端服务器客户端失败则不执行请求,直接拒绝发送请求读取请求ImpersonateNamedPipeClient以客户端权限执行操作用 RevertToSelf 恢复返回结果

图 3:模拟失败就不要执行请求。即便成功,也要把「做完之后用 RevertToSelf 恢复」算进这一连串处理里。

不要在没有检查 ImpersonateNamedPipeClient 返回值的情况下就进入请求处理。失败时并没有切换到客户端的权限,用的是服务器进程自身的权限。服务器若是特权账户,就会把客户端本不该做的操作也放行。Microsoft 的文档同样明确写着,失败时不要执行客户端的请求。4

工作结束后,要用 RevertToSelf 可靠地回到原来的上下文。成功确认和收尾两样都要齐备。令牌、模拟级别、SeImpersonatePrivilege 等细节,在 Windows 模拟令牌的文章中有讨论。

7. 实务中的坑:按「通信会断」来设计

7.1 等待启动和等待空位,是两种不同的失败

连不上时,要分清是服务器还没创建管道,还是已创建的实例全都在使用中。69

状态 客户端的处理
管道不存在 把等待服务器启动纳入考虑,稍等一会儿再重试
ERROR_PIPE_BUSY 用 WaitNamedPipe 等待空位,再重试 CreateFile
连上了 进入通信。必要时还要设置 3.2 的读取模式
客户端连接的重试流程用 CreateFile 打开管道,管道不存在就稍等一会儿再重试,所有实例都在使用中的 ERROR_PIPE_BUSY 就用 WaitNamedPipe 等待空位后再重试,成功则进入通信成功管道不存在ERROR_PIPE_BUSY用 CreateFile 打开结果如何?开始通信稍等一会儿(服务器未启动)用 WaitNamedPipe 等待空位

图 4:「不存在」和「已满」是不同的状态。两者都要按各自的状态等待之后,再重试连接。

服务器一侧的原则,是在客户端启动之前就用 ConnectNamedPipe 开始等待。即便如此,启动顺序的竞态仍然可能发生,所以客户端一侧也要内建重试。9

7.2 把断开做成常规处理路径,而不是异常事态

对方的进程结束后,读写会以 ERROR_BROKEN_PIPE 之类失败。这要当作通信的日常来处理。

服务器检测到断开后,用 DisconnectNamedPipe 把实例摘下来,为下一次连接做准备。客户端则重新连接。休眠恢复的文章里讲过的、重复执行也不会破坏状态的「幂等重连」思路,在这里同样有效。

7.3 大消息既要「续读」也要「上限」

3.3 的 ERROR_MORE_DATA 是把消息分次读完的处理;而最大消息长度是限制接收数据量的约定。不要把这两件事混为一谈。

恶意的一方,或者带 bug 的一方发来巨大的消息时,无限续读的实现会把内存耗光。要在协议里定好上限,超过就断开。

7.4 区分写入成功和对方处理完成

WriteFile 成功,并不意味着对方的应用已经处理了数据。需要确知是否真的执行了的操作,要用响应消息来确认。

另外,为了能分辨响应对应的是哪一个请求,要把请求与响应的对应关系写进协议。把「发出去了」和「处理完了」分开,才谈得上业务上的完成确认。

8. 小结:通道、数据、权限分开来定

命名管道兼具与文件 I/O 相同的易用性,以及 ACL 与模拟这套 Windows 安全模型,是同一机器 IPC 的第一候选。不过,做出一条通道和做出一套安全、不易坏的协议,是两件事。

连接与数据的设计,以「一个实例负责一个客户端」为出发点。请求响应型选消息模式,已有成帧机制则选字节模式,而消息读取在客户端一侧也要设置。拆分读取的应对和最大长度同样必要。要接纳多个连接的新 .NET 实现,异步 I/O 加 async/await 最直接。

权限的设计,把拒绝远程、明确 ACL、保证第一个实例、把客户端侧的模拟级别降到最低配成一套。使用模拟时,先读请求再调用,失败就不执行,做完之后用 RevertToSelf 恢复。

最后,请把启动顺序、断开、消息上限、响应确认写成你自己的协议。实务的要点不是「反正在同一台 PC 里不会失败」,而是先定好即使通信中断,也能不越过对方权限地恢复的形态,然后再动手实现。

相关文章

相关咨询领域

小村软件有限公司承接伴随进程间通信的设计与实现,例如服务与 UI 应用的分离、管理员权限的分离;也承接把既有 IPC(共享内存、自研套接字、COM 等)替换为命名管道,以及特权服务管道通信的安全评审。从协议设计的讨论阶段开始咨询也可以。

参考链接

  1. Microsoft Learn, Named Pipes. 关于命名管道是管道服务器与一个或多个管道客户端之间的单向或双向通道;所有实例共享同一名称但各自拥有独立的缓冲区和句柄;以及可从本地和远程进程使用。 ↩ ↩2 ↩3 ↩4

  2. Microsoft Learn, Named Pipe Security and Access Rights. 关于命名管道访问权限的构成;GENERIC_WRITE 包含 FILE_CREATE_PIPE_INSTANCE,因此给客户端通用写入也会连服务器实例的创建一起允许;以及数据的读和写应当以单独的访问权限授予。 ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, Impersonating a Named Pipe Client. 关于模拟让服务器线程可以在客户端权限的范围内动作;默认的模拟级别是 SecurityImpersonation;以及客户端可在 CreateFile 时用 SECURITY_SQOS_PRESENT 标志控制模拟级别(SECURITY_IDENTIFICATION 则只允许确认身份)。 ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). 关于服务器侧线程在从管道最后读到的那条消息的客户端安全上下文中开始模拟;完成后用 RevertToSelf 返回;以及模拟失败仍继续处理会在服务器进程自身的(特权的)上下文中执行,因此必须检查返回值,失败时不得执行客户端的请求。 ↩ ↩2 ↩3

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). 关于管道方向(入站、出站、双向)、字节型与消息型(PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE)及读取模式(PIPE_READMODE_MESSAGE)、最大实例数(PIPE_UNLIMITED_INSTANCES)、由 FILE_FLAG_OVERLAPPED 启用的异步模式、由 FILE_FLAG_FIRST_PIPE_INSTANCE 提供的第一个实例保证,以及 WaitNamedPipe 用的默认超时。 ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Named Pipe Client. 关于客户端用 CreateFile 打开管道;所有实例都在使用中时会得到 ERROR_PIPE_BUSY,用 WaitNamedPipe 等待空位;以及打开的句柄默认是字节读取、阻塞、非 overlapped,可用 SetNamedPipeHandleState 改为消息读取模式。 ↩ ↩2 ↩3 ↩4 ↩5

  7. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). 关于两种远程客户端模式:PIPE_ACCEPT_REMOTE_CLIENTS(接受远程连接并用安全描述符检查)和 PIPE_REJECT_REMOTE_CLIENTS(自动拒绝远程客户端的连接)。 ↩ ↩2

  8. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). 关于用 NamedPipeServerStream / NamedPipeClientStream 建立连接与读写、用 PipeTransmissionMode.Message 做消息单位的传输,以及用异步方法处理多个客户端。 ↩ ↩2

  9. Microsoft Learn, Named Pipe Operations. 关于用 ReadFileEx / WriteFileEx 进行 overlapped 操作、用 PeekNamedPipe 做不取走数据的读取、在消息型双向管道上用一次调用完成请求发送与响应接收的 TransactNamedPipe,以及客户端启动前的阻塞读取可能引发竞态。 ↩ ↩2 ↩3

  10. Microsoft Learn, Named Pipe Server Using Overlapped I/O. 关于单线程服务器用 overlapped 操作处理与多个客户端的同时连接的官方示例。关于用 WaitForMultipleObjects 等待各实例的 OVERLAPPED 结构和事件、推进已完成实例的状态机的构造,以及用 GetOverlappedResult 确认挂起 I/O 是否完成。 ↩

  11. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). 关于用 Asynchronous 启用异步 I/O,以及用 CurrentUserOnly 只允许与同一用户(且同一提升级别)的进程连接。 ↩

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

命名管道和 TCP(localhost 套接字)该怎么分别使用?
只要是同一台机器内的进程间通信,命名管道就是第一候选。原因在于安全模型。管道可以用 Windows 的安全描述符(ACL)在操作系统层面控制「谁可以连接」,服务器一侧还能用 ImpersonateNamedPipeClient 确认并借用连接方的 Windows 账户。相比之下,localhost 的 TCP 端口谁都能连上,必须自己另做一套认证来确认对方是谁。反过来,如果将来很可能扩展到远程通信、要和其他操作系统上的进程对话、想复用 gRPC 等既有协议资产,那么基于 TCP 的方案更有利。这一判断在进程间通信选型表的文章里也做过梳理。
字节模式和消息模式该用哪一个?
如果想把「一次写入=一个意义单位」来处理,消息模式(PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE)比较方便。接收方可以按发送方写入的单位读取,不必自己管理边界。字节模式和 TCP 一样是「没有断点的字节流」,需要自己设计长度前缀之类的边界(成帧)。如果承载的协议本身已有成帧(例如带长度前缀的序列化格式),用字节模式就没有问题。需要注意的地方是,即便在消息模式下,接收缓冲区比消息小时也会发生拆分读取(ERROR_MORE_DATA),这段处理还是要写。另外,读取模式是按句柄设置的,CreateNamedPipe 设置的只是服务器一侧。客户端必须在 CreateFile 之后用 SetNamedPipeHandleState 指定 PIPE_READMODE_MESSAGE。在 .NET 中,服务器指定 PipeTransmissionMode.Message,客户端则在连接后把 NamedPipeClientStream.ReadMode 设为 Message。
同时面对多个客户端的服务器该怎么写?
命名管道可以用同一名称创建多个实例,一个实例负责一个客户端。结构有两种。一种是给每个客户端分配一条线程的同步型,实现直接,但会按客户端数量消耗线程。另一种是用 FILE_FLAG_OVERLAPPED 做成异步 I/O,用少量线程处理所有实例的 ConnectNamedPipe、ReadFile、WriteFile;Microsoft 的官方示例中也给出了单线程处理多个实例的实现。用 .NET 的话,配合 NamedPipeServerStream 的 WaitForConnectionAsync 和 async/await,可以把异步型写得像同步型一样直接。新写的实现如果没有特别理由,我推荐 .NET 的异步型。
命名管道的安全方面至少要做哪些事?
有四点。第一,若不需要远程连接,就指定 PIPE_REJECT_REMOTE_CLIENTS,明确拒绝经网络的连接。第二,用 SECURITY_ATTRIBUTES 设置合适的 ACL,收窄允许连接的用户和组(默认的 ACL 对某些用途来说太宽松)。第三,创建第一个实例时指定 FILE_FLAG_FIRST_PIPE_INSTANCE,以检测同名管道被抢先创建的「名称劫持」(第二个以后的实例不要加)。第四,只想让服务器确认身份的客户端,应在 CreateFile 上指定 SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION,限制伪服务器借用(模拟)自己的权限。在服务器以客户端权限进行实际访问的代理结构中必须允许模拟,因此这条限制要按「设计上是否让服务器借用权限」来区别对待。
使用 ImpersonateNamedPipeClient 时有哪些需要注意的地方?
最重要的是检查返回值。模拟失败却继续处理,之后的操作就会以服务器进程自身(往往更高)的权限执行,本不该允许客户端做的操作也会放行。官方文档也明确写着:失败时不得执行客户端的请求。另外,模拟是在「最后从管道读到的那条消息」的上下文中进行的,所以务必先读取了什么之后再调用;工作结束后也要用 RevertToSelf 可靠地回到原来的上下文。模拟周边的机制(令牌、模拟级别、SeImpersonatePrivilege)在模拟令牌的文章中有详细讲解。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表