Windows 推送通知服务 (WNS) 概述

Windows 推送通知服务 (WNS) 使第三方开发人员可从自己的云服务发送 Toast、磁贴、锁屏提醒和原始更新。 这提供了一种高效而可靠地向用户提供新更新的机制。

工作原理

下图显示了用于发送推送通知的完整数据流。 它涉及以下步骤:

  1. 应用从 WNS 请求推送通知通道。
  2. Windows 要求 WNS 创建通知通道。 此通道以统一资源标识符 (URI) 的形式返回到调用设备。
  3. 通知通道 URI 由 WNS 返回到应用。
  4. 应用将 URI 发送到你自己的云服务。 然后,将 URI 存储在自己的云服务上,以便在发送通知时访问 URI。 URI 是你自己的应用和服务之间的接口;你有责任使用安全可靠的 Web 标准实现此接口。
  5. 当云服务有要发送的更新时,它会使用通道 URI 通知 WNS。 通过安全套接字层 (SSL) 发出 HTTP POST 请求(包括通知有效负载),即可完成此操作。 此步骤需要身份验证。
  6. WNS 接收请求并将通知路由到相应的设备。

wns data flow diagram for push notification

注册应用并接收云服务的凭据

在使用 WNS 发送通知之前,应用必须先向应用商店仪表板进行注册,如此处所述。

请求通知通道

当能够接收推送通知的应用运行时,它必须首先通过 CreatePushNotificationChannelForApplicationAsync 请求通知通道。 有关完整的讨论和示例代码,请参阅如何请求、创建和保存通知通道。 此 API 返回一个唯一链接到调用应用程序及其磁贴的通道 URI,所有通知类型都可通过它发送。

应用成功创建通道 URI 后,它会将此 URI 以及应与其关联的任何特定于应用的元数据发送到其云服务。

重要事项

  • 我们不能保证应用的通知通道 URI 将始终保持不变。 建议应用每次运行时请求一个新通道,并在 URI 更改时更新其服务。 开发人员不应修改通道 URI,应将其视为黑盒字符串。 此时,通道 URI 会在 30 天后过期。 如果你的 Windows 10 应用将在后台定期续订其通道,则可以下载适用于 Windows 8.1 的推送和定期通知示例,并重新使用其源代码和/或它演示的模式。
  • 云服务和客户端应用之间的接口由开发人员实现。 我们建议应用使用自己的服务完成身份验证过程,并通过安全协议(如 HTTPS)传输数据。
  • 重要的是,云服务始终确保通道 URI 使用“notify.windows.com”域。 服务绝对不应将通知推送到任何其他域上的通道。 如果应用的回调被盗用,恶意攻击者可能会提交通道 URI 来欺骗 WNS。 如果不对域进行检查,你的云服务可能会在你不知情的情况下向此攻击者泄露信息。 通道 URI 的子域可能会更改,在验证通道 URI 时不应考虑该子域。
  • 如果云服务尝试将通知传递到过期通道,WNS 将返回响应代码 410。 为了响应该代码,服务不应再尝试向该 URI 发送通知。

对云服务进行身份验证

若要发送通知,云服务必须通过 WNS 进行身份验证。 将应用注册到 Microsoft Store 仪表板时,将执行此过程的第一步。 在注册过程中,应用将获得包安全标识符 (SID) 和密钥。 云服务使用此信息通过 WNS 进行身份验证。

WNS 身份验证方案是使用 OAuth 2.0 协议中的客户端凭据配置文件实现的。 云服务通过提供其凭据(包 SID 和密钥)来通过 WNS 进行身份验证。 返回时,它会收到访问令牌。 此访问令牌使云服务可以发送通知。 每个发送到 WNS 的通知请求都需要此令牌。

信息链大致如下:

  1. 云服务按照 OAuth 2.0 协议通过 HTTPS 向 WNS 发送凭据。 这会使用 WNS 对服务进行身份验证。
  2. 如果身份验证成功,WNS 将返回访问令牌。 在此访问令牌过期之前,你可在后续通知请求中使用此令牌。

wns diagram for cloud service authentication

在 WNS 身份验证中,云服务通过安全套接字层 (SSL) 提交 HTTP 请求。 参数以“application/x-www-for-urlencoded”格式提供。 在“client_id”字段中提供你的程序包 SID,并在“client_secret”字段中提供你的密钥,如以下示例中所示。 有关语法的详细信息,请参阅访问令牌请求参考。

注意

这仅仅是一个示例,而不是可在你自己的代码中成功使用的剪切和粘贴代码。 

 POST /accesstoken.srf HTTP/1.1
 Content-Type: application/x-www-form-urlencoded
 Host: https://login.live.com
 Content-Length: 211
 
 grant_type=client_credentials&client_id=ms-app%3a%2f%2fS-1-15-2-2972962901-2322836549-3722629029-1345238579-3987825745-2155616079-650196962&client_secret=Vex8L9WOFZuj95euaLrvSH7XyoDhLJc7&scope=notify.windows.com

WNS 对云服务进行身份验证,如果成功,则发送响应“200 正常”。 使用“application/json”媒体类型在 HTTP 响应正文中包含的参数中返回访问令牌。 服务收到访问令牌后,即可发送通知。

以下示例显示了成功的身份验证响应,包括访问令牌。 有关语法的详细信息,请参阅推送通知服务请求和响应标头

 HTTP/1.1 200 OK   
 Cache-Control: no-store
 Content-Length: 422
 Content-Type: application/json
 
 {
     "access_token":"EgAcAQMAAAAALYAAY/c+Huwi3Fv4Ck10UrKNmtxRO6Njk2MgA=", 
     "token_type":"bearer"
 }

重要事项

  • 此过程支持的 OAuth 2.0 协议遵循草稿版本 V16。
  • OAuth 征求意见文档 (RFC) 使用“客户端”一词来指代云服务。
  • 完成 OAuth 草稿后,此过程可能会发生更改。
  • 可将访问令牌重复用于多个通知请求。 这样以来,云服务只需进行一次身份验证即可发送许多通知。 但是,当访问令牌过期时,云服务必须再次进行身份验证才能接收新的访问令牌。

发送通知

如果使用通道 URI,每当云服务对用户有更新时,它都可以发送通知。

上述访问令牌可以重复用于多个通知请求;无需云服务器即可为每个通知请求新的访问令牌。 如果访问令牌已过期,通知请求将返回错误。 如果访问令牌遭到拒绝,建议不要多次尝试重新发送通知。 如果遇到此错误,则需要请求新的访问令牌并重新发送通知。 有关确切的错误代码,请参阅推送通知响应代码

  1. 云服务向通道 URI 发出 HTTP POST。 必须通过 SSL 发出此请求,并包含必要的标头和通知有效负载。 授权标头必须包含获得的用于授权的访问令牌。

    下面显示了一个示例。 有关语法的详细信息,请参阅推送通知响应代码

    如需详细了解如何撰写通知有效负载,请参阅快速入门:发送推送通知。 磁贴、toast 或锁屏提醒推送通知的有效负载作为符合各自定义的自适应磁贴架构旧磁贴架构的 XML 内容提供。 原始通知的有效负载没有指定的结构。 它完全由应用定义。

     POST https://cloud.notify.windows.com/?token=AQE%bU%2fSjZOCvRjjpILow%3d%3d HTTP/1.1
     Content-Type: text/xml
     X-WNS-Type: wns/tile
     Authorization: Bearer EgAcAQMAAAAALYAAY/c+Huwi3Fv4Ck10UrKNmtxRO6Njk2MgA=
     Host: cloud.notify.windows.com
     Content-Length: 24
    
     <body>
     ....
    
  2. WNS 通过响应指示已收到通知,并将在下一个可用机会中传递通知。 但是,WNS 不提供端到端的确认来表明通知已被设备或应用程序接收。

下图演示了相关数据流:

wns diagram for sending a notification

重要事项

  • WNS 不保证通知的可靠性或延迟。
  • 通知不应包括机密、敏感或个人数据。
  • 若要发送通知,云服务必须先使用 WNS 进行身份验证并接收访问令牌。
  • 访问令牌仅允许云服务向创建令牌的单个应用发送通知。 一个访问令牌不能用于跨多个应用发送通知。 因此,如果云服务支持多个应用,则向每个通道 URI 推送通知时,它必须为应用提供正确的访问令牌。
  • 当设备脱机时,WNS 默认将为每个通道 URI 存储每种通知类型(磁贴、锁屏提醒、toast)中的一种,而不存储原始通知。
  • 如果为用户设置了个性化的通知内容,WNS 建议云服务在收到这些更新时立即发送它们。 此类示例包括社交媒体源更新、即时通信邀请、新消息通知或警报。 或者,你也可能遇到将同一通用更新频繁传送给大量用户的情况;例如天气、股票和新闻更新。 WNS 准则明确指出这些更新的频率最多应为 30 分钟一次。 最终用户或 WNS 可能认为更频繁的例行更新是滥用行为。
  • Windows 通知平台与 WNS 保持定期数据连接,以保持套接字处于活动状态和健康状态。 如果没有应用程序请求或使用通知通道,则不会创建套接字。

磁贴和锁屏提醒通知过期时间

默认情况下,磁贴和锁屏提醒通知在下载后三天过期。 通知过期时,系统将从磁贴或队列中移除内容,且不再向用户显示。 最好为所有磁贴和锁屏提醒通知设置过期时间(使用对应用有意义的时间),以便磁贴内容的保留时间不会超过其相关时间。 明确的过期时间对于规定了生命周期的内容来说至关重要。 如果云服务停止发送通知,或者用户长时间与网络断开连接,这也可确保移除过时的内容。

云服务可以设置 X-WNS-TTL HTTP 标头来指定通知在发送后保持有效的时间(以秒为单位),以此为每个通知设置过期时间。 有关详细信息,请参阅推送通知服务请求和响应头

例如,在股市的活跃交易日,可以将股票价格更新的到期时间设置为发送间隔的两倍(例如,如果每半小时发送一次通知,则在收到通知后一小时到期)。 另一个示例是,新闻应用可确定每日新闻磁贴更新的适当到期时间为一天。

推送通知和节电模式

节电模式通过限制设备上的后台活动来延长电池使用时间。 Windows 10 支持用户进行此设置:在电量低于指定阈值时自动启用节电模式。 当节电模式处于启用状态时,将禁止接收推送通知以节省电能。 但也有一些例外。 使用以下 Windows 10 节电模式设置(可在“设置”应用中找到),应用即使在节电模式处于启用状态时也能接收推送通知

  • 在节电模式下允许来自任何应用的推送通知:此设置使所有应用都可在节电模式处于启用状态时接收推送通知。 请注意,此设置仅适用于 Windows 10 桌面版(家庭版、专业版、企业版和教育版)。
  • 始终允许:此设置使特定应用能够在节电模式处于启用状态时在后台运行 - 包括接收推送通知。 此列表由用户手动维护。

无法检查这两个设置的状态,但你可以检查节电模式的状态。 在 Windows 10 中,使用 EnergySaverStatus 属性检查节电模式状态。 应用还可以使用 EnergySaverStatusChanged 事件侦听节电模式的更改

如果应用严重依赖于推送通知,我们建议通知用户,他们可能不会在节电模式处于启用状态时收到通知,还建议为用户简化“节电模式设置”。 使用 Windows 10 中的节电模式设置 URI 方案 ms-settings:batterysaver-settings,可以提供指向“设置”应用的便捷链接。

提示

通知用户有关节电模式设置的事项时,我们建议提供在未来阻止消息的方法。 例如,以下示例中的 dontAskMeAgainBox 复选框保留用户在 LocalSettings 中的首选项

下面是如何检查 Windows 10 中的节电模式是否已打开的示例。 此示例通知用户并启动“设置”应用的“节电模式设置”dontAskAgainSetting 使用户可以禁止显示消息(如果不想再次收到通知)。

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using Windows.System;
using Windows.System.Power;
...
...
async public void CheckForEnergySaving()
{
   //Get reminder preference from LocalSettings
   bool dontAskAgain;
   var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
   object dontAskSetting = localSettings.Values["dontAskAgainSetting"];
   if (dontAskSetting == null)
   {  // Setting does not exist
      dontAskAgain = false;
   }
   else
   {  // Retrieve setting value
      dontAskAgain = Convert.ToBoolean(dontAskSetting);
   }
   
   // Check if battery saver is on and that it's okay to raise dialog
   if ((PowerManager.EnergySaverStatus == EnergySaverStatus.On)
         && (dontAskAgain == false))
   {
      // Check dialog results
      ContentDialogResult dialogResult = await saveEnergyDialog.ShowAsync();
      if (dialogResult == ContentDialogResult.Primary)
      {
         // Launch battery saver settings (settings are available only when a battery is present)
         await Launcher.LaunchUriAsync(new Uri("ms-settings:batterysaver-settings"));
      }

      // Save reminder preference
      if (dontAskAgainBox.IsChecked == true)
      {  // Don't raise dialog again
         localSettings.Values["dontAskAgainSetting"] = "true";
      }
   }
}
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.System.Power.h>
#include <winrt/Windows.UI.Xaml.h>
#include <winrt/Windows.UI.Xaml.Controls.h>
#include <winrt/Windows.UI.Xaml.Navigation.h>
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::System;
using namespace winrt::Windows::System::Power;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Navigation;
...
winrt::fire_and_forget CheckForEnergySaving()
{
    // Get reminder preference from LocalSettings.
    bool dontAskAgain{ false };
    auto localSettings = ApplicationData::Current().LocalSettings();
    IInspectable dontAskSetting = localSettings.Values().Lookup(L"dontAskAgainSetting");
    if (!dontAskSetting)
    {
        // Setting doesn't exist.
        dontAskAgain = false;
    }
    else
    {
        // Retrieve setting value
        dontAskAgain = winrt::unbox_value<bool>(dontAskSetting);
    }

    // Check whether battery saver is on, and whether it's okay to raise dialog.
    if ((PowerManager::EnergySaverStatus() == EnergySaverStatus::On) && (!dontAskAgain))
    {
        // Check dialog results.
        ContentDialogResult dialogResult = co_await saveEnergyDialog().ShowAsync();
        if (dialogResult == ContentDialogResult::Primary)
        {
            // Launch battery saver settings
            // (settings are available only when a battery is present).
            co_await Launcher::LaunchUriAsync(Uri(L"ms-settings:batterysaver-settings"));
        }

        // Save reminder preference.
        if (dontAskAgainBox().IsChecked())
        {
            // Don't raise the dialog again.
            localSettings.Values().Insert(L"dontAskAgainSetting", winrt::box_value(true));
        }
    }
}

下面是本例中使用的 ContentDialog 的 XAML

<ContentDialog x:Name="saveEnergyDialog"
               PrimaryButtonText="Open battery saver settings"
               SecondaryButtonText="Ignore"
               Title="Battery saver is on."> 
   <StackPanel>
      <TextBlock TextWrapping="WrapWholeWords">
         <LineBreak/><Run>Battery saver is on and you may 
          not receive push notifications.</Run><LineBreak/>
         <LineBreak/><Run>You can choose to allow this app to work normally
         while in battery saver, including receiving push notifications.</Run>
         <LineBreak/>
      </TextBlock>
      <CheckBox x:Name="dontAskAgainBox" Content="OK, got it."/>
   </StackPanel>
</ContentDialog>