Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
· 小村 豪 · 进程间通信, 命名管道, Windows, .NET, C#, gRPC, 共享内存, COM, 设计, 判断表, 技术咨询
「把 UI 和服务分开之后,中间的通信该怎么办」「想从 32bit 的应用程序使用 64bit DLL 的功能」「想让另一个进程的测量引擎把数据传到画面上」。把应用程序拆分成多个进程的设计,经常是应对健壮性、权限分离、位数问题的现实解法,但拆分的那一刻,「中间的通信怎么做」这个选择就一定会出现。
这个博客目前为止已经写过各个通信手段,例如共享内存的陷阱、TCP 的分帧、子进程的安全处理、文件对接与锁定。不过「到底该选哪种手段」这个总论篇还没有写过。本文会把 Windows 进程间通信(IPC)的主要选项——文件对接・命名管道・本地 TCP・gRPC・共享内存・COM——各自的优势与陷阱,用这个博客惯用的判断表形式整理出来。
1. 先说结论
- 同一台机器内的请求-响应(发送命令并获取结果)场景,命名管道是首选。 它是 OS 标准功能、不需要端口,并且与 Windows 的访问控制集成,在 .NET 中可以用
System.IO.Pipes直接写出来。1 - 如果未来有可能跨越网络,一开始就选本地 TCP 或 gRPC。 从管道换成 socket 往往不是「以后小改一下」就能解决的差异。不过原生 TCP 是字节流,因此分帧的设计是必需的。
- 当服务或调用的种类增多,自定义协议的维护开始变成负担时,就用 gRPC。 可以获得 proto 定义的 schema 与代码生成、双向流式传输,.NET 8 之后 ASP.NET Core(Kestrel)可以把命名管道当作传输层。2
- 只有大容量・高频率的数据(图像帧、波形)才用共享内存。 速度最快,但同步机制要全部自己设计,因此原则是不要把控制消息也塞进共享内存。3
- 松耦合・异步就够、且想留下审计日志的对接,现在仍然以文件对接为优。 只是成败取决于互斥控制的设计。
- COM(跨进程)不会是新设计的首选,但在 VBA・其他语言的使用场景,或是 32bit/64bit 桥接的场合,现在仍然是现役工具。4
- 不管选哪种手段,消息的分界・版本号・超时・重新连接这类共同的设计课题都逃不掉(第 6 章)。花在这上面的时间,应该和选手段一样多。
2. 各选项的底细
2.1 文件对接 ── 松耦合・异步・可审计
「A 把文件放到输出文件夹,B 拿去处理」是经典的对接方式。双方不需要同时启动,处理过程会留下文件作为证据,出现故障时人可以直接看文件、改好后重新投入。在不要求实时性的批处理式对接中,这种方式至今依然强大。
陷阱几乎集中在一点:互斥控制。「读到写入中途的文件」「两个处理争抢同一个文件」是常见事故,需要遵循先用临时文件名写入再重命名之类的定式。详细内容整理在《文件对接与锁定的最佳实践》中,选择这种方式时务必参考。不适合需要即时响应的交互式通信,或是每秒发生几十次的交换。
2.2 命名管道 ── 同一台机器 IPC 的本命
命名管道是 Windows 在内核层提供的双向通信通道,是同一台机器内客户端・服务器型 IPC 的本命。在 .NET 中用 NamedPipeServerStream / NamedPipeClientStream 处理,一个管道名称可以让多个客户端连接。5 与 TCP 不同,不需要管理端口号,也不会被防火墙拦截。
以下列出实务中需要掌握的要点。
- 可以使用消息模式。 指定
PipeTransmissionMode.Message后,一次写入就会以一条消息为单位切分并到达。这让 OS 代劳了 TCP 中必须自己实现的 length 前缀等分帧工作,是实务上很大的优点(接收端需要用IsMessageComplete读完一条消息。参见第 5 章)。 - 默认访问权限出乎意料地宽松。 管道的默认安全描述符会给 LocalSystem・管理员・创建者完全控制权,但同时也允许 Everyone 与匿名账户读取。6 如果是同一用户的进程之间,只需加上
PipeOptions.CurrentUserOnly就能强制「只与同一用户创建的对象连接」,建议把它当作默认做法。7 不同账户之间(例如与服务通信)则要用PipeSecurity明确指定 ACL。 - 管道名称位于所有人都能看到的命名空间中。 管道名称位于
\\.\pipe\下的单一命名空间中,同一台机器上其他用户的进程也能看到。这里要注意的是名称抢占(squatting)。如果恶意进程先用相同名称创建服务器并等待连接,客户端就会连到那一边。服务器端可以指定PipeOptions.FirstPipeInstance,让同名管道已存在时直接失败;客户端则可以在连接后验证管道的所有者账户。在有不同权限用户共用同一台机器时,应该加入这类对策。8 - 可以进行跨越权限边界的通信。 作为 UAC 分离出的「标准权限 UI + 管理员权限代理(broker)」之间的通信通道,也是常见做法。不过这种架构下无法使用
CurrentUserOnly(因为即使是同一用户,也要求提升级别必须一致7)。需要明确设计 ACL,具体做法已在《管理员权限的分离(broker)》中详述。
弱点是实务上不太适合跨机器通信(规范上可行,但运维上限制很多),也不容易用于与非 Windows 系统的互连。如果看得到这类需求,就选 TCP / gRPC。
2.3 本地 TCP ── 跨语言・跨 OS 的通用性
对 localhost 的 TCP 连接,几乎能和任何语言・运行时・OS 通信,是最通用的 IPC。像「Linux 上的分析引擎与 Windows 的 UI」这种混合架构,首先会考虑 TCP(或架在其上的 HTTP/gRPC)。
需要注意的地方有 3 点。
- 它是字节流。 TCP 没有「按发送单位到达」这种性质。
Send发出的 3 条消息在一次Receive中被拼接到达,或是一条消息被拆成多次到达,都是正常现象。必须在应用层自行设计 length 前缀之类的分帧,省略这一步的代码只是「碰巧能跑」而已。详细内容请参考《以为 TCP 的 Send 单位可以对应 Receive 是一种误解》。 - 收窄监听的公开范围。 即使本意只是同一台机器内的通信,只要用
0.0.0.0监听,网络上的其他机器就能连进来。本地 IPC 原则上应该绑定在127.0.0.1(loopback)上。即便如此,同一台机器上的其他用户依然能连接,所以如果需要确认对方身份,就要在应用层加上认证。这一点与管道那种与 OS 集成的访问控制有明显差异。 - 端口的运维会一直跟着你。 固定端口可能和其他软件冲突,防火墙产品也可能把它检测为「可疑的监听」。应该从一开始就把变更端口号的手段纳入设计。
2.4 gRPC ── 买的是 schema 定义和代码生成
与原生 socket + 自定义协议相比,gRPC 提供的与其说是通信本身,不如说是开发的框架。只要用 proto 文件定义服务与消息,序列化・分帧・客户端/服务器代码就会全部生成出来,双向流式传输(服务器主动推送通知)也能像语言内置功能一样编写。当你进入「每次消息增加就要修改自定义协议的 switch 语句和文档」的阶段,这个框架就能发挥作用。
.NET 8 之后,ASP.NET Core(Kestrel)不仅支持 TCP,还直接支持 Unix 域套接字与命名管道作为传输层。8 服务器端只需要调用 ListenNamedPipe,也能设置 PipeSecurity 的访问控制。2 「通信通道仍用管道、无端口・集成 ACL,协议层用 gRPC」这种组合,在 UI + 服务分离(第 4 章)中是很有力的选项。
另一方面,对于小规模的工具间通信,这往往会过度设计。搭建 gRPC 服务器意味着要扛起 ASP.NET Core 的托管基础设施,会增加分发物、依赖关系与启动成本。如果命令只有 2~3 种的工具之间,命名管道 + JSON(第 5 章)的总成本会更低,请保留这条分界线。等到「proto 里要写的消息超过 10 个」「本质上需要流式通知」「对方不是 .NET」三者之一成立后再引入也不迟。
2.5 共享内存(内存映射文件) ── 最快,但同步全部要自己来
同一台机器内最快的 IPC 是共享内存。在 .NET 中可以用 MemoryMappedFile.CreateNew 创建具名的共享内存,让多个进程直接读写同一段字节序列。3 因为不涉及复制或序列化,对图像帧或波形数据这类「又大又快」的数据,几乎可以说是唯一的选择,性能表现也确实出色。
不过,共享内存并不是「快速的管道」。看到的只是相同的字节序列,完全不提供任何同步机制。 不读取写入中数据的机制、检测对方存活状态、一方异常退出后的恢复——全部都要自己设计。连环形缓冲区结构与布局版本管理都包含在内的设计论,整理在《共享内存的陷阱与实务最佳实践》这篇文章中。
实务中的使用场景很明确,就是只把数据面(data plane)放到共享内存上,控制面(control plane,如开始・停止・修改配置)则交给命名管道等其他通道的架构(第 4 章的架构 3)。一旦连开始・停止・修改配置之类的控制消息都想硬塞进共享内存,就是该重新审视设计的信号。
2.6 COM(跨进程) ── 不是新设计的首选,但仍有现役的场合
COM 的跨进程服务器(EXE 服务器),是 Windows 很早就有的「像调用本地函数一样调用另一个进程里的对象」机制。4 在新的应用程序间通信中,已经不会把它当作首选,但在下列场景中依然实用。
- 32bit/64bit 桥接:32bit 应用程序无法把 64bit 的 DLL(或反过来)加载到同一个进程中,但跨进程 COM 可以跨越这个边界。COM 基础设施会代为处理封送,调用方几乎不用改代码。实际案例写在《32bit→64bit COM 桥接实例》中。
- VBA・其他语言的调用:想让 Excel VBA 之类的旧运行环境调用 .NET 的功能时,把它以 COM 公开,现在依然是最直接的路径。
- 与既有 COM 资产的协作:如果对方只能用 COM 通信,这边也用 COM 通信是最快的做法(COM 本身的说明请见《COM・ActiveX・OCX 是什么》)。
让人对新采用犹豫的原因,是伴随注册表登记的分发麻烦、接口设计与引用计数的学习成本,以及出现问题时排查的难度。请先和「如果目的是 32/64 桥接,就把 64bit 一侧做成单纯的辅助进程,用命名管道通信」这个替代方案(第 4 章的架构 2)比较之后再决定。
2.7 经典手段 ── 新设计不会选
Windows 还有 WM_COPYDATA(用窗口消息传数据)、剪贴板、DDE、邮件槽(mailslot)这类 IPC 手段,官方的 IPC 概览页面现在依然列着它们。4 只是这些手段大多以窗口存在为前提,或是正在走向淘汰(远程邮件槽正处于淘汰过程中),新设计几乎没有理由选择它们。维护既有应用程序时遇到能认出来就够了。
3. 判断表
| 维度 | 文件 | 命名管道 | 本地 TCP | gRPC | 共享内存 | COM |
|---|---|---|---|---|---|---|
| 通信范围 | 用共享文件夹可跨机器 | 同一台机器是实用范围 | 可跨机器 | 可跨机器 | 限同一台机器 | 同一台机器是实用范围 |
| 通信模型 | 文件传递(异步) | 流+消息模式 | 字节流 | RPC+流式传输 | 共享状态 | 方法调用 |
| 服务器→客户端通知 | ✕(轮询) | ○ | ○ | ◎(双向流式传输) | △(需搭配事件) | △(能做但复杂) |
| 跨越权限边界(UAC・服务) | ○(文件夹 ACL) | ◎(PipeSecurity) | △(认证要自己做) | △~○(用管道传输则为◎) | ○(可设 ACL,但设计难) | ○ |
| 32/64・语言混用 | ◎ | ○ | ◎ | ◎(从 proto 生成各语言代码) | △(必须设计 ABI) | ○(擅长桥接) |
| 实现成本 | 低 | 低~中 | 中(分帧要自己做) | 中(要引入基础设施) | 高 | 高(对新设计而言) |
| 调试便利性 | ◎(内容留存在文件中) | ○ | ○(可抓包) | △(HTTP/2+二进制) | △(症状较剧烈) | △ |
| 吞吐量/延迟 | 低 | 中~高 | 中 | 中 | ◎ | 中 |
补充一点。正确的用法不是选「◎ 最多的手段」,而是只看影响到需求的那几行,用排除法收敛范围。例如一旦确定「每秒 30 帧的图像」,数据面就只能选共享内存。
4. 常见架构示例
把判断表套用到具体需求上,实务中往往会收敛到下面 3 种架构。
架构 1:UI 应用程序 + Windows 服务的分离。 把常驻处理或需要特权的处理放在服务中,UI 则是普通的用户进程(服务端的做法请参考同日发布的《Windows 服务文章》)。通信以命名管道为本命,协议从 JSON 消息+消息模式(或字节模式+length 前缀)开始是稳妥的做法,等调用种类增多后,也有迁移到 gRPC 的命名管道传输方式的路可以走。2 因为服务是用不同账户(如 LocalService)运行,无法使用 CurrentUserOnly,重点是要用 PipeSecurity 明确指定「允许 Users 读写、拒绝远程」之类的 ACL。
架构 2:32bit 应用程序 → 64bit 功能的桥接。 想从 32bit 的既有应用程序使用只能在 64bit 上运行的 DLL・驱动 SDK 时,要把 64bit 一侧拆分到另一个进程。实现方式有两种:(a) 启动 64bit 辅助进程并用命名管道通信,(b) 做成 64bit 的跨进程 COM 服务器。如果调用方是 VBA 或旧语言,(b) 更自然;如果双方都是 C#,(a) 在分发与排查上都更轻松。采用 (a) 时,辅助进程的启动・监控・父子联动退出,请直接套用《安全地处理子进程》中提到的 Job Object 模式。
架构 3:测量引擎 → 显示用的高频率数据。 把测量・图像处理引擎拆成另一个进程,再把数据传向 UI 的架构中,控制(开始・停止・配置)用命名管道,数据(帧・波形)用共享内存+命名事件的双通道架构是常见做法。控制每秒只有几次,管道就够用;数据则用共享内存逼近零拷贝。把通道分开后,就能把数据侧的设计(环形缓冲区等)与控制侧的考量分离,各自优化。引擎或外部设备的状态显示,也可以参考《外部设备的状态显示》。
5. 实现示例 ── 命名管道的异步服务器与客户端
以下展示作为命名管道实现基础的、.NET 8 下的最小结构,包含消息模式・支持多客户端・断开连接处理。协议设计为「UTF-8 的 JSON、一条消息一封」,并且一定要带 version 字段。下面的示例以同一用户进程间通信为前提,使用了 CurrentUserOnly。如果像架构 1 那样要与服务(不同账户)通信,请如稍后所述,去掉 CurrentUserOnly 并用 PipeSecurity 明确指定 ACL。
先看服务器端。每次接受连接都会重新创建新的服务器流,并把每个客户端的处理分离到各自的任务中。
using System.IO.Pipes;
using System.Text.Json;
public sealed class PipeServer(string pipeName)
{
// camelCase・不区分大小写的 Web 默认值。System.Text.Json 的默认值
// 会区分大小写,如果不共用这个设置,"version" 就不会
// 绑定到 Request.Version,导致正常的请求也落入 unknown_type
internal static readonly JsonSerializerOptions JsonOptions =
new(JsonSerializerDefaults.Web);
public async Task RunAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var pipe = new NamedPipeServerStream(
pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message, // 写入一次 = 一条消息
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
await pipe.WaitForConnectionAsync(ct);
_ = Task.Run(() => HandleClientAsync(pipe, ct), ct); // 每个连接分开处理
}
}
// 单条消息的上限。即使对方发来巨大的消息,
// 也要保护服务一侧的内存(IPC 的对象也算外部输入。第6章)
private const int MaxMessageBytes = 1024 * 1024;
private static async Task HandleClientAsync(
NamedPipeServerStream pipe, CancellationToken ct)
{
await using (pipe)
{
var buffer = new byte[64 * 1024];
try
{
while (!ct.IsCancellationRequested)
{
// 即使是消息模式,也不能保证一次 Read 就能读完
// 整条消息。要读到 IsMessageComplete 为止
using var ms = new MemoryStream();
do
{
int n = await pipe.ReadAsync(buffer, ct);
if (n == 0) return; // 客户端已断开连接
ms.Write(buffer, 0, n);
if (ms.Length > MaxMessageBytes) return; // 超过上限,断开连接
} while (!pipe.IsMessageComplete);
byte[] response = Dispatch(ms.ToArray());
await pipe.WriteAsync(response, ct);
}
}
catch (IOException)
{
// 只是这个客户端的通信通道坏了。
// 不停掉整个服务器,继续处理其他连接
}
}
}
private static byte[] Dispatch(byte[] payload)
{
Request? req;
try { req = JsonSerializer.Deserialize<Request>(payload, JsonOptions); }
catch (JsonException) { req = null; }
// 格式错误・未知版本・未知 type 都在这里检查并拒绝
object result = req switch
{
null => new { version = 1, error = "bad_request" },
// version 未指定(会绑定成 0)或未知版本,都在这里拒绝
{ Version: not 1 } => new { version = 1, error = "version_unsupported" },
{ Type: "getStatus" } => new { version = 1, running = true },
{ Type: "startJob" } => new { version = 1, jobId = StartJob(req) },
_ => new { version = 1, error = "unknown_type" },
};
return JsonSerializer.SerializeToUtf8Bytes(result, JsonOptions);
}
}
public sealed record Request(int Version, string Type, JsonElement? Body);
客户端一侧则把「连接 → 请求 → 响应」封装在一个方法里,并从一开始就加入超时与重试。
using System.IO.Pipes;
using System.Text.Json;
public sealed class PipeClient(string pipeName)
{
// idempotent: 只有调用方明确声明「这个请求即使到达两次也安全」
// 时才会重试。默认是「不重试」
public async Task<TResponse?> RequestAsync<TResponse>(
object request, CancellationToken ct, bool idempotent = false)
{
for (int attempt = 1; ; attempt++)
{
// 不仅是连接,整个请求→响应的交换过程都设置上限时间。
// 避免服务器只接受连接却没写响应就卡死时,
// 造成无限等待
using var deadline =
CancellationTokenSource.CreateLinkedTokenSource(ct);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
try
{
using var pipe = new NamedPipeClientStream(
".", pipeName, PipeDirection.InOut,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
// 不无限等待。如果服务器没有启动,就会抛出异常
await pipe.ConnectAsync(timeout: 3000, deadline.Token);
pipe.ReadMode = PipeTransmissionMode.Message;
await pipe.WriteAsync(
JsonSerializer.SerializeToUtf8Bytes(
request, PipeServer.JsonOptions),
deadline.Token);
using var ms = new MemoryStream();
var buffer = new byte[64 * 1024];
do
{
int n = await pipe.ReadAsync(buffer, deadline.Token);
if (n == 0) throw new IOException("服务器已断开连接。");
ms.Write(buffer, 0, n);
} while (!pipe.IsMessageComplete);
return JsonSerializer.Deserialize<TResponse>(
ms.ToArray(), PipeServer.JsonOptions);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw; // 调用方主动取消,不重试
}
catch (Exception ex) when (
ex is IOException or TimeoutException or OperationCanceledException
&& idempotent && attempt < 3)
{
// 针对服务器重启中之类的临时性失败进行重试。
// 「发出后但还没读到响应就断开」的情况下,请求有可能
// 已经执行过了,因此像 startJob 这种有副作用的请求,
// 按默认不重试;只有在设计成可用请求 ID 过滤重复之后,
// 才传入 idempotent: true(第6章)
await Task.Delay(500 * attempt, ct);
}
}
}
}
针对这段代码的设计判断,补充 3 点。
- 从第一个版本就加入
version。 UI 和服务一定会遇到分别更新(只有一方变新)的时刻。响应方只要做到「明确拒绝不认识的版本」,原本会悄悄出现异常行为的事故,就会变成「看得懂的错误」。 - 决定连接是每次新建,还是常驻连接。 上面的示例是每次请求都重新连接的一次性方式,不需要管理断开・重新连接的状态,但不适合高频率的调用。如果需要常驻连接+服务器主动推送通知,那种复杂度就是切换到 gRPC 双向流式传输的判断依据。
- 与服务通信时,要去掉
CurrentUserOnly,并设计PipeSecurity。 上面的示例假设是同一用户的进程之间。如果对方是不同账户的服务,请用NamedPipeServerStreamAcl.Create创建带 ACL 的服务器流,并明确指定允许连接的组。6
6. 协议设计的共同注意事项 ── 选定手段之后的工作
不管选哪种 IPC,通信内容的设计都有共同的课题。其实比起选手段,这里才是事故的源头。
- 消息的分界(分帧):除了像管道的消息模式那样由 OS 负责分界的情况之外,「从哪里到哪里算一条消息」是应用程序自己的责任。共享内存的记录边界也是同样的问题。请以 length 前缀方式为基本做法。
- 版本管理:在消息中包含格式版本,接收方要「明确拒绝不认识的版本」。即使用同一个安装程序发布,也不保证两个进程永远会同时更新。
- 超时:把「对方不响应」当成一定会发生的事来设计。为连接・请求・响应各自设定上限时间,不要留下无限等待的代码。在 UI 线程上同步等待更是不可行。
- 重新连接与幂等性:重试意味着会制造出同一个请求到达两次的可能性。要按请求种类分类「执行两次也安全的请求」,对于不安全的请求(如投递任务)则加上请求 ID 来过滤重复。
- 把对方也当成外部输入来验证:如果是同一台机器内的通信,很容易因为「对方是自己的应用程序」就省略验证,但只要存在权限边界,IPC 的对象就等同于来自网络的输入,是同样「不可信任的输入」。管理员权限的代理或服务,不应该把标准权限客户端发来的路径或命令原封不动拿来执行。连同管道名称抢占(2.2 节)一起,「怀疑对方是谁」「怀疑要做什么」两者并重,才是跨越权限边界的 IPC 应有的做法。
- 保留观测手段:只要能输出通信日志(至少要有消息类型与对方・结果),「连不上」「没有响应」这类问题的排查时间就会差好几个量级。
7. 总结
进程间通信的选择,按下面的顺序思考几乎不会迷路。首先确认是否可以限定在同一台机器内。如果可以限定,请求-响应用命名管道、大容量高频率数据只用共享内存、松耦合的批处理用文件对接。如果看得到跨机器或跨语言混用,就用本地 TCP 或 gRPC;当自定义协议的维护规模变成负担时,就选 gRPC。COM 不作为新设计的首选,而是保留作为 32/64 桥接与 VBA 对接的工具——本文的判断表,就是把这个流程整理成表格。
而且一旦决定了手段,就要把第 6 章的共同设计(分帧・版本・超时・重新连接・验证对方)放进第一个版本里。从实务上的体感来说,IPC 的问题比起「选错手段」,更多是因为「省略了协议设计」而发生,而且差距相当大。进程拆分或通信方式的重新审视,建议先从盘点「哪个进程・用谁的权限・做什么・多高频率地交互」开始。
相关文章
- 共享内存的陷阱与实务最佳实践
- 以为 TCP 的 Send 单位可以对应 Receive 是一种误解 ── 把 TCP 当作字节流处理的接收设计
- 管理员权限的分离(broker) ── 跨越 UAC 边界的设计
- Windows 服务的做法 ── Worker Service 指南
相关咨询领域
小村软件有限公司承接进程拆分・通信方式的设计评审、包含 32bit/64bit 桥接的既有资产对接设计,以及「连不上・没有响应」类通信问题的原因排查。
参考链接
-
Microsoft Learn,How to: Use Named Pipes for Network Interprocess Communication。关于用 NamedPipeServerStream / NamedPipeClientStream 实现支持多客户端的服务器・客户端示例。 ↩
-
Microsoft Learn,Inter-process communication with gRPC and Named pipes。关于 .NET 8 之后通过 Kestrel 的 ListenNamedPipe,让 gRPC 运行在命名管道上的架构,以及 PipeSecurity 的访问控制。 ↩ ↩2 ↩3
-
Microsoft Learn,MemoryMappedFile Class。关于内存映射文件的 .NET API,以及用 CreateNew 创建、不绑定文件的共享内存适合用于 IPC 的说明。 ↩ ↩2
-
Microsoft Learn,Interprocess communications。关于 Windows 提供的 IPC 机制(剪贴板、COM、WM_COPYDATA、DDE、文件映射、邮件槽、管道、RPC、Windows 套接字)清单与选用准则。 ↩ ↩2 ↩3
-
Microsoft Learn,NamedPipeServerStream Class。关于命名管道的服务器端流,以及指定 PipeTransmissionMode 或 PipeSecurity 的构造函数。 ↩
-
Microsoft Learn,Named Pipe Security and Access Rights。关于命名管道的默认安全描述符允许 Everyone 与匿名账户读取,以及访问权限的构成。 ↩ ↩2
-
Microsoft Learn,PipeOptions Enum。关于 CurrentUserOnly 只允许与「同一用户创建的对象」连接,在 Windows 上除了用户账户之外,还会验证到提升级别。 ↩ ↩2
-
Microsoft Learn,Inter-process communication with gRPC。关于把 gRPC 用于 IPC 时的传输层(Unix 域套接字、命名管道),以及服务器所有者验证・伪装(impersonation)对策等安全性考量。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
常驻处理应该做成 Windows 服务,还是任务计划程序就够用?本文整理判断表,以及用 .NET Worker Service(UseWindowsService)创建服务的方法、运行账户、恢复选项、事件日志、安全停止处理,从实务角度梳理投入运维所需的设计。
在 WinForms/WPF 应用中集成 Entra ID 认证 —— MSAL.NET 与 WAM Broker 的实务架构
本文以实务视角整理在 WinForms/WPF 桌面应用中集成 Entra ID(原 Azure AD)认证的步骤:公共客户端的思路、ROPC 被弃用的现状、应用注册、MSAL.NET 的 AcquireTokenSilent 模式、WAM Broker、令牌缓存的持久化,...
常见问题
汇总了咨询这一主题时常见的问题。
- Windows 的进程间通信应该以什么为首选?
- 同一台机器内的请求-响应(发送命令并获取结果)场景,命名管道是首选。它是 OS 标准功能,不需要管理端口号,也不会被防火墙拦截,并且与 Windows 的访问控制(PipeSecurity)集成,在 .NET 中可以用 System.IO.Pipes 直接写出来。如果未来有可能跨越网络,一开始就选本地 TCP 或 gRPC;只有大容量・高频率的数据(图像帧、波形)才用共享内存;松耦合・异步且想留下审计日志的对接则用文件对接——用这种排除法收敛范围才是正确的选择方式。
- gRPC 适合在什么时候用于进程间通信?
- 当服务或调用的种类增多,自定义协议的 switch 语句与文档维护开始变成负担时。通过 proto 文件定义 schema 并生成代码、获得双向流式传输,而且 .NET 8 之后 ASP.NET Core(Kestrel)可以直接把命名管道当作传输层。另一方面,对于只有 2~3 种命令的工具之间,扛着 ASP.NET Core 的托管基础设施会是过度设计,命名管道+JSON 的总成本更低。等到「proto 里要写的消息超过 10 个」「本质上需要流式通知」「对方不是 .NET」这三者之一成立后再引入也不迟。
- 从 32bit 应用程序使用 64bit DLL 应该怎么做?
- 32bit 进程无法把 64bit 的 DLL 加载到同一个进程中,因此需要把 64bit 一侧拆分到另一个进程。实现方式有两种:(a) 启动 64bit 的辅助进程并用命名管道通信,(b) 做成 64bit 的跨进程 COM 服务器。如果调用方是 VBA 或旧语言,(b) 更自然,因为 COM 基础设施会代为处理封送,调用方几乎不用改代码。如果双方都是 C#,(a) 在分发与排查上都更轻松。采用 (a) 时,要用 Job Object 设计辅助进程的父子联动退出。
- 共享内存适合在什么场景下使用?
- 只限定在图像帧或波形数据这类大容量・高频率的数据上使用。它是同一台机器内最快的 IPC,不涉及复制或序列化,但完全不提供任何同步机制,因此「不读取写入中的数据」的机制、检测对方存活状态、异常退出后的恢复,全部都要自己设计。实务中常见的做法是只把数据面(data plane)放在共享内存上,开始・停止・修改配置等控制面(control plane)则交给命名管道之类的另一个通道,形成双通道架构。一旦连控制消息都想硬塞进共享内存,就是该重新审视设计的信号。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。