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

Windows 推送通知服务(WNS)使第三方开发人员能够从自己的云服务发送通知、磁贴、徽章和原始更新。 这提供了一种机制,以高效且可靠的方式向用户提供新的更新。

工作原理

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

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

推送通知 的数据流关系图

注册应用并接收云服务的认证信息

在使用 WNS 发送通知之前,您的应用必须在应用商店仪表板上注册,如所述,此处

请求一个消息通知通道

当能够接收推送通知的应用运行时,它必须首先通过 CreatePushNotificationChannelForApplicationAsync 请求通知通道。 有关完整讨论和示例代码,请参阅 如何请求、创建和保存通知通道。 此 API 返回的通道 URI 独特地与调用应用程序及其磁贴关联,并可通过该 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 应用商店仪表板时,将执行此过程的第一步。 在注册过程中,应用将获得包安全标识符(SID)和密钥。 云服务使用此信息通过 WNS 进行身份验证。

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

在高级别上,信息链如下所示:

  1. 云服务遵循 OAuth 2.0 协议,将其凭据通过 HTTPS 发送到 WNS。 这会使用 WNS 对服务进行身份验证。
  2. 如果身份验证成功,WNS 将返回访问令牌。 此访问令牌在所有后续通知请求中使用,直到它过期。

云服务身份验证的 wns 关系图

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

Note

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

 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)使用术语“client”来引用云服务。
  • 完成 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图示

重要事项

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

磁贴和徽章通知过期

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

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

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

推送通知和节电模式

省电模式通过限制设备上的后台活动来延长电池寿命。 Windows 10 允许用户在电池低于指定阈值时自动启用节电器。 当节电模式处于打开状态时,将禁用推送通知的接收以节省能源。 但对此有几个例外。 以下 Windows 10 节电模式设置(在 Windows 设置中找到)允许你的应用接收推送通知,即使节电器处于打开状态。

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

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

如果你的应用严重依赖于推送通知,我们建议通知用户,他们可能不会在节电时收到通知,并使其易于调整 节电设置。 在 Windows 中使用节电模式设置 URI 方案, ms-settings:batterysaver-settings可以提供指向 Windows 设置的便捷链接。

Tip

在通知用户有关节电模式设置时,我们建议提供一种方法来禁止将来显示消息。 例如, 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>