iOS 推送简介

一、Push 的实现大体上可以分为三种:信道、轮询、长链接

方式1:【信道】

信道是最佳方案,但是需要运营商支持。

控制信道push,不利用TCP/ IP,而是利用底层的移动通信的控制信道进行push(就是呈现我们手机是哪家信号,是否有电话呼入,是否注册在网的那个信道),短信也是走的这个信道,我们通常可以看到的通过短信push-alert机制,或者BB的push机制都是这样的机制。而BB这样的做法,是一定要和运营商深度合作才能够实现的,这也是BB目前销售运营模式的要点。

 

方式2:【长连接】

socket常连接机制(http协议本身,是一个典型的请求-返回的偶连接模式,所有http下的推送基本都是依赖轮询实现)。

优点:

  1.更为及时一点。

  2.现在云端的成本变得越来越低,所以这种方式的成本也变的可以承受。

缺点:

  1.长连接对于服务器和数据通道来讲是比较大的负担。
  2.采用这种方式,其实要求TCP/IP一直处在连通的方式,3G来说的话(1)耗电开销太大。(2)要处理关于断线重连以及IP注册的问题。
 
实施方案:
  通常维护一个长连接也需要两端做工作,客户端需要智能的判断通道的活跃情况,因为有时候一个静默的长链接,有可能会被运营商网关GGSN / SGSN给切断(为了保证服务更多的用户),判断方法有动态心跳等;服务器也需要知道这个通道是否活跃,并在适当的时候发送Push内容;BTW:静默的通道其实对于终端电量的消耗还是比较小的。
 
案例:
  顺丰做的项目里采用了这种方式,因为他们是效率和服务质量第一的要求,快递员随身携带多块备用电池(他们还只是使用GPRS,没有3G耗电厉害) 多数情况下,是一个所谓的push-alert机制以保证push协议的可复用性。
  iOS 也是此种方式。

方式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),"\r\n";
//下边的写法就是死写法了,一般不需要修改,
//唯一要修改的就是: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\n";
    return;
}
else {
   print "Connection OK\n<br/>";
}
// send message
$msg = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n",strlen($payload)) . $payload;
print "Sending message :" . $payload . "\n";  
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

posted on 2017-07-05 17:17  iRemark  阅读(676)  评论(0编辑  收藏  举报

导航