活力无限(第 3 部分): 推送通知与 Windows Azure 移动服务

在本博文系列的第 1 部分中,我们探讨了“活力”对于用户的意义,以及应用将如何参与创造活力无限的用户体验的过程。 在第 2 部分中,我们介绍了如何编写和调试 Web 服务来支持动态磁贴的定期更新。 下面是第 3 部分,我们将让您理解如何通过 Windows 推送通知服务 (WNS) 为特定客户端设备按需提供磁贴更新、Toast 和原始通知,以及 Windows Azure 移动服务将如何简化整个过程,进而结束这一博文系列。

推送通知

正如我们在第 2 部分中所看到的,定期更新从客户端启动,并提供了一种轮询或“请求”方法来更新磁贴或锁屏提醒。 “推送”通知将在服务向设备直接发送更新时发生,该更新可能是特定于用户、应用甚至辅助磁贴的更新。

与轮询不同,推送通知可随时发生,而且频率更高,但请注意,如果设备采用电池供电、处于连接待机模式或者通知流量过高时,Windows 将限制设备上的推送通知流量。 这意味着我们无法保证所有通知都将被传送(特别是当设备关闭时)。

因此,请不要认为您可使用推送通知来实施一个时钟磁贴或频率或时间分辨率与时钟相似的其他磁贴小工具。 相反,您可以想一下如何使用推送通知来将磁贴和通知连接到可向用户传达颇有趣味且意义丰富的信息的后端服务,这些服务可吸引用户重新使用您的应用。

在我们深入介绍详细内容之前,您应理解存在两类截然不同的推送通知:

  1. 包含磁贴或锁屏提醒更新或 Toast 通知的负载的 XML 更新: Windows 能够处理此类推送通知,并以某一应用的名义发布更新或 Toast。 如有需要,应用也可直接处理这些通知。
  2. 包含服务希望发送的任何数据的二进制通知或“原始”通知: 由于 Windows 不知道如何处理这些数据,因此这些通知“必须”由应用特定的代码处理。 请参阅原始通知的指导原则和检查列表页面(英文)了解诸如大小限制 (5KB) 和编码 (base64) 等详细信息。

在这两类情形中,一个运行的(前台)应用能够通过 PushNotificationChannel 类及其 PushNotificationReceived 事件直接处理推送通知。 对于 XML 负载,应用能够在向本地发布通知(或选择忽略通知)之前进行内容修改、标签更改等操作。 对于原始通知,应用将先行处理内容,然后再确定发布哪些通知(如有)。

如果应用被挂起或未运行,那么其也可提供一个后台任务,以达到相同的目的。 应用必须请求并被授予锁屏界面访问权限,之后此类应用特定的代码就可在接收到通知时运行。

后台任务通常在通知到达时进行一项或两项操作。 首先,其可能将通知中某些相关信息保存到本地应用数据中,当应用下一次被激活或恢复时即可在此检索这些信息。 其次,后台任务可发布本地磁贴和锁屏提醒更新和/或 Toast 通知。

为了更好地理解原始通知,假设有一个典型的电子邮件应用。 当后端服务为用户检测到新消息时,其将向 WNS 发送一条原始推送通知,其中包含数个电子邮件消息标题。 该服务采用一个连接到用户特定设备上的特定应用的“通道 URI”来进行这一操作。

然后,WNS 将试图把该通知推送给客户端。 假设其成功完成,那么 Windows 将收到该通知,并查找所涉及的通道 URI 相关的应用。 如果 Windows 能够找到适当的应用,那么该通知将被忽略。 如果某一应用存在并处于运行状态,那么 Windows 将启动 PushNotificationReceived 事件,反之,Windows 将为该应用查找并调用可用的后台任务。

这两种方式中的任何一种都将让原始通知最终获得一些应用代码,这些代码将处理数据、向应用磁贴发布锁屏提醒更新,以显示新消息的数量,并最多发布五个包含消息标题的循环磁贴更新。 应用还可为每条到达的新消息,或至少表明有新邮件的消息发布 Toast 通知。

因此,Toast 将告诉用户有新邮件到达,而且“开始”屏幕上的应用磁贴将提供新邮件活动的快捷视图。

欲了解这些客户端事件处理程序和后台任务的更多信息,请查阅原始通知示例、本博客系列中的在后台中高效工作 - 后台任务一文,以及后台网络连接白皮书。 就我们这篇博文的目的而言,让我们转而讨论这一内容的服务端。

使用 Windows 推送通知服务 (WNS)

通过 Windows、应用、服务和 WNS 的通力协作,向某一特定用户的特定设备上的特定应用磁贴(或 Toast 或原始通知头)提供用户特定的数据将变为可能。 下图显示了所有这些要素之间的关系。

 

流程图显示出 Windows、应用、服务和 WNS 相互协作为特定应用磁贴提供数据

 

很显然,为了让整个流程和谐进行,确立一些关系变得尤为必要:

  1. 您(开发人员)在 Windows 应用商店中注册应用以使用推送通知。 这将为服务提供一个 SID 和客户端秘钥来利用 WNS 进行身份验证(为了安全起见,这些 SID 和秘钥不应存储于客户端设备上)。
  2. 在运行时,应用将为其每个动态磁贴(主磁贴和辅助磁贴)从 Windows 请求一个 WNS 通道 URI,或为原始通知请求一个 WNS 通道 URI。 应用还必须每 30 天刷新这些通道 URI,这样您才能使用其他后台任务。
  3. 应用的服务将提供一个 URI,通过该 URI,应用可将通道 URI 与描述其用途(如天气更新的位置或特定用户的帐户和活动)的任何数据一同上载。 进行接收后,服务将存储这些通道 URI 和相关数据以供日后使用。
  4. 服务将监视其后端是否存在应用到每个特定的用户/设备/应用/磁贴组合的变更。 当服务检测到为某一特定通道触发通知的条件时,服务将构建该通知(XML 通知或原始通知)的内容、使用 SID 和客户端秘钥来利用 WNS 对服务进行身份验证,然后将通知与通道 URI 一同发送到 WNS。

让我们详细了解一下每个步骤。 (正如预览中的情形,如果您对处理 HTTP 请求感到紧张,那么正如您将要了解到的,Windows Azure 移动服务则无需您处理很多细节。)

利用 Windows 应用商店注册应用

要为您的服务获取 SID 和客户端秘钥,请参阅 Windows 开发中心中如何利用 Windows 推送通知服务 (WNS) 进行验证身份页面(英文)。 SID 将通过 WNS 识别您的应用,而您的服务将利用客户端秘钥来告知 WNS 可向您的应用发送通知。 再次提醒,SID 和客户端秘钥只应存储于服务中。

请注意,只有在您的服务将发送一个推送通知时,您才需要进行利用 Windows 推送通知服务 (WNS) 验证身份的中第 4 步“将云服务器的凭据发送到 WNS”。 由于这一阶段中您的服务缺乏发送通知所需的关键要素(即通道 URI),因此我们稍后再返回探讨这一内容。

获取并刷新通道 URI

客户端应用将通过 Windows.Networking.PushNotifications.PushNotificationChannelManager 对象在运行时获得通道 URI。 该对象仅拥有两个方法:

  • createPushNotificationChannelForApplicationAsync: 为应用的主磁贴以及 Toast 和原始通知创建一个通道 URI。
  • createPushNotificationChannelForSecondaryTileAsync: 为 tileId 语法所识别的某一特定辅助磁贴创建一个通道 URI。

两类异步操作的结果都是生成 PushNotificationChannel 对象。 该对象的 Uri 属性中包含通道 URI,以及一个 ExpirationTime 来显示出刷新该通道的最后期限。 如有需要,Close 方法将专门终止通道,而最重要的是 PushNotificationReceived 事件,该事件将在应用处于前台,且通过通道接收推送通知时再次启动。

一个通道 URI 的生存期为 30 天,30 天后,WNS 将拒绝为该通道所提出的任何请求。 因此,应用代码每 30 天至少需要采用上述 create 方法来刷新一次这些 URI,并将这些 URI 发送到其服务中。 下面介绍一种很好的策略:

  • 首次启动时,请求一个通道 URI,并将 Uri 属性中的字符串保存到您的本地应用数据中。 由于通道 URI 是设备特定的,因此请勿将其存储于您的漫游应用数据中。
  • 在后续启动中,再次请求一个通道 URI,并将其与此前保存的 URI 进行比较。 如果二者不同,则将其保存到服务中,或如有必要,发送该 URI,并让服务更换旧 URI。
  • 此外,由于应用被挂起的时间可能将超过 30 天,因此请在您应用的“恢复”处理程序中执行此前的步骤(请参阅文档中启动、恢复和多任务处理页面(英文))。
  • 如果您担心应用在 30 天内不会运行,则请实施一个包含维护触发器的后台任务,以每几天或每周运行一次。 欲了解详情,请再次参阅在后台高效工作 - 后台任务一文;此情形中的后台任务所执行的代码与应用用于请求通道并将其发送到您的服务的代码相同。

将通道 URI 发送到服务

通常情况下,推送通知通道将与诸如电子邮件状态、即时消息和其他个性化信息等用户特定的更新相互配合。 您的服务不大可能需要向每名用户和/或每个磁贴发送相同的通知。 因此,服务需要将每个通道 URI 与更为具体的信息相关联。 对于电子邮件应用而言,由于用户的 ID 将制定检查的帐户,因此 ID 至关重要。 而天气应用则相反,其有可能将通道 URI 与特定的纬度和经度相关联,这一磁贴(主磁贴和辅助磁贴)将反映出不同的位置。

然后,应用必须在向其服务发送通道 URI 时包含这些详细信息,而且服务必将存储这些信息供日后使用。

当需要关注用户身份时,应用最好采用服务单独验证用户的身份,使用服务特定的凭据或通过诸如 Facebook、Twitter、Google 或用户的 Microsoft 帐户等 OAuth 提供程序(正如我们将在稍后看到的,使用 OAuth 将对 Windows Azure 移动服务大有裨益)。 如果由于某些原因而无法实现这一点,那么请务必加密您发送到服务的用户 ID,或确保通过 HTTPS 发送。

无论在哪一情况下,您都将最终确定向您的服务发送所有这些信息的方式(位于标题,通过消息主题中的数据,或作为服务 URI 上的参数)。 这部分通信将严格限定于应用及其服务之间。

举一个简单的例子,让我们假设拥有一项服务,(如同我们将在下一部分中看到的)其包含一个名为 receiveuri.aspx 的页面,该名称的完整地址是一个名为 url 的变量。 以下代码将为应用从 Windows 中请求一个主通道 URI,并将其通过 HTTP 发布到页面中。 (该代码从推送和定期通知客户端示例中派生,并加以了简化,其中在其他地方定义的 itemId 变量被用于识别辅助磁贴;该示例还拥有此处并未显示的 C++ 变量):

JavaScript

 

 Windows.Networking.PushNotifications.PushNotificationChannelManager
    .createPushNotificationChannelForApplicationAsync()
    .done(function (channel) {
        //Typically save the channel URI to appdata here.
        WinJS.xhr({ type: "POST", url:url,
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            data: "channelUri=" + encodeURIComponent(channel.uri) 
                + "&itemId=" + encodeURIComponent(itemId)
        }).done(function (request) {
            //Typically update the channel URI in app data here.
        }, function (e) {
            //Error handler
        });
    });

           

C#:

 

 using Windows.Networking.PushNotifications;

PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(url);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
byte[] channelUriInBytes = Encoding.UTF8.GetBytes("ChannelUri=" + WebUtility.UrlEncode(newChannel.Uri) + "&ItemId=" + WebUtility.UrlEncode(itemId));

Task<Stream> requestTask = webRequest.GetRequestStreamAsync();
using (Stream requestStream = requestTask.Result)
{
    requestStream.Write(channelUriInBytes, 0, channelUriInBytes.Length);
}

以下 ASP.NET 代码是一个处理此 HTTP POST 的 receiveuri.aspx 页面的基本实现,请确保其已收到有效的通道 URI、用户和一些对于该项目的标识符。

我之所以强调此代码的“基本”属性,是因为您可看到 SaveChannel 功能仅将请求的内容写入了固定的文本文件中,这显然不会扩展超过一个单一用户! 当然,一项真正的服务将针对这一目的采用一个数据库,但此处的结构将与之相似。

 <%@ Page Language="C#" AutoEventWireup="true" %>

<script runat="server">

protected void Page_Load(object sender, EventArgs e)
{
    //Output page header
    Response.Write("<!DOCTYPE html>\n<head>\n<title>Register Channel URI</title>\n</head>\n<html>\n<body>");
    
    //If called with HTTP GET (as from a browser), just show a message.
    if (Request.HttpMethod == "GET")
    {
        Response.StatusCode = 400;
        Response.Write("<p>This page is set up to receive channel URIs from a push notification client app.</p>");
        Response.Write("</body></html>");
        return;
    }

    if (Request.HttpMethod != "POST") {
        Response.StatusCode = 400;
        Response.Status = "400 This page only accepts POSTs.";
        Response.Write("<p>This page only accepts POSTs.</p>");
        Response.Write("</body></html>");        
        return;
    }
    
    //Otherwise assume a POST and check for parameters    
    try
    {
        //channelUri and itemId are the values posted from the Push and Periodic Notifications Sample in the Windows 8 SDK
        if (Request.Params["channelUri"] != null && Request.Params["itemId"] != null)
        {
            // Obtain the values, along with the user string
            string uri = Request.Params["channelUri"];
            string itemId = Request.Params["itemId"];
            string user = Request.Params["LOGON_USER"];
                 
            //TODO: validate the parameters and return 400 if not.
            
            //Output in response
            Response.Write("<p>Saved channel data:</p><p>channelUri = " + uri + "<br/>" + "itemId = " + itemId + "user = " + user + "</p>");

            //The service should save the URI and itemId here, along with any other unique data from the app such as the user;
            SaveChannel(uri, itemId, user);

            Response.Write("</body></html>");
        }
    }
    catch (Exception ex)
    {
        Trace.Write(ex.Message);
        Response.StatusCode = 500;
        Response.StatusDescription = ex.Message; 
        Response.End();
    }
}
</script>

protected void SaveChannel(String uri, String itemId, String user)
{
    //Typically this would be saved to a database of some kind; to keep this demonstration very simple, we'll just use
    //the complete hack of writing the data to a file, paying no heed to overwriting previous data.
    
    //If running in the debugger on localhost, this will save to the project folder
    string saveLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt";
    string data = uri + "~" + itemId + "~" + user;

    System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
    byte[] dataBytes = encoding.GetBytes(data);

    using (System.IO.FileStream fs = new System.IO.FileStream(saveLocation, System.IO.FileMode.Create))
    {
        fs.Write(dataBytes, 0, data.Length);
    }

    return;
}

您可在我的免费电子书籍使用 HTML、CSS 和 JavaScript 编程 Windows 8 应用的第 13 章中找到这一服务的代码,特别是在配套内容的 HelloTiles 示例中。 其目的在于与此前提到的客户端 SDK 示例配合使用。 如果您在启用了本地主机的调试程序(Visual Studio 2012 旗舰版或 Visual Studio Express 2012 for Web)中运行 HelloTiles,那么您将拥有诸如 https://localhost:52568/HelloTiles/receiveuri.aspx 等 URL,您可将该 URL 粘贴到客户端 SDK 示例中。 当该示例随后向该 URL 提出请求时,您将停止服务中的任何断点,并可穿行于代码。

发送通知

在真正的服务中,您将拥有某些持续进程类型,这些进程将监视器数据源,并适时向特定用户发送推送通知。 这一过程的发生方式可能包括以下几种:

  • 计划的作业可能每 15 分钟到 30 分钟检查一次特定位置的检查天气警报,这一频率取决于额这些警报发布的频率,以及发布响应推送通知的频率。
  • 例如消息传递后端等其他服务可能将在有新消息时向您的服务器上的页面提出请求。 该页面然后将发布适当的通知。
  • 用户可能正与您的服务器上的网页交互,而他们的活动将向特定通道触发推送通知。

简言之,推送通知拥有众多触发器,具体数量完全取决于您后端服务或网站的属性。 就本文的目的而言,使用 HTML、CSS 和 JavaScript 编程 Windows 8 应用第 13 章中出现的相同 HelloTiles 示例服务就有一个名为 sendBadgeToWNS.aspx 的页面,该页面将在您使用浏览器访问页面时使用此前在文本文件中保存的任何通道 URI 发送推送通知。 一项真正的服务将执行一些数据库查找操作以获取通道 URI,而不是读取文件内容,但是再次说明,二者的整体结构将非常相似。

ASP.NET 页面:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="sendBadgeToWNS.aspx.cs" Inherits="sendBadgeToWNS" %>

<!DOCTYPE html>

<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">    
    <title>Send WNS Update</title>
</head>
<body>
    <p>Sending badge update to WNS</p>    
</body>
</html>

 

C# 代码隐藏:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;

public partial class sendBadgeToWNS : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //Load our data that was previously saved. A real service would do a database lookup here
        //with user- or tile-specific criteria.
        string loadLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt
        byte[] dataBytes;
        
        using (System.IO.FileStream fs = new System.IO.FileStream(loadLocation, System.IO.FileMode.Open))
        {
            dataBytes = new byte[fs.Length];
            fs.Read(dataBytes, 0, dataBytes.Length);
        }

        if (dataBytes.Length == 0)
        {
            return;
        }
        
        System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();

        string data = encoding.GetString(dataBytes);
        string[] values = data.Split(new Char[] { '~' });
        string uri = values[0]; //Channel URI
        string secret = "9ttsZT0JgHAFveYahK1B6jQbvMOIWYbm";
        string sid = "ms-app://s-1-15-2-2676450768-845737348-110814325-22306146-1119600341-293560589-2707026538";
        
        //Create some simple XML for a badge update
        string xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>";
        xml += "<badge value='alert'/>";
                    
        PostToWns(secret, sid, uri, xml, "wns/badge");
    }
}

 

我们此处所进行的所有操作是检索通道 URI,构建 XML 负载,然后利用 WNS 进行身份验证,并对该通道 URI 进行 HTTP POST 操作。 最后两个步骤将在 Windows 开发中心内快速入门: 发送推送通知主题中提到的 PostToWns 功能中进行。 由于完整列表中的大部分内容是围绕使用您从 Windows 应用商店获得的客户端秘钥和 SID 通过 OAuth(位于 https://login.live.com/accesstoken.srf)利用 WNS 进行身份验证,因此我在此处省略了完整列表。 这一身份验证的结果是生成一个随后将包含于我们发送到通道 URI 的 HTTP POST 中的访问标记:

C#:

 public string PostToWns(string secret, string sid, string uri, string xml, string type = "wns/badge")
{
    try
    {
        // You should cache this access token
        var accessToken = GetAccessToken(secret, sid);

        byte[] contentInBytes = Encoding.UTF8.GetBytes(xml);

        // uri is the channel URI
        HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
        request.Method = "POST";
        request.Headers.Add("X-WNS-Type", type);
        request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken.AccessToken));

        using (Stream requestStream = request.GetRequestStream())
            requestStream.Write(contentInBytes, 0, contentInBytes.Length);

        using (HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse())
            return webResponse.StatusCode.ToString();
    }
    catch (WebException webException)
    {
        // Implements a maximum retry policy (omitted)
    }
}

在本示例中,请注意 HTTP 请求中的 X-WNS-Type 标头将设为 wns/badge,而且 Content-Type 标头将被默认设为 text/xml。 对于磁贴而言,该类型应为 wns/tile;而 Toast 将使用 wns/toast。 对于原始通知,请使用 wns/raw,并将 Content-Type 设置为 application/octet-stream。 欲了解有关标头的详细内容,请参阅文档中推送通知服务请求和响应标头页面(英文)。

推送通知失败

当然,发送一个像这样的 HTTP 请求可能无法确保始终有效,WNS 将对 200 代码(成功)以外的其他内容发出响应的原因很多。 欲了解具体细节,请参阅推送通知服务请求和响应标头页面(英文)中的“响应代码”部分;此处列出了一些更常见的错误和原因:

  • 通道 URI 无效(404 未找到)或过期(410 过期)。 在这些情况中,服务应将通道 URI 从其数据库中删除,并不再对该 URI 提出任何请求。
  • 客户端秘钥和 SID 可能无效(401 未授权)或应用自身清单中的程序包 ID 与 Windows 应用商店中的 ID 不匹配(403 已禁止)。 确保这些 ID 匹配的最佳方式是使用 Visual Studio 中“应用商店” >“将应用与应用商店关联”的菜单命令(您可在 Visual Studio 2012 旗舰版的“项目”菜单中找到这一命令)。
  • 原始通知负载超过 5KB(413 请求实体过大)。
  • 客户端可能处于脱机状态,此时 WNS 将自动再次尝试,并最终报告错误。 对于 XML 通知,默认行为是缓存推送通知,并在客户端恢复联机状态时传送。 对于原始通知,系统将在默认情况下禁用缓存,您可通过将针对通道 URI 的请求中的 X-WNS-Cache-Policy 标头设置为 cache而更改这一默认设置。

对于其他错误(400 请求无效),请确保 XML 负载包含 UTF-8 编码文本,而且原始通知位于 base64,并将 Content-Type 标头设为 application/octet-stream。 WNS 也有可能因为您试图在一个特定时期内发送过多推送通知而限制传送。

如果应用未显示于锁屏界面中,而且设备处于连接待机状态,那么系统也有可能拒绝原始推送通知。 由于在这一情形中(以及应用未处于前台时),Windows 将阻止向非锁屏界面应用发送的原始通知,因此 WNS 对此进行了优化,其可删除其知道不会被传送的通知。

Windows Azure 移动服务

既然我们已经了解了使用推送通知的复杂细节,甚至是省略了存储的问题,那么您可能在想“有没有什么办法能让这一切变得更加简单?”试想一下为了有望十分庞大,并不断扩展的客户库而管理有可能成千上万个通道 URI 需要花费多少精力!

幸运的是,您并不是提出此类问题的第一人。 除了诸如 Urban Airship 等公司所提供的第三方解决方案以外,Windows Azure 移动服务也可大幅简化您的生活。

Windows Azure 移动服务(我将其简称 AMS)为我们一直在探讨的服务细节提供了一个预构建的解决方案(基本为一些 REST 端点)。 “移动服务”基本上是在以您的名义管理数据库,并提供库功能,进而让您向 WNS 轻松发送负载。 您可在使用 Windows Azure 移动服务将云添加到您的应用页面中找到 AMS 简介。

特别是对于推送通知而言,请首先在面向 Windows 8 的 Windows Azure 移动服务 SDK 中获得客户端库。 然后参阅移动服务中的推送通知入门页面(英文)(请注意,其中还包含同一主题 JavaScript 版本的内容),该页面(英文)描述了 AMS 将如何帮助您满足我们此前所提到的所有连接要求:

  • 利用 Windows 应用商店注册应用: 当您从 Windows 应用商店获得了您应用的客户端秘钥和 SID 之后,请在移动服务配置中保存这些值。 参阅此前提到的入门主题中“为 Windows 应用商店注册您的应用”的部分。
  • 获取并刷新通道 URI: 请求并管理应用中的通道 URI 纯粹是一个客户端的问题,而且这一问题与之前没有任何变化。
  • 将通道 URI 发送到服务: 利用 AMS,这一步骤的难度将大幅降低。 首先,您将(通过 Windows Azure 门户)在移动服务中创建一个数据库表格。 然后,应用可简单地将记录插入该表格,并附加上通道 URI 以及任何其他您希望附加的关键信息。 AMS 客户端库将在后台处理 HTTP 请求,甚至将服务器中的任何变更更新到客户端记录中。 此外,AMS 能够通过用户的 Microsoft 帐户来自动处理身份验证,如果您使用三个其他 OAuth 提供商(Facebook、Twitter 或 Google)之一注册了您的应用,那么您也可通过该提供商来自动处理身份验证。 请参阅移动服务中的身份验证入门页面(英文)。
  • 发送通知: 在移动服务中,您可将脚本(采用名为 Node.js 的 JavaScript 的变种语言编写)附加到数据操作中,并使用 JavaScript 创建计划的作业。 在这些脚本中,使用一个通道 URI 和一个负载来简单地调用 push.wns 对象将向该通道生成必要的 HTTP 请求。 捕获推送失败并使用 console.log 记录响应也非常简单。 您可在 Windows Azure 门户中轻松查看这些日志。

欲了解更多详细内容,请参阅以下两份示例教程: 使用 Windows Azure 移动服务的磁贴、Toast 和锁屏提醒推送通知(英文)和 Windows Azure 移动服务的原始通知(英文)。 在此处,我们并不会重复这些指令,而是介绍其中的一些要点。

当您设置了一项移动服务后,该服务将拥有一个特定的服务 URL。 您将在创建 AMS SDK 内的 MobileServiceClient 对象的实例时使用该 URL:

JavaScript

 var mobileService = new Microsoft.WindowsAzure.MobileServices.MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C#:

 using Microsoft.WindowsAzure.MobileServices;

MobileServiceClient MobileService = new MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C++ (引自其他示例):

 using namespace Microsoft::WindowsAzure::MobileServices;

auto MobileService = ref new MobileServiceClient(
    ref new Uri(L" https://{mobile-service-url}.azure-mobile.net/"), 
    ref new String(L"{mobile-service-key}"));

这一类别将所有 HTTP 通信与服务相结合,因而无需您考虑所有低级别的管道。

要使用特定的 OAuth 提供程序进行身份验证,请使用 loginLoginAsync 方法,这一方法的结果是生成向应用提供该信息的 User 对象。 (一旦完成身份验证,客户端对象的 CurrentUser 属性也将包含用户 ID。) 当您直接这样验证移动服务的身份时,服务将拥有访问用户 ID 的权限,无需客户端显示发送:

JavaScript

 mobileService.login("facebook").done(function (user) { /* ... */ });
mobileService.login("twitter").done(function (user) { /* ... */ });
mobileService.login("google").done(function (user) { /* ... */ });
mobileService.login("microsoftaccount").done(function (user) { /* ... */ });

C#:

 MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Twitter);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Google);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);

C++:

 task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Facebook))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Twitter))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Google))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::MicrosoftAccount))
    .then([this](MobileServiceUser^ user) { /* */ } );

向服务发送通道 URI 也是一个在该服务的数据库中存储记录的简单操作,客户端对象将发出 HTTP 请求。 要进行这一操作,只需按照此前提供链接的推送通知教程中的一下示例所显示的,请求数据库对象,并插入记录。 在每个代码段中,假设 ch 包含来自 WinRT API 的 PushNotificationChannel。 您也可在构建的 channel 对象中包含任何其他属性,例如一个辅助磁贴 ID 或可识别通道目的的其他数据。

JavaScript:

 var channelTable = MobileServicesSample.mobileService.getTable('Channels');

var channel = {
    uri: ch.uri, 
    expirationTime: ch.expirationTime.
};

channelTable.insert(channel).done(function (item) {

    },
    function () {
        // Error on the insertion.
    });
}

C#:

 var channel = new Channel { Uri = ch.Uri, ExpirationTime = ch.ExpirationTime };
var channelTable = privateClient.GetTable<Channel>();

if (ApplicationData.Current.LocalSettings.Values["ChannelId"] == null)
{
    // Use try/catch block here to handle exceptions
    await channelTable.InsertAsync(channel);
}

C++:

 auto channel = ref new JsonObject();
channel->Insert(L"Uri", JsonValue::CreateStringValue(ch->Uri));
channel->Insert(L"ExpirationTime", JsonValue::CreateBooleanValue(ch->ExpirationTime));

auto table = MobileService->GetTable("Channel");
task<IJsonValue^> (table->InsertAsync(channel))
    .then([this, item](IJsonValue^ V) { /* ... */ });

请注意,一旦通道记录被插入,那么服务可能对该记录进行的任何变更和添加都将在客户端中反映出来。

此外,如果您在 GetTable/getTable 调用中拼写错数据库的名称,那么您在试图插入一条记录之前不会看到任何异常。 这将是一个难于跟踪的错误,因此在您确信所有内容都应运行无误,但事实并非如此时,请查看数据库的名称。

现在,这些对客户端的插入内容已经转换成面向服务的 HTTP 请求,但是即使是在服务端中,这一过程也未向您显示。 您不需要接收和处理请求,您只需将自定义脚本附加到每个数据库操作中(插入、读取、更新和删除)。

这些脚本采用 Node.js 中可用的相同固有对象和方法被编写成 JavaScript 函数(所有这些均与应用内任何客户端 JavaScript 无关)。 每个函数将接收适当的参数: insert 和 update 将接收新纪录,delete 将接收项目的 ID,而 read 将接收一项查询。 如果应用采用移动服务对用户进行身份验证,那么所有函数还将接收一个 user 用户对象和一个可让您执行操作并生成将成为 HTTP 响应的内容的 request 对象。

最简单(和默认的) insert 脚本,只需原样执行请求并插入记录,item:

 function insert(item, user, request) {
    request.execute();
}

如果您希望在记录中附加时间戳和用户 ID,那么这与在执行请求之前将这些属性添加到 item 参数一样简单:

 function insert(item, user, request) {
    item.user = user.userId;
    item.createdAt = new Date();
    request.execute();
}

请注意,在插入数据库之前对本脚本中 item 所做的变更将自动传播回客户端。 在上述代码中,一旦插入操作成功,则客户端中的 channel 对象将包含 user 和 createdAt 属性。 一切非常方便!

服务脚本也可在 request.execute 后执行其他操作,特别是作为对成功或失败的响应,但是此处我不做详细解释,请参阅服务器脚本示例操作文档(英文)

现在,让我们返回推送通知,将通道 URI 保存到表格中只是中的一部分,服务可能将发送通知作为对特定事件的响应,也可能不发送。 服务更有可能拥有包含更多信息的其他表格,在这些表格中所执行的操作将对通道 URI 的一些子集触发通知。 我们将在下一部分探讨一些示例。 无论是在哪一情形中,您都将使用 push.wns 对象从一个脚本发送一个推送通知。 发送特定类型的更新(包括原始)也有许多方法,其中磁贴、Toast 和锁屏提醒直接通过与可用模板相匹配的名称方法运行。 例如:

 push.wns.sendTileSquarePeekImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image1.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Tile Square:", pushResponse);
    },
    error: function (err) {
        console.log("Error sending tile:", err);
    }

});

push.wns.sendToastImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image2.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Toast:", pushResponse);
    }
});

push.wns.sendBadge(channel.uri, {
    value: value,
    text1: "Notification Received"
}, {
    success: function (pushResponse) {
        console.log("Sent Badge:", pushResponse);
    }
});

此外,console.log 函数将在您可在 Azure Mobile 服务门户中看到的日志中创建一个条目,通常情况下,您希望记录错误处理程序中的调用,如上述磁贴通知所示。

您可能注意到 send* 方法分别与一个特定的模板所绑定,对于磁贴而言,其意味着宽形负载和方形负载必须作为两个通知分别发送。 请记住,由于用户将控制磁贴在其“开始”屏幕上的显示方式,因此您几乎始终希望同时发送两种尺寸。 借助 push.wns 中模板特定 send 函数,那么,这就意味着进行两次顺序调用,其中每次调用将生成一个单独的推送通知。

为了合并多个更新,其中包括一次发送包含不同标记的多个磁贴更新或发送多个 Toast,您应使用 push.wns.sendTile 和 push.wns.sendToast 方法。 例如:

 var channel = '{channel_url}';

push.wns.sendTile(channel,
    { type: 'TileSquareText04', text1: 'Hello' },
    { type: 'TileWideText09', text1: 'Hello', text2: 'How are you?' },
    { client_id: '{your Package Security Identifier}', client_secret: '{your Client Secret}' },

    function (error, result) {
        // ...
    });

在更低的级别上,push.wns.send 可让您准确获得通知内容;push.wns.sendRaw 也将在这一级别中面向原始通知可用。 欲查看全部详细内容,请再次参阅 push.wns 对象文档(英文)。

使用 Windows Azure 移动服务的实际应用场景

使用 Windows Azure 移动服务的磁贴、Toast 和锁屏提醒推送通知中的示例应用向您展示了如何发送推送通知以响应插入数据库表格中的新消息。 然而,这意味着应用最终将向自身发送推送通知,而通常情况中都无需进行这一操作(除非可能向用户的其他设备上的同一应用发送通知)。

服务更有可能发送推送通知响应发生于最终将接收这些通知的应用/磁贴以外的事件。 让我们考虑一些应用场景:

  • 使用社交网络

应用可采用用户的社交网络来实施诸如向好友发起挑战等功能。 例如,当一名用户在某一游戏中创造了一个新的最高分时,您可能希望通过动态磁贴或 Toast 向该用户同样安装了该款游戏的朋友发起挑战。 同样情形还可能发生于健身应用中,您可能在该应用中发布了进行某类活动的最佳时间。

要进行这一操作,应用可在适当的移动服务表格中插入一个新的记录(分数、最佳时间等)。 在插入的脚本内,服务将查询当前用户的合适的朋友,并向这些通道 URI 发送通知。 其他查询标准描述了游戏的确切方面、(辅助磁贴)将描述某类特别的运动等。

  • 天气更新和警报

天气应用通常可让您向应用主磁贴分配一个位置,并为其他位置创建一个辅助磁贴。 对于每个磁贴而言,重要的信息是该位置的纬度和经度,应用(通过将这些信息插入表格)而将其与每个通道 URI 一同发送到服务。 为了向该通道触发一个更新,该服务可能拥有另一可为更新和警报而定期查询中央天气服务,然后处理这些响应,并将适当的消息插入移动服务警报表格中的进程(例如下面所描述的计划的作业)。 插入脚本然后将检索适当的通道 URI 并发送更新。 另外,如果一项天气服务自身可让您注册警报或定期更新,则该服务中的另一页面将接收这些请求(最有可能为 HTTP PUT),处理这些请求,并调用移动服务来插入记录,进而触发一项更新。

  • 消息传递

处理即时消息与接收天气更新的方式一模一样。 另一进程将监视新消息的接收,例如定期检查传入消息,或注册消息源以在新消息到达时发出警报。 在这两种情形的任何一种中,新消息的到达将向适当的通道触发推送通知。 在这一情形中,通道 URI 与用户的磁贴相关联,以进行特定类型的消息传递。 由于这与本博文开始所描述的电子邮件应用场景较为相似,因此您可能将在这一情形中使用原始通知。

请注意,在所有这些应用场景中,您并没有必要在数据库中插入任何内容。 如果您不在插入脚本中调用 request.execute,那么所有操作都不会在数据库中结束,但是您仍需要在该脚本中执行诸如发送通知等其他任务。 换句话说,您不需要将您此后不会使用的记录填充到数据库中,特别是当考虑到存储数据将引发成本时,您更应避免这一操作。

另请注意 Azure 移动服务中拥有一个用于计划作业的设施,您可在在移动服务中计划重复执行作业页面(英文)阅读有关内容。 该类作业可经常从其他服务检索数据、处理数据,并将记录插入该服务的数据库中,并再次触发推送通知。 同样的,正如我们在本博文系列的第 2 部分中指出的,其他网页和移动应用也可更改该数据库并触发推送通知。 移动服务中的数据库脚本将在所有这些情形中运行。

结语

总结一下这一博文系列,我们探讨了“活力无限”对于用户、开发人员、应用和服务的全部含义。 我们了解了磁贴、锁屏提醒和 Toast 更新,如何设置定期通知,如何将后台任务作为这一过程中的一部分来使用,以及处理定期和推送通知的服务结构。 很显然,Windows Azure 移动服务让我们从更高的级别了解了整个推送通知的运行过程。 相比从零开始编写全新服务,Windows Azure 无疑有助于您提高工作效率,并有望免除您很多烦恼。

Kraig Brockschmidt

Windows 生态系统团队项目经理

使用 HTML、CSS 和 JavaScript 编程 Windows 8 应用作者