最近在研究iOS的推送问题,遇到了好多问题,经过很多大神的文章指点最终整理了一下,放在这里和大家分享。
准备工作
首先要有一台苹果的设备,模拟器是不支持推送的,所以你需要一台iphone,ipod touch或者ipad。
我们的客户端与苹果服务器之间和我们自己的服务器与苹果服务器之间都需要证书来进行链接。下面我们来开始进入证书的制作过程。
一 CSR文件
首先我们要有生成一个Certificate Signing Request(也就是CSR)的请求文件。
在应用程序里的使用工具中找到钥匙串访问。
选择从证书颁发机构请求证书
填上你的邮箱和常用名,常用名要记一下,一会会用到。然后选择保存到磁盘,继续,保存位置在桌面,点击存储。
到这里点击完成后我们会在桌面上看到一个CertificateSigningRequest.certSigningRequest的请求文件,也就是我们说的CSR文件。在我们生成CSR文件的同时,会在钥匙串访问中生成一对秘钥,名称为刚才我们填写的常用名
二、 创建Appid
(这里我为了大家能看清楚,已经把之前的证书事先吊销了)
到https://developer.apple.com/devcenter/ios/index.action 登录后,选择Certificates
点击左侧的App IDs,找到我们要做推送功能的程序的id(如果没有的话要先New一个。注意,这里的App ID必须不能是通配的,通配的不可以做推送)。点击Configure
点击右上角的“+”号,显示如下:
Name起个有意义的名字,bundle ID 就像当于安卓当中的包名吧,我是这样的理解的,这个一定要写对!然后会出现这样的界面
点“submit”-----"done",然后点击“Certificates”,下面有"development"(这个是测试要用到的)和"Production"(发布到用到),两个生成的原理一样,就先生成测试证书吧,点右上角的“+”号,出现如图
因为我们生成的是测试版本的,就选Development下的App Push Notification Server(Sandbox),点击继续,出现下图:
App ID选择刚刚我们创建的那个,点击继续出现下图:
再点继续:
点击“Choose File”选择之前生成的CSR文件,创建好后下载到电脑。安装
四、下载Provisioning证书,下载之前要创建一个,也选择测试版本的,和上面的步骤差不多,注意App ID不要选错就好了。就不上图了。
创建好后下载到电脑,双击安装就好了!
五 从钥匙串访问中导出秘钥
打开钥匙串访问,找到我们的专用秘钥(专用秘钥的名称就是我们在最开始生成CSR请求的时候填写的常用名)
右键选择导出,
导出的文件名我们叫做push吧
在这里需要输入一个密码来对文件进行加密。这里我们选择123456,当然你也可以自己选择是什么,但是这个密码必须要铭记,切记!
然后输入你电脑的密码,点击允许。这样我们就在桌面上生成了一个Push.p12文件。到此为止,我们在桌面上一共生成了三个文件。一个是CSR请求文件,一个是aps_development .cer的SSL证书文件,还有一个刚才生成的push.p12秘钥文件。
现在我们的准备工作已经做完了。要开始对生成的文件进行处理了。原因上面已经解释过,因为我们的服务器链接苹果服务器也是需要证书的,但是我们直接生成的证书windows系统(我们一般的服务器都是win系统的)是不识别的,所以我们需要生成一个后缀为pem的带证书带秘钥的文件。
六 处理证书
下面我们打开终端(位置:应用程序à实用工具à终端)。
cd到桌面,我们那三个文件所在的位置
1、把.cer的SSL证书转换为.pem文件,执行命令:
openssl x509 -in aps_development.cer -inform der -out PushChatCert.pem
在桌面上会生成一个PushChatCert.pem文件
2、把私钥Push.p12文件转化为.pem文件:
openssl pkcs12 -nocerts -out PushChatKey.pem -in Push.p12
这里需要我们输入密码,这个密码也就是我们导出p12文件时的密码,也就是我们上面设置的abcabc。然后,需要我们对生成的pem文件设置一个密语,这里我们推荐还是用上面这个123456,防止混乱(当然你也可以设置成别的更有意义的密语),这里的密语是要告诉我们服务器的。这样,桌面上又会生成一个PushChatKey.pem文件
3、对生成的这两个pem文件再生成一个pem文件,来把证书和私钥整合到一个文件里:
cat PushChatCert.pem PushChatKey.pem > ck.pem
生成ck.pem文件
这样,我们的文件就制作完了。下面进入测试阶段
为了测试证书是否工作,执行下面的命令:
telnet gateway.sandbox.push.apple.com 2195
它将尝试发送一个规则的,不加密的连接到APNS服务。如果你看到上面的反馈,那说明你的MAC能够到达APNS。按下Ctrl+C关闭连接。如果得到一个错误信息,那么你需要确保你的防火墙允许2195端口。一般这里都不会出现什么问题。
下面我们要使用我们生成的SSL证书和私钥来设置一个安全的链接去链接苹果服务器:
openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushChatCert.pem -key PushChatKey.pem
执行完这一句命令后需要我们输入密语
Enter pass phrase for PushChatKey.pem:
我们输入abcabc按回车
你会看到一个完整的输出,让你明白OpenSSL在后台做什么。如果链接是成功的,你可以随便输入一个字符,按下回车,服务器就会断开链接,如果建立连接时有问题,OpenSSL会给你返回一个错误信息。
当你在最后的时候你看到这样说明你已经成功了:
CONNECTED(00000003)
depth=1 /C=US/O=Entrust, Inc./OU=www.entrust.net/rpa isincorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust CertificationAuthority - L1C
verify error:num=20:unable to get local issuercertificate
verify return:0
---
Certificate chain
0s:/C=US/ST=California/L=Cupertino/O=Apple Inc./OU=iTMSEngineering/CN=gateway.sandbox.push.apple.com
i:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated byreference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
1s:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated byreference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
i:/O=Entrust.net/OU=www.entrust.net/CPS_2048incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.netCertification Authority (2048)
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFGzCCBAOgAwIBAgIETBz90jANBgkqhkiG9w0BAQUFADCBsTELMAkGA1UEBhMC
……省略……
fMGbLqkGn8YogdPqe5T1
-----END CERTIFICATE-----
subject=/C=US/ST=California/L=Cupertino/O=AppleInc./OU=iTMS Engineering/CN=gateway.sandbox.push.apple.com
issuer=/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa isincorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust CertificationAuthority - L1C
---
No client certificate CA names sent
---
SSL handshake has read 2731 bytes and written 2165 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
Protocol : TLSv1
Cipher : AES256-SHA
Session-ID:
Session-ID-ctx:
Master-Key:C7A47EED5E1F5……省略……369D4
Key-Arg : None
Start Time:1361862882
Timeout : 300 (sec)
Verify return code: 0 (ok)
---
在这里提醒一下,也许你会看到像我这样的提示:verify error:num=20:unable to get local issuercertificate
verify return:0
其实是没问题的。
在Appdelegate.mm中添加
//推送 ,注册
[[UIApplication sharedApplication] cancelAllLocalNotifications];
if ([application respondsToSelector:@selector(isRegisteredForRemoteNotifications)])
{
//IOS8
//创建UIUserNotificationSettings,并设置消息的显示类类型
NSLog(@"IOS版本是8.0以上的");
// UIUserNotificationSettings *notiSettings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeBadge | UIUserNotificationTypeAlert | UIRemoteNotificationTypeSound) categories:nil];
// [application registerUserNotificationSettings:notiSettings];
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
[application registerForRemoteNotifications];
//本地推送
[self addLocalNotification];
application.applicationIconBadgeNumber -= 1;
} else{ // ios7
NSLog(@"IOS版本是8.0以下的");
[application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge |UIRemoteNotificationTypeSound |UIRemoteNotificationTypeAlert)];
//本地推送
[self addLocalNotification];
application.applicationIconBadgeNumber -= 1;
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
NSLog(@"didReceiveRemoteNotification - (%@)", userInfo);
// 진동!
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
//推送
if (application.applicationState == UIApplicationStateActive) { //转为后台显示
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.userInfo = userInfo;
localNotification.soundName = UILocalNotificationDefaultSoundName;
localNotification.alertBody = [[userInfo objectForKey:@"aps"] objectForKey:@"alert"];
localNotification.fireDate = [NSDate date];
[[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
}
application.applicationIconBadgeNumber -= 1;
}
本地推送:
#pragma mark 添加本地通知
-(void)addLocalNotification{
// NSDate *now = [NSDate date];
// //取得系统时间
// NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
// NSDateComponents *components = [[NSDateComponents alloc] init];
// NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
// components = [calendar components:unitFlags fromDate:now];
// NSInteger hour = [components hour];
// NSInteger min = [components minute];
// NSInteger sec = [components second];
// NSInteger week = [components weekday];
// NSLog(@"现在是%ld:%ld:%ld,周%ld",hour,min,sec,week);
//
// NSDate *date = [NSDate date];
// NSTimeZone *zone = [NSTimeZone systemTimeZone];//修改时区
// NSInteger interval1 = [zone secondsFromGMTForDate: date];//修改时区
// NSDate *localDate1 = [date dateByAddingTimeInterval: interval1];//修改时区
// NSLog(@"今天%@", localDate1);
// //2),创建一个明天此时的日期(时间间隔是以秒为单位的)dateWithTimeIntervalSinceNow:
// NSDate *tomorrow = [NSDate dateWithTimeIntervalSinceNow:24 * 60 * 60];
// NSInteger interval2 = [zone secondsFromGMTForDate: tomorrow];
// NSDate *localDate2 = [tomorrow dateByAddingTimeInterval: interval2];
// NSLog(@"明天%@", localDate2);
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss"];
//定义本地通知对象
UILocalNotification * notification=[[UILocalNotification alloc]init];
//设置调用时间
notification.timeZone=[NSTimeZone defaultTimeZone];
//notification.fireDate=[NSDate dateWithTimeIntervalSinceNow:10.0];//通知触发的时间,10s以后
//notification.fireDate= [NSDate dateWithTimeIntervalSince1970:11*60*60*24];//通知触发的时间,10s以后
//notification.fireDate= localDate2;
NSDate *date = [NSDate date];
NSTimeZone *zone = [NSTimeZone systemTimeZone];//修改时区
NSInteger interval1 = [zone secondsFromGMTForDate: date];//修改时区
NSDate *localDate1 = [date dateByAddingTimeInterval: interval1];//修改时区
localDate1 = [formatter dateFromString:@"10:00:00"];
//通知发出的时间
notification.fireDate = localDate1;
NSLog(@"时间%@", notification.fireDate);
notification.repeatInterval=kCFCalendarUnitWeekday;//通知重复次数
notification.repeatCalendar=[NSCalendar currentCalendar];//当前日历,使用前最好设置时区等信息以便能够自动同步时间
NSLog(@"日历%@", notification.repeatCalendar);
//设置通知属性
notification.alertBody=@"最近添加了诸多有趣的特性,是否立即体验?"; //通知主体
notification.applicationIconBadgeNumber=1;//应用程序图标右上角显示的消息数
notification.alertAction=@"打开应用"; //待机界面的滑动动作提示
notification.alertLaunchImage=@"Default";//通过点击通知打开应用时的启动图片
notification.soundName=UILocalNotificationDefaultSoundName;//收到通知时播放的声音,默认消息声音
//notification.soundName=@"msg.caf";//通知声音(需要真机)
//设置用户信息
notification.userInfo=@{@"id":@1,@"user":@"Kenshin Cui"};//绑定到通知上的其他额外信息
//调用通知
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
[notification release];
}
远程推送:
- (void)application:(UIApplication *)applicationdid RegisterForRemoteNotificationsWithDeviceToken:(NSData *)pToken {
NSLog(@"regisger success:%@",pToken);
//注册成功,将deviceToken保存到应用服务器数据库中
NSLog(@"注册成功,将deviceToken保存到应用服务器数据库中");
}
#pragma mark //调用过用户注册通知方法之后执行(也就是调用完registerUserNotificationSettings:方法之后执行)
-(void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings{
if (notificationSettings.types!=UIUserNotificationTypeNone) {
[self addLocalNotification];
}
}
-(void)addDeviceToken:(NSData *)deviceToken{
NSString *key=@"DeviceToken";
//[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
NSData *oldToken= [[NSUserDefaults standardUserDefaults]objectForKey:key];
//如果偏好设置中的已存储设备令牌和新获取的令牌不同则存储新令牌并且发送给服务器端
if (![oldToken isEqualToData:deviceToken]) {
[[NSUserDefaults standardUserDefaults] setObject:deviceToken forKey:key];
[self sendDeviceTokenWidthOldDeviceToken:oldToken newDeviceToken:deviceToken];
}
//[self sendDeviceTokenWidthOldDeviceToken:oldToken newDeviceToken:deviceToken];
}
-(void)sendDeviceTokenWidthOldDeviceToken:(NSData *)oldToken newDeviceToken:(NSData *)newToken{
//注意一定确保真机可以正常访问下面的地址
NSString *urlStr=@"http://192.168.1.1/test/setPush.php";
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0];
[requestM setHTTPMethod:@"POST"];
NSString* str = [[[newToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
NSString *bodyStr=[NSString stringWithFormat:@"devicetoken=%@",str];
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];
}
//- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification*)notification{
//
// UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"iWeibo" message:notification.alertBody delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
//
// [alert show];
//
// // 图标上的数字减1
//
// application.applicationIconBadgeNumber -= 1;
//
//}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString* tokenString = [[[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]] stringByReplacingOccurrencesOfString:@" " withString:@""];
if ( tokenString != nil )
{
NSLog(@"didRegisterForRemoteNotificationsWithDeviceToken - (%@)", tokenString);
// [Todo] 발급받은 tokenString을 게임 서버를 통해 위미 푸시 서버로 전달합니다.
// ~~~~
[self addDeviceToken:deviceToken];
}
}
下面我们把php服务器代码和生成的ck.pem文件放在统一文件夹下面(这里我们还是统一放在桌面上)。
用Xcode打开(其他工具也可以)php服务器端的代码,把deviceToken改成我们现在要进行测试的iphone的deviceToken(获得方法,:在Xcode的顶部工具栏点击windowàOrganizer,在左侧选中我们的iphone后,右边的Identifier后面的就是了),密语改成我们之前设置的abcabc。然后保存。
然后在终端运行命令(如果刚才你关闭了终端的话,最好ls一下,看看当前是不是在桌面),执行命令:
php pushMe.php
然后回车(pushMe为服务器文件名称)
如果出现这样的提示说明成功了,然后在iphone上,我们期待已久的推送消息终于来了。
最后贴上PHP的服务器的代码,这代码是从网上http://www.cocoachina.com/industry/20130321/5862.html拿来的,http://www.cocoachina.com/bbs/read.php?tid=102110&page=1 感谢大神无私的奉献。
<?php
// Put your device token here (without spaces):
//$deviceToken = '577a952a3a08b1f90effee0c8fe45142e33e084d743e34068b6474f32c92cfb6'; //测试版本
//$deviceToken = '954ed011b0d64b7de1e16098c8394cd353a32f00a4968375510100ad5fe80b8f'; //发布版本
// Put your private key's passphrase here:密语
$passphrase = '密码';
// Put your alert message here:
$info = mysql_connect("127.0.0.1","用户名","密码");//连接数据库
mysql_query("SET NAMES 'UTF8'");
if (!$info)
{
die('Could not connect: ' . mysql_error());//连接失败显示的错误
}
mysql_select_db("starfg", $info);
$pushList = mysql_query("select * from 表名");
while($row = mysql_fetch_array($pushList))//循环读取数据
{
echo $row['title'];//输出数据
echo "<br />";
$message = $row['title'];
}
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem');
stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase);
// Open a connection to the APNS server
//测试地址gateway.sandbox.push.apple.com:2195
//发布地址 gateway.push.apple.com:2195
$fp = stream_socket_client(
'ssl://gateway.push.apple.com:2195', $err,
$errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
if (!$fp)
exit("Failed to connect: $err $errstr" . PHP_EOL);
echo 'Connected to APNS' . PHP_EOL;
echo "<br />";
// Create the payload body
$body['aps'] = array(
'alert' => $message,
'sound' => 'default'
);
// Encode the payload as JSON
$payload = json_encode($body);
// Build the binary notification
//$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
$activityList = mysql_query("select * from 表名");
while($row = mysql_fetch_array($activityList))//循环读取数据
{
echo $row['devicetoken'];//输出数据
echo "<br />";
$msg = chr(0) . pack('n', 32) . pack('H*', $row['devicetoken']) . pack('n', strlen($payload)) . $payload;
// Send it to the server
$result = fwrite($fp, $msg, strlen($msg));
}
// Send it to the server
//$result = fwrite($fp, $msg, strlen($msg));
if (!$result)
echo 'Message not delivered' . PHP_EOL;
else
echo 'Message successfully delivered' . PHP_EOL;
// Close the connection to the server
fclose($fp);
?>