本章内容
● 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:kSomeErrorCode userInfo: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 请求的代码示例:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil; NSError *error=nil; NSData *myData=[NSURLConnectionsendSynchronousRequest:request returningResponse:&response error:&error]; if (!error) { // No OS Errors, keep going in the process ... } else { // Something low level broke }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error</span></span>这是传递给请求委托的最终消息,委托必须能识别出错误的原因并作出恰当的反应。在如下示例中,委托会向用户展UIAlertView:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) connection:conndidFailWithError:error { UIAlertView *alert = [UIAlertViewalloc] initWithTitle:@"Network Error" message:[error description] delegate:self cancelButtonTitle:@"Oh Well" otherButtonTitles:nil]; [alert show]; [alert release]; }</span></span>
选定好项目目标后,找到设置中的Linked Frameworks and Libraries,单击+按钮添加框架,这时会出现框架选择界面。选择SystemConfiguration 框架,单击add 按钮将其添加到项目中。
如下代码片段会检查是否存在网络连接。不保证任何特定的主机或IP 地址是可达的,只是标识是否存在网络连接。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h" ... if([[Reachability reachabilityForInternetConnection] currentReachabilityStatus] == NotReachable) { // handle the lack of a network }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#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 }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h" ... [[NSNotificationCenterdefaultCenter] addObserver:self selector:@selector(networkChanged:) name:kReachabilityChangedNotification object:nil]; Reachability *reachability; reachability = [[Reachability reachabilityForInternetConnection] retain]; [reachability startNotifier];</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) networkChanged: (NSNotification* )notification { Reachability* reachability = [notification object]; 第Ⅱ部分 HTTP 请求:iOS 网络功能 98 if(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 } }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">Reachability *reach = [Reachability reachabilityWithHostName:@"www.captechconsulting.com"]; if(reachability == NotReachable) { // The target host is not reachable available }</span></span>
5.1.2 HTTP 错误
HTTP 错误是由HTTP 请求、HTTP 服务器或应用服务器的问题造成的。HTTP 错误通过HTTP 响应的状态码发送给请求客户端。
404 状态是常见的一种HTTP 错误,表示找不到URL 指定的资源。下述代码片段中的HTTP 头就是当HTTP 服务器找不到请求资源时给出的原始输出:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">HTTP/1.1 404 Not Found Date: Sat, 04 Feb 2012 18:32:25 GMT Server: Apache/2.2.14 (Ubuntu) Vary: Accept-Encoding Content-Encoding: gzip Content-Length: 248 Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil; NSError *error=nil; NSData *myData = [NSURLConnectionsendSynchronousRequest:request returningResponse:&response error:&error]; //Check the return if((!error) && ([response statusCode] == 200)) { // looks like things worked } else { // things broke, again. }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">if([response isKindOfClass:[NSHTTPURLResponse class]]) { // It is a HTTP response, so we can check the status code ...</span></span>要想了解关于HTTP 状态码的权威信息,请参考W3 RFC 2616,网址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{ "transferResponse":{ "fromAccount":1, "toAccount":5, "amount":500.00, "confirmation":232348844 } }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;"> </span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{"error":{ "code":900005, "messages":"Insufficient Funds to Complete Transfer" }, "data":{ "fromAccount":1, "toAccount":5, "amount":500.00 } }</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;">- (NSMutableURLRequest *) createRequestObject:(NSURL *)url { NSMutableURLRequest *request = [[[NSMutableURLRequestalloc] initWithURL:url cachePolicy:NSURLCacheStorageAllowed timeoutInterval:20 autorelease]; return request; }</span>
<span style="font-family:Microsoft YaHei;font-size:14px;">- (void)main { NSLog(@"Starting getFeed operation"); // Check to see if the user is logged in if([self isUserLoggedIn]) { // only do this if the user is logged in // Build the request NSString *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 request NSHTTPURLResponse *response=nil; NSError *error=nil; NSData *myData = [self sendSynchronousRequest:request response_p:&response error:&error]; // Check to see if the request was successful if([super wasCallSuccessful:responseerror:error]) { [self buildDictionaryAndSendCompletionNotif: myData]; } } }</span>
<span style="font-family:Microsoft YaHei;font-size:14px;">- (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; }</span>应用委托会将自身注册为网络错误与需要登录通知的监听器(如代码清单5-3 所示),收集异常通知并在错误发生时管理正确的视图控制器的呈现。上述代码展示了需要登录通知的通知处理器。由于要处理用户界面,因此其中的内容必须使用GCD 在主线程中执行。
<span style="font-family:Microsoft YaHei;font-size:14px;">/** * Handles login needed notifications generated by commands **/ - (void) loginNeeded:(NSNotification *)notif { // make sure it all occurs on the main thread dispatch_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:loginViewController animated:YES]; } loginViewController.displayed = YES; } }); // End of GC Dispatch block }</span>
<span style="font-family:Microsoft YaHei;font-size:14px;">(void)requestVideoFeed { // create the command GetFeed *op = [[GetFeedalloc] init]; // add the current authentication token to the command CommandDispatchDemoAppDelegate *delegate = (CommandDispatchDemoAppDelegate *)[[UIApplication sharedApplication] delegate ]; op.token = delegate.token; // register to hear the completion of the command</span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="background-color: rgb(255, 255, 255);">[op listenForMyCompletion:self selector:@selector(gotFeed:)];</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;">// put it on the queue for execution [op enqueueOperation]; [op release]; }</span>
<span style="font-family:Microsoft YaHei;font-size:14px;">- (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 element id 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:self cancelButtonTitle:@"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 unique cmd.completionNotificationName = imageUrl; [cmd listenForMyCompletion:self selector:@selector(didReceiveImage:)]; [cmdenqueueOperation]; [cmd release]; }</span>这个类会改变完成通知名,这样它(也只有它)就可以接收到特定图片的通知了。否则,它还需要检查返回的通知来确定是否是之前发出的命令。
iOS网络高级编程:iPhone和iPad的企业应用开发之错误处理,布布扣,bubuko.com
iOS网络高级编程:iPhone和iPad的企业应用开发之错误处理
原文:http://blog.csdn.net/qinghuawenkang/article/details/37560517