2015-05-27
本章内容
● iOS应用中的网络错误源
● 检测网络的可达性
● 错误处理的经验法则
● 处理网络错误的设计模式
到目前为止,我们所介绍的iPhone与其他系统的网络交互都是基于一切正常这个假设。本章将会放弃这个假设,并深入探究网络的真实世界。在真实世界中,事情是会出错的,有时可能是非常严重的错误:手机进入与离开网络、包丢掉或是延迟;网络基础设施出错;偶尔用户还会出错。如果一切正常,那么编写iOS应用就会简单不少,不过遗憾的是现实并非如此。本章将会探讨导致网络操作失败的几个因素,介绍系统如何将失败情况告知应用,应用又该如何优雅地通知用户。此外,本章还将介绍如何在不往应用逻辑中添加错误处理代码的情况下,以一种整洁且一致的方式处理错误的软件模式。
早期的iOS有个很棒的天气预报应用。它在Wi-Fi和信号良好的蜂窝网络下使用正常,不过当网络质量不那么好时,这个天气预报应用就像感冒似的,在主屏幕上崩溃。有不少应用在出现网络错误时表现很差劲,会疯狂弹出大量UIAlertView以告诉用户出现了“404 Error on Server X”等类似信息。还有很多应用在网络变慢时界面会变得没有响应。这些情况的出现都是没有很好地理解网络失败模式以及没有预期到可能的网络降级或是失败。如果想要避免这类错误并能够充分地处理网络错误,那么你首先需要理解它们的起源。
考虑一个字节是如何从设备发往远程服务器以及如何从远程服务器将这个字节接收到设备,这个过程只需要几百毫秒的时间,不过却要求网络设备都能正常工作才行。设备网络与网络互联的复杂性导致了分层网络的产生。分层网络将这种复杂环境划分成了更加易于管理的模块。虽然这对程序员很有帮助,不过当数据在各个层之间流动时可能会产生之前提到的网络错误。图5-1展示了Internet协议栈的各个层次。
图5-1
每一层都会执行某种错误检测,这可能是数学意义上的、逻辑意义上的,或是其他类型的检测。比如,当网络接口层接收到某一帧时,它首先会通过错误校正码来验证内容,如果不匹配,那么错误就产生了。如果这个帧根本就没有到达,那就会产生超时或是连接重置。错误检测出现在栈的每一层,自下而上直到应用层,应用层则会从语法和语义上检查消息。
在使用iOS中的URL加载系统时,虽然手机与服务器之间的连接可能会出现各种各样的问题,不过可以将这些原因分成3种错误类别,分别是操作系统错误、HTTP错误与应用错误。这些错误类别与创建HTTP请求的操作序列相关。图5-2展示了向应用服务器发出的HTTP请求(提供来自于企业网络的一些数据)的简单序列图。每块阴影区域都表示这3种错误类型的错误域。典型地,操作系统错误是由HTTP服务器问题导致的。HTTP错误是由HTTP服务器或应用服务器导致的。应用错误是由请求传输的数据或应用服务器查询的其他系统导致的。
图5-2
如果请求是安全的HTTPS请求,或是HTTP服务器被重定向客户端,那么上面这个序列的步骤将会变得更加复杂。上述很多步骤都包含着大量的子步骤,比如在建立TCP连接时涉及的SYN与SYN-ACK包序列等。下面将会详细介绍每一种错误类别。
操作系统错误是由数据包没有到达预定目标导致的。数据包可能是建立连接的一部分,也可能位于连接建立的中间阶段。OS错误可能由如下原因造成:
● 没有网络――如果设备没有数据网络连接,那么连接尝试很快就会被拒绝或是失败。这些类型的错误可以通过Apple提供的Reachability框架检测到,本节后面将会对此进行介绍。
● 无法路由到目标主机――设备可能有网络连接,不过连接的目标可能位于隔离的网络中或是处于离线状态。这些错误有时可以由操作系统迅速检测到,不过也有可能导致连接超时。
● 没有应用监听目标端口――在请求到达目标主机后,数据包会被发送到请求指定的端口号。如果没有服务器监听这个端口或是有太多的连接请求在排队,那么连接请求就会被拒绝。
● 无法解析目标主机名――如果无法解析目标主机名,那么URL加载系统就会返回错误。通常情况下,这些错误是由配置错误或是尝试访问没有外部名字解析且处于隔离网络中的主机造成的。
在iOS的URL加载系统中,操作系统错误会以NSError对象的形式发送给应用。iOS通过NSError在软件组件间传递错误信息。相比简单的错误代码来说,使用NSError的主要优势在于NSError对象包含了错误域属性。
不过,NSError对象的使用并不限于操作系统。应用可以创建自己的NSError对象,使用它们在应用内传递错误消息。如下代码片段展示的应用方法使用NSError向调用的视图控制器传递回失败信息:
-(id)fetchMyStuff:(NSURL*)url error:(NSError**)error{BOOL errorOccurred = NO; // some code that makes a call and may fail if(errorOccurred) //some kind of error{NSMutableDictionary *errorDict = [NSMutableDictionary dictionary];[errorDictsetValue:@"Failed to fetch my stuff"forKey:NSLocalizedDescriptionKey];*error = [NSErrorerrorWithDomain:@"myDomain"code:kSomeErrorCodeuserInfo:errorDict];return nil;} else {return stuff} }
NSError 对象有如下3 个主要属性:
● code――标识错误的NSInteger 值。对于产生该错误的错误域来说,这个值是唯一的。
● domain ―― 指定错误域的NSString 指针, 比如NSPOSIXErrorDomain 、NSOSStatusErrorDomain 及NSMachErrorDomain。
● userInfo――NSDictionary 指针,其中包含特定于错误的值。
URL 加载系统中产生的很多错误都来自于NSURLErrorDomain 域,代码值基本上都来自于CFNetworkErrors.h 中定义的错误代码。与iOS 提供的其他常量值一样,代码应该使用针对错误定义好的常量名而不是实际的错误代码值。比如,如果客户端无法连接到主机,那么错误代码是1004,并且有定义好的常量kCFURLErrorCannotConnectToHost。代码绝不应该直接引用1004,因为这个值可能会在操作系统未来的修订版中发生变化;相反,应该使用提供的枚举名kCFURLError。
如下是使用URL 加载系统创建HTTP 请求的代码示例:
NSHTTPURLResponse *response=nil;NSError *error=nil;NSData *myData=[NSURLConnectionsendSynchronousRequest:requestreturningResponse:&responseerror:&error];if (!error) {// No OS Errors, keep going in the process...} else {// Something low level broke}
- (void)connection:(NSURLConnection *)connectiondidFailWithError:(NSError *)error
这是传递给请求委托的最终消息,委托必须能识别出错误的原因并作出恰当的反应。在如下示例中,委托会向用户展UIAlertView:- (void) connection:conndidFailWithError:error {UIAlertView *alert = [UIAlertViewalloc] initWithTitle:@"Network Error"message:[error description]delegate:selfcancelButtonTitle:@"Oh Well"otherButtonTitles:nil];[alert show];[alert release];}
选定好项目目标后,找到设置中的Linked Frameworks and Libraries,单击+按钮添加框架,这时会出现框架选择界面。选择SystemConfiguration 框架,单击add 按钮将其添加到项目中。
如下代码片段会检查是否存在网络连接。不保证任何特定的主机或IP 地址是可达的,只是标识是否存在网络连接。
#import "Reachability.h"...if([[Reachability reachabilityForInternetConnection]currentReachabilityStatus] == NotReachable) {// handle the lack of a network}
#import "Reachability.h"...NetworkStatus reach = [[Reachability reachabilityForInternetConnection]currentReachabilityStatus];if(reach == ReachableViaWWAN) {// Network Is reachable via WWAN (aka. carrier network)} else if(reach == ReachableViaWiFi) {// Network is reachable via WiFi}
#import "Reachability.h"...[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(networkChanged:)name:kReachabilityChangedNotificationobject:nil];Reachability *reachability;reachability = [[Reachability reachabilityForInternetConnection] retain];[reachability startNotifier];
- (void) networkChanged: (NSNotification* )notification{Reachability* reachability = [notification object];第Ⅱ部分 HTTP 请求:iOS 网络功能98if(reachability == ReachableViaWWAN) {// Network Is reachable via WWAN (a.k.a. carrier network)} else if(reachability == ReachableViaWiFi) {// Network is reachable via WiFi} else if(reachability == NotReachable) {// No Network available}}
Reachability *reach = [ReachabilityreachabilityWithHostName:@"www.captechconsulting.com"];if(reachability == NotReachable) {// The target host is not reachable available}
5.1.2 HTTP 错误
HTTP 错误是由HTTP 请求、HTTP 服务器或应用服务器的问题造成的。HTTP 错误通过HTTP 响应的状态码发送给请求客户端。
404 状态是常见的一种HTTP 错误,表示找不到URL 指定的资源。下述代码片段中的HTTP 头就是当HTTP 服务器找不到请求资源时给出的原始输出:
HTTP/1.1 404 Not FoundDate: Sat, 04 Feb 2012 18:32:25 GMTServer: Apache/2.2.14 (Ubuntu)Vary: Accept-EncodingContent-Encoding: gzipContent-Length: 248Keep-Alive: timeout=15, max=100Connection: Keep-AliveContent-Type: text/html; charset=iso-8859-1
NSHTTPURLResponse *response=nil;NSError *error=nil;NSData *myData = [NSURLConnectionsendSynchronousRequest:requestreturningResponse:&responseerror:&error];//Check the returnif((!error) && ([response statusCode] == 200)) {// looks like things worked} else {// things broke, again.}
if([response isKindOfClass:[NSHTTPURLResponse class]]) {// It is a HTTP response, so we can check the status code...
要想了解关于HTTP 状态码的权威信息,请参考W3 RFC 2616,网址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。{ "transferResponse":{"fromAccount":1,"toAccount":5,"amount":500.00,"confirmation":232348844}}
{"error":{"code":900005,"messages":"Insufficient Funds to Complete Transfer"},"data":{"fromAccount":1,"toAccount":5,"amount":500.00}}
- (NSMutableURLRequest *) createRequestObject:(NSURL *)url {NSMutableURLRequest *request = [[[NSMutableURLRequestalloc]initWithURL:urlcachePolicy:NSURLCacheStorageAllowedtimeoutInterval:20autorelease];return request;}
- (void)main {NSLog(@"Starting getFeed operation");// Check to see if the user is logged inif([self isUserLoggedIn]) { // only do this if the user is logged in// Build the requestNSString *urlStr =@"https://gdata.youtube.com/feeds/api/users/default/uploads";NSLog(@"urlStr=%@",urlStr);NSMutableURLRequest *request =[ self createRequestObject:[NSURL URLWithString:urlStr]];// Sign the request with the user’s auth token[self signRequest:request];// Send the requestNSHTTPURLResponse *response=nil;NSError *error=nil;NSData *myData = [self sendSynchronousRequest:requestresponse_p:&responseerror:&error];// Check to see if the request was successfulif([super wasCallSuccessful:responseerror:error]) {[self buildDictionaryAndSendCompletionNotif: myData];}}}
- (void) viewDidDisappear:(BOOL)animated {if(retryFlag) {// re-enqueue all of the failed commands[self performSelectorAndClear:@selector(enqueueOperation)];} else {// just send a failure notification for all failed commands[self performSelectorAndClear:@selector(sendCompletionFailureNotification)];}self.displayed = NO;}
应用委托会将自身注册为网络错误与需要登录通知的监听器(如代码清单5-3 所示),收集异常通知并在错误发生时管理正确的视图控制器的呈现。上述代码展示了需要登录通知的通知处理器。由于要处理用户界面,因此其中的内容必须使用GCD 在主线程中执行。/*** Handles login needed notifications generated by commands**/- (void) loginNeeded:(NSNotification *)notif {// make sure it all occurs on the main threaddispatch_async(dispatch_get_main_queue(), ^{// make sure only one thread adds a command at a time@synchronized(loginViewController) {[loginViewController addTriggeringCommand:[notif object];if(!loginViewController.displayed) {// if the view is not displayed then display it.[[self topOfModalStack:self.window.rootViewController]presentModalViewController:loginViewControlleranimated:YES];}loginViewController.displayed = YES;}}); // End of GC Dispatch block}
(void)requestVideoFeed {// create the commandGetFeed *op = [[GetFeedalloc] init];// add the current authentication token to the commandCommandDispatchDemoAppDelegate *delegate =(CommandDispatchDemoAppDelegate *)[[UIApplicationsharedApplication] delegate ];op.token = delegate.token;// register to hear the completion of the command
[op listenForMyCompletion:self selector:@selector(gotFeed:)];
// put it on the queue for execution[op enqueueOperation];[op release];}
- (void) gotFeed:(NSNotification *)notif {NSLog(@"User info = %@", notif.userInfo);BaseCommand *op = notif.object;if(op.status == kSuccess) {self.feed = op.results;// if entry is a single item, change it to an array,// the XML reader cannot distinguish single entries// from arrays with only one elementid entries = [[feed objectForKey:@"feed"] objectForKey:@"entry"];if([entries isKindOfClass:[NSDictionary class]]) {NSArray *entryArray = [NSArrayarrayWithObject:entries];[[feed objectForKey:@"feed"] setObject:entryArrayforKey:@"entry"];}dispatch_async(dispatch_get_main_queue(), ^{[self.tableViewreloadData];});} else {dispatch_async(dispatch_get_main_queue(), ^{UIAlertView *alert = [[UIAlertViewalloc]initWithTitle:@"No Videos"message:@"The login to YouTube failed"delegate:selfcancelButtonTitle:@"Retry"otherButtonTitles:nil];[alert show];[alert release];});}}YouTubeVideoCell 是UITableViewCell 的子类,它会异步加载视频的缩略图。它通过LoadImageCommand 对象完成加载处理:/*** Start the process of loading the image via the command queue**/- (void) startImageLoad {LoadImageCommand *cmd = [[LoadImageCommandalloc] init];cmd.imageUrl = imageUrl;// set the name to something uniquecmd.completionNotificationName = imageUrl;[cmd listenForMyCompletion:self selector:@selector(didReceiveImage:)];[cmdenqueueOperation];[cmd release];}
这个类会改变完成通知名,这样它(也只有它)就可以接收到特定图片的通知了。否则,它还需要检查返回的通知来确定是否是之前发出的命令。1
CI框架连接数据库配置操作以及多数据库操作
09-05
2
asp 简单读取数据表并列出来 ASP如何快速从数据库读取大量数据
05-17
3
C语言关键字及其解释介绍 C语言32个关键字详解
04-05
4
C语言中sizeof是什么意思 c语言里sizeof怎样用法详解
04-26
5
最简单的asp登陆界面代码 asp登陆界面源代码详细介绍
04-12
6
PHP中的魔术方法 :__construct, __destruct , __call, __callStatic,__get, __set, __isset, __unset , __sleep,
09-05
7
PHP中的(++i)前缀自增 和 (i++)后缀自增
09-05
8
PHP中include和require区别之我见
09-05
常用dos命令及语法
2014-09-27
将视频设置为Android手机开机动画的教程
2014-12-11
php递归返回值的问题
2014-09-05
如何安装PHPstorm并配置方法教程 phpstorm安装后要进行哪些配置
2017-05-03
java中的info是什么意思
2022-03-24
PHP 教程之如何使用BLOB存取图片信息实例
2014-09-05
IcePHP框架中的快速后台中的通用CRUD功能框架
2014-09-05
单片机编程好学吗?单片机初学者怎样看懂代码
2022-03-21
PHP数组函数array
2014-09-05
学ug编程如何快速入门?
2022-03-17
趣游捕鱼高爆版官方最新版下载v12.0 安卓版
休闲益智 120.2M
下载趣游捕鱼官方正版手游下载v12.0 安卓最新版
休闲益智 117.1M
下载永夜降临复苏游戏下载v3.5.7 安卓最新版
卡牌对战 537.6M
下载创世战车手游下载v1.45.1.98037 安卓版
射击枪战 2.36G
下载dokkan七龙珠爆裂激战国际版手游下载v5.28.6 安卓版
动作闯关 97.6M
下载这就是江湖官方版下载v12.6.6 安卓最新版本
卡牌对战 211.6M
下载热血美职篮手游下载v1.19020.1722.0 安卓版
体育运动 1.62G
下载斗罗大陆魂师对决手游官方版下载v2.31.1 安卓版
卡牌对战 1.55G
下载坦克大决战游戏下载v1.9462 安卓官方正版
下载
孤胆车神维加斯官方正版(Gangstar Vegas)下载v8.5.1c 安卓手机版
下载
孤胆车神维加斯僵尸城版本下载v8.5.1c 安卓版
下载
英雄联盟云顶之弈手机版本下载v14.8.5768838 安卓最新版本
下载
苍蓝前线手游下载v1.1.0 安卓版
下载
崩坏学园2手游下载v12.2.8 安卓最新版本
下载
彩虹联萌手游下载v7.3.23015 安卓官方版
下载
花花填色苹果版下载v2.2.5 iPhone版
下载