042* APNS推送和第三方推送
1:APNS的推送机制
2:APNS推送通知的详细工作流程
3:准备工作
4:TCP长连接
5:消息格式
6:卸载后接受不到消息
1.APNS的推送机制
首先我们看一下苹果官方给出的对ios推送机制的解释。如下图
Provier 代表我们自己的应用服务器
APNs 代表苹果的APNS推送服务器
接着是代表的就是苹果的设备
Client App 代表我们开发的应用
Provider就是我们自己程序的后台服务器,APNS是Apple Push Notification Service的缩写,也就是苹果的推送服务器。
上图可以分为三个阶段:
第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。
2:APNS推送通知的详细工作流程
下面这张图是说明APNS推送通知的详细工作流程:
根据图片我们可以概括一下:
1、应用程序注册APNS消息推送。
2、iOS从APNSServer获取devicetoken
3、应用程序将device token发送给自己的服务器。
4、服务端程序向APNS服务发送消息。
5、APNS服务将消息发送给iPhone应用程序。
3:准备工作
首先要有一台苹果的设备,模拟器是不支持推送的,所以你需要一台iphone,ipod touch或者ipad。
我们的客户端与苹果服务器之间和我们自己的服务器与苹果服务器之间都需要证书来进行链接。
4:TCP长连接
我们的设备和APNS服务器之间的通讯是基于SSL协议的TCP流通讯,二者之间维持一个长连接,当从APNS服务器注册成功后,一定要将device_token发送给我们的应用服务器,因为在推送过程中,首相是由我们的应用服务器(上图中Provider)将需要推送的消息结合device_token按指定格式(后面会提到)打包然后发送给APNS服务器,然后由APNS服务器推送给我们的设备。
5:消息格式
deviceToken获取
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
上面提到了将device_token和推送消息打包的过程,那么,接下来就看看这个信息包结构是怎样的:
这其实就是个JSON结构体,
{ "aps" : { "category" : "NEW_MESSAGE_CATEGORY" "alert" : { "body" : "Acme message received from Johnny Appleseed", }, "badge" : 3, "sound" : “chime.aiff" }, "acme-account" : "jane.appleseed@apple.com", "acme-message" : "message123456" }
alert标签的内容就是会显示在用户手机上的推送信息,
badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等,
sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音,如果传比如“beep.wav”就会播放在我们应用工程目录下名称为beep.wav的音频文件,比如当手机锁屏时QQ在后台收到新消息时的滴滴声。
6:APNs推送中的问题
1:卸载后接受不到消息
有这么一种情况,当我们将应用从设备卸载后,推送的消息改如何处理呢。
我们知道,当我们将应用从设备卸载后,我们是收不到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。
结构中包含三个部分
- 第一部分是一个时间戳,记录的是设备失效后的时间信息
- 第二个部分是deviceToken的长度
- 第三部分就是失效的deviceToken,我们所要获取的就是第三部分,跟我们的数据库进行对比后,删除对应的deviceToken,下次不再向这些设备发送推送信息
3:收不到 APNs 推送怎么办?
1:首先要知道服务器推送成功,并不代表设备就能收到推送。服务器推送成功只是将消息交给了苹果服务器而已,苹果服务器还需要设备在线才能推送的。
2:其次 deviceToken变化是上传。可能会在 APP 卸载重装后发生变化,客户端对此需要制定相应的汇报策略,以便服务器及时更新存储的 deviceToken 。 客户端只要能上报正确的 deviceToken 就可以说明客户端实现没问题了。
3:否则检查客户端是否开启了远程推送通知服务,
4:Bundle Identifier 是否与申请的推送证书匹配。
5:检查服务器内存缓存的 deviceToken 或者数据库存储的 deviceToken 是否与客户端汇报的一致。
6:排查服务器配置的证书是否过期,是否与客户端的 Bundle Identifier 匹配,或者是否勿用了其他类型的推送证书。
7:采用抓包工具(如 Wireshark )抓包分析,看看服务器是否将消息交给苹果服务器,客户端是否收到了相应的推送通知。
对于大部分人来说,最不理解的就是,休眠时候都保持在那里的 TCP 长连接,不会耗电很厉害么?
答案是:不会。这是手机的设计来做到的。TCP长连接有个心跳的时间,在国外可以很长,比如30分钟,在国内则因为网络环境复杂一般10分钟。客户端发起的心跳,会短暂地消耗手机电能,但在这个心跳间隔期间,则消耗电能是很少的。当在心跳期间服务器端有推送信息过来时,客户端可以收到并做处理。
APNs存在的好处是:
1.安全:只有身份验证成功者才可以通过APNs推送消息
2.快速,稳定,可靠:这一点由APNs和iOS系统来保障
3.更省电:有了APNs就可以实现App无需后台运行
4.让整个系统的体验更统一和简单:不用大量 App和App的服务为了推送挂后台,
也不会出现 App 被杀就收不到推送这种脑残事
5.开发容易:当然,开发者还是要做些事情,比如维护个服务器什么的,但是复杂度无疑降低了很多
二:为什么用第三方的推送
除了技术实现难度低、统一移动端的推送之外,第三方推送平台的有点之一是能实现更多复杂的功能。
对于 APNS 官方推送服务来说,它只允许我们推送一个系统通知,用户点击之后跳转到 App 里的某个页面。但第三方推送服务则可以实现更多复杂的操作,比如用户点击通知后直接播放。
所以我们使用第三方推送的原因就是:
- Android 官方推送的缺陷,我们只能使用第三方推送服务。
- 使用第三方推送实现难度低,可以节省成本。
- 使用第三方推送能实现更多的复杂功能。统一管理安卓和iOS。
优点:不论应用是否开启,都会发送到手机端。
缺点:消息推送的机制是苹果服务端控制,个别时候可能会有延迟,因为苹果服务器也会有队列来处理所有的消息请求。
2、第三方推送机制,普遍使用Socket机制来实现的,它几乎可以达到及时的发送到目标用户手机端,适用于即时通讯类应用。
优点:它几乎是实时的,主要取决于它心跳包的节奏。
缺点:因为iOS系统的限制,应用不能长时间的后台运行,所以在应用关闭的情况下这种推送机制不可用。
极光推送 https://www.jiguang.cn/
百度云推送 http://push.baidu.com/
个推 http://www.getui.com/
腾讯信鸽推送 https://www.qcloud.com/product/XGPush.html
云巴推送 http://yunba.io/
友盟U-push http://mobile.umeng.com/push
第三:个推推送的原理
个推官方技术人员说明:
iOS的消息是分两部分的:一部分是走APNS的通知栏, 另一部分是走我们通道的透传消息 ;这两部分是服务端推送代码里面你们的人员会设定好的,分别是两个不同的方法 ,如果消息下发的时候,你客户端是在后台的(也就是客户端是离线)那么会收到APNS的通知;透传消息就进离线了,只有你下次在线的时候 (也就是下次应用到前台的时候)才会下发下来。 如果下发的时候应用是在前台的,那这样的话客户端就直接收到透传消息了,APNS那部分消息就不会下发了。
app没启动,或者在后台,或者锁屏,都是离线状态,你推送透传时,首先走的是苹果的apns通道,此时会收到apn通知,点开通知,进入应用,app就在线了,就会走个推通道,下发透传消息
补充说明:
在个推后台推送消息有任务中和任务停止操作的区别,如下图:
根据上图中的操作,测试结果如下:
在个推后台推送消息,收到推送消息后续操作
- App在前台时,走透传消息
- App在后台时,任务中,点AppIcon唤起App,走透传消息
- App在后台时,任务中,点推送消息唤起App,同时走透传消息和APNS
- App在后台时,任务中,点通知栏消息唤起App,同时走透传消息和APNS
- App在后台时,任务停止,点AppIcon唤起App,收不到推送消息
- App在后台时,任务停止,点击推送消息唤起App,走APNS
- App在后台时,任务停止,点击通知栏消息唤起App,走APNS
- App未启动时,任务中,点击AppIcon启动App,走透传消息
- App未启动时,任务中,点击推送消息启动App,同时走透传消息和APNS
- App未启动时,任务中,点击通知栏推送消息启动App,同时走透传消息和APNS
- App未启动时,任务停止,点击AppIcon启动App,收不到推送消息
- App未启动时,任务停止,点击推送消息启动App,走APNS
- App未启动时,任务停止,点击通知栏消息启动App,走APNS
在服务端推送消息,收到推送消息后续操作(相当于在个推后台任务中操作)
- App在前台时,走透传消息
- App在后台时,点击AppIcon唤起App,走透传消息
- App在后台时,点击推送消息唤起App,同时走透传消息和APNS
- App在后台时,点击通知栏推送消息唤起App,同时走透传消息和APNS
- App未启动时,点击AppIcon启动App,走透传消息
- App未启动时,点击推送消息启动App,同时走透传消息和APNS
- App未启动时,点击通知栏消息启动App,同时走透传消息和APNS
iOS10以上用新API实现
1.注册个推
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 个推 [GeTuiSdk startSdkWithAppId:GetuiAppId appKey:GetuiAppKey appSecret:GetuiAppSecret delegate:self]; // 注册APNS [self registerUserNotification]; // 是否允许SDK 后台运行(这个一定要设置,否则后台apns不会执行) [GeTuiSdk runBackgroundEnable:true]; return YES; }
2.注册用户通知
/** 注册用户通知 */ - (void)registerUserNotification { /* 注册通知(推送) 申请App需要接受来自服务商提供推送消息 */ if (@available(iOS 10.0, *)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!granted) { [self showNoticeAlert]; } }]; // 注册通知 [[UIApplication sharedApplication] registerForRemoteNotifications]; }else { if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]) { // 定义用户通知类型(Remote.远程 - Badge.标记 Alert.提示 Sound.声音) UIUserNotificationType types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; // 定义用户通知设置 UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; // 注册用户通知 - 根据用户通知设置 [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; [[UIApplication sharedApplication] registerForRemoteNotifications]; if ([[UIApplication sharedApplication] currentUserNotificationSettings].types == UIUserNotificationTypeNone) { [self showNoticeAlert]; } } } } #pragma mark - iOS 10以下注册通知 /** 已登记用户通知 */ - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { // 注册远程通知(推送) [application registerForRemoteNotifications]; }
3.远程通知(推送)回调
#pragma mark - 远程通知(推送)回调 /** 远程通知注册成功委托 */ - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // [3]:向个推服务器注册deviceToken 为了方便开发者,建议使用新方法 [GeTuiSdk registerDeviceTokenData:deviceToken]; NSLog(@"\n>>>[DeviceToken(NSData)]: %@\n\n", deviceToken); }
4.GeTuiSdkdelegate 注册回调,获取CID信息
#pragma mark - GeTuiSdkdelegate 注册回调,获取CID信息 /** SDK启动成功返回cid */ - (void)GeTuiSdkDidRegisterClient:(NSString *)clientId { //个推SDK已注册,返回clientId NSLog(@"\n>>>[GeTuiSdk RegisterClient]:%@\n\n", clientId); } /** SDK遇到错误回调 */ - (void)GeTuiSdkDidOccurError:(NSError *)error { //个推错误报告,集成步骤发生的任何错误都在这里通知,如果集成后,无法正常收到消息,查看这里的通知。 NSLog(@"\n>>>[GexinSdk error]:%@\n\n", [error localizedDescription]); }
5.GeTuiSdkdelegate 透传消息回调
/** SDK收到透传消息回调 */ - (void)GeTuiSdkDidReceivePayloadData:(NSData *)payloadData andTaskId:(NSString *)taskId andMsgId:(NSString *)msgId andOffLine:(BOOL)offLine fromGtAppId:(NSString *)appId { // [4]: 收到个推消息 NSString *payloadMsg = nil; NSDictionary *payloadDict = nil; if (payloadData) { payloadMsg = [[NSString alloc] initWithBytes:payloadData.bytes length:payloadData.length encoding:NSUTF8StringEncoding]; NSData *data = [[NSData alloc] initWithData:[payloadMsg dataUsingEncoding:NSUTF8StringEncoding]]; payloadDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil]; } if (payloadDict == nil) { return; } //isAPNS 针对同时走APNS和透传消息时在APNS代理方法中处理,透传消息不做处理 if (self.isAPNS) { self.isAPNS = NO; return; } NSDictionary *userInfo = [payloadDict dictionaryValueForKey:@"message"]; NSString *string = [userInfo stringValueForKey:@"schema"]; NSString *title = [payloadDict stringValueForKey:@"title"]; NSString *message = [payloadDict stringValueForKey:@"content"]; NSString *type = [payloadDict stringValueForKey:@"type"]; // offLine 为YES时表示是离线消息 // 离线消息并且非营销类的消息不做弹窗处理 // operation 营销类push消息 if (offLine && ![type isEqualToString:@"operation"]) { return; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (string.length == 0 || [string isEqualToString:StringToastNone]) { [UIAlertController alertViewController:title message:message cancelActionTitle:@"我知道了" otherActionTitle:nil cancelBlock:nil otherBlock:nil]; }else { [UIAlertController alertViewController:title message:message cancelActionTitle:@"取消" otherActionTitle:@"前往" cancelBlock:nil otherBlock:^(UIAlertAction *action) { [HSQSchema openURLSpmExtern:string]; }]; } }); }
4.APNS回调
#pragma mark - iOS 10以下处理 /** APP已经接收到“远程”通知(推送) - 透传推送消息 */ - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { // 处理APN,用来标识同时走APNS和透传消息时只处理APNS时 self.isAPNS = true; // 跳转处理 [self dealwithAPNSMessage:userInfo]; completionHandler(UIBackgroundFetchResultNewData); } #pragma mark - UNUserNotificationCenterDelegate iOS 10以上(包括iOS 10)处理 //App在后台运行及程序退出杀死 会调用的方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { // 处理APN,用来标识同时走APNS和透传消息时只处理APNS时 self.isAPNS = YES; // 远程推送消息 if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { NSDictionary *userInfo = response.notification.request.content.userInfo; // 跳转处理 [self dealwithAPNSMessage:userInfo]; } completionHandler(); }