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 的问题比起「选错手段」,更多是因为「省略了协议设计」而发生,而且差距相当大。进程拆分或通信方式的重新审视,建议先从盘点「哪个进程・用谁的权限・做什么・多高频率地交互」开始。

相关文章

相关咨询领域

小村软件有限公司承接进程拆分・通信方式的设计评审、包含 32bit/64bit 桥接的既有资产对接设计,以及「连不上・没有响应」类通信问题的原因排查。

参考链接

  1. Microsoft Learn,How to: Use Named Pipes for Network Interprocess Communication。关于用 NamedPipeServerStream / NamedPipeClientStream 实现支持多客户端的服务器・客户端示例。 

  2. Microsoft Learn,Inter-process communication with gRPC and Named pipes。关于 .NET 8 之后通过 Kestrel 的 ListenNamedPipe,让 gRPC 运行在命名管道上的架构,以及 PipeSecurity 的访问控制。  2 3

  3. Microsoft Learn,MemoryMappedFile Class。关于内存映射文件的 .NET API,以及用 CreateNew 创建、不绑定文件的共享内存适合用于 IPC 的说明。  2

  4. Microsoft Learn,Interprocess communications。关于 Windows 提供的 IPC 机制(剪贴板、COM、WM_COPYDATA、DDE、文件映射、邮件槽、管道、RPC、Windows 套接字)清单与选用准则。  2 3

  5. Microsoft Learn,NamedPipeServerStream Class。关于命名管道的服务器端流,以及指定 PipeTransmissionMode 或 PipeSecurity 的构造函数。 

  6. Microsoft Learn,Named Pipe Security and Access Rights。关于命名管道的默认安全描述符允许 Everyone 与匿名账户读取,以及访问权限的构成。  2

  7. Microsoft Learn,PipeOptions Enum。关于 CurrentUserOnly 只允许与「同一用户创建的对象」连接,在 Windows 上除了用户账户之外,还会验证到提升级别。  2

  8. Microsoft Learn,Inter-process communication with gRPC。关于把 gRPC 用于 IPC 时的传输层(Unix 域套接字、命名管道),以及服务器所有者验证・伪装(impersonation)对策等安全性考量。  2

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

常见问题

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

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

返回博客列表