转载

iOS应用架构谈(三):网络层设计方案(下)

编者按:iOS客户端应用架构看似简单,但实际上要考虑的事情不少。本文作者将以系列文章的形式来讨论iOS应用架构中的种种问题,本文是其中的第三篇,主要讲网络层设计以及安全机制和优化方案。下篇主要讲网络层的安全机制和优化。

网络层的安全机制

判断API的调用请求是来自于经过授权的APP

使用这个机制的目的主要有两点:

  1. 确保API的调用者是来自你自己的APP,防止竞争对手爬你的API
  2. 如果你对外提供了需要注册才能使用的API平台,那么你需要有这个机制来识别是否是注册用户调用了你的API

解决方案:设计签名

要达到第一个目的其实很简单,服务端需要给你一个密钥,每次调用API时,你使用这个密钥再加上API名字和API请求参数算一个hash出来,然后请求的时候带上这个hash。服务端收到请求之后,按照同样的密钥同样的算法也算一个hash出来,然后跟请求带来的hash做一个比较,如果一致,那么就表示这个API的调用者确实是你的APP。为了不让别人也获取到这个密钥,你最好不要把这个密钥存储在本地,直接写死在代码里面就好了。另外适当增加一下求Hash的算法的复杂度,那就是各种Hash算法(比如MD5)加点盐,再回炉跑一次Hash啥的。这样就能解决第一个目的了:确保你的API是来自于你自己的App。

一般情况下大部分公司不会出现需要满足第二种情况的需求,除非公司开发了自己的API平台给第三方使用。这个需求跟上面的需求有一点不同:符合授权的API请求者不只是一个。所以在这种情况下,需要的安全机制会更加复杂一点。

这里有一个较容易实现的方案:客户端调用API的时候,把自己的密钥通过一个可逆的加密算法加密后连着请求和加密之后的Hash一起送上去。当然,这个可逆的加密算法肯定是放在在调用API的SDK里面,编译好的。然后服务端拿到加密后的密钥和加密的Hash之后,解码得到原始密钥,然后再用它去算Hash,最后再进行比对。

保证传输数据的安全

使用这个机制的主要目的有两点:

  1. 防止中间人攻击,比如说运营商很喜欢往用户的Http请求里面塞广告...
  2. SPDY依赖于HTTPS,而且是未来HTTP/2的基础,他们能够提高你APP在网络层整体的性能。

解决方案:HTTPS

目前使用HTTPS的主要目的在于防止运营商往你的Response Data里面加广告啥的(中间人攻击),面对的威胁范围更广。从2011年开始,国外业界就已经提倡所有的请求(不光是API,还有网站)都走HTTPS,国内差不多晚了两年(2013年左右)才开始提倡这事,天猫是这两个月才开始做HTTPS的全APP迁移。

关于速度,HTTPS肯定是比HTTP慢的,毕竟多了一次握手,但挂上SPDY之后,有了链接复用,这方面的性能就有了较大提升。这里的性能提升并不是说一个请求原来要500ms能完成,然后现在只要300ms,这是不对的。所谓整体性能是基于大量请求去讨论的:同样的请求量(假设100个)在短期发生时,挂上SPDY之后完成这些任务所要花的时间比不用SPDY要少。SPDY还有Header压缩的功能,不过因为一个API请求本身已经比较小了,压缩数据量所带来的性能提升不会特别明显,所以就单个请求来看,性能的提升是比较小的。不过这是下一节要讨论的事儿了,这儿只是顺带说一下。

安全机制小总结

这一节说了两种安全机制,一般来说第一种是标配,第二种属于可选配置。不过随着我国互联网基础设施的完善,移动设备性能的提高,以及优化技术的提高,第二种配置的缺点(速度慢)正在越来越微不足道,因此HTTPS也会成为不久之后的未来App的网络层安全机制标配。各位架构师们,如果你的App还没有挂HTTPS,现在就已经可以开始着手这件事情了。

网络层的优化方案

网络层的优化手段主要从以下三方面考虑:

  1. 针对链接建立环节的优化
  2. 针对链接传输数据量的优化
  3. 针对链接复用的优化

这三方面是所有优化手段的内容,各种五花八门的优化手段基本上都不会逃脱这三方面,下面我就会分别针对这三方面讲一下各自对应的优化手段。

1. 针对链接建立环节的优化

在API发起请求建立链接的环节,大致会分这些步骤:

  1. 发起请求
  2. DNS域名解析得到IP
  3. 根据IP进行三次握手(HTTPS四次握手),链接建立成功

其实第三步的优化手段跟第二步的优化手段是一致的,我会在讲第二步的时候一起讲掉。

1.1 针对发起请求的优化手段

其实要解决的问题就是网络层该不该为此API调用发起请求。

1.1.1 使用缓存手段减少请求的发起次数

对于大部分API调用请求来说,有些API请求所带来的数据的时效性是比较长的,比如商品详情,比如App皮肤等。那么我们就可以针对这些数据做本地缓存,这样下次请求这些数据的时候就可以不必再发起新的请求。

一般是把API名字和参数拼成一个字符串然后取MD5作为key,存储对应返回的数据。这样下次有同样请求的时候就可以直接读取这里面的数据。关于这里有一个缓存策略的问题需要讨论:什么时候清理缓存?要么就是根据超时时间限制进行清理,要么就是根据缓存数据大小进行清理。这个策略的选择要根据具体App的操作日志来决定。

比如安居客App,日志数据记录显示用户平均使用时长不到3分钟,但是用户查看房源详情的次数比较多,而房源详情数据量较大。那么这个时候,就适合根据使用时长来做缓存,我当时给安居客设置的缓存超时时间就是3分钟,这样能够保证这个缓存能够在大部分用户使用时间产生作用。嗯,极端情况下做什么缓存手段不考虑,只要能够服务好80%的用户就可以了,而且针对极端情况采用的优化手段对大部分普通用户而言是不必要的,做了反而会对他们有影响。

再比如网络图片缓存,数据量基本上都特别大,这种就比较适合针对缓存大小来清理缓存的策略。

另外,之前的缓存的前提都是基于内存的。我们也可以把需要清理的缓存存储在硬盘上(APP的本地存储,我就先用硬盘来表示了,虽然很少有手机硬盘的说法,哈哈),比如前面提到的图片缓存,因为图片很有可能在很长时间之后,再被显示的,那么原本需要被清理的图片缓存,我们就可以考虑存到硬盘上去。当下次再有显示网络图片的需求的时候,我们可以先从内存中找,内存找不到那就从硬盘上找,这都找不到,那就发起请求吧。

当然,有些时效性非常短的API数据,就不能使用这个方法了,比如用户的资金数据,那就需要每次都调用了。

1.1.2 使用策略来减少请求的发起次数

这个我在前面提到过,就是针对重复请求的发起和取消,是有对应的请求策略的。我们先说取消策略。

如果是界面刷新请求这种,而且存在重复请求的情况(下拉刷新时,在请求着陆之前用户不断执行下拉操作),那么这个时候,后面重复操作导致的API请求就可以不必发送了。

如果是条件筛选这种,那就取消前面已经发送的请求。虽然很有可能这个请求已经被执行了,那么取消所带来的性能提升就基本没有了。但如果这个请求还在队列中待执行的话,那么对应的这次链接就可以省掉了。

以上是一种,另外一种情况就是请求策略:类似用户操作日志的请求策略。

用户操作会触发操作日志上报Server,这种请求特别频繁,但是是暗地里进行的,不需要用户对此有所感知。所以也没必要操作一次就发起一次的请求。在这里就可以采用这样的策略:在本地记录用户的操作记录,当记录满30条的时候发起一次请求将操作记录上传到服务器。然后每次App启动的时候,上传一次上次遗留下来没上传的操作记录。这样能够有效降低用户设备的耗电量,同时提升网络层的性能。

小总结

针对建立连接这部分的优化就是这样的原则:能不发请求的就尽量不发请求,必须要发请求时,能合并请求的就尽量合并请求。然而,任何优化手段都是有前提的,而且也不能保证对所有需求都能起作用,有些API请求就是不符合这些优化手段前提的,那就老老实实发请求吧。不过这类API请求所占比例一般不大,大部分的请求都或多或少符合优化条件,所以针对发送请求的优化手段还是值得做的。

1.2 & 1.3 针对DNS域名解析做的优化,以及建立链接的优化

其实在整个DNS链路上也是有DNS缓存的,理论上也是能够提高速度的。这个链路上的DNS缓存在PC用户上效果明显,因为PC用户的DNS链路相对稳定,信号源不会变来变去。但是在移动设备的用户这边,链路上的DNS缓存所带来的性能提升就不太明显了。因为移动设备的实际使用场景比较复杂,网络信号源会经常变换,信号源每变换一次,对应的DNS解析链路就会变换一次,那么原链路上的DNS缓存就不起作用了。而且信号源变换的情况特别特别频繁,所以对于移动设备用户来说,链路的DNS缓存我们基本上可以默认为没有。所以大部分时间是手机系统自带的本地DNS缓存在起作用,但是一般来说,移动设备上网的需求也特别频繁,专门为我们这个App所做的DNS缓存很有可能会被别的DNS缓存给挤出去被清理掉,这种情况是特别多的,用户看一会儿知乎刷一下微博查一下地图逛一逛点评再聊个Q,回来之后很有可能属于你自己的App的本地DNS缓存就没了。这还没完,这里还有一个只有在中国特色社会主义的互联网环境中才会有的问题:国内的互联网环境由于GFW的存在,就使得DNS服务速度会比正常情况慢不少。

基于以上三个原因所导致的最终结果就是,API请求在DNS解析阶段的耗时会很多。

那么针对这个的优化方案就是,索性直接走IP请求,那不就绕过DNS服务的耗时了嘛。

另外一个,就是上面提到的建立链接时候的第三步,国内的网络环境分北网通南电信(当然实际情况更复杂,这里随便说说),不同服务商之间的连接,延时是很大的,我们需要想办法让用户在最适合他的IP上给他提供服务,那么就针对我们绕过DNS服务的手段有一个额外要求:尽可能不要让用户使用对他来说很慢的IP。

所以综上所述,方案就应该是这样:本地有一份IP列表,这些IP是所有提供API的服务器的IP,每次应用启动的时候,针对这个列表里的所有IP取ping延时时间,然后取延时时间最小的那个IP作为今后发起请求的IP地址。

针对建立连接的优化手段其实是跟DNS域名解析的优化手段是一样的。不过这需要你的服务器提供服务的网络情况要多,一般现在的服务器都是双网卡,电信和网通。由于中国特色的互联网ISP分布,南北网络之间存在瓶颈,而我们App针对链接的优化手段主要就是着手于如何减轻这个瓶颈对App产生的影响,所以需要维护一个IP列表,这样就能就近连接了,就起到了优化的效果。

我们一般都是在应用启动的时候获得本地列表中所有IP的ping值,然后通过NSURLProtocol的手段将URL中的HOST修改为我们找到的最快的IP。另外,这个本地IP列表也会需要通过一个API来维护,一般是每天第一次启动的时候读一次API,然后更新到本地。

如果你还不熟悉NSURLProtocol应该怎么玩,看完 官方文档 和 这篇文章 以及 这个Demo 之后,你肯定就会了,其实很简单的。另外,刚才提到那篇文章的作者(mattt)还写了这个 基于NSURLProtocol的工具 ,相当好用,是可以直接拿来集成到项目中的。

不用NSURLProtocol的话,用其他手段也可以做到这一点,但那些手段未免又比较愚蠢。

2. 针对链接传输数据量的优化

这个很好理解,传输的数据少了,那么自然速度就上去了。这里没什么花样可以讲的,就是压缩呗。各种压缩。

3. 针对链接复用的优化

建立链接本身是属于比较消耗资源的操作,耗电耗时。SPDY自带链接复用以及数据压缩的功能,所以服务端支持SPDY的时候,App直接挂SPDY就可以了。如果服务端不支持SPDY,也可以使用PipeLine,苹果原生自带这个功能。

一般来说业界内普遍的认识是SPDY优于PipeLine,然后即便如此,SPDY能够带来的网络层效率提升其实也没有文献上的图表那么明显,但还是有性能提升的。还有另外一种比较笨的链接复用的方法,就是维护一个队列,然后将队列里的请求压缩成一个请求发出去,之所以会存在滞留在队列中的请求,是因为在上一个请求还在外面飘的时候。这种做法最终的效果表面上看跟链接复用差别不大,但并不是真正的链接复用,只能说是请求合并。

还是说回来,我建议最好是用SPDY,SPDY和pipeline虽然都属于链接复用的范畴,但是pipeline并不是真正意义上的链接复用,SPDY的链接复用相对pipeline而言更为彻底。SPDY目前也有现成的客户端SDK可以使用,一个是twitter的CocoaSPDY,另一个是Voxer/iSPDY,这两个库都很活跃,大家可以挑合适的采用。

不过目前业界趋势是倾向于使用HTTP/2.0来代替SPDY,不过目前HTTP/2.0还没有正式出台,相关实现大部分都处在demo阶段,所以我们还是先SPDY搞起就好了。未来很有可能会放弃SPDY,转而采用HTTP/2.0来实现网络的优化。这是要提醒各位架构师注意的事情。嗯,我也不知道HTTP/2.0什么时候能出来。

这里 是我当年设计并实现的安居客的网络层架构代码。当然,该脱敏的地方我都已经脱敏了,所以编不过是正常的,哈哈哈。但是代码比较齐全,重要地方注释我也写了很多。另外,为了让大家能够把这些代码看明白,我还附带了当年介绍这个框架演讲时的PPT。(是个key后缀名的文件,用keynote打开)

然后就是,当年也有很多问题其实考虑得并没有现在清楚,所以有些地方还是做得不够好,比如拦截器和继承。而且当时的优化手段只有本地cache,安居客没有那么多IP可以给我ping,当年也没流行SPDY,而且API也还不支持HTTPS,所以当时的代码里面没有在这些地方做优化,比较原始。然而整个架构的基本思路一直没有变化:优先服务于业务方。另外,安居客的网络层多了一个service的概念,这是我这篇文章中没有讲的。主要是因为安居客的API提供方很多,二手房,租房,新房,X项目等等API都是不同的API team提供的,以service作区分,如果你的app也是类似的情况,我也建议你设计一套service机制。现在这些service被我删得只剩下一个google的service,因为其他service都属于敏感内容。

总结

第一部分主要讲了网络层应当如何跟业务层进行数据交互,进行数据交互时采用怎样的数据格式,以及设计时代码结构上的一些问题,诸如继承的处理,回调的处理,交互方式的选择,reformer的设计,保持数据可读性等等等等,主要偏重于设计(这可是艺术活,哈哈哈)。

第二部分讲了网络安全上,客户端要做的两点。当然,从网络安全的角度上讲,服务端也要做很多很多事情,客户端要做的一些边角细节的事情也还会有很多,比如做一些代码混淆,尽可能避免代码中明文展示key。不过大头主要就是这两个,而且也都是需要服务端同学去配合的。主要偏重于介绍。(主要是也没啥好实践的,google一下教程照着来就好了)。

第三部分讲了优化,优化的所有方面都已经列出来了,如果业界再有七七八八的别的手段,也基本逃离不出本文的范围。这里有些优化手段是需要服务端同学配合的,有些不需要,大家看各自情况来决定。主要偏重于实践。

最后给出了我之前在安居客做的网络层架构的主要代码,以及当时演讲时的PPT。

编后语

为了更好地向读者输出更优质的内容,InfoQ将精选来自国内外的优秀文章,经过整理审校后,发布到网站。本篇文章作者为田伟宇,原文链接为 Casa Taloyum 。本文已由原作者授权InfoQ中文站转载。

正文到此结束
Loading...