设计真实世界的网络 <- 网络概述
在理想的世界,网络“只要运行”就可以。网络连接是可靠的、快速的、低延时的。在真实的世界,网络大多数时候是运行的,但它会断,它经常以匪夷所思的方式断开。例如:
- 网络链路过载或断开会导致数据包丢失。如果链路丢失了一定数量的数据包,则可能难以在这条链路上建立连接,并且性能会与你期望的相去甚远。
- 当网络链接饱和时,该链路两侧的路由会缓存数据流以避免丢失数据。这增加了额外的延时。在大量加载的DSL连接上看到几秒钟的延时并不罕见。
- 捕获(captive)网络(通常用于宾馆、咖啡店、或其他公共场所)可以阻止软件的HTTP请求,并提供一个登录页面来取代被期望的数据。
- 用户和目标之间的防火墙,可能会阻止除少数端口外的所有其他连接。
- 执行网络地址转换(NAT)的防火墙,可能不允许远程服务器连接用户电脑或其他设备的端口。
- 第三方防火墙软件可能阻止应用的传出连接请求,用以等待用户允许。
虽然你的软件不可能修复一个真正断开的网络,但是糟糕的网络代码可以轻松地让事情变得更加糟糕。例如,假设一个服务器严重超载,并且需要45秒才能响应每个请求。如果你的应用连接到这个服务器设定了30秒超时,它占用了服务器的资源,但是却从没有成功接收过任何数据。
甚至在网络运行良好的情况下,糟糕的网络代码也会给用户带来问题——电池寿命降低、性能差等等。本章部分描述了无论是网络条件是否理想,应用都应该做到的事情,以便减轻用户的痛苦。
高效的使用电量和带宽
在写网络代码的时候,首先要考虑到的重要事情,是应用无论何时上传或下载数据,它都会消耗用户的时间和金钱。
网络操作消耗用户时间是因为:
- 用户必须等待这个操作完成才能执行其他任务。
- 数据传输经常要求无线电保持活跃。对于电池供电的设备,就减少用户使用设备的时间。
网络操作也消耗用户的金钱是因为带宽不是免费的。这些花费包括:
- 电能。无线硬件(Wi-Fi、蜂窝网络、等等)消耗大量电能。无线硬件处于活动状态的时间越长,就会越消耗电量。
- 实际数据传输。很多用户(特别是蜂窝网络用户)会为它们的真实的数据传输付费。应用传输的数据越多,他们所花费的钱越多。即使用户使用统一费率服务,ISP也会基于用户的平均消耗带宽来设置费率。
- 带宽。无论用户的网络连接是按流量的还是按统一费率,用户通常会为更高的连接速度支付更高的费率。
作为一个网络软件的开发者,你有责任让你的应用的电量和带宽的消耗最小化。
批量传输,并尽可能的空闲
在编写通用代码的时候,你应该尽可能的执行更多工作,然后返回到空闲状态。这对于网络活动来说是双赢的。例如:
- 如果应用从HTTP服务器接收流视频片段,一次下载整个文件(或至少是文件的大部分)要比每次请求一小段要好。
- 如果应用提供广告,一次下载多个广告并在一段时间内显示他们,要比在需要的时候再下载要好。
- 如果应用从服务器下载email,基于假设用户会阅读大部分email,所以一次下载多封email要比在用户选中的时候才下载更好。
一次下载一点内容会导致两个问题。首先,他会使应用对细微的网络延迟非常敏感,造成停顿、视频卡顿等等。其次,它会一直让蜂窝网络或Wi-Fi无线电持续使用。这会浪费电量,特别是当应用和蜂窝网络连接通信的时候。如果应用在一小段时间下载大量数据,然后允许无线连接进入完全休眠,这会显著提高用户的电池寿命。
这同样适用于socket编程。除了少数例外(例如远程终端程序),你永远不要一次只发送少量字节。这样做在CPU负载方面是非常低效的,并会导致操作系统发送更多不必要的数据包。
尽可能下载最小可用资源,并在本地缓存资源
下载数据有很多与之相关的成本——电池寿命、性能、以及在多种情况下真实的数据传输成本。因此,你应该始终根据需要来下载最小化的资源。
例如,如果你有一个图片应用,它下载一系列大型图片,并以缩略图的方式呈现,那你应该在服务器端完成缩略图的加工。应用应该在一开始只下载缩略图,只有当用户选择了某个缩略图的时候,才下载全尺寸的图片版本。这样做有两个原因:
- 传输数据会因为要持续运行网络硬件以及长时间CPU供电而导致电能消耗。通过减少应用传输资源的尺寸,可以提高用户的电池寿命(因为这减少了总传输数据)。
- 如果用户使用计费网络(例如蜂窝电话),传输较少的资源可以减少用户的数据流量费。
因为同样的原因,把下载资源保存在本地缓存,可以节省时间、带宽、以及电池寿命。想要做到这一点,当下载完数据之后,不再向服务器请求资源,而是向它请求有改变的资源。如果没有改变,就使用本地的副本。
在OS X和iOS中有很多高级APIs(例如NSURL)提供缓存支持(例如NSURLCache)。但是你你必须选择合适的缓存尺寸。无论你是使用内置的缓存API还是自建的,你都应该尝试使用缓存尺寸和替换策略来决定哪些对你的应用最有意义。
注意:这个目的和之前的目的(一次下载大量数据以便网络硬件变成闲置状态)之间经常存在冲突。
例如,考虑一个加载图像缩略图的应用。如果用户滚动几个被缩略图填充的屏幕,那么应用应该在一次下载足够多的图片来填充前面几个屏幕,以便网络硬件进入空闲状态。但是,如果用户基本上不滚动到第二屏,那么所有额外下载的图片就浪费了带宽
每个网络应用都应该平衡这种冲突。这取决于开发者的决定。
优雅的处理网络问题
在当今高度移动的世界,你不能再假设网络连接一旦建立,就会保持不变;或者认为带宽是不变的。这也就是说,唯一不变的是变化。作为一个开发者,你必须为这些常见的故障进行规划,并设计代码来恰当的处理它们。
为变动网络接口可用性设计
网络接口的可用性会因为无数原因而经常改变,特别是iOS。例如用户的如下行为:
- 乘坐地铁,每次进站的时候获得无线信号,而在出站之后失去信号。
- 离开当前的Wi-Fi网络范围。
- 激活飞行模式或关闭Wi-Fi。
- 拔掉网络线缆。
因此,在写使用网络的应用的时候,必须准备好处理网络故障。当网络故障发生的时候,应用应该基于多种考虑来决定做什么,最重要的是,这个请求是否由用户明确指定的。
对于用户要求的请求:
- 始终尝试建立连接。不要尝试猜测网络服务是否可用,也不要缓存该决定。
- 如果连接失败,使用SCNetworkReachability来帮助分析导致失败的原因。然后:
- 如果连接失败是因为暂时错误,尝试再次建立连接。
- 如果连接失败是因为无法连接主机,等待SCNetworkReachabilityAPI调用你注册的回调。当主机再次变成可达时,应用应该可以在无用户干涉的情况下,自动尝试重新建立连接(除非用户取消了这个请求,例如关闭了浏览窗口或点击了取消按钮)。
- 尝试以非模态的方式显示连接状态信息。但是,如果你必须显示错误对话框,请确保它不会在远程主机再次可达时妨碍应用自动重试的能力。在主机再次可达的时候自动关闭该对话框。
对于后台提出的请求:
- 尝试建立连接。如果需要,使用SCNetworkReachability来避免在不恰当的时候建立连接。例如,通过检查kSCNetworkReachabilityFlagsIsWWAN标识来避免蜂窝连接的不必要流量。
重要:检查可达性标识不保证你的流量将不通过蜂窝网络连接发送。详见Restrict Cellular Networking Correctly。
- 如果连接失败,如果它还可用,使用SCNetworkReachability来等待主机重新变为可达,然后再次尝试请求。
- 不要显示任何对话框;用户通常不关心不是由他们启动的后台下载的故障。
- 即使当网络可达API告诉应用网络已经改变,也要避免快速的重试。你应该逐渐的增加两次尝试之间的间隔时间,直到到达一个相当长的间隔(例如15分钟)。
应用应该能够优雅的响应当前网络接口的改变。为了支持这点,使用SCNetworkReachabilityAPI。通过注册网络改变通知,应用可以在可用的网络接口改变的时候收到警告。Reachability样本代码演示了注册回调,它会在当前网络接口改变时响应接到的通知。阅读SCNetworkReachability Reference,可看到对于SCNetworkReachability的完整讨论。
重要:SCNetworkReachabilityAPI不适用于确定网络连接的预检机制。你通过尝试连接来确定网络的连接。如果连接失败,查阅SCNetworkReachability API来帮助诊断导致失败的原因。
无论请求是用户产生的还是还是后台产生的,SCNetworkReachabilityAPI提供一个很好的方式来查看接口可达性的改变。当你使用的网络接口消失时,你应该快速的重连,来避免给用户带来不必要的延迟感。
还有,在iOS中,如果你连接的是蜂窝网络,你应该在Wi-Fi服务再次可用的时候快速在后台进行重连。Wi-Fi连接使用更少的电量,通常也更快,且比蜂窝网络省钱。
为变动的网络速度设计
应用必须为网络速度的改变做好准备,即便当前的网络接口不改变也要如此。例如,当一个移动设备用户改变位置时,Wi-Fi或者蜂窝网络的性能会显著变化,可能是因为干扰的增减,也可能是因为设备切换到了繁忙的基站。这种变化甚至不需要有太大的位置变化;从一个房间走到另一个房间,就可能会产生显著的网速变化。
此外,即便忽略争用和干扰,接口本身也没有告诉你有关全路径的真实带宽。当用户尝试连接Google的时候Wi-Fi网络可能是最快的,但是在用户和服务器之间的路由可能经过蜂窝网络或者卫星网络。类似的,用户可能有一个千兆以太网来连接局域网上的服务器,但与外部世界只有128k的上行连接。由于这个原因,你不应该基于当前的网络接口对网速进行假定。
只有一种方式可以确定网速:使用它。在你下载少量的数据之后,你可以对网速进行初步的估算。你应该持续监控你的下载速率来保持精确的估算,然后相应地调整你的预期。例如,如果你正在传输视频流,当确定视频流的帧率不能够维持播放的时候,你应该切换到较低带宽流来支持播放,用户不会感受到这种变化。如果之后下载速度提高,你可以再切换回来。
为高延时设计
作为开发者,要假设用户可能会使用高延时的连接。高延时在一些蜂窝网络接口类型上是特别常见的,因为它给设备使用的时隙有限。例如,在EDGE连接上的往返延时通常以秒计。但是在卫星连接或一般忙碌的DSL连接,如果应用设计时对此没有估计的话,即使是半秒钟的延时也会造成严重的问题。
当应用向多个远程主机发送多个请求时,如果它在创建第二个请求之前需要等待第一个请求的返回结果,连接延迟会相加。两次请求之间的延时会不断累加。
为了避免这个问题,当应用需要发送任何彼此不相互依赖的多个信息时(资源请求、确认等等),同时发送它们要比一个个的发送要好。图1-1说明了同时发送多个信息应用获得的增速。
图1-1 比较同时和顺序请求的响应时间
如果在iOS应用中使用NSURLConnection,你可以通过启用HTTP管道(pipelining)很轻松的获得增速。当管道被启用时,你的连接自动同时发送多个HTTP请求。通过调用你提供给连接的NSMutableURLRequest对象的setHTTPShouldUsePipelining:方法来启用管道。
注意:一些服务器不支持管道。如果你连接到的服务器不支持管道,这个连接依然工作,但不会提高性能。
在各种条件下测试
Xcode提供了一个名为Network Link Conditioner(网络链接调节器)的工具,它能模拟各种网络条件,包括减少带宽、高延时、DNS延时、丢包等等。在你准备发布任何使用网络的软件之前,你应该安装这个工具,启用它,然后运行你的应用来查看它在真实条件下的性能。
有几件需要测试的事:
- 确保应用即便是在恶劣的带宽条件下也保持可用。尽可能多的调整带宽消耗。
- 将延时增加3到4秒。确保任何用户操作只延时几秒钟,而不是几分钟。
- 当网络链接丢包时,应用应该可以继续运行,只是慢一点而已。
你或许还可以发现使用一些第三方工具(例如tcptrace),可以帮助你在恶劣网络条件下对应用的网络访问模式进行可视化观察。