news 2026/9/24 14:06:25

Windows-universal-samples 之 AppServices:UWP 应用服务跨应用与跨设备通信实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows-universal-samples 之 AppServices:UWP 应用服务跨应用与跨设备通信实战指南
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

本篇技术指南以 Samples/AppServices/README.md 为骨架,结合 Windows-universal-samples 仓库中 C# 与 C++/WinRT 两个版本的完整实现源码,系统讲解 UWP「应用服务(App Service)」的声明、实现、连接、消息收发与远程调用全流程。读完本文,你将掌握如何用AppServiceConnection让一个应用向另一个应用(甚至另一台电脑)暴露可调用的服务,并能在自己的 UWP 项目中复刻「打开即用」与「长连接复用」两种典型通信模式。

示例概览:一次跨应用调用的最小闭环

App Services(应用服务)是 UWP 提供的应用间通信机制:一个应用(Provider,服务提供方)把一段能力包装成服务并注册到系统,另一个应用(Client,客户端)通过AppServiceConnection发现并调用它,消息以ValueSet(键值对字典)为载体传递。整个过程无需两个应用直接握手 socket,系统负责把客户端进程与服务进程连接起来。

本示例由两个 UWP 项目外加一个后台任务组成(见 Samples/AppServices 目录):

项目职责C# 路径C++/WinRT 路径
AppServicesProvider服务提供方,注册名为com.microsoft.randomnumbergenerator的应用服务cs/AppServicesProvidercppwinrt/AppServicesProvider
RandomNumberService承载服务逻辑的 Windows 运行时后台任务(IBackgroundTaskcs/RandomNumberService/RandomNumberGeneratorTask.cscppwinrt/RandomNumberService/RandomNumberGeneratorTask.cpp
AppServicesClient服务调用方,内置两个场景页cs/AppServicesClientcppwinrt/AppServicesClient

服务本身的功能很纯粹:接收客户端传入的最小值/最大值,返回一个区间内的随机整数。客户端则演示了两种连接策略:

  • 打开-连接-关闭(OpenCloseConnection):调用一次就建立连接、发送消息、取回结果并立即释放连接,对应 OpenCloseConnectionScenario.xaml;
  • 保持连接(KeepConnectionOpen):连接打开后持续复用,直到用户显式关闭,对应 KeepConnectionOpenScenario.xaml。

两个场景的 XAML 界面(最小值、最大值、结果三个控件)存放在共享目录 Samples/AppServices/shared,由各语言的代码后置文件驱动逻辑,这正是 Windows-universal-samples 仓库典型的「共享 UI + 多语言实现」组织方式。

服务提供方:两步声明一个可被调用的应用服务

要让系统知道某个应用「提供」了服务,必须完成两件事:在清单中声明扩展实现后台任务并挂接RequestReceived事件

1. 清单声明(Package.appxmanifest)

以 C# 版本的 cs/AppServicesProvider/Package.appxmanifest 为例,关键片段如下:

<Identity Name="Microsoft.SDKSamples.AppServicesProvider.CS" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Version="1.0.0.0" /> ... <Extensions> <uap:Extension Category="windows.appService" EntryPoint="RandomNumberService.RandomNumberGeneratorTask"> <uap3:AppService Name="com.microsoft.randomnumbergenerator" SupportsRemoteSystems="true"/> </uap:Extension> </Extensions>

逐项说明:

  • uap:ExtensionCategory="windows.appService"声明这是一个应用服务扩展;
  • EntryPoint指向后台任务类RandomNumberService.RandomNumberGeneratorTask,它必须实现IBackgroundTask
  • uap3:AppServiceName是服务的全局标识符,客户端连接时必须精确匹配这个字符串(本示例为com.microsoft.randomnumbergenerator,采用了反向域名命名惯例);
  • SupportsRemoteSystems="true"允许其他电脑上的客户端通过远程系统框架连接本服务,这是 README 特别强调的声明,也是与 Samples/RemoteSystems 示例联动的关键开关。

同样地,C++/WinRT 版本的清单 cppwinrt/AppServicesProvider/Package.appxmanifest 结构完全一致,仅Identity.Name变为Microsoft.SDKSamples.AppServicesProvider.CPPWINRT。注意两个清单的TargetDeviceFamily都声明了MinVersion="10.0.14393.0"MaxVersionTested="10.0.22621.0",说明该示例面向 Windows 10 周年更新(14393)及以上的 Universal 平台。

2. 后台任务:挂接请求并回复

服务本体是 RandomNumberGeneratorTask.cs 中的IBackgroundTask,核心逻辑浓缩在RunOnRequestReceived两个方法中:

public void Run(IBackgroundTaskInstance taskInstance) { // 1. 获取服务延迟(deferral),防止后台任务在等待期间被系统终止 serviceDeferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnTaskCanceled; // 2. 初始化随机数生成器 randomNumberGenerator = new Random((int)DateTime.Now.Ticks); // 3. 从触发器详情中取出系统注入的 AppServiceConnection var details = taskInstance.TriggerDetails as AppServiceTriggerDetails; connection = details.AppServiceConnection; // 4. 监听客户端发来的请求 connection.RequestReceived += OnRequestReceived; }

请求处理函数拿到AppServiceRequestReceivedEventArgs后,先取消息延迟(保证能安全地使用异步 API 回复),再解析ValueSet参数并回写结果:

async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) { var messageDeferral = args.GetDeferral(); // 消息级延迟 try { var input = args.Request.Message; // 客户端传来的 ValueSet int minValue = (int)input["minvalue"]; int maxValue = (int)input["maxvalue"]; var result = new ValueSet(); result.Add("result", randomNumberGenerator.Next(minValue, maxValue)); await args.Request.SendResponseAsync(result); // 回复客户端 } finally { messageDeferral.Complete(); // 通知平台本次响应处理完毕 } }

两个延迟(deferral)是应用服务编程中最重要的细节:任务级延迟taskInstance.GetDeferral())让后台任务在连接生命周期内不被系统回收;消息级延迟args.GetDeferral())则保证异步的SendResponseAsync执行完之前,平台不会判定响应超时。任务被取消(OnTaskCanceled)时则要释放连接并Complete服务延迟,形成对称的清理路径。

C++/WinRT 版本 RandomNumberGeneratorTask.cpp 的职责划分与 C# 完全一致,只是 API 形态换成投影类型:事件用{ get_weak(), &... }弱引用回调防悬挂、ValueSetInsert/TryLookup、随机数改用<random>uniform_int_distribution,返回类型是fire_and_forget协程;并且它对maxvalue < minvalue的异常输入做了防御性兜底(把最大值钳制为最小值),可作为健壮性处理的参考。两种语言都遵循「Run 建连接 → RequestReceived 响应 → 取消时清理」这一固定流程。

客户端场景一:打开连接、调用一次、立即关闭

对应 OpenCloseConnectionScenario.xaml.cs,这是应用服务最简调用范式:用using块把连接的生命周期限制在一次调用内。

连接建立阶段——设置服务名与包系列名(PFN),随后OpenAsync

using (var connection = new AppServiceConnection()) { connection.AppServiceName = "com.microsoft.randomnumbergenerator"; connection.PackageFamilyName = "Microsoft.SDKSamples.AppServicesProvider.CS_8wekyb3d8bbwe"; AppServiceConnectionStatus status = await connection.OpenAsync(); ... }

两个标识符缺一不可,其来源分别是:

  • AppServiceName:与提供方清单中uap3:AppService Name完全一致的字符串;
  • PackageFamilyName:提供方应用的程序包系列名。提供方自带一个 ShowPackageFamilyName.xaml.cs 辅助页面,通过Package.Current.Id.FamilyName直接读出当前包的 PFN 展示在界面上——当提供方部署到你的设备后,PFN 以该页面显示为准,代码里硬编码的..._8wekyb3d8bbwe后缀对应微软 SDK 签名证书。

OpenAsync的返回值需要完整处理,示例对每种失败原因都给出了可操作的提示:

AppServiceConnectionStatus含义与排查方向(源自源码注释/提示文案)
Success连接成功
AppNotInstalled提供方应用未部署到设备,需先部署 AppServicesProvider
AppUnavailable提供方不可用,可能正在更新,或安装在已不可用的可移动设备上
AppServiceUnavailable提供方已安装,但未提供AppServiceName指定的服务(查清单声明)
Unknown未知错误

消息收发阶段——用ValueSet装载参数并发送:

var inputs = new ValueSet(); inputs.Add("minvalue", minValueInput); inputs.Add("maxvalue", maxValueInput); AppServiceResponse response = await connection.SendMessageAsync(inputs); if (response.Status == AppServiceResponseStatus.Success && response.Message.ContainsKey("result")) { var resultText = response.Message["result"].ToString(); Result.Text = resultText; }

SendMessageAsync返回的AppServiceResponse同样需要按状态分支处理:Failure表示服务未能确认消息(可能已被终止或RequestReceived处理器异常)、ResourceLimitsExceeded表示服务超出系统分配的资源而被终止、Unknown为未知失败。UI 侧则在GenerateRandomNumber_Click里先对文本框做int.TryParse校验,并强制maxValue > minValue,保证发往服务的参数永远合法——这些校验逻辑与 XAML 中的 OpenCloseConnectionScenario.xaml 控件一一对应。

客户端场景二:长连接复用与生命周期管理

对应 KeepConnectionOpenScenario.xaml.cs。与场景一不同,这里把AppServiceConnection提升为页面级字段,连接只建立一次,可被多次SendMessageAsync复用:

private AppServiceConnection connection; // 打开连接:防重入 + 注册 ServiceClosed if (connection != null) { rootPage.NotifyUser("A connection already exists", NotifyType.ErrorMessage); return; } connection = new AppServiceConnection(); connection.AppServiceName = "com.microsoft.randomnumbergenerator"; connection.PackageFamilyName = "Microsoft.SDKSamples.AppServicesProvider.CS_8wekyb3d8bbwe"; connection.ServiceClosed += Connection_ServiceClosed; AppServiceConnectionStatus status = await connection.OpenAsync(); // 注意:await 期间连接可能已被 ServiceClosed 置空,必须判空 if (connection == null) { rootPage.NotifyUser("Connection was closed", NotifyType.ErrorMessage); return; }

这段代码示范了长连接场景下最容易踩的三个坑,源码里都有对应处理:

  1. 防重入:点击「Open Connection」前先检查connection != null,避免重复建连;
  2. await竞态await connection.OpenAsync()挂起期间,用户可能已点击关闭、ServiceClosed可能已触发,因此恢复执行后必须先判空再继续;
  3. 服务端断开通知:订阅ServiceClosed事件,在回调里通过Dispatcher.RunAsync切回 UI 线程,Dispose()并置空连接引用:
private async void Connection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) { await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { if (connection != null) { connection.Dispose(); connection = null; } }); }

关闭路径同样值得注意:CloseConnection_Click直接Dispose()连接并置空,而页面级清理放在OnNavigatedFrom中,保证离开场景页时连接一定被释放,不留悬挂引用。发送消息的方法体与场景一高度相似,只是在发送前先校验connection == null并给出「You need to open a connection before trying to generate a random number.」的引导提示,形成完整的 UX 闭环。

跨设备:SupportsRemoteSystems 与 RemoteSystems 示例的联动

README 明确指出:RemoteSystems 示例包含一个从另一台电脑连接本服务的场景,而本示例正是通过在清单中声明SupportsRemoteSystems="true"来允许这种远程连接。两处仓库证据相互印证:

  • 提供方清单(C# 版与 C++/WinRT 版)都带SupportsRemoteSystems="true"
  • 远程客户端实现在 Samples/RemoteSystems/cs/Scenario3_LaunchAppServices.xaml.cs:它先通过RemoteSystem发现选定远端设备,构造RemoteSystemConnectionRequest,再用同一个AppServiceConnection对象调用OpenRemoteAsync而不是OpenAsync
RemoteSystemConnectionRequest connectionRequest = new RemoteSystemConnectionRequest(selectedSystem); using (AppServiceConnection connection = new AppServiceConnection { AppServiceName = "com.microsoft.randomnumbergenerator", PackageFamilyName = "Microsoft.SDKSamples.AppServicesProvider.CS_8wekyb3d8bbwe" }) { AppServiceConnectionStatus status = await connection.OpenRemoteAsync(connectionRequest); if (status == AppServiceConnectionStatus.Success) { // 连接成功后即可像本地一样 SendMessageAsync await SendMessageToRemoteAppServiceAsync(connection); } }

可见客户端侧的编程模型在本地与远程之间完全一致——只需把OpenAsync换成OpenRemoteAsync并传入连接请求。这也解释了为什么 README 建议把 AppServices 与 RemoteSystems 两个示例搭配学习:前者是服务的「提供端 + 本地消费端」,后者是服务的「远程消费端」。

构建与运行

环境与系统要求

README 列出的系统要求为:客户端:Windows 10;服务器:Windows Server 2016 Technical Preview;手机:Windows 10。结合清单中的TargetDeviceFamilyMinVersion="10.0.14393.0"),构建环境需为 Windows 10 周年更新或更高版本,并使用 Visual Studio 与 Windows 开发者工具。

构建步骤

  1. 若从 ZIP 下载整个示例集合,务必解压完整归档(不是只解压目标示例文件夹),因为示例依赖仓库根目录的 SharedContent 共享内容(参见 README front-matter 中的extendedZipContentSharedContentLICENSE的引用);
  2. 启动 Visual Studio,选择File > Open > Project/Solution
  3. 进入 Samples/AppServices 下的子目录,按偏好选择语言版本的解决方案文件(.sln):当前仓库提供cs(C#)与cppwinrt(C++/WinRT)两套,与 README 的 front-matter(languages: csharp, cpp, cppwinrt)对应;早期的 C++/CX 与 Visual Basic 版本已归档至 archived/AppServices,仅供历史参考;
  4. Ctrl+Shift+B(或Build > Build Solution)编译。

运行步骤

  • 仅部署:选择Build > Deploy Solution
  • 部署并运行
    1. 确保AppServicesProvider 项目已部署(否则客户端会得到AppNotInstalled错误,源码中对此有专门提示);
    2. AppServicesClient 设为启动项目
    3. F5(调试运行)或Ctrl+F5(不调试运行)。

两个场景的界面都要求先输入整数形式的 Minimum Value 与 Maximum Value 再点击 Generate Random Number;场景二还需先点击打开连接,这是两份客户端源码中共有的交互前提。

小结

从 README 的两个项目、两个场景出发,结合仓库源码可以看到 UWP 应用服务的完整技术栈:提供方用「清单windows.appService扩展 +IBackgroundTask+RequestReceived事件」暴露服务,客户端用「AppServiceName+PackageFamilyName定位服务、OpenAsync/OpenRemoteAsync建立连接、ValueSet收发消息、ServiceClosed与双 deferral 管理生命周期」,再加上SupportsRemoteSystems="true"即可一键扩展到跨设备调用。这套模式可直接迁移到任意「两个 UWP 应用间解耦通信」的真实需求中,例如主应用调用独立打包的扩展应用能力、IoT 场景下的控制面服务等。更多联动示例可继续阅读 Samples/RemoteSystems 的远程应用服务场景。

相关主题

  • 关联示例:Samples/RemoteSystems(远程应用服务调用)
  • 历史实现:archived/AppServices(C++/CX 与 Visual Basic 归档版)
  • 核心 API 命名空间:Windows.ApplicationModel.AppServices,其中AppServiceConnection用于客户端建立连接与发送消息,AppServiceTriggerDetails用于服务端接收并响应消息——两者在本示例的提供端与消费端源码中均有直接使用。
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

相关推荐

上一篇:HackRF Jawbreaker硬件平台深度解析
下一篇:Turbo项目开发环境搭建与测试指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:06:09

用 Zig 集成 PRQL 编译器:prqlc-c FFI 最小示例全解析

后端 【免费下载链接】prql PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement 项目地址&#xff1a; https://gitcode.com/gh_mirrors/pr/prql 点击查看 免费下载 PRQL&#xff08;Pipelined Relational Query Language&am…

作者头像 李华
网站建设 2026/9/24 14:05:20

Logistics | “Stock Days ” vs.“Inventory Coverage”

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:04:48

Python | 地址解析经纬度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:04:12

【Dv3Admin】系统视图菜单按钮管理API文件解析

后台权限系统的发展趋势是细化到接口及操作按钮级别,以满足复杂业务下的安全与控制需求。菜单按钮权限管理模块基于 Django 与 DRF 实现,为后台平台提供了标准、细粒度的权限配置能力。 围绕 dvadmin/system/views/menu_button.py 源码,解析菜单按钮增删改查的实现方式,说…

作者头像 李华
网站建设 2026/9/24 14:04:10

Unity 引擎源码剖析:ICall 的 ABI 契约,为什么写错一个类型就会崩

开篇:一个查了三天的 bug QA 提的单子只有一行: 子弹偶尔穿过掩体,复现率约 5%,只在城区地图出现查射线检测的逻辑——没问题。 查碰撞体的配置——没问题。 查物理层级——也没问题。 最后在自己写的原生插件里,找到了这一行: extern "C" int IsBlocked(Vecto…

作者头像 李华
网站建设 2026/9/24 14:04:00

【Dv3Admin】应用URL路由配置文件解析

统一路由配置是 Django 项目结构的重要组成部分。通过集中管理接口、页面和文档入口,可以减少重复代码、提升维护效率,并支持模块化开发与动态扩展,满足中大型项目对灵活性和可控性的要求。 文章解析 application/urls.py 的关键实现,包括系统初始化、文档集成、认证接口注…

作者头像 李华