教程:使用 Azure 通知中心向特定用户推送通知Tutorial: Push notifications to specific users using Azure Notification Hubs

本教程说明如何使用 Azure 通知中心将推送通知发送到特定设备上的特定应用程序用户。This tutorial shows you how to use Azure Notification Hubs to send push notifications to a specific app user on a specific device. ASP.NET WebAPI 后端用于对客户端进行身份验证并生成通知,如指南主题从应用后端注册中所述。An ASP.NET WebAPI backend is used to authenticate clients and to generate notifications, as shown in the guidance topic Registering from your app backend.

在本教程中,我们将执行以下步骤:In this tutorial, you take the following steps:

  • 创建 WebAPI 项目Create the WebAPI project
  • 在 WebAPI 后端对客户端进行身份验证Authenticate clients to the WebAPI backend
  • 使用 WebAPI 后端注册通知Register for notifications by using the WebAPI backend
  • 从 WebAPI 后端发送通知Send notifications from the WebAPI backend
  • 发布新的 WebAPI 后端Publish the new WebAPI backend
  • 修改 iOS 应用Modify your iOS app
  • 测试应用程序Test the application

先决条件Prerequisites

本教程假设已根据通知中心入门 (iOS) 中所述创建并配置了通知中心。This tutorial assumes that you have created and configured your notification hub as described in Getting Started with Notification Hubs (iOS). 此外,只有在学习本教程后,才可以学习安全推送 (iOS) 教程。This tutorial is also the prerequisite to the Secure Push (iOS) tutorial. 如果要使用移动应用作为后端服务,请参阅移动应用中的推送通知入门If you want to use Mobile Apps as your backend service, see the Mobile Apps Get Started with Push.

创建 WebAPI 项目Create the WebAPI project

后续部分讨论如何创建新的 ASP.NET WebAPI 后端。The following sections discuss the creation of a new ASP.NET WebAPI backend. 此过程有三个主要目的:This process has three main purposes:

  • 对客户端进行身份验证:添加消息处理程序,以便对客户端请求进行身份验证,并将用户与请求相关联。Authenticate clients: You add a message handler to authenticate client requests and associate the user with the request.
  • 使用 WebAPI 后端注册通知:添加一个控制器来处理新的注册,使客户端设备能够接收通知。Register for notifications by using the WebAPI backend: You add a controller to handle new registrations for a client device to receive notifications. 经过身份验证的用户名将作为标记自动添加到注册。The authenticated username is automatically added to the registration as a tag.
  • 将通知发送到客户端:添加一个控制器,以便用户触发安全推送,将内容推送到与标记关联的设备和客户端。Send notifications to clients: You add a controller to provide a way for users to trigger a secure push to devices and clients associated with the tag.

通过执行以下操作创建新的 ASP.NET WebAPI 后端:Create the new ASP.NET WebAPI backend by doing the following actions:

Important

如果使用 Visual Studio 2015 或更低版本,则在开始学习本教程之前,请确保已安装用于 Visual Studio 的最新版 NuGet 包管理器。If you are using Visual Studio 2015 or earlier, before starting this tutorial, ensure that you have installed the latest version of NuGet Package Manager for Visual Studio.

若要进行检查,请启动 Visual Studio。To check, start Visual Studio. 在“工具”菜单上,选择“扩展和更新”。On the Tools menu, select Extensions and Updates. 在你的 Visual Studio 版本中搜索“NuGet 包管理器”,确保你的版本为最新。Search for NuGet Package Manager in your version of Visual Studio, and make sure you have the latest version. 如果你的版本不是最新版本,请卸载它,然后重新安装 NuGet 包管理器。If your version is not the latest version, uninstall it, and then reinstall the NuGet Package Manager.

Note

请确保已安装 Visual Studio Azure SDK 以便进行网站部署。>Make sure you have installed the Visual Studio Azure SDK for website deployment.>

  1. 启动 Visual Studio 或 Visual Studio Express。Start Visual Studio or Visual Studio Express.

  2. 选择“服务器资源管理器”并登录到 Azure 帐户。Select Server Explorer, and sign in to your Azure account. 若要在帐户中创建网站资源,必须先登录。To create the web site resources on your account, you must be signed in.

  3. 在 Visual Studio 中,右键单击 Visual Studio 解决方案,指向“添加”,然后单击“新建项目”。In Visual Studio, right-click Visual Studio solution, point to Add, and click New Project.

  4. 展开“Visual C#”,选择“Web”,然后单击“ASP.NET Web 应用程序”。Expand Visual C#, select Web, and click ASP.NET Web Application.

  5. 在“名称”框中,键入 AppBackend,然后选择“确定”。In the Name box, type AppBackend, and then select OK.

    “新建项目”窗口

  6. 在“新建 ASP.NET 项目”窗口中,选择“Web API”复选框,然后选择“确定”。In the New ASP.NET Project window, select the Web API check box, and then select OK.

    “新建 ASP.NET 项目”窗口

  7. 在“配置 Azure Web 应用”窗口中选择一个订阅,然后在“应用服务计划”列表中执行以下任一操作:In the Configure Azure Web App window, select a subscription and then, in the App Service plan list, do either of the following actions:

    • 选择已创建的应用服务计划。Select an app service plan that you've already created.
    • 选择“创建新的应用服务计划”,然后新建一个应用服务计划。Select Create a new app service plan, and then create one.

    在本教程中,不需要使用数据库。You do not need a database for this tutorial. 选择应用服务计划后,选择“确定”以创建项目。After you have selected your app service plan, select OK to create the project.

“配置 Azure Web 应用”窗口

在 WebAPI 后端对客户端进行身份验证Authenticate clients to the WebAPI backend

在本部分中,将为新的后端创建名为“AuthenticationTestHandler”的新消息处理程序类。In this section, you create a new message-handler class named AuthenticationTestHandler for the new backend. 此类派生自 DelegatingHandler 并已添加为消息处理程序,使它可以处理传入后端的所有请求。This class is derived from DelegatingHandler and added as a message handler so that it can process all requests that come into the backend.

  1. 在“解决方案资源管理器”中,右键单击“AppBackend”项目,依次选择“添加”、“类”。In Solution Explorer, right-click the AppBackend project, select Add, and then select Class.

  2. 将新类命名为 AuthenticationTestHandler.cs,并选择“添加”生成该类。Name the new class AuthenticationTestHandler.cs, and then select Add to generate the class. 为简单起见,此类将通过使用基本身份验证对用户进行身份验证。This class authenticates users by using Basic Authentication for simplicity. 请注意,应用可以使用任何身份验证方案。Your app can use any authentication scheme.

  3. 在 AuthenticationTestHandler.cs 中,添加以下 using 语句:In AuthenticationTestHandler.cs, add the following using statements:

    using System.Net.Http;
    using System.Threading;
    using System.Security.Principal;
    using System.Net;
    using System.Text;
    using System.Threading.Tasks;
    
  4. 在 AuthenticationTestHandler.cs 中,将 AuthenticationTestHandler 类定义替换为以下代码:In AuthenticationTestHandler.cs, replace the AuthenticationTestHandler class definition with the following code:

    当以下三个条件都成立时,此处理程序授权请求:The handler authorizes the request when the following three conditions are true:

    • 请求包含 Authorization 标头。The request includes an Authorization header.

    • 请求使用基本身份验证。The request uses basic authentication.

    • 用户名字符串和密码字符串是相同的字符串。The user name string and the password string are the same string.

      否则,会拒绝该请求。Otherwise, the request is rejected. 此身份验证不是真正的身份验证和授权方法。This authentication is not a true authentication and authorization approach. 它只是本教程中一个简单的示例。It is only a simple example for this tutorial.

      如果请求消息已经过 AuthenticationTestHandler 的身份验证和授权,则基本身份验证用户将附加到 HttpContext 上的当前请求。If the request message is authenticated and authorized by AuthenticationTestHandler, the basic authentication user is attached to the current request on HttpContext. 稍后,另一个控制器 (RegisterController) 会使用 HttpContext 中的用户信息,将标记添加到通知注册请求。User information in HttpContext will be used by another controller (RegisterController) later to add a tag to the notification registration request.

      public class AuthenticationTestHandler : DelegatingHandler
      {
         protected override Task<HttpResponseMessage> SendAsync(
         HttpRequestMessage request, CancellationToken cancellationToken)
         {
             var authorizationHeader = request.Headers.GetValues("Authorization").First();
      
             if (authorizationHeader != null && authorizationHeader
                 .StartsWith("Basic ", StringComparison.InvariantCultureIgnoreCase))
             {
                 string authorizationUserAndPwdBase64 =
                     authorizationHeader.Substring("Basic ".Length);
                 string authorizationUserAndPwd = Encoding.Default
                     .GetString(Convert.FromBase64String(authorizationUserAndPwdBase64));
                 string user = authorizationUserAndPwd.Split(':')[0];
                 string password = authorizationUserAndPwd.Split(':')[1];
      
                 if (verifyUserAndPwd(user, password))
                 {
                     // Attach the new principal object to the current HttpContext object
                     HttpContext.Current.User =
                         new GenericPrincipal(new GenericIdentity(user), new string[0]);
                     System.Threading.Thread.CurrentPrincipal =
                         System.Web.HttpContext.Current.User;
                 }
                 else return Unauthorized();
             }
             else return Unauthorized();
      
             return base.SendAsync(request, cancellationToken);
         }
      
         private bool verifyUserAndPwd(string user, string password)
         {
             // This is not a real authentication scheme.
             return user == password;
         }
      
         private Task<HttpResponseMessage> Unauthorized()
         {
             var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
             var tsc = new TaskCompletionSource<HttpResponseMessage>();
             tsc.SetResult(response);
             return tsc.Task;
         }
      }
      

      Note

      安全说明:AuthenticationTestHandler 类不提供真正的身份验证。Security note: The AuthenticationTestHandler class does not provide true authentication. 它仅用于模拟基本身份验证并且是不安全的。It is used only to mimic basic authentication and is not secure. 必须在生产应用程序和服务中实现安全的身份验证机制。You must implement a secure authentication mechanism in your production applications and services.

  5. 若要注册消息处理程序,请在 App_Start/WebApiConfig.cs 类中 Register 方法的末尾添加以下代码:To register the message handler, add the following code at the end of the Register method in the App_Start/WebApiConfig.cs class:

    config.MessageHandlers.Add(new AuthenticationTestHandler());
    
  6. 保存所做更改。Save your changes.

使用 WebAPI 后端注册通知Register for notifications by using the WebAPI backend

在本部分中,要将新的控制器添加到 WebAPI 后端来处理请求,以使用通知中心的客户端库为用户和设备注册通知。In this section, you add a new controller to the WebAPI backend to handle requests to register a user and a device for notifications by using the client library for notification hubs. 控制器将为已由 AuthenticationTestHandler 验证并附加到 HttpContext 的用户添加用户标记。The controller adds a user tag for the user that was authenticated and attached to HttpContext by AuthenticationTestHandler. 该标记采用以下字符串格式:"username:<actual username>"The tag has the string format, "username:<actual username>".

  1. 在“解决方案资源管理器”中,右键单击“AppBackend”项目,并选择“管理 NuGet 包”。In Solution Explorer, right-click the AppBackend project and then select Manage NuGet Packages.

  2. 在左窗格中,选择“联机”,然后在“搜索”框中,键入 Microsoft.Azure.NotificationHubsIn the left pane, select Online and then, in the Search box, type Microsoft.Azure.NotificationHubs.

  3. 在结果列表中选择“Azure 通知中心”,然后选择“安装”。In the results list, select Azure Notification Hubs, and then select Install. 完成安装后,关闭“NuGet 程序包管理器”窗口。Complete the installation, and then close the NuGet Package Manager window.

    此操作会使用 Microsoft.Azure.Notification Hubs NuGet 包添加对 Azure 通知中心 SDK 的引用。This action adds a reference to the Azure Notification Hubs SDK by using the Microsoft.Azure.Notification Hubs NuGet package.

  4. 创建新的类文件,以表示与用于发送通知的通知中心的连接。Create a new class file that represents the connection with the notification hub that's used to send notifications. 在“解决方案资源管理器”中,右键单击“模型”文件夹,选择“添加”,并选择“类”。In Solution Explorer, right-click the Models folder, select Add, and then select Class. 将新类命名为 Notifications.cs,并选择“添加”生成该类。Name the new class Notifications.cs, and then select Add to generate the class.

    “添加新项”窗口

  5. 在 Notifications.cs 中,在文件顶部添加以下 using 语句:In Notifications.cs, add the following using statement at the top of the file:

    using Microsoft.Azure.NotificationHubs;
    
  6. Notifications 类定义替换为以下代码,并将两个占位符替换为通知中心的连接字符串(具有完全访问权限)和中心名称(可在 Azure 门户中找到):Replace the Notifications class definition with the following code, and replace the two placeholders with the connection string (with full access) for your notification hub and the hub name (available at Azure portal):

    public class Notifications
    {
        public static Notifications Instance = new Notifications();
    
        public NotificationHubClient Hub { get; set; }
    
        private Notifications() {
            Hub = NotificationHubClient.CreateClientFromConnectionString("<your hub's DefaultFullSharedAccessSignature>", 
                                                                            "<hub name>");
        }
    }
    
  7. 接下来将创建一个名为 RegisterController 的新控制器。Next, create a new controller named RegisterController. 在“解决方案资源管理器”中,右键单击“控制器”文件夹,选择“添加”,并选择“控制器”。In Solution Explorer, right-click the Controllers folder, select Add, and then select Controller.

  8. 选择“Web API 2 控制器 - 空”,并选择“添加”。Select Web API 2 Controller - Empty, and then select Add.

    “添加基架”窗口

  9. 在“控制器名称”框中,键入 RegisterController 以命名新类,并选择“添加”。In the Controller name box, type RegisterController to name the new class, and then select Add.

    “添加控制器”窗口

  10. 在 RegisterController.cs 中,添加以下 using 语句:In RegisterController.cs, add the following using statements:

    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Azure.NotificationHubs.Messaging;
    using AppBackend.Models;
    using System.Threading.Tasks;
    using System.Web;
    
  11. RegisterController 类定义中添加以下代码:Add the following code inside the RegisterController class definition. 在此代码中,将为已附加到 HttpContext 的用户添加用户标记。In this code, you add a user tag for the user that's attached to HttpContext. 添加的消息筛选器 AuthenticationTestHandler 将对该用户进行身份验证并将其附加到 HttpContext。The user was authenticated and attached to HttpContext by the message filter that you added, AuthenticationTestHandler. 还可以通过添加可选复选框来验证用户是否有权注册以获取请求标记。You can also add optional checks to verify that the user has rights to register for the requested tags.

    private NotificationHubClient hub;
    
    public RegisterController()
    {
        hub = Notifications.Instance.Hub;
    }
    
    public class DeviceRegistration
    {
        public string Platform { get; set; }
        public string Handle { get; set; }
        public string[] Tags { get; set; }
    }
    
    // POST api/register
    // This creates a registration id
    public async Task<string> Post(string handle = null)
    {
        string newRegistrationId = null;
    
        // make sure there are no existing registrations for this push handle (used for iOS and Android)
        if (handle != null)
        {
            var registrations = await hub.GetRegistrationsByChannelAsync(handle, 100);
    
            foreach (RegistrationDescription registration in registrations)
            {
                if (newRegistrationId == null)
                {
                    newRegistrationId = registration.RegistrationId;
                }
                else
                {
                    await hub.DeleteRegistrationAsync(registration);
                }
            }
        }
    
        if (newRegistrationId == null) 
            newRegistrationId = await hub.CreateRegistrationIdAsync();
    
        return newRegistrationId;
    }
    
    // PUT api/register/5
    // This creates or updates a registration (with provided channelURI) at the specified id
    public async Task<HttpResponseMessage> Put(string id, DeviceRegistration deviceUpdate)
    {
        RegistrationDescription registration = null;
        switch (deviceUpdate.Platform)
        {
            case "mpns":
                registration = new MpnsRegistrationDescription(deviceUpdate.Handle);
                break;
            case "wns":
                registration = new WindowsRegistrationDescription(deviceUpdate.Handle);
                break;
            case "apns":
                registration = new AppleRegistrationDescription(deviceUpdate.Handle);
                break;
            case "gcm":
                registration = new GcmRegistrationDescription(deviceUpdate.Handle);
                break;
            default:
                throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
    
        registration.RegistrationId = id;
        var username = HttpContext.Current.User.Identity.Name;
    
        // add check if user is allowed to add these tags
        registration.Tags = new HashSet<string>(deviceUpdate.Tags);
        registration.Tags.Add("username:" + username);
    
        try
        {
            await hub.CreateOrUpdateRegistrationAsync(registration);
        }
        catch (MessagingException e)
        {
            ReturnGoneIfHubResponseIsGone(e);
        }
    
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    
    // DELETE api/register/5
    public async Task<HttpResponseMessage> Delete(string id)
    {
        await hub.DeleteRegistrationAsync(id);
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    
    private static void ReturnGoneIfHubResponseIsGone(MessagingException e)
    {
        var webex = e.InnerException as WebException;
        if (webex.Status == WebExceptionStatus.ProtocolError)
        {
            var response = (HttpWebResponse)webex.Response;
            if (response.StatusCode == HttpStatusCode.Gone)
                throw new HttpRequestException(HttpStatusCode.Gone.ToString());
        }
    }
    
  12. 保存所做更改。Save your changes.

从 WebAPI 后端发送通知Send notifications from the WebAPI backend

在本部分中,添加一个新的控制器,使客户端设备可以发送通知。In this section, you add a new controller that exposes a way for client devices to send a notification. 通知基于在 ASP.NET WebAPI 后端中使用 Azure 通知中心 .NET 库的用户名标记。The notification is based on the username tag that uses Azure Notification Hubs .NET Library in the ASP.NET WebAPI backend.

  1. 以在前面的部分中创建 RegisterController 的相同方式创建另一个名为 NotificationsController 的新控制器。Create another new controller named NotificationsController the same way you created RegisterController in the previous section.

  2. 在 NotificationsController.cs 中,添加以下 using 语句:In NotificationsController.cs, add the following using statements:

    using AppBackend.Models;
    using System.Threading.Tasks;
    using System.Web;
    
  3. NotificationsController 类中添加以下方法:Add the following method to the NotificationsController class:

    此代码会发送基于平台通知服务 (PNS) pns 参数的通知类型。This code sends a notification type that's based on the Platform Notification Service (PNS) pns parameter. to_tag 的值用于设置消息中的 username 标记。The value of to_tag is used to set the username tag on the message. 此标记必须与活动的通知中心注册的用户名标记相匹配。This tag must match a username tag of an active notification hub registration. 将从 POST 请求正文提取通知消息,并根据目标 PNS 将其格式化。The notification message is pulled from the body of the POST request and formatted for the target PNS.

    通知受多种格式支持,具体取决于受支持设备用来接收通知的 PNS。Depending on the PNS that your supported devices use to receive notifications, the notifications are supported by a variety of formats. 例如,在 Windows 设备上,可能会将 toast 通知与其他 PNS 不直接支持的 WNS 配合使用For example, on Windows devices, you might use a toast notification with WNS that isn't directly supported by another PNS. 在这种情况下,后端需要将通知格式化为打算使用的设备 PNS 所支持的通知。In such an instance, your backend needs to format the notification into a supported notification for the PNS of devices you plan to support. 然后针对 NotificationHubClient 类使用相应的发送 API。Then use the appropriate send API on the NotificationHubClient class.

    public async Task<HttpResponseMessage> Post(string pns, [FromBody]string message, string to_tag)
    {
        var user = HttpContext.Current.User.Identity.Name;
        string[] userTag = new string[2];
        userTag[0] = "username:" + to_tag;
        userTag[1] = "from:" + user;
    
        Microsoft.Azure.NotificationHubs.NotificationOutcome outcome = null;
        HttpStatusCode ret = HttpStatusCode.InternalServerError;
    
        switch (pns.ToLower())
        {
            case "wns":
                // Windows 8.1 / Windows Phone 8.1
                var toast = @"<toast><visual><binding template=""ToastText01""><text id=""1"">" + 
                            "From " + user + ": " + message + "</text></binding></visual></toast>";
                outcome = await Notifications.Instance.Hub.SendWindowsNativeNotificationAsync(toast, userTag);
                break;
            case "apns":
                // iOS
                var alert = "{\"aps\":{\"alert\":\"" + "From " + user + ": " + message + "\"}}";
                outcome = await Notifications.Instance.Hub.SendAppleNativeNotificationAsync(alert, userTag);
                break;
            case "gcm":
                // Android
                var notif = "{ \"data\" : {\"message\":\"" + "From " + user + ": " + message + "\"}}";
                outcome = await Notifications.Instance.Hub.SendGcmNativeNotificationAsync(notif, userTag);
                break;
        }
    
        if (outcome != null)
        {
            if (!((outcome.State == Microsoft.Azure.NotificationHubs.NotificationOutcomeState.Abandoned) ||
                (outcome.State == Microsoft.Azure.NotificationHubs.NotificationOutcomeState.Unknown)))
            {
                ret = HttpStatusCode.OK;
            }
        }
    
        return Request.CreateResponse(ret);
    }
    
  4. 若要运行应用程序并确保到目前为止操作的准确性,请选择 F5 键。To run the application and ensure the accuracy of your work so far, select the F5 key. 应用将打开 Web 浏览器,并且将显示在 ASP.NET 主页上。The app opens a web browser, and it is displayed on the ASP.NET home page.

发布新的 WebAPI 后端Publish the new WebAPI backend

接下来会将此应用部署到 Azure 网站,以便可以从任意设备访问它。Next, you deploy the app to an Azure website to make it accessible from all devices.

  1. 右键单击 AppBackend 项目,并选择“发布”。Right-click the AppBackend project, and then select Publish.

  2. 选择“Azure 应用服务”作为发布目标,然后选择“发布”**。Select Azure App Service as your publish target, and then select **Publish. “创建应用服务”窗口将打开。The Create App Service window opens. 可以在这里创建在 Azure 中运行 ASP.NET Web 应用所需的全部 Azure 资源。Here you can create all the necessary Azure resources to run the ASP.NET web app in Azure.

    “Azure 应用服务”磁贴

  3. 在“创建应用服务”窗口中,选择 Azure 帐户。In the Create App Service window, select your Azure account. 选择“更改类型” > “Web 应用”。Select Change Type > Web App. 保留默认的“Web 应用名称”,然后依次选择“订阅”、“资源组”和“应用服务计划”。Keep the default Web App Name, and then select the Subscription, Resource Group, and App Service Plan.

  4. 选择“创建” 。Select Create.

  5. 记下“摘要”部分的“站点 URL”属性。Make a note of the Site URL property in the Summary section. 此 URL 是本教程中稍后提到的后端终结点This URL is your back-end endpoint later in the tutorial.

  6. 选择“发布”。Select Publish.

完成向导后,它会将 ASP.NET Web 应用发布到 Azure,然后在默认浏览器中打开该应用。After you've completed the wizard, it publishes the ASP.NET web app to Azure and then opens the app in the default browser. 可以在 Azure 应用服务中查看应用程序。Your application is viewable in Azure App Services.

URL 使用前面指定的 Web 应用名称,其格式为 http://<app_name>.chinacloudsites.cn。The URL uses the web app name that you specified earlier, with the format http://<app_name>.chinacloudsites.cn.

修改 iOS 应用Modify your iOS app

  1. 打开在通知中心入门 (iOS) 教程中创建的“单页视图”应用。Open the Single Page view app you created in the Getting Started with Notification Hubs (iOS) tutorial.

    Note

    本节假定项目配置了空的组织名称。This section assumes that your project is configured with an empty organization name. 如果未配置,需要在所有类名前面追加组织名称。If not, you need to prepend your organization name to all class names.

  2. Main.storyboard 文件中,添加屏幕截图中显示的对象库中的组件。In the Main.storyboard file, add the components shown in the screenshot from the object library.

    在 Xcode 接口生成器中编辑情节提要

    • 用户名:包含占位符文本“输入用户名”的 UITextField,直接位于发送结果标签的下面并受左右边距的限制。Username: A UITextField with placeholder text, Enter Username, immediately beneath the send results label and constrained to the left and right margins and beneath the send results label.

    • 密码:包含占位符文本“输入密码”的 UITextField,直接位于用户名文本字段的下面并受左右边距的限制。Password: A UITextField with placeholder text, Enter Password, immediately beneath the username text field and constrained to the left and right margins and beneath the username text field. 选中属性检查器中“返回密钥”下的“安全文本输入”选项。Check the Secure Text Entry option in the Attribute Inspector, under Return Key.

    • 登录:直接位于密码文本字段下方的标签式 UIButton,并取消选中属性检查器中“控件内容”下的“已启用”选项Log in: A UIButton labeled immediately beneath the password text field and uncheck the Enabled option in the Attributes Inspector, under Control-Content

    • WNS:标签和开关,用于已在中心设置 Windows 通知服务时,启用将通知发送到 Windows 通知服务。WNS: Label and switch to enable sending the notification Windows Notification Service if it has been set up on the hub. 请参阅 Windows 入门教程。See the Windows Getting Started tutorial.

    • APNS:标签和开关,用于启用将通知发送到 Apple 平台通知服务。APNS: Label and switch to enable sending the notification to the Apple Platform Notification Service.

    • 收件人用户名:包含占位符文本“收件人用户名标记”的 UITextField,直接位于 GCM 标签下,受左右边距限制。Recipient Username:A UITextField with placeholder text, Recipient username tag, immediately beneath the GCM label and constrained to the left and right margins and beneath the GCM label.

      某些组件已在通知中心入门 (iOS) 教程中添加。Some components were added in the Getting Started with Notification Hubs (iOS) tutorial.

  3. Ctrl 的同时从视图中的组件拖至 ViewController.h 并添加这些新插座。Ctrl drag from the components in the view to ViewController.h and add these new outlets.

    @property (weak, nonatomic) IBOutlet UITextField *UsernameField;
    @property (weak, nonatomic) IBOutlet UITextField *PasswordField;
    @property (weak, nonatomic) IBOutlet UITextField *RecipientField;
    @property (weak, nonatomic) IBOutlet UITextField *NotificationField;
    
    // Used to enable the buttons on the UI
    @property (weak, nonatomic) IBOutlet UIButton *LogInButton;
    @property (weak, nonatomic) IBOutlet UIButton *SendNotificationButton;
    
    // Used to enabled sending notifications across platforms
    @property (weak, nonatomic) IBOutlet UISwitch *WNSSwitch;
    @property (weak, nonatomic) IBOutlet UISwitch *GCMSwitch;
    @property (weak, nonatomic) IBOutlet UISwitch *APNSSwitch;
    
    - (IBAction)LogInAction:(id)sender;
    
  4. ViewController.h 中,在 import 语句后面添加以下 #defineIn ViewController.h, add the following #define after your import statements. 将 <输入你的后端终结点>> 占位符替换为在上一节中用于部署应用后端的目标 URL。Substitute the <Enter Your Backend Endpoint> placeholder with the Destination URL you used to deploy your app backend in the previous section. 例如,http://you_backend.chinacloudsites.cnFor example, http://you_backend.chinacloudsites.cn.

    #define BACKEND_ENDPOINT @"<Enter Your Backend Endpoint>"
    
  5. 在项目中,创建一个名为 RegisterClient 的新 Cocoa Touch 类,以便与你创建的 ASP.NET 后端交互。In your project, create a new Cocoa Touch class named RegisterClient to interface with the ASP.NET back-end you created. 创建继承自 NSObject的类。Create the class inheriting from NSObject. 然后在 RegisterClient.h 中添加以下代码。Then add the following code in the RegisterClient.h.

    @interface RegisterClient : NSObject
    
    @property (strong, nonatomic) NSString* authenticationHeader;
    
    -(void) registerWithDeviceToken:(NSData*)token tags:(NSSet*)tags
        andCompletion:(void(^)(NSError*))completion;
    
    -(instancetype) initWithEndpoint:(NSString*)Endpoint;
    
    @end
    
  6. RegisterClient.m 中,更新 @interface 节:In the RegisterClient.m, update the @interface section:

    @interface RegisterClient ()
    
    @property (strong, nonatomic) NSURLSession* session;
    @property (strong, nonatomic) NSURLSession* endpoint;
    
    -(void) tryToRegisterWithDeviceToken:(NSData*)token tags:(NSSet*)tags retry:(BOOL)retry
                andCompletion:(void(^)(NSError*))completion;
    -(void) retrieveOrRequestRegistrationIdWithDeviceToken:(NSString*)token
                completion:(void(^)(NSString*, NSError*))completion;
    -(void) upsertRegistrationWithRegistrationId:(NSString*)registrationId deviceToken:(NSString*)token
                tags:(NSSet*)tags andCompletion:(void(^)(NSURLResponse*, NSError*))completion;
    
    @end
    
  7. 将 RegisterClient.m 中的 @implementation 节替换为以下代码:Replace the @implementation section in the RegisterClient.m with the following code:

    @implementation RegisterClient
    
    // Globals used by RegisterClient
    NSString *const RegistrationIdLocalStorageKey = @"RegistrationId";
    
    -(instancetype) initWithEndpoint:(NSString*)Endpoint
    {
        self = [super init];
        if (self) {
            NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
            _session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
            _endpoint = Endpoint;
        }
        return self;
    }
    
    -(void) registerWithDeviceToken:(NSData*)token tags:(NSSet*)tags
                andCompletion:(void(^)(NSError*))completion
    {
        [self tryToRegisterWithDeviceToken:token tags:tags retry:YES andCompletion:completion];
    }
    
    -(void) tryToRegisterWithDeviceToken:(NSData*)token tags:(NSSet*)tags retry:(BOOL)retry
                andCompletion:(void(^)(NSError*))completion
    {
        NSSet* tagsSet = tags?tags:[[NSSet alloc] init];
    
        NSString *deviceTokenString = [[token description]
            stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
        deviceTokenString = [[deviceTokenString stringByReplacingOccurrencesOfString:@" " withString:@""]
                                uppercaseString];
    
        [self retrieveOrRequestRegistrationIdWithDeviceToken: deviceTokenString
            completion:^(NSString* registrationId, NSError *error) {
            NSLog(@"regId: %@", registrationId);
            if (error) {
                completion(error);
                return;
            }
    
            [self upsertRegistrationWithRegistrationId:registrationId deviceToken:deviceTokenString
                tags:tagsSet andCompletion:^(NSURLResponse * response, NSError *error) {
                if (error) {
                    completion(error);
                    return;
                }
    
                NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
                if (httpResponse.statusCode == 200) {
                    completion(nil);
                } else if (httpResponse.statusCode == 410 && retry) {
                    [self tryToRegisterWithDeviceToken:token tags:tags retry:NO andCompletion:completion];
                } else {
                    NSLog(@"Registration error with response status: %ld", (long)httpResponse.statusCode);
    
                    completion([NSError errorWithDomain:@"Registration" code:httpResponse.statusCode
                                userInfo:nil]);
                }
    
            }];
        }];
    }
    
    -(void) upsertRegistrationWithRegistrationId:(NSString*)registrationId deviceToken:(NSData*)token
                tags:(NSSet*)tags andCompletion:(void(^)(NSURLResponse*, NSError*))completion
    {
        NSDictionary* deviceRegistration = @{@"Platform" : @"apns", @"Handle": token,
                                                @"Tags": [tags allObjects]};
        NSData* jsonData = [NSJSONSerialization dataWithJSONObject:deviceRegistration
                            options:NSJSONWritingPrettyPrinted error:nil];
    
        NSLog(@"JSON registration: %@", [[NSString alloc] initWithData:jsonData
                                            encoding:NSUTF8StringEncoding]);
    
        NSString* endpoint = [NSString stringWithFormat:@"%@/api/register/%@", _endpoint,
                                registrationId];
        NSURL* requestURL = [NSURL URLWithString:endpoint];
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"PUT"];
        [request setHTTPBody:jsonData];
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
                                                self.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
        [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
        NSURLSessionDataTask* dataTask = [self.session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            if (!error)
            {
                completion(response, error);
            }
            else
            {
                NSLog(@"Error request: %@", error);
                completion(nil, error);
            }
        }];
        [dataTask resume];
    }
    
    -(void) retrieveOrRequestRegistrationIdWithDeviceToken:(NSString*)token
                completion:(void(^)(NSString*, NSError*))completion
    {
        NSString* registrationId = [[NSUserDefaults standardUserDefaults]
                                    objectForKey:RegistrationIdLocalStorageKey];
    
        if (registrationId)
        {
            completion(registrationId, nil);
            return;
        }
    
        // request new one & save
        NSURL* requestURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/api/register?handle=%@",
                                _endpoint, token]];
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"POST"];
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
                                                self.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
    
        NSURLSessionDataTask* dataTask = [self.session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
            if (!error && httpResponse.statusCode == 200)
            {
                NSString* registrationId = [[NSString alloc] initWithData:data
                    encoding:NSUTF8StringEncoding];
    
                // remove quotes
                registrationId = [registrationId substringWithRange:NSMakeRange(1,
                                    [registrationId length]-2)];
    
                [[NSUserDefaults standardUserDefaults] setObject:registrationId
                    forKey:RegistrationIdLocalStorageKey];
                [[NSUserDefaults standardUserDefaults] synchronize];
    
                completion(registrationId, nil);
            }
            else
            {
                NSLog(@"Error status: %ld, request: %@", (long)httpResponse.statusCode, error);
                if (error)
                    completion(nil, error);
                else {
                    completion(nil, [NSError errorWithDomain:@"Registration" code:httpResponse.statusCode
                                userInfo:nil]);
                }
            }
        }];
        [dataTask resume];
    }
    
    @end
    

    此代码使用 NSURLSession 对应用后端执行 REST 调用并使用 NSUserDefaults 在本地存储通知中心返回的 registrationId,实现了指南文章从应用后端注册中所述的逻辑。This code implements the logic explained in the guidance article Registering from your app backend using NSURLSession to perform REST calls to your app backend, and NSUserDefaults to locally store the registrationId returned by the notification hub.

    该类需要设置其属性 authorizationHeader,才能正常工作。This class requires its property authorizationHeader to be set in order to work properly. 登录后,由 ViewController 类设置此属性。This property is set by the ViewController class after the login.

  8. ViewController.h 中,为 RegisterClient.h 添加一个 #import 语句。In ViewController.h, add a #import statement for RegisterClient.h. 然后,在 @interface 中添加设备令牌的声明和对 RegisterClient 实例的引用:Then add a declaration for the device token and reference to a RegisterClient instance in the @interface section:

    #import "RegisterClient.h"
    
    @property (strong, nonatomic) NSData* deviceToken;
    @property (strong, nonatomic) RegisterClient* registerClient;
    
  9. 在 ViewController.m 的 @interface 中添加私有方法声明:In ViewController.m, add a private method declaration in the @interface section:

    @interface ViewController () <UITextFieldDelegate, NSURLConnectionDataDelegate, NSXMLParserDelegate>
    
    // create the Authorization header to perform Basic authentication with your app back-end
    -(void) createAndSetAuthenticationHeaderWithUsername:(NSString*)username
                    AndPassword:(NSString*)password;
    
    @end
    

    Note

    下面的代码片段不是安全的身份验证方案,应将 createAndSetAuthenticationHeaderWithUsername:AndPassword: 的实现替换为特定身份验证机制,该机制将生成要供注册客户端类(例如,OAuth、Active Directory)使用的身份验证令牌。The following snippet is not a secure authentication scheme, you should substitute the implementation of the createAndSetAuthenticationHeaderWithUsername:AndPassword: with your specific authentication mechanism that generates an authentication token to be consumed by the register client class, e.g. OAuth, Active Directory.

  10. 然后在 ViewController.m@implementation 部分中添加以下代码,这段代码会添加用于设置设备令牌和身份验证标头的实现。Then in the @implementation section of ViewController.m, add the following code, which adds the implementation for setting the device token and authentication header.

    -(void) setDeviceToken: (NSData*) deviceToken
    {
        _deviceToken = deviceToken;
        self.LogInButton.enabled = YES;
    }
    
    -(void) createAndSetAuthenticationHeaderWithUsername:(NSString*)username
                    AndPassword:(NSString*)password;
    {
        NSString* headerValue = [NSString stringWithFormat:@"%@:%@", username, password];
    
        NSData* encodedData = [[headerValue dataUsingEncoding:NSUTF8StringEncoding] base64EncodedDataWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
    
        self.registerClient.authenticationHeader = [[NSString alloc] initWithData:encodedData
                                                    encoding:NSUTF8StringEncoding];
    }
    
    -(BOOL)textFieldShouldReturn:(UITextField *)textField
    {
        [textField resignFirstResponder];
        return YES;
    }
    

    请注意设置设备令牌时如何启用登录按钮。Notice how setting the device token enables the log in button. 这是因为在登录操作过程中,视图控制器将使用应用后端注册推送通知。It's because as a part of the login action, the view controller registers for push notifications with the app backend. 因此,在正确设置设备令牌前,系统不希望出现登录操作。Hence, do not want Log In action to be accessible until the device token has been properly set up. 只要登录操作发生在推送注册前,即可分离这两个操作。You can decouple the login from the push registration as long as the former happens before the latter.

  11. 在 ViewController.m 中,使用以下代码段实现“登录”按钮的操作方法以及使用 ASP.NET 后端发送通知消息的方法 。In ViewController.m, use the following snippets to implement the action method for your Log In button and a method to send the notification message using the ASP.NET backend.

    - (IBAction)LogInAction:(id)sender {
        // create authentication header and set it in register client
        NSString* username = self.UsernameField.text;
        NSString* password = self.PasswordField.text;
    
        [self createAndSetAuthenticationHeaderWithUsername:username AndPassword:password];
    
        __weak ViewController* selfie = self;
        [self.registerClient registerWithDeviceToken:self.deviceToken tags:nil
            andCompletion:^(NSError* error) {
            if (!error) {
                dispatch_async(dispatch_get_main_queue(),
                ^{
                    selfie.SendNotificationButton.enabled = YES;
                    [self MessageBox:@"Success" message:@"Registered successfully!"];
                });
            }
        }];
    }
    
    - (void)SendNotificationASPNETBackend:(NSString*)pns UsernameTag:(NSString*)usernameTag
                Message:(NSString*)message
    {
        NSURLSession* session = [NSURLSession
            sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil
            delegateQueue:nil];
    
        // Pass the pns and username tag as parameters with the REST URL to the ASP.NET backend
        NSURL* requestURL = [NSURL URLWithString:[NSString
            stringWithFormat:@"%@/api/notifications?pns=%@&to_tag=%@", BACKEND_ENDPOINT, pns,
            usernameTag]];
    
        NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:@"POST"];
    
        // Get the mock authenticationheader from the register client
        NSString* authorizationHeaderValue = [NSString stringWithFormat:@"Basic %@",
            self.registerClient.authenticationHeader];
        [request setValue:authorizationHeaderValue forHTTPHeaderField:@"Authorization"];
    
        //Add the notification message body
        [request setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"];
        [request setHTTPBody:[message dataUsingEncoding:NSUTF8StringEncoding]];
    
        // Execute the send notification REST API on the ASP.NET Backend
        NSURLSessionDataTask* dataTask = [session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
            if (error || httpResponse.statusCode != 200)
            {
                NSString* status = [NSString stringWithFormat:@"Error Status for %@: %d\nError: %@\n",
                                    pns, httpResponse.statusCode, error];
                dispatch_async(dispatch_get_main_queue(),
                ^{
                    // Append text because all 3 PNS calls may also have information to view
                    [self.sendResults setText:[self.sendResults.text stringByAppendingString:status]];
                });
                NSLog(status);
            }
    
            if (data != NULL)
            {
                xmlParser = [[NSXMLParser alloc] initWithData:data];
                [xmlParser setDelegate:self];
                [xmlParser parse];
            }
        }];
        [dataTask resume];
    }
    
  12. 更新“发送通知”按钮的操作以使用 ASP.NET 后端,发送开关启用的任何 PNS 。Update the action for the Send Notification button to use the ASP.NET backend and send to any PNS enabled by a switch.

    - (IBAction)SendNotificationMessage:(id)sender
    {
        //[self SendNotificationRESTAPI];
        [self SendToEnabledPlatforms];
    }
    
    -(void)SendToEnabledPlatforms
    {
        NSString* json = [NSString stringWithFormat:@"\"%@\"",self.notificationMessage.text];
    
        [self.sendResults setText:@""];
    
        if ([self.WNSSwitch isOn])
            [self SendNotificationASPNETBackend:@"wns" UsernameTag:self.RecipientField.text Message:json];
    
        if ([self.GCMSwitch isOn])
            [self SendNotificationASPNETBackend:@"gcm" UsernameTag:self.RecipientField.text Message:json];
    
        if ([self.APNSSwitch isOn])
            [self SendNotificationASPNETBackend:@"apns" UsernameTag:self.RecipientField.text Message:json];
    }
    
  13. ViewDidLoad 函数中,添加以下内容来实例化 RegisterClient 实例并设置文本字段的委托。In the ViewDidLoad function, add the following to instantiate the RegisterClient instance and set the delegate for your text fields.

    self.UsernameField.delegate = self;
    self.PasswordField.delegate = self;
    self.RecipientField.delegate = self;
    self.registerClient = [[RegisterClient alloc] initWithEndpoint:BACKEND_ENDPOINT];
    
  14. 现在,在 AppDelegate.m 中,删除 application:didRegisterForPushNotificationWithDeviceToken: 方法的所有内容并将其替换为以下内容,以确保视图控制器包含从 APN 中检索到的最新设备令牌:Now in AppDelegate.m, remove all the content of the method application:didRegisterForPushNotificationWithDeviceToken: and replace it with the following to make sure that the view controller contains the latest device token retrieved from APNs:

    // Add import to the top of the file
    #import "ViewController.h"
    
    - (void)application:(UIApplication *)application
                didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
    {
        ViewController* rvc = (ViewController*) self.window.rootViewController;
        rvc.deviceToken = deviceToken;
    }
    
  15. 最后,在 AppDelegate.m 中,确保使用了以下方法:Finally in AppDelegate.m, make sure you have the following method:

    - (void)application:(UIApplication *)application didReceiveRemoteNotification: (NSDictionary *)userInfo {
        NSLog(@"%@", userInfo);
        [self MessageBox:@"Notification" message:[[userInfo objectForKey:@"aps"] valueForKey:@"alert"]];
    }
    

测试应用程序Test the application

  1. 在 XCode 中,在物理 iOS 设备上运行此应用(推送通知无法在模拟器中正常工作)。In XCode, run the app on a physical iOS device (push notifications do not work in the simulator).

  2. 在 iOS 应用 UI 中,为用户名和密码输入相同的值。In the iOS app UI, enter same value for both username and password. 然后,单击“登录”。Then click Log In.

    iOS 测试应用程序

  3. 应看到弹出窗口通知你注册成功。You should see a pop-up informing you of registration success. 单击 “确定”Click OK.

    显示的 iOS 测试通知

  4. 在“*收件人用户名标记”文本字段中,输入用于从另一台设备注册的用户名标记。In the *Recipient username tag text field, enter the user name tag used with the registration from another device.

  5. 输入通知消息,并单击“发送通知”。Enter a notification message and click Send Notification. 只有使用该用户名标记注册的设备才会收到通知消息。Only the devices that have a registration with the recipient user name tag receive the notification message. 该消息将只发送给那些用户。It is only sent to those users.

    iOS 测试带标记的通知

后续步骤Next steps

本教程介绍了如何向其标记与注册相关联的特定用户推送通知。In this tutorial, you learned how to push notifications to specific users that have tags associated with their registrations. 若要了解如何推送基于位置的通知,请转到以下教程:To learn how to push location-based notifications, advance to the following tutorial: