快速入门:发送推送通知 (XAML)

云服务器可以通过 Windows 推送通知服务 (WNS) 向应用发送推送通知。 此过程适用于磁贴、toast、锁屏提醒和原始推送通知。

目标:创建和发送磁贴、toast、锁屏提醒或原始推送通知。

先决条件

若要了解本主题或使用所提供的代码,需要:

Instructions

1. 包含必需的命名空间引用

本主题中提供的示例可以按原样使用,但要求代码中包含以下命名空间引用:

using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Web;
using System.Text;

2. 创建 HTTP POST 请求

uri 参数是由应用请求并传递到云服务器的通道统一资源标识符 (URI)。 有关详细信息,请参阅如何请求、创建和保存通知通道

HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
request.Method = "POST";

3. 添加必需的标头

所有推送通知中必须包含四个必需的标头:X-WNS-Type、Content-Type、Content-Length 和 Authorization

  • X-WNS-Type 标头指定是磁贴、toast、锁屏提醒还是原始通知。
  • Content-Type 根据 X-WNS-Type 的值进行设置。
  • Content-Length 提供所含通知有效负载的大小。
  • Authorization 标头指定身份验证凭据,该凭据允许你通过通道向用户发送推送通知。

Authorization 标头的 accessToken 参数指定云服务器请求身份验证时从 WNS 接收的访问令牌,该访问令牌存储在服务器上。 如果没有访问令牌,通知将被拒绝。

有关可能的标头的完整列表,请参阅推送通知服务请求和响应标头

request.Headers.Add("X-WNS-Type", notificationType);
request.ContentType = contentType;
request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken.AccessToken));

4. 添加准备的内容

就 HTTP 请求而言,通知的 XML 内容为请求正文中的数据 Blob。 例如,系统不会验证此 XML 是否符合 X-WNS-Type 规范。 将内容指定为 XML 有效负载,并在此处作为字节流添加到请求中。

byte[] contentInBytes = Encoding.UTF8.GetBytes(xml);
                        
using (Stream requestStream = request.GetRequestStream())
    requestStream.Write(contentInBytes, 0, contentInBytes.Length);

5. 侦听 WNS 响应,确认收到通知

注意

你不会收到通知送达的确认信息,这只是确认 WNS 收到通知。

using (HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse())
    return webResponse.StatusCode.ToString();

6. 处理 WNS 响应代码

应用服务发送通知时可以接收许多响应代码。 其中一些响应代码相比于其他代码更常见,可以在 catch 块中轻松处理。

catch (WebException webException)
{
    HttpStatusCode status = ((HttpWebResponse)webException.Response).StatusCode;

HttpStatusCode.Unauthorized:你提供的访问令牌已过期。 获取新的令牌,然后尝试再次发送通知。 由于缓存的访问令牌在 24 小时后过期,因此预计每天会从 WNS 收到至少一次此响应。 建议实现最大重试策略。

    if (status == HttpStatusCode.Unauthorized)
    {
        GetAccessToken(secret, sid);
        return PostToWns(uri, xml, secret, sid, notificationType, contentType);
    }

HttpStatusCode.Gone/HttpStatusCode.NotFound:通道 URI 不再有效。 从数据库中删除此通道,以防止继续尝试向它发送通知。 用户下次启动应用时,请求新的 WNS 通道。 应用应检测到其通道已更改,这应会触发应用向应用服务器发送新的通道 URI。 有关详细信息,请参阅如何请求、创建和保存通知通道

    else if (status == HttpStatusCode.Gone || status == HttpStatusCode.NotFound)
    {
        return "";
    }

HttpStatusCode.NotAcceptable:通道受到 WNS 限制。 实现重试策略,以指数方式减少发送的通知数,防止再次受到限制。 此外,重新考虑导致通知受到限制的方案。 通过将发送的通知限制为真正增加价值的通知,你将提供更丰富的用户体验。

    else if (status == HttpStatusCode.NotAcceptable)
    {
        return "";
    }

其他响应代码:WNS 使用较少见的响应代码进行响应。 记录此代码以协助调试。 有关 WNS 响应代码的完整列表,请参阅推送通知服务请求和响应标头

    else
    {
        string[] debugOutput = {
                                   status.ToString(),
                                   webException.Response.Headers["X-WNS-Debug-Trace"],
                                   webException.Response.Headers["X-WNS-Error-Description"],
                                   webException.Response.Headers["X-WNS-Msg-ID"],
                                   webException.Response.Headers["X-WNS-Status"]
                               };
        return string.Join(" | ", debugOutput);            
    }

7. 将代码封装到单个函数中

以下示例将上述步骤中提供的代码打包到单个函数中。 此函数将撰写一个 HTTP POST 请求,该请求包含要发送到 WNS 的通知。 通过更改 type 参数的值并调整其他标头,此代码可用于 toast、磁贴、锁屏提醒或原始推送通知。 可以将此函数用作云服务器代码的一部分。

请注意,此函数也处理访问令牌过期的错误。 在这种情况下,它会调用另一个云服务器函数,该函数使用 WNS 重新进行身份验证以获取新的访问令牌。 然后,它会对原始函数进行新的调用。

// Post to WNS
public string PostToWns(string secret, string sid, string uri, string xml, string notificationType, string contentType)
{
    try
    {
        // You should cache this access token.
        var accessToken = GetAccessToken(secret, sid);

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

        HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
        request.Method = "POST";
        request.Headers.Add("X-WNS-Type", notificationType);
        request.ContentType = contentType;
        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)
    {
        HttpStatusCode status = ((HttpWebResponse)webException.Response).StatusCode;

        if (status == HttpStatusCode.Unauthorized)
        {
            // The access token you presented has expired. Get a new one and then try sending
            // your notification again.
              
            // Because your cached access token expires after 24 hours, you can expect to get 
            // this response from WNS at least once a day.

            GetAccessToken(secret, sid);

            // We recommend that you implement a maximum retry policy.
            return PostToWns(uri, xml, secret, sid, notificationType, contentType);
        }
        else if (status == HttpStatusCode.Gone || status == HttpStatusCode.NotFound)
        {
            // The channel URI is no longer valid.

            // Remove this channel from your database to prevent further attempts
            // to send notifications to it.

            // The next time that this user launches your app, request a new WNS channel.
            // Your app should detect that its channel has changed, which should trigger
            // the app to send the new channel URI to your app server.

            return "";
        }
        else if (status == HttpStatusCode.NotAcceptable)
        {
            // This channel is being throttled by WNS.

            // Implement a retry strategy that exponentially reduces the amount of
            // notifications being sent in order to prevent being throttled again.

            // Also, consider the scenarios that are causing your notifications to be throttled. 
            // You will provide a richer user experience by limiting the notifications you send 
            // to those that add true value.

            return "";
        }
        else
        {
            // WNS responded with a less common error. Log this error to assist in debugging.

            // You can see a full list of WNS response codes here:
            // https://msdn.microsoft.com/library/windows/apps/hh868245.aspx#wnsresponsecodes

            string[] debugOutput = {
                                       status.ToString(),
                                       webException.Response.Headers["X-WNS-Debug-Trace"],
                                       webException.Response.Headers["X-WNS-Error-Description"],
                                       webException.Response.Headers["X-WNS-Msg-ID"],
                                       webException.Response.Headers["X-WNS-Status"]
                                   };
            return string.Join(" | ", debugOutput);            
        }
    }

    catch (Exception ex)
    {
        return "EXCEPTION: " + ex.Message;
    }
}

// Authorization
[DataContract]
public class OAuthToken
{
    [DataMember(Name = "access_token")]
    public string AccessToken { get; set; }
    [DataMember(Name = "token_type")]
    public string TokenType { get; set; }
}

private OAuthToken GetOAuthTokenFromJson(string jsonString)
{
    using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonString)))
    {
        var ser = new DataContractJsonSerializer(typeof(OAuthToken));
        var oAuthToken = (OAuthToken)ser.ReadObject(ms);
        return oAuthToken;
    }
}

protected OAuthToken GetAccessToken(string secret, string sid)
{
    var urlEncodedSecret = HttpUtility.UrlEncode(secret);
    var urlEncodedSid = HttpUtility.UrlEncode(sid);

    var body = String.Format("grant_type=client_credentials&client_id={0}&client_secret={1}&scope=notify.windows.com", 
                             urlEncodedSid, 
                             urlEncodedSecret);

    string response;
    using (var client = new WebClient())
    {
        client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
        response = client.UploadString("https://login.live.com/accesstoken.srf", body);
    }
    return GetOAuthTokenFromJson(response);
}

下面显示了 toast 推送通知的 HTTP POST 请求的示例内容。

POST https://db3.notify.windows.com/?token=AgUAAADCQmTg7OMlCg%2fK0K8rBPcBqHuy%2b1rTSNPMuIzF6BtvpRdT7DM4j%2fs%2bNNm8z5l1QKZMtyjByKW5uXqb9V7hIAeA3i8FoKR%2f49ZnGgyUkAhzix%2fuSuasL3jalk7562F4Bpw%3d HTTP/1.1
Authorization: Bearer EgAaAQMAAAAEgAAACoAAPzCGedIbQb9vRfPF2Lxy3K//QZB79mLTgK
X-WNS-RequestForStatus: true
X-WNS-Type: wns/toast
Content-Type: text/xml
Host: db3.notify.windows.com
Content-Length: 196

<toast launch="">
  <visual lang="en-US">
    <binding template="ToastImageAndText01">
      <image id="1" src="World" />
      <text id="1">Hello</text>
    </binding>
  </visual>
</toast>

下面显示了一个示例 HTTP 响应,该响应由 WNS 发送到云服务器以响应 HTTP POST 请求。

HTTP/1.1 200 OK
Content-Length: 0
X-WNS-DEVICECONNECTIONSTATUS: connected
X-WNS-STATUS: received
X-WNS-MSG-ID: 3CE38FF109E03A74
X-WNS-DEBUG-TRACE: DB3WNS4011534

总结

在本快速入门中,你撰写了要发送到 WNS 的 HTTP POST 请求。 随后 WNS 将通知传送到应用。 此时,你已注册应用、使用 WNS 对云服务器进行身份验证、创建 XML 内容来定义通知,并将该通知从服务器发送到应用。