推送通知
和本地通知不同,推送通知是由应用服务提供商发起的,通过苹果的APNs(Apple Push Notification Server)发送到应用客户端。下面是苹果官方关于推送通知的过程示意图:
推送通知的过程可以分为以下几步:
- 应用服务提供商从服务器端把要发送的消息和设备令牌(device token)发送给苹果的消息推送服务器APNs。
- APNs根据设备令牌在已注册的设备(iPhone、iPad、iTouch、mac等)查找对应的设备,将消息发送给相应的设备。
- 客户端设备接将接收到的消息传递给相应的应用程序,应用程序根据用户设置弹出通知消息。
当然,这只是一个简单的流程,有了这个流程我们还无从下手编写程序,将上面的流程细化可以得到如下流程图(图片来自互联网),在这个过程中会也会提到如何在程序中完成这些步骤:
1.应用程序注册APNs推送消息。
说明:
a.只有注册过的应用才有可能接收到消息,程序中通常通过UIApplication的registerUserNotificationSettings:方法注册,iOS8中通知注册的方法发生了改变,如果是iOS7及之前版本的iOS请参考其他代码。
b.注册之前有两个前提条件必须准备好:开发配置文件(provisioning profile,也就是.mobileprovision后缀的文件)的App ID不能使用通配ID必须使用指定APP ID并且生成配置文件中选择Push Notifications服务,一般的开发配置文件无法完成注册;应用程序的Bundle Identifier必须和生成配置文件使用的APP ID完全一致。
2.iOS从APNs接收device token,在应用程序获取device token。
说明:
a.在UIApplication的-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken代理方法中获取令牌,此方法发生在注册之后。
b.如果无法正确获得device token可以在UIApplication的-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error代理方法中查看详细错误信息,此方法发生在获取device token失败之后。
c.必须真机调试,模拟器无法获取device token。
3.iOS应用将device token发送给应用程序提供商,告诉服务器端当前设备允许接收消息。
说明:
a.device token的生成算法只有Apple掌握,为了确保算法发生变化后仍然能够正常接收服务器端发送的通知,每次应用程序启动都重新获得device token(注意:device token的获取不会造成性能问题,苹果官方已经做过优化)。
b.通常可以创建一个网络连接发送给应用程序提供商的服务器端, 在这个过程中最好将上一次获得的device token存储起来,避免重复发送,一旦发现device token发生了变化最好将原有的device token一块发送给服务器端,服务器端删除原有令牌存储新令牌避免服务器端发送无效消息。
4.应用程序提供商在服务器端根据前面发送过来的device token组织信息发送给APNs。
说明:
a.发送时指定device token和消息内容,并且完全按照苹果官方的消息格式组织消息内容,通常情况下可以借助其他第三方消息推送框架来完成。
5.APNs根据消息中的device token查找已注册的设备推送消息。
说明:
a.正常情况下可以根据device token将消息成功推送到客户端设备中,但是也不排除用户卸载程序的情况,此时推送消息失败,APNs会将这个错误消息通知服务器端以避免资源浪费(服务器端此时可以根据错误删除已经存储的device token,下次不再发送)。
下面将简单演示一下推送通知的简单流程:
首先,看一下iOS客户端代码:
#import "AppDelegate.h" #import "KCMainViewController.h" @interface AppDelegate () @end @implementation AppDelegate #pragma mark - 应用程序代理方法 #pragma mark 应用程序启动之后 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1]; //设置全局导航条风格和颜色 [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]]; [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack]; KCMainViewController *mainController=[[KCMainViewController alloc]init]; _window.rootViewController=mainController; [_window makeKeyAndVisible]; //注册推送通知(注意iOS8注册方法发生了变化) [application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]; [application registerForRemoteNotifications]; return YES; } #pragma mark 注册推送通知之后 //在此接收设备令牌 -(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{ [self addDeviceToken:deviceToken]; NSLog(@"device token:%@",deviceToken); } #pragma mark 获取device token失败后 -(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{ NSLog(@"didFailToRegisterForRemoteNotificationsWithError:%@",error.localizedDescription); [self addDeviceToken:nil]; } #pragma mark 接收到推送通知之后 -(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{ NSLog(@"receiveRemoteNotification,userInfo is %@",userInfo); } #pragma mark - 私有方法 /** * 添加设备令牌到服务器端 * * @param deviceToken 设备令牌 */ -(void)addDeviceToken:(NSData *)deviceToken{ NSString *key=@"DeviceToken"; NSData *oldToken= [[NSUserDefaults standardUserDefaults]objectForKey:key]; //如果偏好设置中的已存储设备令牌和新获取的令牌不同则存储新令牌并且发送给服务器端 if (![oldToken isEqualToData:deviceToken]) { [[NSUserDefaults standardUserDefaults] setObject:deviceToken forKey:key]; [self sendDeviceTokenWidthOldDeviceToken:oldToken newDeviceToken:deviceToken]; } } -(void)sendDeviceTokenWidthOldDeviceToken:(NSData *)oldToken newDeviceToken:(NSData *)newToken{ //注意一定确保真机可以正常访问下面的地址 NSString *urlStr=@"http://192.168.1.101/RegisterDeviceToken.aspx"; urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:10.0]; [requestM setHTTPMethod:@"POST"]; NSString *bodyStr=[NSString stringWithFormat:@"oldToken=%@&newToken=%@",oldToken,newToken]; NSData *body=[bodyStr dataUsingEncoding:NSUTF8StringEncoding]; [requestM setHTTPBody:body]; NSURLSession *session=[NSURLSession sharedSession]; NSURLSessionDataTask *dataTask= [session dataTaskWithRequest:requestM completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { NSLog(@"Send failure,error is :%@",error.localizedDescription); }else{ NSLog(@"Send Success!"); } }]; [dataTask resume]; } @end
iOS客户端代码的代码比较简单,注册推送通知,获取device token存储到偏好设置中,并且如果新获取的device token不同于偏好设置中存储的数据则发送给服务器端,更新服务器端device token列表。
其次,由于device token需要发送给服务器端,这里使用一个Web应用作为服务器端接收device token,这里使用了ASP.NET程序来处理令牌接收注册工作,当然你使用其他技术同样没有问题。下面是对应的后台代码:
using System; using System.Collections.Generic; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using CMJ.Framework.Data; namespace WebServer { public partial class RegisterDeviceToken : System.Web.UI.Page { private string _appID = @"com.cmjstudio.pushnotification"; private SqlHelper _helper = new SqlHelper(); protected void Page_Load(object sender, EventArgs e) { try { string oldToken = Request["oldToken"] + ""; string newToken = Request["newToken"] + ""; string sql = ""; //如果传递旧的设备令牌则删除旧令牌添加新令牌 if (oldToken != "") { sql = string.Format("DELETE FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}';", _appID, oldToken); } sql += string.Format(@"IF NOT EXISTS (SELECT ID FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}') INSERT INTO dbo.Device ( AppID, DeviceToken ) VALUES ( N'{0}', N'{1}');", _appID, newToken); _helper.ExecuteNonQuery(sql); Response.Write("注册成功!"); } catch(Exception ex) { Response.Write("注册失败,错误详情:"+ex.ToString()); } } } }
这个过程主要就是保存device token到数据库中,当然如果同时传递旧的设备令牌还需要先删除就的设备令牌,这里简单的在数据库中创建了一张Device表来保存设备令牌,其中记录了应用程序Id和设备令牌。
第三步就是服务器端发送消息,如果要给APNs发送消息就必须按照Apple的标准消息格式组织消息内容。但是好在目前已经有很多开源的第三方类库供我们使用,具体消息如何包装完全不用自己组织,这里使用一个开源的类库Push Sharp来给APNs发送消息 ,除了可以给Apple设备推送消息,Push Sharp还支持Android、Windows Phone等多种设备,更多详细内容大家可以参照官方说明。前面说过如果要开发消息推送应用不能使用一般的开发配置文件,这里还需要注意:如果服务器端要给APNs发送消息其秘钥也必须是通过APNs
Development iOS类型的证书来导出的,一般的iOS Development 类型的证书导出的秘钥无法用作服务器端发送秘钥。下面通过在一个简单的WinForm程序中调用Push Sharp给APNs发送消息,这里读取之前Device表中的所有设备令牌循环发送消息:
using System; using System.IO; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using PushSharp; using PushSharp.Apple; using CMJ.Framework.Data; using CMJ.Framework.Logging; using CMJ.Framework.Windows.Forms; namespace PushNotificationServer { public partial class frmMain : PersonalizeForm { private string _appID = @"com.cmjstudio.pushnotification"; private SqlHelper _helper = new SqlHelper(); public frmMain() { InitializeComponent(); } private void btnClose_Click(object sender, EventArgs e) { this.Close(); } private void btnSend_Click(object sender, EventArgs e) { List<string> deviceTokens = GetDeviceToken(); SendMessage(deviceTokens, tbMessage.Text); } #region 发送消息 /// <summary> /// 取得所有设备令牌 /// </summary> /// <returns>设备令牌</returns> private List<string> GetDeviceToken() { List<string> deviceTokens = new List<string>(); string sql = string.Format("SELECT DeviceToken FROM dbo.Device WHERE AppID='{0}'",_appID); DataTable dt = _helper.GetDataTable(sql); if(dt.Rows.Count>0) { foreach(DataRow dr in dt.Rows) { deviceTokens.Add((dr["DeviceToken"]+"").TrimStart('<').TrimEnd('>').Replace(" ","")); } } return deviceTokens; } /// <summary> /// 发送消息 /// </summary> /// <param name="deviceToken">设备令牌</param> /// <param name="message">消息内容</param> private void SendMessage(List<string> deviceToken, string message) { //创建推送对象 var pusher = new PushBroker(); pusher.OnNotificationSent += pusher_OnNotificationSent;//发送成功事件 pusher.OnNotificationFailed += pusher_OnNotificationFailed;//发送失败事件 pusher.OnChannelCreated += pusher_OnChannelCreated; pusher.OnChannelDestroyed += pusher_OnChannelDestroyed; pusher.OnChannelException += pusher_OnChannelException; pusher.OnDeviceSubscriptionChanged += pusher_OnDeviceSubscriptionChanged; pusher.OnDeviceSubscriptionExpired += pusher_OnDeviceSubscriptionExpired; pusher.OnNotificationRequeue += pusher_OnNotificationRequeue; pusher.OnServiceException += pusher_OnServiceException; //注册推送服务 byte[] certificateData = File.ReadAllBytes(@"E:\KenshinCui_Push.p12"); pusher.RegisterAppleService(new ApplePushChannelSettings(certificateData, "123")); foreach (string token in deviceToken) { //给指定设备发送消息 pusher.QueueNotification(new AppleNotification() .ForDeviceToken(token) .WithAlert(message) .WithBadge(1) .WithSound("default")); } } void pusher_OnServiceException(object sender, Exception error) { Console.WriteLine("消息发送失败,错误详情:" + error.ToString()); PersonalizeMessageBox.Show(this, "消息发送失败,错误详情:" + error.ToString(), "系统提示"); } void pusher_OnNotificationRequeue(object sender, PushSharp.Core.NotificationRequeueEventArgs e) { Console.WriteLine("pusher_OnNotificationRequeue"); } void pusher_OnDeviceSubscriptionExpired(object sender, string expiredSubscriptionId, DateTime expirationDateUtc, PushSharp.Core.INotification notification) { Console.WriteLine("pusher_OnDeviceSubscriptionChanged"); } void pusher_OnDeviceSubscriptionChanged(object sender, string oldSubscriptionId, string newSubscriptionId, PushSharp.Core.INotification notification) { Console.WriteLine("pusher_OnDeviceSubscriptionChanged"); } void pusher_OnChannelException(object sender, PushSharp.Core.IPushChannel pushChannel, Exception error) { Console.WriteLine("消息发送失败,错误详情:" + error.ToString()); PersonalizeMessageBox.Show(this, "消息发送失败,错误详情:" + error.ToString(), "系统提示"); } void pusher_OnChannelDestroyed(object sender) { Console.WriteLine("pusher_OnChannelDestroyed"); } void pusher_OnChannelCreated(object sender, PushSharp.Core.IPushChannel pushChannel) { Console.WriteLine("pusher_OnChannelCreated"); } void pusher_OnNotificationFailed(object sender, PushSharp.Core.INotification notification, Exception error) { Console.WriteLine("消息发送失败,错误详情:" + error.ToString()); PersonalizeMessageBox.Show(this, "消息发送失败,错误详情:"+error.ToString(), "系统提示"); } void pusher_OnNotificationSent(object sender, PushSharp.Core.INotification notification) { Console.WriteLine("消息发送成功!"); PersonalizeMessageBox.Show(this, "消息发送成功!", "系统提示"); } #endregion } }
服务器端消息发送应用运行效果:
iOS客户端接收的消息的效果:
到目前为止通过服务器端应用可以顺利发送消息给APNs并且iOS应用已经成功接收推送消息。