到目前为止,我们所介绍的iPhone与其他系统的网络交互都是基于一切正常这个假设。本章将会放弃这个假设,并深入探究网络的真实世界。在真实世界中,事情是会出错的,有时可能是非常严重的错误:手机进入与离开网络、包丢掉或是延迟;网络基础设施出错;偶尔用户还会出错。如果一切正常,那么编写iOS应用就会简单不少,不过遗憾的是现实并非如此。本章将会探讨导致网络操作失败的几个因素,介绍系统如何将失败情况告知应用,应用又该如何优雅地通知用户。此外,本章还将介绍如何在不往应用逻辑中添加错误处理代码的情况下,以一种整洁且一致的方式处理错误的软件模式。
5.1 理解错误源
早期的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包序列等。下面将会详细介绍每一种错误类别。
5.1.1 操作系统错误
操作系统错误是由数据包没有到达预定目标导致的。数据包可能是建立连接的一部分,也可能位于连接建立的中间阶段。OS错误可能由如下原因造成:
1.没有网络——如果设备没有数据网络连接,那么连接尝试很快就会被拒绝或是失败。这些类型的错误可以通过Apple提供的Reachability框架检测到,本节后面将会对此进行介绍。
2.无法路由到目标主机——设备可能有网络连接,不过连接的目标可能位于隔离的网络中或是处于离线状态。这些错误有时可以由操作系统迅速检测到,不过也有可能导致连接超时。
3.没有应用监听目标端口——在请求到达目标主机后,数据包会被发送到请求指定的端口号。如果没有服务器监听这个端口或是有太多的连接请求在排队,那么连接请求就会被拒绝。
4.无法解析目标主机名——如果无法解析目标主机名,那么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 } } |
域属性根据产生错误代码的库或框架对这些错误代码进行隔离。借助域,框架开发者无须担心覆盖错误代码,因为域属性定义了产生错误的框架。比如,框架A
与B 都会产生错误代码1,不过这两个错误代码会被每个框架提供的唯一域值进行区分。因此,如果代码需要区分NSError
值,就必须对NSError 对象的code 与domain 属性进行比较。
NSError 对象有如下3 个主要属性:
1.code——标识错误的NSInteger 值。对于产生该错误的错误域来说,这个值是唯一的。
2.domain —— 指定错误域的NSString 指针, 比如NSPOSIXErrorDomain
、NSOSStatusErrorDomain 及NSMachErrorDomain。
3.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> |
注意,NSError 对象被声明为指向nil 的指针。如果出现错误,那么NSURLConnection对象只会实例化NSError
对象。URL 加载系统拥有NSError 对象;如果稍后代码会用到它,那么应该保持这个对象。如果在同步请求完成后NSError
指针依然指向nil,那就说明没有产生底层的OS 错误。这时,代码就知道没有产生OS 级别的错误,不过错误可能出现在协议栈的某个高层。
如果应用创建的是异步请求,那么NSError 对象就会返回到委托类的下面这个方法:
<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> |
上述代码以一种生硬且不友好的方式将错误展现给了用户。在iOS 人机界面指南(HiG)中,Apple
建议不要过度使用UIAlertViews,因为这会破坏设备的使用感受。5.3 节“优雅地处理网络错误”中介绍了如何通过良好的用户界面以一种干净且一致的方式处理错误的模式。
iOS 设备通信错误的另一主要原因就是由于没有网络连接而导致设备无法访问目标服务器。可以在尝试发起网络连接前检查一下网络状态,这样可以避免很多OS
错误。请记住,这些设备可能会很快地进入或是离开网络。因此,在每次调用前检查网络的可达性是非常合情合理的事情。
iOS 的SystemConfiguration 框架提供了多种方式来确定设备的网络连接状态。可以在SCNetworkReachability
参考文档中找到关于底层API 的详尽信息。这个API 非常强大,不过也有点隐秘。幸好,Apple 提供了一个名为Reachability
的示例程序,它为SCNetworkReachability实现了一个简化、高层次的封装器。Reachability
位于iOS 开发者库中。
Reachability 封装器提供如下4 个主要功能:
1.标识设备是否具备可用的网络连接
2.标识当前的网络连接是否可以到达某个特定的主机
3.标识当前使用的是哪种网络技术:Wi-Fi、WWAN 还是什么技术都没用
4.在网络状态发生变化时发出通知要想使用Reachability API,请从iOS
开发者库中下载示例程序,地址是http://developer.apple.com/library/ios/#samplecode/Reachability/Introduction/Intro.html,然后将Reachability.h与Reachability.m
添加到应用的Xcode 项目中。此外,还需要将SystemConfiguration 框架添加到Xcode
项目中。将SystemConfiguration 框架添加到Xcode 项目中需要编辑项目配置。图5-3
展示了将SystemConfiguration 框架添到Xcode 项目中所需的步骤。
(3) 选择SystemConfiguration.framework
选定好项目目标后,找到设置中的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> |
在某些情况下,你可能想要修改某些动作、禁用UI 元素或是当设备处于有限制的网络中时修改超时值。如果应用需要知道当前正在使用的连接类型,那么请使用如下代码:
<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> |
上述代码将当前对象注册为通知观察者,名为kReachabilityChangedNotification。
NSNotificationCenter 会调用当前对象的名为networkChanged:的方法。当可达性状态发生变化时,就向该对象传递NSNotification
及新的可达性状态。如下示例展示了通知监听者:
<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> |
可达性还可以确定当前网络上某个特定的主机是否是可达的。可以通过该特性根据应用是处于内部隔离的网络上还是公开的Internet
上调整企业应用的行为。如下代码示例展示了该特性:
<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> |
请记住,该特性对目标主机的访问有个来回。如果每个请求都使用该特性,那就会极大增加应用的网络负载与延迟。Apple
建议不要在主线程上检测主机的可达性,因为尝试访问主机可能会阻塞主线程,这会导致UI 被冻结。
OS 错误首先就表明请求出现了问题。应用开发者有时会忽略掉它们,不过这样做是有风险的。因为HTTP
使用了分层网络,这时HTTP 层或是应用层可能会出现其他类型的潜在失败情况。
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> |
响应的第一行有状态码。HTTP 响应可以带有消息体,其中包含友好、用户可读的信息,用于描述发生的事情。你不应该将是否有响应体作为判断HTTP
请求成功与否的标志。
一共有5 类HTTP 错误:
1.信息性质的100 级别——来自于HTTP 服务器的信息,表示请求的处理将会继续,不过带有警告。
2.成功的200 级别——服务器处理了请求。每个200 级别的状态都表示成功请求的不同结果。比如,204
表示请求成功,不过没有向客户端返回负载。
3.重定向需要的300 级别——表示客户端必须执行某个动作才能继续请求,因为所需的资源已经移动了。URL
加载系统的同步请求方法会自动处理重定向而无须通知代码。如果应用需要对重定向进行自定义处理,那么应该使用异步请求。
4.客户端错误400 级别——表示客户端发出了服务器无法正确处理的错误数据。比如,未知的URL
或是不正确的HTTP 头会导致这个范围内的错误。
5.下游错误500 级别——表示HTTP 服务器与下游应用服务器之间出现了错误。比如,如果Web
服务器调用了JavaEE 应用服务器,Servlet 出现了NullPointerException,那么客户端就会收到500
级别的错误。
iOS 中的URL 加载系统会处理HTTP 头的解析,并可以轻松获取到HTTP
状态。如果代码通过HTTP 或HTTPS URL 发出了同步调用,那么返回的响应对象就是一个NSHTTPURLResponse
实例。NSHTTPURLResponse 对象的statusCode 属性会返回数值形式的请求的HTTP
状态。如下代码演示了对NSError 对象以及从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]; //Check the return if((!error) && ([response statusCode] == 200)) { // looks like things worked } else { // things broke, again. }</span></span> |
如果请求的URL不是HTTP,那么应用就应该验证响应对象是否是NSHTTPURLResponse对象。验证对象类型的首选方法是使用返回对象的isKindOfClass:方法,如下所示:
<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。
5.1.3 应用错误
本节将会介绍网络协议栈的下一层(应用层)产生的错误。应用错误不同于OS
错误或HTTP 错误,因为并没有针对这些错误的标准值或是原因的集合。这些错误是由运行在服务层之上的业务逻辑和应用造成的。在某些情况下,错误可能是代码问题,比如异常,不过在其他一些情况下,错误可能是语义错误,比如向服务提供了无效的账号等。对于前者来说,建议生成HTTP
500 级别的错误;对于后者来说,应该在应用负载中返回错误码。
比如,如果用户尝试从账户中转账的金额超出了账户的可用余额,那么手机银行就应该报告应用错误。如果发出了这样的请求,那么OS
会说请求成功发送并接收到了响应。HTTP 服务器会报告接收到了请求并发出了响应,不过应用层必须报告这笔交易失败。报告应用错误的最佳实践是将应用的负载数据封装在标准信封中,信封中含有一致的应用错误位置信息。在上述资金转账示例中,成功的转账响应的业务负载应该如下所示:
<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> |
响应包含了源账号与目标账号、转账的资金数额及确认号。直接将错误码与错误消息放到transferResponse
对象中会导致错误码与错误消息的定位变得困难。如果每个动作都将错误信息放到自己的响应对象中,就无法在应用间重用错误报告逻辑了。使用如下代码中的数据包结构可以让应用快速确定是否出现了错误,方式是检查响应的JSON负载中是否存在“error”对象:
<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> |
|