苹果消息推送服务教程:第2部分(共2部分)

转自
http://www.raywenderlich.com/zh-hans/24733/苹果消息推送服务教程:第而部分(共2部分)

这个教程的原作者是Matthijs Hollemans, 他是一个经验丰富的iOS开发者和设计师,可供聘用!

这是我们苹果消息推送服务教程的第二部分。

在第一部分,我们学习了如何让iPhone app接收推送信息,并用一个简单的PHP脚本推送了一个提示信息。

在我们教程的第二部分,你将会学习编写一个运用APNS服务的app,并会写一个简单的PHP Web服务来推送信息!

注意: 这部分教程相对比较长,所以请留足一大段时间(以及零食)。但这么做是值得的,一旦你完成这个教程,你会有一个完整的app以及一个推送信息的Web服务。

PushChat

在这段教程里, 我们将完成一个简单的聊天app叫做PushChat。PushChat将用苹果的推送方式来传递信息。我们的app最后会是这样的:

用户看到的第一个界面是登陆界面。用户要输入他们的昵称和一个密码。用户应该告诉他的朋友这个密码。

所有使用这个密码的用户能看到彼此的信息。所以这个密码其实就像是聊天室的名字一样。当然,如果你成功地猜到了别人的密码你就可以偷看他们的聊天了。所以这是个密码。 ;-)

用户点击“Start!”按钮后,app会给服务器发送信号以便将这个用户与相应的聊天室联系起来。然后会进入聊天界面:

密码会显示在导航栏上。当前用户的信息会出现在屏幕右边,其他人的信息会出现在左边。以上图为例,当前用户和一个叫 SteveJ(猜猜他是谁?)的人都登陆了名为“TopSecretRoom123”的聊天室。

第三个,也是最后一个界面是信息编写界面:

没什么特别的。只是一个文本视图外加一个键盘。信息会被限制在190个字节以内。屏幕顶端会显示剩余的字节。

我加入这条限制的原因是推送的信息有256字节的长度限制,包括信息的格式所需要的字节。

当用户点击“Save”按钮时, 那个信息会被传送到我们的服务器,然后以推送信息的形式发送到所有登陆到这个聊天室的用户那里。

服务器API

刚才我们已经多次提到“我们的服务器”。我们需要一个服务器来将信息传递到不同用户设备上。

我用PHP和MySQL写了一个简单的网络Web服务。我们的iPhone程序将把下面的指令发到服务器上:

JOIN. 当一个用户登陆时,我们会将他的昵称,密码和设备代码发到服务器。服务器将这个用户资料加入到所有在线用户中。从此以后,同一个聊天室的任何一个成员发的信息就会被推送到这个新成员的设备上。

LEAVE. 这是和“JOIN”指令相反的指令。用户可以通过点击聊天界面的“Exit”按钮来离开聊天室。我们这时会把LEAVE指令发到服务器。服务器会把这个用户从在线用户列表中移除。他也不会再说收到这个聊天室的信息了。

MESSAGE. 当用户点击编写信息界面的“Save”按钮时,我们会把当前信息发到服务器。服务器会把这个文字信息转化成一个完整的推送信息,通过APNS发送给聊天室的每一个人。

UPDATE. 这个指令会让服务器知道用户有一个新的设备代码。设备代码有时会变。所以我们需要让服务器及时更新。我们等下会详细解释这是为什么的。

服务器指令的作用可以用下图表示出来:

我们的app会在适当时候将用户的在线情况和发送的信息传递到服务器。服务器再将这个信息通过APNS推送到其他聊天室成员的app上。

当app收到一个新的推送信息,在了聊天界面会出现一个新的信息泡泡。

创建服务器

如果你对Web服务完全不了解,你不妨先看看这个过于PHP和MySQL的教程。

在app的开发初期阶段,我们将会用MAMP在Mac上建立服务器和数据库。MAMP很容易使用,你也不用交付昂贵的独立服务器费用。

你的Mac和iPhone需要在同一个本地网络上,否则你的app是无法和服务器交流的。大多数人家里都有WiFi,所以这应该不成问题。

当然,在你的app要上交App Store审核时,你应该有一个真正的服务器来接收这些指令。

你可以在这里免费下载MAMP。(那个网站上还有一个付费的专业版,但是这个免费的版本对于我们已经够用了。)

MAMP内含一个阿帕奇服务器,PHP语言和一个MySQL数据库。我们三个都会用到。(如果你要在一个非MAMP的服务器上用我们教程中的PHP代码,你需要确保已经安装以下的扩展软件:PDO, pdo_mysql, mbstring, OpenSSL。)

安装MAMP是很容易的。解压并打开下载的DMG文件。接收许可协议然后把MAMP文件夹拖到Applications文件夹中就可以了!

在Applications/MAMP文件夹中点击MAMP图标(那个大象)。你应该看到如下MAMP的界面:

点击“Open start page”按钮。你应该会打开你的默认浏览器并看到如下欢迎界面:

太好了!现在下载PushChatServer服务器的代码并解压。我假设你解压到了桌面上,因为我们需要将相应的路径填入阿帕奇服务器的配置文件中。

打开Applications/MAMP/conf/apache/httpd.conf文件并加入:

Listen 44447<VirtualHost *:44447>DocumentRoot "/Users/matthijs/Desktop/PushChatServer/api"ServerName 192.168.2.244:44447ServerAlias pushchat.localCustomLog "/Users/matthijs/Desktop/PushChatServer/log/apache_access.log" combinedErrorLog "/Users/matthijs/Desktop/PushChatServer/log/apache_error.log"SetEnv APPLICATION_ENV developmentphp_flag magic_quotes_gpc off<Directory "/Users/matthijs/Desktop/PushChatServer/api">Options Indexes MultiViews FollowSymLinksAllowOverride AllOrder allow,denyAllow from all</Directory>
</VirtualHost>

有几行还需要进一步改动。我的PushChatServer文件放置在“/Users/matthijs/Desktop”路径上。你需要将它改成你解压至的路径。

将“ServerName”那一行的IP地址改成你Mac的IP地址。如果你不知道怎么找到你Mac的IP地址,你需要打开System Preferences按后打开Network面板。

我用的是我的MacBook的无线网的IP地址,但是以太网端口的IP地址也是可用的。注意,你的端口号应该仍然是44447

ServerName <your IP address>:44447

我们的Web服务使用44447号端口。这是一个任意选择的数字。网站大多使用80号端口。MAMP默认的网页使用的是8888号端口。我们选择的端口不会喝这两个冲突。

我同时用“pushchat.local”作为服务器的别名。这就好比是一个只有在本地网才能使用的域名。我们需要让这个域名与一个IP地址邦定。最简单的办法是修改“/etc/hosts”文件。将下面这行加入到文件的地端并保存:

127.0.0.1       pushchat.local

在MAMP窗口中,点击“Stop Servers”按钮。等指示灯变成红色后,点击“Start Servers”。如果你正确地修改了httpd.conf文件,服务器的两个指示灯都应该重新变成绿色的。

打开你最爱的浏览器,前往 http://pushchat.local:44447。你应该看到如下信息:

If you can see this, it works!

太好了。这说明阿帕奇和服务器API的PHP代码已经成功地安装上了。现在我们需要设置数据库。

设置数据库

返回MAMP的开始页(在MAMP桌面窗口中点击“Open start page”按钮)并点击“phpMyAdmin”按钮。你的页面应该和下图相似:

在Create Database栏输入“pushchat”并在Collation栏选择“utf8_general_ci”。点击“Create”按钮来创建一个叫做“pushchat”的数据库。

在页面上端找到“Privileges”栏,然后点击“Add a new user”

填写空格如下:

  • User name: pushchat
  • Host: localhost
  • Password: d]682#%yI1nb3
  • Privileges: 在 “Grant all privileges on database “pushchat””旁打钩

点击“Go”按钮来添加这个用户。你可以选择另外一个密码。但是你需要在相应的PHP脚本中更新这个新的密码。

数据库和用户都设置好了。现在我们需要在数据库中加入表格。在屏幕上方选择“SQL”栏然后将下面的指令粘贴到文本栏中:

USE pushchat;SET NAMES utf8;DROP TABLE IF EXISTS active_users;CREATE TABLE active_users
(udid varchar(40) NOT NULL PRIMARY KEY,device_token varchar(64) NOT NULL,nickname varchar(255) NOT NULL,secret_code varchar(255) NOT NULL,ip_address varchar(32) NOT NULL
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;

(你也可以在PushChatServer/database/api.sql文件中找到这些指令。)

点击“Go”按钮来执行这些指令。我们在数据库中加入了一个叫做“active_users”的表格。我们会在服务器API接收到一个JOIN指令时在这个表格中加入一个新的用户档案。我们也会在服务器API接收到一个LEAVE指令时将相应的用户档案从这个表格中删除。

我们还需要加入另外一个表格。重复上述步骤并粘贴下面指令:

USE pushchat;SET NAMES utf8;DROP TABLE IF EXISTS push_queue;CREATE TABLE push_queue
(message_id integer NOT NULL AUTO_INCREMENT,device_token varchar(64) NOT NULL,payload varchar(256) NOT NULL,time_queued datetime NOT NULL,time_sent datetime,PRIMARY KEY (message_id)
)
ENGINE=InnoDB DEFAULT CHARSET=latin1;

(你也可以在PushChatServer/database/push.sql文件里找到这些指令。)

在服务器收到一个MESSAGE指令时,我们会把要推送的信息加到这个表格里。

设置服务器API

我们在这个教程中没有足够的篇幅来详细描述如何操作服务器的API。但是我在api.php代码中加入了详细的注解,所以就算你对PHP并不熟悉,你应该还是可以理解API是怎么运作的。

我们现在将注意力转到api_config.php文件上。这个文件包含了我们服务器API的设置信息。

当前我们有两组配置选项,一组在开发时用,另一组在生产时用。我们可以随意地交换这两组选项。

但是我们怎么让我们的API知道我们在使用开发配置还是生产配置呢? 记得我们在httpd.conf文件中加入了关于VirtualHost的一块代码:

<VirtualHost *:44447>…SetEnv APPLICATION_ENV development...
</VirtualHost>

“SetEnv APPLICATION_ENV development”设置了一个环境变量。我们的服务器会根据这个变量来选择相应的配置选项。 如果你想使用生产选项,你可以把这快代码删掉或者将“development” 改成 “production”。
如果你用了和我一样的数据库名称,用户名和密码,那你就不需要更改你的api_config.php文件。否则你需要更改相应的值,否则你的API将无法连上数据库。

用你最爱的浏览器打开http://pushchat.local:44447/test/database.php链接。你应该看到如下信息:

Database connection successful!

小贴士: 如果你遇到错误,那就看看Applications/MAMP/logs/php_error.log文件以及PushChatServer/log/apache_error.log文件。这些错误记录文件将提供关于你遇到的错误的有用信息。

我们的服务器API就完成了!

让我们开始写代码吧!

下载PushChatStarter的代码并解压缩。这个PushChat程序的版本不包含任何网络或者推送代码。我会先简单介绍现有的代码,然后解释如何让我们的app和服务器交流并接收推送信息。

因为PushChat还没有任何接收推送信息的功能,你暂时还可以在模拟器中运行这个程序。

首先,在Xcode中打开这个项目并来到“Target Settings”选项栏。你需要将“bundle ID”的值从“com.hollance.PushChat”改为你自己的bundle ID的值。(你在苹果置备门户网站上创建App ID时使用的那个bundle ID)。这是因为你的app需要使用你自己的配置文件。

编译并运行那个app。你最好先卸载之前的那个用于测试的app然后再编译现在的这个app,因为这个新的app使用了一样的bundle ID,iPhone可能会分不清楚这两个app。

打开MainWindow.xib文件,看看这个app的用户界面的架构:

主界面的ChatViewController里还包含了一个导航控制器。这个视图控制器将负责显示发送和接收的信息。正如你在nib文件中看到的,这是一个UITableViewController的子类。

信息泡泡属于MessageTableViewCell的对象。 MessageTableViewCell又是UITableViewCell的子类。SpeechBubbleView 是UIView的子类.这里我们会使用基本的表格视图,你应该已经非常熟悉如何使用这些表格对象了,所以我们就不详细揭示了。

App中的其他两个界面分别是用户登陆界面和编写信息界面。它们都是modal view,并将会出现在ChatViewController上方。登陆界面的代码在LoginViewController中,编写信息界面的代码在ComposeViewController。

现在就剩DataModel和Message这两个数据模型的类了。Message用来储存一个信息的内容。每个信息都会有发送者的名字,时间以及信息的内容数据。

DataModel是用来管理一组Message的对象的。当用户发送或者接收到一则信息的时候,DataModel将它加入自己的列表中并储存到app的文档中。当app启动时,DataModel再将信息从文档中加载到app里。

上面是对这个app非常简短的介绍。我建议你可以浏览一下app的代码,读读代码的注释,加深理解。

连接服务器

我们现在将逐个地在各个界面中加入相应的代码,让app能和服务器交流。我们的服务器能接收app发送的HTTP POST请求,所以我将用ASIFormDataRequest来发送数据到服务器。
如果你没有用过IOS app中的web服务,我建议你先看看Ray写的一篇相关的教程。

在defs.h文件中加入如下:

#define ServerApiURL @"http://192.168.2.244:44447/api.php"

我们将把HTTP POST请求发送到上面的那个URL。你应该把IP地址改为你的服务器的IP地址,并且确保你已经开启了MAMP程序以及你的iPhone和服务器都链接到了同一个网络中。

我们先从登陆界面开始吧。当用户点击了“Start”按钮后,我们将发送一个JION指令到服务器上。让服务器知道一个用户成功登陆了。服务器将把这个用户加入到在线用户表(active_users table)中。

在LoginViewController顶端加入下面代码:

#import "ASIFormDataRequest.h"
#import "MBProgressHUD.h"

在userDidJoin和loginAction方法间加入下面代码:

- (void)postJoinRequest
{MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Connecting", nil);NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self];[request setPostValue:@"join" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:[dataModel deviceToken] forKey:@"token"];[request setPostValue:[dataModel nickname] forKey:@"name"];[request setPostValue:[dataModel secretCode] forKey:@"code"];[request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidJoin];}}}];[request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}];[request startAsynchronous];
}

让我们一行一行来解释上面的代码。

  MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Connecting", nil);

这里我们显示一个全屏的负荷指示。你可以在Ray的其他教程中深入学习MBProgressHUD的使用方法。

 NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self];

我们使用服务器的URL来创建了一个ASIFormDataRequest对象。ASIFormDataRequest能方便我们发送POST请求到服务器上。你只需要设置URL地址和POST的内容就行了。我等下会解释“__block”是什么。

   [request setPostValue:@"join" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:[dataModel deviceToken] forKey:@"token"];[request setPostValue:[dataModel nickname] forKey:@"name"];[request setPostValue:[dataModel secretCode] forKey:@"code"];

在这里我们设置了POST请求的数据。我设计的服务器API会在POST的数据里寻找“cmd”选项。这个选项决定了API将会执行哪一项指令。在这里,它将执行“join”指令。

“Join”需要四个参数。用户昵称和密码是当然需要的,用户会在登陆时输入这些数据。但是什么事“udid” 和 “token”呢?“token”其实是当前设备的编码。这样服务器才知道应该将信息推送到哪个设备上。

那个UDID是设备的id号码。我们在数据库中决定用设备的id号码来辨认用户。我们也可以使用用户的昵称,但是这样的话我们就需要保证连个用户不会使用同一个昵称。编写这些逻辑会使app更复杂。
我们还可以使用设备的编码来辨别用户,但是设备的编码有时会改变,所以也不是最好的选择。UDID是不会改变的。我并不是说所有web服务都应该用设备的UDID,但在我们现在的情况下,用UDID就足够了。

 [request setCompletionBlock:^{...}];

我们在用“blocks”!一般有两种办法来处理ASIFormDataRequest返回的结果。一是使用delegate的requestFinished方法和requestedFailed方法– Ray在他的web服务的教程中用了这种方法。

但是在OS 4.0后,使用blocks的方法出现了,并能替代delegates的方法。以学习为目的,我们将在这个教程中使用blocks。这里我们设置了请求完成时执行的block。在“^{ }”符号之间的代码只有在请求成功完成后才会执行。

还记得在创建ASIFormDataRequest对象时我们用了一个奇怪的“__block”关键字吗?这确保blocks不会记住对ASIFormDataRequest对象的引用。因为ASIFormDataRequest对象已经记住了对block的引用,如果block再记住对ASIFormDataRequest对象的引用,就会形成一个环形引用的情况,从而造成内存泄露。如果这听起来很复杂,那只要记住当你用blocks而不是delegate方法时,都要在在ASIFormDataRequest对象前加上“__block”关键字就行了。

在block里,我们有下面的代码:

     if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];

首先,我们检查那个视图是否仍然活跃。因为请求是在后台线程中异步完成的,理论上说当它完成时,发送请求的视图已经不再活跃了。比如低内存警告可能会导致当前视图被中止。在这种情况下,我们就忽略已经发送了的请求结果。

这样做可能对于我们写的这个简单的app来说是有点夸张了。但是我总喜欢做到万无一失。特别是在有后台线程和异步请求的情况下更要特别小心。

剩下的请求成功后执行的block代码如下:

         if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidJoin];}

我们先检查服务器API发的HTTP回复的状态代码。如果一切正常,代码会是“200 OK”。但是,电脑和网络可能会出错。比如我们的MySQL数据库可能会掉线,那服务器API就会回复“500 Server Error”来告诉用户服务器有问题。我们需要显示一个错误视图让用户知道。

ShowErrorAlert()函数会创建一个UIAlertView并在当前用户界面上显示。注意我总是用NSLocalizedString()来创建需要显示的字符串。如果将来我们要把这个app翻译到其他语言,这样创建字符串会使翻译更加简单。如果想了解更多关于翻译app到其他语言,可以读读Sean Berry写的这篇教程。

如果一切正常,我们调用userDidJoin方法。这个方法会更新我们的数据模型,关闭弹出的视图并回到主用户界面。

如果请求因某种原因失败了,我们会执行下面的代码。一般这种失败是因为请求无法到达服务器,或者服务器很长时间都没有回应造成的。

 [request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}];

这里,我们就隐藏那个负荷指示并显示一个错误信息的视图。

 [request startAsynchronous];

在对这个ASIFormDataRequest对象做了这么多的设置后,我们终于发送了这个请求。如果你将请求发送到服务器,你都应该使用异步请求,因为这个请求可能会需要好几秒钟才能完成。如果你用同步请求,你app的用户界面在这段时间内就会完全无法使用。并且如果用户界面一直反应迟钝,操作系统最终会中止你的程序。

在LoginViewController.m文件里,对loginAction做如下改动:

- (IBAction)loginAction
{...// 将下面这行代码改成:[self userDidJoin];// 这样:[self postJoinRequest];
}

之前,我们在用户点击start按钮后直接调用userDidJoin函数来假装用户登陆。现在我们只有在服务器成功将用户登陆后才调用这个函数。

注意我们调用了DataModel类的两个还不存在的函数udid和deviceToken,下面的代码能消除编译器的警告信息。

在DataModel.h文件中,加入:

- (NSString*)udid;
- (NSString*)deviceToken;
- (void)setDeviceToken:(NSString*)token;

在DataModel.m文件中, 在@implementation后加入:

- (NSString*)udid
{UIDevice* device = [UIDevice currentDevice];return [device.uniqueIdentifier stringByReplacingOccurrencesOfString:@"-" withString:@""];
}

这个函数会帮我们获取用户设备的UDID。UDID一般会有破折号,但是我们将消除这些破折号,只剩下一个40字节长的字符串。(注意,模拟器的设备ID只有32个字节。)

还在DataModel.m文件中,将下面的这行代码加入文件顶端,在@implementation上面:

static NSString* const DeviceTokenKey = @"DeviceToken";

在@implementation后加入下面的函数:

- (NSString*)deviceToken
{return [[NSUserDefaults standardUserDefaults] stringForKey:DeviceTokenKey];
}- (void)setDeviceToken:(NSString*)token
{[[NSUserDefaults standardUserDefaults] setObject:token forKey:DeviceTokenKey];
}

我们还需要对初始方法做如下修改:

+ (void)initialize
{if (self == [DataModel class]){[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:@"", NicknameKey,@"", SecretCodeKey,[NSNumber numberWithInt:0], JoinedChatKey,// 加入下面这行代码:@"0", DeviceTokenKey,nil]];}
}

在setDeviceToken函数里,我们把设备的编码储存到了NSUserDefaults字典里。在deviceToken函数中,我们可以从NSUserDefaults中重新获取这个编码。如果你还不太了解什么是NSUserDefaults,它其实是一个方便储存app设置的类。

DataModel在创建一个对象时,会自动调用初始函数。我们在这里将所有存在NSUserDefaults内的属性都设为相应的默认值。

我们加入的那行代码将设备编码属性的值设为字符串@”0″。我们等会会解释为什么这是必要的。但是现在的情况是,当我们把JOIN指令发送到服务器,@”0″会作为设备的编码被发送到服务器。

我们刚才解释了很多代码。现在是时候试试编译并运行这些代码看看到底能不能用。

输入用户昵称和密码然后点击“Start”按钮。这里我们用了“MisterX”和“TopSecret”。写着“Connecting”的符合指示会出现一会。如果和服务器的交流顺利的话,登陆界面会被主用户界面所替代。

但如果你得到一个错误信息的话,可以试试这些贴士:确保MAMP已经启动了。在Apache Server和MySQL Server项旁边应该都有一个绿色的指示灯。确保你可以用浏览器打开你服务器的IP地址。同时确保你在defs.h文件中的服务器IP是正确的。最后,你的iPhone应该和服务器在同一个网络里。

如果我们在服务器这边也能确认请求的接收和处理过程就好了。其实这不难做到。在接收了JOIN请求后,服务器API会在数据库的active_users表中加入这个用户的记录。我们可以同过phpMyAdmin来看这个表的内容来确认这一点。

点击MAMP的“Open start page”按钮然后在浏览器中点击phpMyAdmin。进入pushchat数据库,选择active_users表,然后点击Browse(查看)选项。你应该看到和下图类似的情况:

表格中应该有一行关于MisterX的数据,密码为TopSecret,设备编码是“0”。服务器API还记录了发送请求设备的IP地址。

在真实的服务器中,我们一般会把所有信息全部记录下来,比如app的版本,操作系统版本,用户用的是哪种设备,请求发送的时间等等。这些信息在分析错误原因和用户使用情况时会起到很大的作用。

完成与服务器的交流

如果你理解我们在LoginViewController中做得修改,那下面的部分将会非常简单,因为本质上我们在做同一件事。

将下面几行代码加到ChatViewController.m文件顶端:

#import "ASIFormDataRequest.h"
#import "MBProgressHUD.h"

在userDidLeave函数和exitAction函数之间,加入:

- (void)postLeaveRequest
{MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];hud.labelText = NSLocalizedString(@"Signing Out", nil);NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self];[request setPostValue:@"leave" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidLeave];}}}];[request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}];[request startAsynchronous];
}

上面的代码看起来应该非常熟悉。我们创建一个ASIFormDataRequest对象并将“cmd”的值设为“leave”。我们还加上了UDID,这样服务器才会知道是哪一个用户要登出。剩下的代码和之前我们写的几乎一摸一样,除了在请求成功后,我们调用了userDidLeave函数来做用户登出后的收尾工作。

将exitAction函数修改如下:

- (IBAction)exitAction
{[self postLeaveRequest];
}

编译并运行app,在主界面点击“Exit”按钮。现在打开phpMyAdmin然后刷新active_users表。这个表格应该没有任何内容了。在服务器API收到“LEAVE”指令后,它就将那个用户从数据库的表格中移除了。

最后对ComposeViewController.m文件进行一些修改,在文件顶端加入:

#import "ASIFormDataRequest.h"
#import "MBProgressHUD.h"

在userDidCompose函数与cancelAction函数间,加入:

- (void)postMessageRequest
{[messageTextView resignFirstResponder];MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Sending", nil);NSString* text = self.messageTextView.text;NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self];[request setPostValue:@"message" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:text forKey:@"text"];[request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"Could not send the message to the server", nil));}else{[self userDidCompose:text];}}}];[request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}];[request startAsynchronous];
}

大部分代码和上面一致。唯一的区别是在这里我们先将键盘隐藏了。否则符合指示会出现在键盘的后面,看起来很难看。然后我们把“message”指令以及用户的UDID和信息内容发送到API。如果一切正常,我们调用userDidCompose函数将一个Message对象加入到DataModel中,让其显示在屏幕上。

修改saveAction函数如下:

- (IBAction)saveAction
{[self postMessageRequest];
}

编译并运行程序。登陆然后点击“compose”按钮。键入一则信息然后点击保存按钮。片刻后,你的信息应该在屏幕上显示出来。

推送的准备工作

终于,我们可以在app里加入推送信息的功能了。我们已经介绍过如何为在app中注册推送信息的功能以及如何获取设备编码。我们会在AppDelegate.m文件中重复这一步骤。

在application:didFinishLaunchingWithOptions:函数中,在return语句前加入下面代码:

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{...[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)];return YES;
}

我们的app会用提示音和提示信息,但不会显示一个数量小图标。

在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];}

didRegisterForRemoteNotificationsWithDeviceToken方法在成功注册信息推送后才会被调用。因为这是异步操作,可能需要好几秒才会完成。特别是在用户第一次试图获取设备代码时所需的时间会长些。

所以理论上说当didRegisterForRemoteNotificationsWithDeviceToken被调用时,用户可能已经点击“Start”按钮并登陆到聊天界面了。在这中情况下,app会把设备编码的默认值,@”0″,先发送到服务器。这不是一个有效的设备编码,所以我们不能用它推送信息。 

如果我们在用户加入聊天后才收到设备编码,我们需要尽快将这个编码更新到服务器。这就是API里“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的交流就完成了。现在让我们看看服务器接收到新的信息后需要如何处理并将这个信息推送出去。

服务器推送信息

在PushChatServer文件夹中有一个push文件夹,这个文件夹包含推送信息所需要的PHP代码。 你应该把这些文件放到服务器上用户无法直接访问的地方。因为你不能让用户下载你的服务器密钥。
在push文件夹中最重要的文件是push.php。你的服务器应该在后台线程上跑这个脚本。每隔几秒钟这个脚本的代码会将未发送的推送信息发到苹果的推送服务器。

首先我们需要修改push_config.php文件。push.php文件会使用这个文件中的设置选项的值。你可能需要更改密钥的密码和数据库的密码,将它们改为你自己之前设的值。

这个脚本可以在开发和生产两个模式下运行,和服务器API类似。在开发模式下,它会和苹果推送服务的沙盒服务器联系并使用你的开发SSL证书。你服务器的开发模式应该和app的debug模式一起使用。你的app放到app store后才用生产模式。

在push文件夹中有一个叫做ck_development.pem的文件。你应该用教程一开始生成的PEM文件来替代这个文件。

现在打开一个新的终端窗口并执行下面的指令:

$ /Applications/MAMP/bin/php5.2/bin/php push.php development

我们在开发模式下启动了push.php脚本。注意我们上面用的是MAMP版本的PHP,而不是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必须在后台线程中运行,所以它不能直接将输出显示在终端窗口里。它会把输出导入到这个log文件中。每当这个脚本发送一个推送信息,这个log文件就会增加一行记录。

注意我们现在并不是在后台线程上运行这个脚本。在开发过程中直接运行会比较方便。(如果你想停止push.php的运行,你只需要按下Ctrl和C键。)但是在你的生产服务器上,你应该这样启动这个脚本:

$ /Applications/MAMP/bin/php5.2/bin/php push.php production &

“&”符号会将脚本放入后台线程中运行。

push.php文件的作用到底是什么呢?首先,它会和苹果推送服务器建立安全连接,并保持这个连接。我看过许多错误的例子。他们每次发送信息到苹果服务器时都会重新建立连接。苹果其实并不鼓励这么做。每次都建立新的连接是非常消耗处理器和网络资源的。保持这个连接相对比较有效。

一旦连接建立后,那个脚本会进入一个无限循环。每一次循环它都会检查push_queue数据库表格。如果有一行的“time_sent”栏的值为NULL,这说明这个信息还没有被推送。那个脚本就会将设备编码和JSON格式的信息数据打包成二进制格式,发送到苹果的推送服务器上(简称APNS)。

如果你对这个二进制格式感兴趣,我建议你看看苹果开发文档中,本地和推送提示编程指南的“The Binary Interface and Notification Formats”(二进制提示信息格式)这一章节。

在push.php发送完新的信息后,它会在time_sent栏填入当时的时间印章。然后这个脚本会休眠几秒钟,之后会重复上述步骤无限循环下去。

这意味着如果你想给用户推送一个信息,你只需要将这个信息输入到push_queue表格中就可以了。而服务器API脚本在从iPhone app接收到MESSAGE指令时恰恰就是这么做的。

我们真的能推送一些信息了吗?!

如果你有两个iPhone,你可以在两个设备上同时登陆并加入一个聊天室(通过使用同一个密码)。当其中一个手机发送信息到服务器时,几秒钟后另外一个手机应该能收到推送的信息。
但是如果你没有两个设备怎么办?那我们只能假装在和另外一个用户聊天了。用你的浏览器打开:http://pushchat.local:44447/test/api_join.html

你应该看到一个简单的HTML表格:

我们可以用这个表格来给服务器发送POST请求,就像我们在真实设备里做的一样。对于服务器来说是一样的,所以这对测试非常方便。
填入40个字符的UDID和64字符的设备编码(device token)以及一个昵称。code应该和你的app登陆的密码一致。UDID和设备编码(device token)的值其实并不重要,只要他们和你的iPhone上的不一样就行。否则服务器无法区分这两个不同的客户端。

点击Submit按钮。你可以通过在phpMyAdmin中查看active_users表格来确认我们的虚拟用户已经成功登陆了。
现在用你的浏览器打开:http://pushchat.local:44447/test/api_message.html
确保UDID和你刚才登陆时用的一样,输入一个信息然后点击Submit按钮。几秒钟后你应该能在手机app上看到这个推送的信息。祝贺你!

如果你没有接收到推送的信息,关闭app然后再试一次。我们在app里还没有代码来处理收到的信息,所以推送的信息只有在app没有在前台运行时才会出现。

苹果消息推送服务教程:第2部分(共2部分)相关推荐

  1. 苹果消息推送服务教程:第二部分(共2部分)

    这篇文章还可以在这里找到 英语 Create a simple chat app with Apple Push Notification Services! 这个教程的原作者是Matthijs Ho ...

  2. 苹果消息推送服务教程:第一二部分(共2部分)

    苹果消息推送服务教程:第一部分(共2部分) 转自 http://www.raywenderlich.com/zh-hans/24732/苹果消息推送服务教程:第一部分(共2部分) 这是iOS教程团队的 ...

  3. 苹果消息推送服务教程(下有php服务端DEMO,已验证可用)

    转载自:http://www.raywenderlich.com/zh-hans/24732/苹果消息推送服务教程:第一部分(共2部分) 这篇文章还可以在这里找到 英语 文琳照着这个做的:http:/ ...

  4. 苹果消息推送服务教程:第一部分(共2部分)

    这篇文章还可以在这里找到 英语 Learn how to add Push Notifications into your iPhone app! 这是iOS教程团队的Matthijs Hollema ...

  5. 苹果消息推送服务教程(三步曲)-超详细

    mtrabelsi  (原作者) 第一部分 在iOS系统中,在后台运行的程序能够进行的操作是非常有限的.这种限制是为了节省手机电池. 但是,如果你需要在用户没有使用你的程序的情况下给他们推送消息该怎么 ...

  6. 苹果消息推送服务教程

    目前找到的网上关于iOS Push最详细的文章,很不错,而且还有php代码. http://www.raywenderlich.com/3443/apple-push-notification-ser ...

  7. 苹果百度手机消息推送服务器,苹果消息推送服务教程

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 创建PEM文件 现在我们总共有三个文件: CSR 私有密钥(PushChatKey.p12) SSL证书(aps_developer_identity.c ...

  8. 手把手教你配置苹果APNS推送服务|钿畑的博客 | 钿畑的博客

    http://www.360doc.com/content/15/0118/17/1073512_441822850.shtml# 钿畑的文章索引 1. 什么是推送通知 2. 什么是APNS? 3. ...

  9. 手把手教你配置苹果APNS推送服务

    1. 什么是推送通知 消息通知分本地通知和远程推送通知,是没有运行在前台的应用程序可以让它们的用户获得相关消息通知的方式.消息通知可能是一条消息,即将发生的日历事件,或远程服务器的新数据.当被操作系统 ...

最新文章

  1. 涂抹功能_贴片面膜、水洗面膜、涂抹面膜这三款怎么选?
  2. html 按下和松开事件,JQuery通过键盘控制键盘按下与松开触发事件
  3. 基于epoll实现简单的web服务器
  4. 学python找什么工作-学Python能找到什么工作?这4种工作最热门!
  5. Oracle 临时表解决ORA-22992问题
  6. oracle11g-asm实例中asmlib和raw的使用问题
  7. 最新17个紫色风格网页设计作品欣赏
  8. iOS 自定义view里实现控制器的跳转
  9. lombok 的使用
  10. chainmaker go.mod no such file or directory
  11. SAS安装时出现的问题:Diagram Control
  12. linux中不用命令安装flash,如何在Linux下安装flash player
  13. python图片分析中央气象台降水量预报_全国降水量预报图。图片来源:中央气象台网站...
  14. idea工具首次提交代码到git上
  15. 十年BAT架构履历,铁柱磨针成188页Java核心知识点
  16. wps提示系统缺失字体
  17. 一个域名可以对应多个IP吗?如何通过DNS实现?
  18. 3g安卓市场_现在你还纠结选择苹果还是安卓手机吗?
  19. web系统快速开发_开发一个快速销售系统
  20. DFC-3C和DFC-3B的区别和注意事项

热门文章

  1. 【C++】关于char * tempbuffer = new char[100];
  2. PHP 图片处理类(水印、透明度、缩放、相框、锐化、旋转、翻转、剪切、反色)...
  3. 图书管理程序(c语言实现)
  4. 试用计算机绘制*组成的图案c语言,C语言绘图与计算机仿真技术
  5. 通信系统的组成(精简介绍)
  6. 各种品牌的PLC协议转换网关介绍
  7. OpenWrt之IPTV单线复用详细教程
  8. CSS transform属性
  9. 安装QQ的时候显示创建文件夹失败,无法正常安装,请尝试选择新的安装目录的解决办法
  10. Java--敲重点!JDK1.8 HashMap特性及底层数组+单链表+红黑树知识(建议收藏)