一、Push 的实现大体上可以分为三种:信道、轮询、长链接
方式1:【信道】
信道是最佳方案,但是需要运营商支持。
控制信道push,不利用TCP/ IP,而是利用底层的移动通信的控制信道进行push(就是呈现我们手机是哪家信号,是否有电话呼入,是否注册在网的那个信道),短信也是走的这个信道,我们通常可以看到的通过短信push-alert机制,或者BB的push机制都是这样的机制。而BB这样的做法,是一定要和运营商深度合作才能够实现的,这也是BB目前销售运营模式的要点。
方式2:【长连接】
socket常连接机制(http协议本身,是一个典型的请求-返回的偶连接模式,所有http下的推送基本都是依赖轮询实现)。
优点:
1.更为及时一点。
2.现在云端的成本变得越来越低,所以这种方式的成本也变的可以承受。
缺点:
方式3:【轮询】:
优点:可以相对的 优化算法来节省系统资源。
缺点:算法相对要求比较高
云服务相比推送通知来说,要相对简单一些,因为云服务的推送的内容,很多是在特定应用启动以后才需要使用的,可以在应用内部做文章,所以轮询可以做得更方便一些(拿通讯录为例,可以在启动通讯录的时候插入一个云端轮询同步,甚至可以在手指滑动或者搜索的时候加入云端查询同步),这样会让人感觉及时性很高。
通知通常是在应用执行之前到达,用户的行为更不好判断,情况更多,所以麻烦一些。
轮询不一定是定时定周期的轮询,可以和特定的算法,操作,结合,在一些应用的情况下,可以让人感觉到是及时的更新。
轮询算法优化角度:
1.设定一个最小的轮询间隔,例如5秒,也就是说不能无休止的添加轮询密度,这个数字一般感觉是用户在此系统和使用环境下2~3次正常单次操作-响应过程的时间。
这样对于一般的用户体验来说会比较能够接受。
2.屏幕解锁时插入一次轮询。
3.手机处在使用状态,降低轮询周期
4.使用WIFI时,降低轮询周期。
5.1分钟内发生过3次高密度推送的,暂时降低轮询周期
6.启动特定应用(例如注册了推送的应用),插入一次轮询
因为有些算法优化的东西都是第三方开发者无法做到的,所以iOS的推送一定是要apple自己来提供比较好。
二、iOS 推送机制
苹果的推送服务APNs基本原理简单来说就是苹果利用自己专门的推送服务器(APNs)接收来自我们自己应用服务器的需要被推送的信息,然后推送到指定的iOS设备上,然后由设备通知到我们的应用程序,设备以通知或者声音的形式通知用户有新的消息。
推送的前提是装有我们应用的设备需要向APNs服务器注册,注册成功后APNs服务器会返给我们一个device_token,拿到这个token后我们将这个token发给我们自己的应用服务器,当有需要被推送的消息时,我们的应用服务器会将消息按指定的格式打包,然后结合设备的device_token一并发给APNs服务器,由于我们的应用和APNs维持一个基于TCP的长连接,APNs将新消息推送到我们设备上,然后在屏幕上显示出新消息来。
1.【远程推送注册方式】
1.Device连接APNs服务器并携带设备序列号
2.连接成功,APNs经过打包和处理产生device_token并返回给注册的Device
3.Device携带获取的device_token向我们自己的应用服务器注册
2.【远程推送推送过程】
Provider就是我们自己程序的后台服务器,APNS是Apple Push Notification Service的缩写,也就是苹果的推送服务器。
下图可以分为三个阶段:
第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。
上图显示了我们的应用服务器将消息推送到我们的App的完整路径,其实真正完成推送的是APNS服务器,我们自己的应用服务器只是将需要推送的消息告诉苹果服务器,至于如何维护消息队列或如何保证消息能被推送到指定的设备上,这些都由苹果APNS给我们做完了。
3.【远程推送数据结构】
上图显示的这个消息体就是我们的服务器(Provider)发送给APNS服务器的消息结构,APNS验证这个结构正确并提取其中的信息后,再将消息推送到指定的设备。
这个结构体包括五个部分
第一个部分是命令标示符。
第二个部分是我们的device_token的长度。
第三部分是我们的device_token字符串。
第四部分是推送消息体(Payload)的长度。
第五部分也就是真正的消息内容了,里面包含了推送消息的基本信息,比如消息内容,应用Icon右上角显示多少数字以及推送消息到达时所播放的声音等。
下面看下(消息体)的结构:
这其实就是个JSON结构体。
alert标签的内容就是会显示在用户手机上的推送信息。
badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等。
sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音,如果传比如“beep.wav”就会播放在我们应用工程目录下名称为beep.wav的音频文件,比如当手机锁屏时QQ在后台收到新消息时的滴滴声。
4.【应用卸载后的远程推送注销】
有这么一种情况,当我们将应用从设备卸载后,推送的消息改如何处理呢?
我们知道,当我们将应用从设备卸载后,我们是收不到Provider给我们推送的消息的,但是,如何让APNS和Provider都知道不去向这台卸载了应用的设备推送消息呢?针对这个问题,苹果也已经帮我们解决了,那就是Feedback service。他是APNS的一部分,APNS会持续的更新Feedback service的列表,当我们的Provider将信息发给APNS推送到我们的设备时,如果这时设备无法将消息推送到指定的应用,就会向APNS服务器报告一个反馈信息,而这个信息就记录在feedback service中。按照这种方式,Provider应该定时的去检测Feedback service的列表,然后删除在自己数据库中记录的存在于反馈列表中的device_token,从而不再向这些设备发送推送信息。连接Feedback service的过程同样使用Socket的方式,连接上后,直接接收由APNS传输给我们的反馈列表,传输完成后断开连接,然后我们根据这个最新的反馈列表在更新我们自己的数据库,删除那些不再需要推送信息的设备的device_token。
从Feedback service读取的数据结构如下:
结构中包含三个部分:
第一部分是一个时间戳,记录的是设备失效后的时间信息
第二个部分是device_token的长度
第三部分就是失效的device_token,我们所要获取的就是第三部分,跟我们的数据库进行对比后,删除对应的device_token,下次不再向这些设备发送推送信息。
【注:最后以 极光推送 设备系统>=iOS 10为例 说下调用顺序】
1. 用户在使用当前app应用 收到推送时候:调用这两个方法
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger))completionHandler - (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler
2. 用户在使用其他app应用、锁屏(包括:当前应用下锁屏、其他应用下锁屏、桌面锁屏) 收到推送时候:直接调用上面第二个方法。
特别提醒的是:从这点看苹果还是这样希望, 在当前app处于不活跃状态的时候(包括:锁屏,未开启,后台等),此app里面的代码是不调用的。
【远程推送】
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. //APNS(苹果推送服务器)注册远程推送通知,APNS返回device token float version = [[[UIDevice currentDevice] systemVersion] floatValue]; if (version >= 8.0) { [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeSound|UIUserNotificationTypeBadge categories:nil]]; //注册远程推送通知 [[UIApplication sharedApplication] registerForRemoteNotifications]; }else{ //ios8以下 [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeBadge]; } return YES; } //当向APNS注册成功时的回调函数 - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSLog(@"%@",deviceToken); //需要把deviceToken上传到服务端,要服务端提供接口 } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{ NSLog(@"%@",error.localizedDescription); } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { NSLog(@"收到了远程推送通知"); }
【本地推送】
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. application.applicationIconBadgeNumber = 0; //申请用户许可 float version = [[[UIDevice currentDevice] systemVersion] floatValue]; if (version >= 8.0) { UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes: UIUserNotificationTypeSound|UIUserNotificationTypeBadge|UIUserNotificationTypeAlert categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:setting]; } UILocalNotification * localNotificaiton = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; if (localNotificaiton != nil) { [self processLocalNotifiction:localNotificaiton]; } return YES; } -(void)processLocalNotifiction:(UILocalNotification*)localNotification { UIAlertView *alterView = [[UIAlertView alloc]initWithTitle:@"lanchApp" message:@"loc" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alterView show]; } //注册设置提醒后,调用的代理方法 -(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { if (notificationSettings.types != UIUserNotificationTypeNone ) { //注册本地推送,首先生成UILocalNotification对象 UILocalNotification *localNotifcation = [[UILocalNotification alloc]init]; //提示出发的时间 localNotifcation.fireDate = [NSDate dateWithTimeIntervalSinceNow:10]; //提示的内容 localNotifcation.alertBody = @"这是一个测试"; //制定消息到来时的播放的声音文件,一定要在bundle内,而且声音的持续时间不能超过30s localNotifcation.soundName = @"CAT2.WAV"; //设置系统角标 localNotifcation.applicationIconBadgeNumber = 1; //注册本地通知道系统中,这样系统在指定的时间会出发该通知 [[UIApplication sharedApplication] scheduleLocalNotification:localNotifcation]; } } //当程序运行在后台,或者程序没有启动,当注册的本地通知到达时。ios会弹框提示,并播放你设置的声音。 //当应用程序运行在前台会调用该代理方法,不会播放声音,不会弹框 - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { //判断应用程序状态来决定是否弹框 if (application.applicationState == UIApplicationStateActive) { UIAlertView *alterView = [[UIAlertView alloc]initWithTitle:@"本地推送" message:notification.alertBody delegate:nil cancelButtonTitle:@"ok" otherButtonTitles:nil]; [alterView show]; }else if (application.applicationState == UIApplicationStateInactive) { NSLog(@"UIApplicationStateInactive"); }else{ //background NSLog(@"UIApplicationStateBackground"); } }
github 地址:里面附带有推送证书 https://github.com/lc081200/pushExample
【此外】:
【远程推送 App服务端相关】
客户端实现了,服务端怎么实现了?
原理就是实现 SSL Socket 并按照协议给苹果的服务器发送数据。
iOS服务端工作有:
1.证书配置,app 申请的证书iOS来做(p12证书有密码),服务器用证书做一些配置转换。
2.还有设备的 token(一个设备一个 token)
3.定义给设备发送推送的消息
<?php //手机注册应用返回唯一的deviceToken $deviceToken = '6ad7b13f b05e6137 a46a60ea 421e5016 4b701671 cc176f70 33bb9ef4 38a8aef9'; //ck.pem通关密码 $pass = 'jetson'; //消息内容 $message = 'A test message!'; //badge 消息红色个数 $badge = 4; //sound(推送消息到手机时的提示音) $sound = 'Duck.wav'; //建设的通知有效载荷(即通知包含的一些信息) $body = array(); $body['id'] = "4f94d38e7d9704f15c000055"; $body['aps'] = array('alert' => $message); if ($badge) $body['aps']['badge'] = $badge; if ($sound) $body['aps']['sound'] = $sound; //把数组数据转换为json数据 $payload = json_encode($body); echo strlen($payload)," "; //下边的写法就是死写法了,一般不需要修改, //唯一要修改的就是:ssl://gateway.sandbox.push.apple.com:2195这个是沙盒测试地址,ssl://gateway.push.apple.com:2195正式发布地址 $ctx = stream_context_create(); stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem'); stream_context_set_option($ctx, 'ssl', 'passphrase', $pass); $fp = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195', $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx); if (!$fp) { print "Failed to connect $err $errstr "; return; } else { print "Connection OK <br/>"; } // send message $msg = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n",strlen($payload)) . $payload; print "Sending message :" . $payload . " "; fwrite($fp, $msg); fclose($fp); ?>
服务器参考连接:
http://blog.csdn.net/sinat_34380438/article/details/53582199
http://blog.csdn.net/chenyong05314/article/details/8725763
C# 的实现 网上下载的我也没看
https://github.com/lc081200/pushServer_C3
关于后台长连接的参考
http://www.jianshu.com/p/174fd2673897