进程间通信 (IPC)

本主题介绍在Windows 应用 SDK桌面应用和其他 Win32 应用程序之间执行进程间通信(IPC)的各种方法。 由于Windows 应用 SDK桌面应用作为完全信任的 Win32 进程运行,因此它们可以直接访问所有 OS 级 IPC 机制。 当应用打包到 MSIX 时,某些机制还有其他要求,如以下部分所述。

应用服务

应用服务使应用程序能够在后台公开服务,这些服务接受并返回包含基本类型的属性包(ValueSet)。 复杂对象如果经过序列化,就可以传递。

应用服务既可以作为后台任务在进程外运行,也可以在前台应用程序内在进程中运行。

注释

应用服务需要具有包标识的已打包应用。 它们适用于使用 MSIX 打包的 Windows 应用 SDK 应用。

应用服务最适合用于共享少量数据,其中几乎不需要实时延迟。

COM

COM 是一个面向对象的分布式系统,用于创建可交互和通信的二进制软件组件。 作为开发人员,可以使用 COM 为应用程序创建可重用的软件组件和自动化层。 COM 组件可以处于进程内或进程外,并且它们可以通过 客户端和服务器 模型进行通信。 进程外 COM 服务器长期以来一直用作 对象间通信的手段。

具有此功能的 runFullTrust 打包应用程序可以通过 包清单为 IPC 注册进程外 COM 服务器。 这被称为 打包式 COM

Windows 应用 SDK桌面应用作为完全信任进程运行,因此它们还可以直接通过 Windows 注册表注册和使用 COM 服务器,就像传统的 Win32 应用程序一样。

Filesystem

BroadFileSystemAccess

打包的应用程序可以通过声明 broadFileSystemAccess 受限功能,使用广泛的文件系统执行 IPC。 此功能授予Windows。存储 API 和 Win32 FromApp API 访问广泛的文件系统。

默认情况下,对于已打包的应用程序,基于文件系统的 IPC 仅限于本节中所述的其他机制。

PublisherCacheFolder

PublisherCacheFolder 使打包的应用程序能够声明其清单中的文件夹,这些文件夹可由同一发布者与其他包共享。

共享存储文件夹具有以下要求和限制:

  • 共享存储文件夹中的数据不会备份或漫游。
  • 用户可以清除共享存储文件夹的内容。
  • 不能使用共享存储文件夹在不同发布者的应用程序之间共享数据。
  • 不能使用共享存储文件夹在不同用户之间共享数据。
  • 共享存储文件夹没有版本管理。

如果发布多个应用程序,并且正在寻找在它们之间共享数据的简单机制,则 PublisherCacheFolder 是基于文件系统的简单选项。

管道

管道 支持管道服务器与一个或多个管道客户端之间的简单通信。

以下约束支持匿名管道命名管道

  • 默认情况下,打包应用中的命名管道仅支持在同一包内的进程之间使用,除非某个进程具有完全信任权限。
  • 可以按照 共享命名对象的准则跨包共享命名管道。
  • 打包应用中的命名管道应包含在 LOCAL\ 管道名称中(例如, \\.\pipe\LOCAL\<pipename>)。 LOCAL\ 这一段将管道的作用域限定在调用方的登录会话内,并且是 MSIX 打包应用所必需的。 使用诸如 NamedPipeServerStream 之类的 .NET API 时,仅传递 LOCAL\<pipename> 部分——\\.\pipe\ 前缀由内部处理。

Windows 应用 SDK桌面应用作为完全信任进程运行,因此它们可以创建和使用命名管道,而不受同一包限制。 但是,如果要与另一个没有完全信任的打包应用通信,上述约束仍适用。

命名管道示例(C#)

以下示例演示完整的命名管道服务器和客户端作为两个控制台应用程序。 由于Windows 应用 SDK桌面应用是完全信任的进程,因此命名管道 IPC 无需任何特殊功能或清单条目即可工作。

若要尝试此示例,请创建两个控制台应用项目(以 net8.0-windows 为目标并启用 ImplicitUsings),先运行服务器,然后在单独的终端中运行客户端。

管道服务器 - 创建命名管道并将消息回显回客户端:

using System.Text;

string PipeName = @"LOCAL\WinAppSdkIpcDemo";

Console.WriteLine("Named Pipe Server");
Console.WriteLine($"  Process ID: {Environment.ProcessId}");
Console.WriteLine($"  User:       {Environment.UserName}");
Console.WriteLine($"  Pipe:       \\\\.\\pipe\\{PipeName}");
Console.WriteLine();

using var server = new System.IO.Pipes.NamedPipeServerStream(
    PipeName,
    System.IO.Pipes.PipeDirection.InOut,
    maxNumberOfServerInstances: 1,
    System.IO.Pipes.PipeTransmissionMode.Message,
    System.IO.Pipes.PipeOptions.Asynchronous);

Console.WriteLine("Waiting for client...");
await server.WaitForConnectionAsync();
Console.WriteLine("Client connected!");

byte[] buffer = new byte[4096];

while (server.IsConnected)
{
    try
    {
        int bytesRead = await server.ReadAsync(buffer);
        if (bytesRead == 0) break;

        string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);
        Console.WriteLine($"  Received: \"{received}\"");

        if (received.Equals("QUIT", StringComparison.OrdinalIgnoreCase))
            break;

        // Echo the message back with the server's process ID
        string reply = $"Echo from PID {Environment.ProcessId}: {received}";
        await server.WriteAsync(Encoding.UTF8.GetBytes(reply));
        await server.FlushAsync();
    }
    catch (IOException)
    {
        break;
    }
}

Console.WriteLine("Done.");

管道客户端 - 连接到服务器并发送用户输入:

using System.Text;

string PipeName = @"LOCAL\WinAppSdkIpcDemo";

Console.WriteLine("Named Pipe Client");
Console.WriteLine($"  Process ID: {Environment.ProcessId}");
Console.WriteLine();

using var client = new System.IO.Pipes.NamedPipeClientStream(
    serverName: ".",
    pipeName: PipeName,
    System.IO.Pipes.PipeDirection.InOut,
    System.IO.Pipes.PipeOptions.Asynchronous);

Console.WriteLine("Connecting...");
await client.ConnectAsync(timeout: 5000);
client.ReadMode = System.IO.Pipes.PipeTransmissionMode.Message;
Console.WriteLine("Connected! Type messages (or QUIT to exit):");

byte[] buffer = new byte[4096];

while (true)
{
    Console.Write("> ");
    string? input = Console.ReadLine();
    if (string.IsNullOrEmpty(input)) continue;

    await client.WriteAsync(Encoding.UTF8.GetBytes(input));
    await client.FlushAsync();

    if (input.Equals("QUIT", StringComparison.OrdinalIgnoreCase))
        break;

    int bytesRead = await client.ReadAsync(buffer);
    string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
    Console.WriteLine($"  <- {response}");
}

Console.WriteLine("Done.");

当你运行这两个应用程序时,客户端会发送消息,服务器会将其回显,从而确认两个完全受信任的桌面进程之间能够进行跨进程通信,且无需任何特殊配置。

注册表

通常不建议将注册表用于 IPC,但为了兼容现有代码,仍提供对这种用法的支持。 打包的应用程序只能访问他们有权访问的注册表项。

打包的桌面应用(请参阅 从代码生成 MSIX 包)利用 注册表虚拟化 ,以便全局注册表写入包含在 MSIX 包中的专用配置单元中。 这样可以实现源代码兼容性,同时最大程度地降低全局注册表影响,并可用于同一包中的进程之间的 IPC。 如果必须使用注册表,则首选此模型,而不是操作全局注册表。

RPC

RPC 可用于将打包的应用程序连接到 Win32 RPC 终结点,前提是打包的应用程序具有与 RPC 终结点上的 ACL 匹配的正确功能。

自定义能力使 OEM 和 IHV 能够 定义任意能力使用这些能力对其 RPC 端点实施 ACL 访问控制,然后将这些能力授予已获授权的客户端应用程序。 有关完整的示例应用程序,请参阅 CustomCapability 示例。

RPC 终结点也可以通过 ACL 限制为仅特定的打包应用程序可访问,而无需承担管理自定义功能所带来的开销。 可以使用 DeriveAppContainerSidFromAppContainerName API 从包系列名称派生 SID,然后使用 SID 为 RPC 终结点 ACL,如 CustomCapability 示例中所示。

共享内存

文件映射 可用于在具有以下约束的两个或多个进程之间共享文件或内存:

  • 默认情况下,打包应用中的文件映射仅支持在同一包内的进程之间进行,除非某个进程具有完全信任权限。
  • 可以按照 共享命名对象的准则跨包共享文件映射。

Windows 应用 SDK桌面应用作为完全信任进程运行,因此它们可以创建和使用共享内存文件映射,且不受限制。 与不具有完全信任的另一个打包应用通信时,请使用 共享命名对象中所述的 ACL 方法。

建议使用共享内存来有效共享和操作大量数据。

Loopback

环回是指与监听 localhost(环回地址)的网络服务器进行通信的过程。

为了保持安全性和网络隔离,默认情况下,打包的应用程序会阻止 IPC 的环回连接。 可以通过功能声明和清单属性,在受信任的已打包应用之间启用环回连接。

  • 参与环回连接的所有打包应用程序都需要在其privateNetworkClientServer中声明该功能。
  • 两个打包的应用程序可以通过环回进行通信,方法是在其包清单中声明 LoopbackAccessRules
    • 每个应用程序都必须在其 LoopbackAccessRules 中列出另一个应用程序。 客户端声明服务器的“out”规则,服务器为其支持的客户端声明“in”规则。

注释

这些规则中用于标识应用的包系列名称可通过以下方式查找:在开发期间,通过 Visual Studio 中的包清单编辑器;对于通过 Microsoft Store 发布的应用,通过 Partner Center;对于已安装的应用,通过 Get-AppxPackage PowerShell 命令。

未打包的应用程序和服务没有包标识,因此无法在 LoopbackAccessRules 中声明它们。 可以通过 CheckNetIsolation.exe 将打包应用程序配置为可通过环回与未打包应用程序和服务建立连接;不过,这仅适用于旁加载或调试场景,即你对该计算机具有本地访问权限并且拥有管理员权限。

  • 如果打包的应用程序连接到未打包的应用程序或服务,请运行 CheckNetIsolation.exe LoopbackExempt -a -n=<PACKAGEFAMILYNAME> 以添加打包应用程序的环回豁免。
  • 如果未打包的应用程序或服务正在连接到打包的应用程序,请运行 CheckNetIsolation.exe LoopbackExempt -is -n=<PACKAGEFAMILYNAME>,以使打包的应用程序能够接收入站回环连接。

注释

CheckNetIsolation.exe-n 标志所需的包系列名称,可以在开发期间通过 Visual Studio 中的包清单编辑器找到;对于通过 Microsoft Store 发布的应用,可通过 Partner Center 找到;对于已安装的应用,可通过 Get-AppxPackage PowerShell 命令找到。

选择 IPC 机制

下表总结了 IPC 机制及其最佳用例:

机制 最适用于 Requirements
应用服务 使用属性包进行小规模数据交换 具有包标识的打包应用
COM 可重用组件、自动化层 无(Win32)或程序包清单(已打包的 COM)
命名管道 基于流的双向通信 完全信任应用不适用
共享内存 大型数据,高性能 对于完全信任的应用,则无此限制
RPC 分布式客户端/服务器程序 ACL 必须允许访问
注册表 遗留代码兼容性 不建议使用新代码
Loopback 基于网络的协议(TCP/UDP) privateNetworkClientServer 打包应用的功能