该文章是我16年在公司博客上写的,除了证书注册的过程大致没有改变,像接收通知的方法都有所改变,所以将iOS 10 之后的接收通知及注册通知的方法在文章中补全,希望对正在处理远程推送的伙伴们有所帮助
推送通知,是现在的应用必不可少的功能。那么在 iOS 中,我们是如何实现远程推送的呢?iOS 的远程推送原理又是什么呢?在做 iOS 远程推送时,我们会遇到各种各样的问题。 那么首先让我们准备一些做推送需要的东西。我们需要一个付费的苹果开发者账号(免费的不可以做远程推送),有了开发者账号,我们可以去苹果开发者网站,配置自己所需要的推送的相关证书。然后下载证书,供我们后面使用,详细的证书配置过程,我们下面再说。
首先我们要说说iOS推送通知的基本原理:
苹果的推送服务通知是由自己专门的推送服务器APNs (Apple Push Notification service)来完成的,其过程是 APNs 接收到我们自己的应用服务器发出的被推送的消息,将这条消息推送到指定的 iOS 的设备上,然后再由 iOS设备通知到我们的应用程序,我们将会以通知或者声音的形式收到推送回来的消息。 iOS 远程推送的前提是,装有我们应用程序的 iOS 设备,需要向 APNs 服务器注册,注册成功后,APNs 服务器将会给我们返回一个 devicetoken,我们获取到这个 token 后会将这个 token 发送给我们自己的应用服务器。当我们需要推送消息时,我们的应用服务器将消息按照指定的格式进行打包,然后结合 iOS 设备的 devicetoken 一起发给 APNs 服务器。我们的应用会和 APNs 服务器维持一个基于 TCP 的长连接,APNs 服务器将新消息推送到iOS 设备上,然后在设备屏幕上显示出推送的消息。
上图完成了如下步骤:
Device(设备)连接APNs服务器并携带设备序列号(UUID)
连接成功,APNs经过打包和处理产生devicetoken并返回给注册的Device(设备)
Device(设备)携带获取的devicetoken发送到我们自己的应用服务器
完成需要被推送的Device(设备)在APNs服务器和我们自己的应用服务器的注册
推送的过程经过如下步骤:
首先,我们的设备安装了具有推送功能的应用(应用程序要用代码注册消息推动),我们的 iOS设备在有网络的情况下会连接APNs推送服务器,连接过程中,APNS 服务器会验证devicetoken,连接成功后维持一个基于TCP 的长连接;
Provider(我们自己的应用服务器)收到需要被推送的消息并结合被推送的 iOS设备的devicetoken一起打包发送给APNS服务器;
APNS服务器将推送信息推送给指定devicetoken的iOS设备;
iOS设备收到推送消息后通知我们的应用程序并显示和提示用户(声音、弹出框)
上图显示的这个消息体就是我们的应用服务器(Provider)发送给APNs服务器的消息结构,APNs验证这个结构正确并提取其中的信息后,再将消息推送到指定的iOS设备。这个结构体包括五个部分,第一个部分是命令标示符,第二个部分是我们的devicetoken的长度,第三部分是我们的devicetoken字符串,第四部分是推送消 息体(Payload)的长度,最后一部分也就是真正的消息内容了,里面包含了推送消息的基本信息,比如消息内容,应用Icon右上角显示多少数字以及推送消息到达时所播放的声音等
Payload(消息体)的结构:
{
“aps”:{
“alert”:“CSDN给您发送了新消息”,
“badge”:1,
“sound”:“default”
},
}
这其实就是个JSON结构体,alert标签的内容就是会显示在用户手机上的推送信息,badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等,sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音。
保存位置在 tingyun(指定自己的文件夹,这里我选择的是我的文件夹),点击存储。
然后点击完成后我们会在 tingyun 里看到一个CertificateSigningRequest.certSigningRequest的请求文件,也就是我们说的CSR文件。在我们生成CSR文件的同时,会在钥匙串访问中生成一对秘钥,名称为刚才我们填写的常用名。
到苹果开发者网站https://developer.apple.com
Name: 填写 App 的名字就行
App ID Suffix 选择不用通配符的及 Explicit App ID
在下面的 App Services 中选择自己需要的服务,我们需要推送服务,所以在Push Notifications上打勾,然后点击continue。
证书需要创建两种,一种是开发的、一种是发布的,开发的是做测试用的。
选择Apple Push Notification service SSL (Sandbox),创建推送服务证书点击下一步
这儿的 App ID 选择我们刚才创建的 App ID,然后点击下一步,下一步
配置文件也有两种,一种是开发的,一种是发布的,开发的使我们做测试需要的,发布的是我们在 Appstore 上发布时需要的,我们都需要生成。
我们先生成开发配置文件,选择Provisioning Profiles->Development点击右上角的+号。
发布配置文件和开发配置文件一样创建,选择Distribution->Ad Hoc即可,后面与发布配置文件一样。
打开AppDelegate.m 文件,在- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中添加下面代码,注册消息推送
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
/*
注册通知(推送)
申请App需要接受来自服务商提供推送消息
*/
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
//iOS10特有
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
// 必须写代理,不然无法监听通知的接收与点击
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
// 点击允许
//NSLog(@"注册成功");
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
//NSLog(@"%@", settings);
}];
} else {
// 点击不允许
// NSLog(@"注册失败");
}
}];
}
else if ([[UIDevice currentDevice].systemVersion floatValue] >8.0){
//iOS8 - iOS10
[[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge categories:nil]];
}
else
{
UIRemoteNotificationType apn_type = (UIRemoteNotificationType)(UIRemoteNotificationTypeAlert |UIRemoteNotificationTypeSound |
UIRemoteNotificationTypeBadge);
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:apn_type];
}
// 注册获得device Token
[[UIApplication sharedApplication] registerForRemoteNotifications];
return YES;
}
//下面方法是返回 ANPs 苹果推送服务器生成的唯一标识
/** 接收服务器传回的设备唯一标识 token */
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
// 第一次运行获取到DeviceToken时间会比较长!
// 将deviceToken转换成字符串,以便后续使用
NSString *token = [deviceToken description];
NSLog(@"description %@", token);
}
//下面方法是当注册推送服务失败时,接收错误信息
/** 注册推送服务失败 */
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
NSLog(@"注册失败 %@",error);
}
//下面方法是当有消息推送回来时,接收推送消息
/** 设备接收到来自苹果推送服务器的消息时触发的,用来显示推送消息 */
// iOS 10收到通知(前台)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
NSDictionary * userInfo = notification.request.content.userInfo;
UNNotificationRequest *request = notification.request; // 收到推送的请求
UNNotificationContent *content = request.content; // 收到推送的消息内容
NSNumber *badge = content.badge; // 推送消息的角标
NSString *body = content.body; // 推送消息体
UNNotificationSound *sound = content.sound; // 推送消息的声音
NSString *subtitle = content.subtitle; // 推送消息的副标题
NSString *title = content.title; // 推送消息的标题
if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
NSLog(@"iOS10 前台收到远程通知:%@", [self logDic:userInfo]);
}
else {
// 判断为本地通知
NSLog(@"iOS10 前台收到本地通知:{\\\\nbody:%@,\\\\ntitle:%@,\\\\nsubtitle:%@,\\\\nbadge:%@,\\\\nsound:%@,\\\\nuserInfo:%@\\\\n}",body,title,subtitle,badge,sound,userInfo);
}
completionHandler(UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound|UNNotificationPresentationOptionAlert); // 需要执行这个方法,选择是否提醒用户,有Badge、Sound、Alert三种类型可以设置
}
//iOS 10 通知的点击事件(点击通知)
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
NSDictionary * userInfo = response.notification.request.content.userInfo;
UNNotificationRequest *request = response.notification.request; // 收到推送的请求
UNNotificationContent *content = request.content; // 收到推送的消息内容
NSNumber *badge = content.badge; // 推送消息的角标
NSString *body = content.body; // 推送消息体
UNNotificationSound *sound = content.sound; // 推送消息的声音
NSString *subtitle = content.subtitle; // 推送消息的副标题
NSString *title = content.title; // 推送消息的标题
if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
NSLog(@"iOS10 收到远程通知:%@", [self logDic:userInfo]);
}
else {
// 判断为本地通知
NSLog(@"iOS10 收到本地通知:{\\\\nbody:%@,\\\\ntitle:%@,\\\\nsubtitle:%@,\\\\nbadge:%@,\\\\nsound:%@,\\\\nuserInfo:%@\\\\n}",body,title,subtitle,badge,sound,userInfo);
}
// Warning: UNUserNotificationCenter delegate received call to -userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: but the completion handler was never called.
completionHandler(); // 系统要求执行这个方法
}
//iOS 6 及以下系统通知
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo {
NSLog(@"iOS6及以下系统,收到通知:%@",userInfo);
}
//iOS 7 及以上系统通知(前台/后台)
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:
(void (^)(UIBackgroundFetchResult))completionHandler {
NSLog(@"iOS7及以上系统,收到通知:%@",userInfo);
completionHandler(UIBackgroundFetchResultNewData);
}
服务器端我们需要,一个后缀为. p12的证书,以及需要的 jar 包
服务器端的证书生成方式:
1、打开我们前面下载的证书,在钥匙串中找到它
3、导出后缀为.p12的文件保存到自己的电脑上,需要输入一个密码,在 Java 服务器端要用到
4、Java服务器端需要的 Jar 包
5、Java 服务器端代码:
import javapns.back.PushNotificationManager;
import javapns.back.SSLConnectionHelper;
import javapns.data.Device;
import javapns.data.PayLoad;
public class pushService {
public static void main(String[] args) {
try {
//这个token 就是客户端从APNs服务器获取到的devicetoken
String deviceToken = "eab6df47eb4f81e0aaa93bb208cffd7dc3884fd346ea0743fcf93288018cfcb6";
//被推送的iphone应用程序标示符
PayLoad payLoad = new PayLoad();
payLoad.addAlert("测试我的push消息");
payLoad.addBadge(1);
payLoad.addSound("default");
PushNotificationManager pushManager = PushNotificationManager.getInstance();
pushManager.addDevice("iphone", deviceToken);
//测试推送服务器地址:gateway.sandbox.push.apple.com /2195
//产品推送服务器地址:gateway.push.apple.com / 2195
String host="gateway.sandbox.push.apple.com"; //测试用的苹果推送服务器
int port = 2195;
String certificatePath = "/Users/hsw/Desktop/PushTest/PushTest.p12"; //刚才在mac系统下导出的证书
String certificatePassword= "123456";
pushManager.initializeConnection(host, port, certificatePath,certificatePassword, SSLConnectionHelper.KEYSTORE_TYPE_PKCS12);
//Send Push
Device client = pushManager.getDevice("iphone");
pushManager.sendNotification(client, payLoad); //推送消息
pushManager.stopConnection();
pushManager.removeDevice("iphone");
}
catch (Exception e) {
e.printStackTrace();
System.out.println("push faild!");
return;
}
System.out.println("push succeed!");
}
}
注意,我有看到转载的提问,服务端是否需要deviceToken,这个肯定是需要的,在上面Java的代码中也有写到。deviceToken是设备唯一的推送标识,服务端只有知道这个标识,才能够将通知推送到你的设备上。 注意:iOS 13后获取token的规则有变化