准备推送
虽然花点时间,但是我们最终准备在应用程序中添加推送通知(push notifications)功能。我们已经知道了怎么注册推送通知(push notifications)以及如何获得设备标识。我们在一次回顾一下在“AppDelegate.m”中如何获得设备标识。
在“application:didFinishLaunchingWithOptions:”方法中,在return语句之前,添加如下代码:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { ... [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; return YES; }
我们将注册推送通知的响铃和提醒消息的功能。如果我们的应用程序不需要在它的图标上显示推送消息的数量标记,那么实现这种消息的重点就不在注册方式上。
在“ AppDelegate.m,”文件的底部,就在“@end”的前面,添加:
- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { NSString* oldToken = [dataModel deviceToken]; NSString* newToken = [deviceToken description]; newToken = [newToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; newToken = [newToken stringByReplacingOccurrencesOfString:@" " withString:@""]; NSLog(@"My token is: %@", newToken); [dataModel setDeviceToken:newToken]; if ([dataModel joinedChat] && ![newToken isEqualToString:oldToken]) { [self postUpdateRequest]; } } - (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { NSLog(@"Failed to get token, error: %@", error); }
你曾经见过这些方法;这就如我们如何获取“设备标识”的方法一样。我们再分析一些“didRegisterForRemoteNotificationsWithDeviceToken”方法的一些细节:
NSString* newToken = [deviceToken description]; newToken = [newToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; newToken = [newToken stringByReplacingOccurrencesOfString:@" " withString:@""];
已经见过“设备标识”是类似下面的一段字符串:
<0f744707 bebcf74f 9b7c25d4 8e335894 5f6aa01d a5ddb387 462c7eaf 61bbad78>
但是更简单的方法是将“设备标识”字符串处理为下面的格式:
0f744707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bbad78
上面的代码片段是将前者转换为后者。
下面的着一小段代码需要解释一下:
if ([dataModel joinedChat] && ![newToken isEqualToString:oldToken]) { [self postUpdateRequest]; }
在你注册了推送通知(push notifications)功能后,“didRegisterForRemoteNotificationsWithDeviceToken”方法总是被正确调用。通常,但并非总是。获得“ 设备标识(device token)”总是异步发生的,并且这会花费几秒钟的事件,特别是应用程序第一次获得“设备标识(device token)”。
在我们获得有效的“设备标识(device token)”之前,用户已经点击了启动图标,理论上是可能的!这种情况下,应用程序将发送一个默认的“ 设备标识(device token)”的值,@”0″,到服务器。很明显,这个不是有效的“ 设备标识(device token)”值,所以我们无法成功发送推送通知(push notifications)。
在用户已经开始聊天的时候,我们收到了“ 设备标识(device token)”,然后我们必须尽快的让服务器知道我们获得了“设备标识(device token)”,直接是“update” API 所作的事情。它保证服务器的API总是保持有用户设备最新的“ device token”。(这就是为什么当“ device token”变化的时候,我们只是发送UPDATE命令,而不是每次应用程序开始运行的时候发送)。
在“didRegisterForRemoteNotificationsWithDeviceToken”方法前面添加下面代码:
- (void)postUpdateRequest { NSURL* url = [NSURL URLWithString:ServerApiURL]; ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url]; [request setPostValue:@"update" forKey:@"cmd"]; [request setPostValue:[dataModel udid] forKey:@"udid"]; [request setPostValue:[dataModel deviceToken] forKey:@"token"]; [request setDelegate:self]; [request startAsynchronous]; }
不要忘记导入相应的头文件:
#import “ASIFormDataRequest.h”
我们不关心ASIFormDataRequest发生了什么。它是在后台默默的执行。所以如果失败了,我们也不会显示错误信息。
这就完成了应用程序发送到服务器的所有的通信的API,现在,我们看一下当服务器接受到信息是,做些什么工作,以及服务器怎么发推送通知(push notifications)。
服务器的推送通知的实现
在PushChatServer目录中,有一个push的文件夹,这个文件夹中包含了你需要发送推送通知的PHP脚本。你需要将这些文件放到服务器的一个目录中,这样就不能总互联网中访问这些文件,换句话,不在你的DocumentRoot目录中。这是很重要的,因为你并不想让你网站的访问者能够下载你的“private key”!(在MAMP设置中,我们已经处理好了。)
在push文件夹中,最重要的脚本文件是push.php。这个脚本文件需要服务器的在后台进程中运行。每隔几秒,它就检查是否由新的推送通知(push notifications)被发送。如果有,那么它把这些信息发送给苹果的推送通知服务器(Apple Push Notification Service)。
首先,我们需要编辑“ push_config.php”文件,它包含了push.php的选项配置。你需要修改“private key”的密码,可能的话改变数据库的密码。
使用服务器的API,推送脚本不仅可以以开发模式(development mode)运行,也可以以产品模式(production mode)运行。在开发模式(development mode)下,就需要谈谈APNS沙盒服务(sandbox server),并且它需要使用你的SSL开发证书( development SSL certificate)。你需要将你的开发模式(development mode)与与你应用程序的“Debug builds”结合。产品模式(production mode)需要在“Ad Hoc”和你应用程序的“App Store builds”下使用。
在push文件夹中有一个“ck_development.pem”文件。用我们在教程的开始部分创建的PEM文件,替换“ck_development.pem”文件。
现在打开新的终端窗口,执行下面的命令:
$ /Applications/MAMP/bin/php5.2/bin/php push.php development
在开发模式(development mode)启动push.php脚本。注明我们现在用的是PHP的MAMP版本,不是你Mac上预装的PHP。这很重要,否则的脚本就不能连接到MySQL数据库。
push.php脚本不应该存在。如果它存在,会出现一些错误。看一下在log文件夹的push_development.log文件,我的是这样的:
2011-05-06T16:32:19+02:00 Push script started (development mode)
2011-05-06T16:32:19+02:00 Connecting to gateway.sandbox.push.apple.com:2195
2011-05-06T16:32:21+02:00 Connection OK
因为push.php脚本应该在后台进程中运行,他不能在控制台中输出。而在日志文件中写日,。每次发送一个推送通知消息,加载日志文件中添加一行日志。
注:现在我们实际上并没有在后台进程运行push.php脚本。为了开发我并不想这样做。(如果你想要停止运行push.php,就同时点击键盘的Ctrl+C键)。
$ /Applications/MAMP/bin/php5.2/bin/php push.php production &
“&”将脚本从shell中分离,并将脚本放到后台运行。
push.php脚本是做什么的?首先,它打开与APNS服务器的安全连接。它一直保持打开这个连接。我见过许多push的例子都是每次有新的推送消息的时候,创建一个与APNS服务器新的连接。苹果并不赞成这样的实现方式。重复建立一个新的安全连接,是非常消耗处理能力和网络资源(network resources,应该翻译成带宽么?)。保持连接打开,是更好的选择。
一旦连接成功,脚本进入死循环。每次循环都要检查push队列和数据库,是否由新的消息。他通过“time_sent”字段来识别新的消息。如果“time_sent”的值是NULL,那么就不发送通知。脚本创建了一个二进制文件,存储“device token”和“JSON payload”,并将它发送给APNS服务器。
如果你有兴趣学习二进制格式(binary format),我建议你学习“ Local and Push Notification Programming Guide”的“ The Binary Interface and Notification Formats ”章节。不管怎样,阅读苹果官方指南是一个好的学习习惯。
一旦“push.php”发出新的通知,它会使用当前时间填充“time_sent”字段。然后脚本休眠2秒钟,这样往复循环。
这就意味着发送一条推送通知,你所做的就是将一个新的记录放到推送队列表(the push_queue table)中。这就是当服务器从iPhone应用程序接到一条消息(MESSAGE)请求时,服务器的脚本API所作的事情。几秒之后,push.php 进程被唤醒,检查是否有新的记录,并发送新的记录。
我们最终能否发送推送通知!?
如果你有2个iPhone,你应该在两个设备上安装该应用程序,让这两个用户拥有相同的密码。当一个用户发向服务器发送信息。API发送一个推送给另一方。
但是如果你没两个设备用来测试怎么办?好的,这种情况下我们只能假设另一个用户,这个用户你可以在这个网址http://pushchat.local:44447/test/api_join.html中建立。
这是由非常基本的HTML形成的网页:
我们可以使用这个网页想服务器发送推送(POST)请求,就像我们应用操程序所操作的那样。服务器不能辨别它们(设备与网页)之间的差异,所以这个网页对于测试API命令是个有用的工具。
填充40个字节的UDID【Unique Device Identifier Description】,64字节的“device token”,还有昵称。验证位(签名字段-secret code)应该和应用程序的签署相同。它和UDID,还有“device token”没有关系。只要他们来自不同设备上的应用程序。否则服务器的API显然不会认为这是不同设备上的应用程序发送的。
点击Submit(提交)按钮。你可以通过使用“phpMyAdmin”权限,浏览active_users表,来验证一个用户的“签名”。
现在让你的浏览器连接到: http://pushchat.local:44447/test/api_message.html
确定UDID指向的是一个不存在的用户,输入消息文本,点击Submit。 Tadaa… ,几秒钟之内,你的iPhone会播放提示声音并显示一条推送通知的消息。祝贺你!
如果你没有受到任何消息。首先关闭应用程序,并从新是测试一遍。实际上并没有在应用程序中添加任何代码,来处理收到的推送通知消息,所以当应用程序当前没有运行的时候,我们知识显示推送通知消息。
如果你没有受到任何的推送消息,你要确认push.php仍然在运行,并检查push_development.log的日志信息。日志信息会显示的内容会和下面内容类似:
2011-05-06T23:57:29+02:00 Sending message 1 to
’0f744707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bbad78′,
payload: ‘{“aps”:{“alert”:”SteveJ: Hello, world!”,”sound”:”default”}}’
2011-05-06T23:57:29+02:00 Message successfully delivered
你也可以使用phpMyAdmin权限检查是否由新的信息被添加到push_queue的数据库表中。
将一条推送信息传递到目的设备上需要花费一些时间,并且我之前也提到过推送信息有时可能会丢失掉。试着多尝试几次,看看你的幸运指数。
在app中接受推送信息
当你的iPhone接收到推送信息的时候会发什么什么呢?这里有三种可能的状态
· app 正在前台运行着。屏幕上没有任何事情会发生并且不会播放任何声音,但是你的app delegate会接收到消息。这决定于app有没有为notification做些什么。
· app 是关闭的,同时iPhone没有被锁上或者正在运行其他的app。一个带警告视图会弹出来,显示消息并播放声音。用户可以点击Close来忽略消息,或者按下View来打开你的app。如果用户按了Close,你的app就永远不会告知有这条消息的存在。
· iPhone是锁着的. 同样弹出一个警告试图并且播放声音。但是警告不具有Close和View按钮。取而代之的是解锁iPhone打开app。
对你的app的最终修改全部发生在AppDelegate.m中,因为程序委托(application delegate)是负责接受推送信息的地方。有两个函数是关于这个的:
1. application:didFinishLaunchingWithOptions:. 如果你的app在信息推来的时候不在运行,他会启动消息会成为launchOptions字典中的一部分。
2. application:didReceiveRemoteNotification:. 这个方法当你的app是活动的且有消息推进来时被调用。在iOS 4.0或者之后的版本,如果你的app被后台延缓,他会被唤醒并且这个方法被调用。你可以通过使用UIApplication’sapplicationState属性来找出你的app是否被延缓。
两个方法都会接收到JSON格式的字典。我们不用手工去解析JSON,OS已经帮我们搞定了。
向didFinishLaunchingWithOptions的return语句之前添加下面几行:
if(launchOptions!=nil) { NSDictionary* dictionary = [launchOptionsobjectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; if(dictionary !=nil) { NSLog(@"Launched from push notification: %@", dictionary); [selfaddMessageFromRemoteNotification:dictionaryupdateUI:NO]; } }
这个用于判断他的启动选项并且它是否包含一个推送消息。如果是,我们调用addMessageFromRemoteNotification来处理消息。
向AppDelegate.m里面添加如下方法:
-(void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo { NSLog(@"Received notification: %@", userInfo); [selfaddMessageFromRemoteNotification:userInfoupdateUI:YES]; }
非常简单。这个方法依赖于addMessageFromRemoteNotification来处理所有的工作。
复制粘贴下面的方法到didFinishLaunchingWithOptions之上:
-(void)addMessageFromRemoteNotification:(NSDictionary*)userInfoupdateUI:(BOOL)updateUI { Message* message =[[Message alloc]init]; message.date=[NSDate date]; NSString*alertValue=[[userInfovalueForKey:@"aps"]valueForKey:@"alert"]; NSMutableArray* parts =[NSMutableArrayarrayWithArray: [alertValuecomponentsSeparatedByString:@": "]]; message.senderName=[parts objectAtIndex:0]; [parts removeObjectAtIndex:0]; message.text=[parts componentsJoinedByString:@": "]; int index =[dataModeladdMessage:message]; if(updateUI) [self.chatViewControllerdidSaveMessage:messageatIndex:index]; [message release]; }
这是最后一段代码,我发誓。让我们一行行的解释他。
Message* message =[[Message alloc]init];
message.date=[NSDate date];
首先我们创建了一个新的消息对象。我们将它填充推送消息的内容并且立即将它添加到DataModel中。
NSString*alertValue=[[userInfovalueForKey:@"aps"]valueForKey:@"alert"];
这是获得推送消息的警告框的文本。我们的推送消息被JSON封装成这样:
{ "aps": { "alert":"SENDER_NAME: MESSAGE_TEXT", "sound":"default" }, }
服务器把消息内容和发送者的名字放入了alert的里面。我们不关心字典中的其他成员。
NSMutableArray* parts =[NSMutableArrayarrayWithArray:[alertValuecomponentsSeparatedByString:@": "]]; message.senderName=[parts objectAtIndex:0]; [parts removeObjectAtIndex:0]; message.text=[parts componentsJoinedByString:@": "];
这段代码提取发送者的名字和消息内容。并将它们放入Message对象中。Sender的名字在第一个冒号之前。
int index =[dataModeladdMessage:message];
现在我们已经设置好了Message对象的属性,我们可以将它添加到DataModel中。
if(updateUI) [self.chatViewControllerdidSaveMessage:messageatIndex:index];
最后,我们告诉ChatViewController在它的table view中插入一个新行。然而我们不需要通过didFinishLaunchingWithOptions接收到信息的时候执行它。因为这时table view还没有被载入。如果我们试图插入一行,他会困惑的,程序将会崩溃。
好了,编译运行。用test_message.html 来发送推送信息,当app接收到消息的时候,屏幕左侧会添加对话泡泡。太棒了!
自定义推送
你可以回想起最初对推送消息的分析时说过我们能做的不只是发送消息。他还可以在收到推送消息的时候修改iPhone的提示音。我已经向app的resources中添加了一个叫做beep.caf的短音频文件
去api.php然后修改makePayload()函数下的这行:
$payload=’{“aps”:{“alert”:”‘.$nameJson.’: ‘.$textJson.’”,”sound”:”default”}}’;
变成这样 变成这样
$payload='{"aps":{"alert":"'.$nameJson.': '.$textJson.'","sound":"beep.caf"}}';
你不需要修改app或者重新编译他。关掉设备上的app,因为如果app开启的时候是不会播放任何声音的,故会影响这次实验。现在用test_message.html发送另一条推送吧。当警告窗口在你手机上弹出的时候,提示音将会改变。
来尝试下其他的参数吧!请尝试本地化按钮或者给app icon设置一个badge。(注意:如果你想使用badges,不要忘记在app中注册badge消息,现在只是注册了警告窗口和声音)
玩的愉快!
反馈服务器
APNS is happy to give you feedback!
你现在可能已经准备睡觉了(放松,休息片刻),但是这里还有一段服务器端需要的代码
设想一个场景:你服务器数据库中的一个表是记录很多用户的device Tokens。有时,一些用户将把你的app从他们的设备上移除。这是让你很难过但又无法避免的一件事。
然而你的服务器将不会知道的,因为没有人会告诉他app被卸载了。他还会继续开心的发送推送消息,但是实际没发出去,因为你的app已经不会接收到他们了。
这是一个我们不希望有的状态,所以我们引入了反馈服务器。你可以周期性的连接这个服务器,并且下载一份哪些device tokens不再使用的list。你需要停止给这些device tokens 发送消息。
在PushChatServer/push 文件夹中你可以找到用来连接反馈服务器并且下载tokens的feedback.php脚本和用来设置的feedback_config.php。这个脚本同样使用你的PEM文件和SSL证书以及私有Key来通过苹果服务器的认证。
不像push.php,反馈脚本不用像一个后台线程一样持续运行。取而代之,你应该使他在服务器上每小时工作一次。
脚本会和反馈服务器创建一个链接,下载那些废弃的device tokens,并且关闭链接。然后要做的就是从active_users里面删除对应的tokens
改善与限制
PushChat使用推送消息作为app唯一的接收消息的方式。这个对于此教程是OK的,但是拥有一个大问题。推送消息的投递是没保证的,所以一旦一条消息丢失了用户可能会缺少一部分数据。另外,如果用户按了关闭推送警告的按钮,app 永远不会看到消息。对于PushChat来说,就不会有聊天气泡被添加进来。
一个更好的做法是在服务器上存储数据,我们的API可以将每条信息放到数据库中一个叫做message的表中。当用户直接打开app或者回应一个推送消息时,他会发送一个”give me all new messages”的请求到服务器并把这些消息下载下来。这个方法使得app并不只依赖于推送消息来接收数据。
Push.php脚本工作的相当好,但是我不会在有很多用户的时候使用它。一个大数据规模的网络软件中PHP不是一个很好的选择。如果你有个大计划,你应该选择用C或者C++来实现的更强劲一些。或者去查看这个PHP的实现方式http://code.google.com/p/php-apns/.
何去何从?
这里有份样例工程包含这个教程所开发的全部代码。
虽然这篇教程的长度已经像短篇小说了,但是关于推送消息依然还有很多可以谈论的。如果你对你app中添加推送很较真,我建议你去学习一下下面那些do’s and don’ts的资源:
· Apple’s Local and Push Notification Programming Guide
· Section 5 of the App Store Approval Guidelines
· Session 129 of the WWDC 2010 videos. Most of this talk is about local notifications but there’s some discussion on push in the beginning.