iOS网络请求优化
几乎所有的应用都会网络请求模块,AFNetworking的URL请求开发者无人不知。网络情况也愈发复杂,如何优化应用的网络模块变得尤其重要。接下来就结合大牛们的博客谈谈几点自己的见解。
DNS映射
所有网络请求的第一步都要找到域名对应的IP地址,DNS解析的效率直接决定了请求的延迟多少。而Cache的存在使得解析过程减少了很多延迟。但是缓存策略在不同系统上不一样,在iOS系统上,缓存策略一般是,24小时之后会过期、飞行模式的开启关闭、开关机、重置网络等,都会导致cache的清除,所以新的DNS请求又会导致请求耗时。
如果客户端将自己的IP、国家码等加入映射文件的请求参数中,DNS服务器就可以根据客户端位置的不同,下发离客户端最近的服务器IP地址,减少网络传输时间,实现动态部署。
DNS劫持现象是指改变DNS的返回结果,使得目标IP地址指向另一个地址。一种方式是病毒感染本机DNS服务器的地址,二是攻击DNS服务器使得改变行为。不管哪种方式,都是APP本身业务所不能接受的。恶意的劫持会有安全问题,所以客户端本地缓存一份映射表就显得尤为重要,这让劫持者无法得逞,还能加快请求速度。
那如何设计一份DNS映射表?几点要求如下:
- app打包时就加入一个默认映射文件,可以避免第一次就去服务器获取数据的延迟
- 开启一个定时器,每隔一段时间就从服务器获取最新的映射,并且覆盖本地
- 每次取到最新的映射信息后,同时保存上一次的数据作为备份,防止最新数据由于失误而导致无法处理
- 如果映射文件中没有相应信息,就需要回滚使用默认的DNS解析服务
- 如果一个映射过后的IP持续导致请求失败,需要一个淘汰机制将无效域名删除,并及时上报服务器,及时发现问题更新文件
请求压缩
DNS查询之后是TCP握手建立连接,并发送请求数据。对于TCP来说,单个IP包大小受限于MSS值,大部分用户所处网络环境下每个包的大小约在1.5KB,新建立的HTTP连接由于TCP的slow start特性,会导致本地的部分IP包本临时缓存,从而增加了整体request的延迟。所以我们应该尽可能尝试去压缩我们的网络请求业务数据,减少一个Request的IP包数量,或许可以让用户少经历一个RTT,降低请求延迟的用户感知。
请求合并
对于非关键性的业务数据,或者对实时性要求不高的请求来说,通过合并请求的方式可以减少和服务器交互的次数,一则降低服务器压力,二则合并之后再压缩能节约客户端的流量。这类请求一般见于打点SDK,crash日志收集等非业务型请求。
请求的安全性
总结其中的几点:
- 尽量使用HTTPS https可以过滤掉大部分的安全问题。https在证书申请,服务器配置,性能优化,客户端配置上都需要投入精力,所以缺乏安全意识的开发人员容易跳过https,或者拖到以后遇到问题再优化。https除了性能优化麻烦一些以外其他都比想象中的简单,如果没精力优化性能,至少在注册登录模块需要启用https,这部分业务对性能要求比较低。
- 不要传输密码 不知道现在还有多少app后台是明文存储密码的。无论客户端,server还是网络传输都要避免明文密码,要使用hash值。客户端不要做任何密码相关的存储,hash值也不行。存储token进行下一次的认证,而且token需要设置有效期,使用refresh token去申请新的token。
- Post并不比Get安全 事实上,Post和Get一样不安全,都是明文。参数放在QueryString或者Body没任何安全上的差别。在Http的环境下,使用Post或者Get都需要做加密和签名处理。
- 不要使用301跳转 301跳转很容易被http劫持攻击。移动端http使用301比桌面端更危险,用户看不到浏览器地址,无法察觉到被重定向到了其他地址。如果一定要使用,确保跳转发生在https的环境下,而且https做了证书绑定校验。
- http请求都带上MAC 所有客户端发出的请求,无论是查询还是写操作,都带上MAC(Message Authentication Code)。MAC不但能保证请求没有被篡改(Integrity),还能保证请求确实来自你的合法客户端(Signing)。当然前提是你客户端的key没有被泄漏,如何保证客户端key的安全是另一个话题。MAC值的计算可以简单的处理为hash(request params+key)。带上MAC之后,服务器就可以过滤掉绝大部分的非法请求。MAC虽然带有签名的功能,和RSA证书的电子签名方式却不一样,原因是MAC签名和签名验证使用的是同一个key,而RSA是使用私钥签名,公钥验证,MAC的签名并不具备法律效应。
- http请求使用临时密钥 高延迟的网络环境下,不经优化https的体验确实会明显不如http。在不具备https条件或对网络性能要求较高且缺乏https优化经验的场景下,http的流量也应该使用AES进行加密。AES的密钥可以由客户端来临时生成,不过这个临时的AES key需要使用服务器的公钥进行加密,确保只有自己的服务器才能解开这个请求的信息,当然服务器的response也需要使用同样的AES key进行加密。由于http的应用场景都是由客户端发起,服务器响应,所以这种由客户端单方生成密钥的方式可以一定程度上便捷的保证通信安全。
合理的并发数
有些业务场景会出现多个Request集中产生的情况,此时我们需要设置一个合理的并发数。并发数如果太小,会导致“劣质”的请求block住“优质”的请求。如果并发数太大,带宽有限的场景下,会增加请求的整体延迟。
可靠性保障
对于关键核心的数据请求、重要内容的请求、一般性内容的请求,需要有不同的策略。理论上我们应该尽可能让所有的请求成功率达到最高,但客户端的流量,带宽,手机电量,服务器的压力等都是有限的资源,所以我们采取的策略是只对关键性的网络请求做高强度的可靠性保障。
如核心请求,类似大家用微信时发送的消息,消息数据一旦从输入框发出,从用户来的角度感知这个消息数据是一定会到达对方的。如果网络环境差,网络模块会自动在后头悄悄重试,一段时间后仍无法成功就通过产品交互的方式告知用户发送失败了,即使失败,请求的数据(消息本身)一直存在客户端。对于这类请求的处理方式第一步不是通过网络发送,而是持久化到Database当中。一旦入了DB,即使断网,断电,重启,请求数据依然还在,只需在App重启的时候还原请求数据,再次发送即可。我们用代码来进一步阐释。第二步发送请求,如果请求失败则将请求加入重试队列,成功则从重试队列中移除。重试队列背后也需要一套通用机制,比如多久重试一次,重试几次之后放弃。遇到最恶劣的场景,请求发送失败之后,App被kill。我们需要在App重启之后从DB当中重新load所有失败的请求再次重试。
通过上述几步基本上可以使请求的可靠性得到极大的保障。但100%是无法实现的理想,失败的时候用户重装App,所有持久化的数据丢失,请求数据也就丢了,不过这种极端的场景非常少。
第二类请求的可靠性为PPRequestReliability_Retry,这类请求的例子可以是我们App启动时用户看到的首页,首页的内容从服务器获取,如果第一次请求就失败体验较差,这种场景下我们应该允许请求有机会多试几次,增加一个retryCount即可。
一般3次的重试基本可以排除网络抖动的情况。三次失败之后即可认为请求失败,通过产品交互告知用户。
第三类请求的重要性最低,比如进入Controller的UV采集打点。这类请求只需要做一次,即使失败也不会对产品体验产生什么负面影响。
网络环境监控
现在网络环境虽然越来越好,WiFi,4G,3G在一二线城市都有很好的普及,但还是有不少场景会导致网络状态突然变差,比如进电梯,做火车,人多的集会场所,从公司回家4G切Wifi等等,这些场景在生活当中并不少见,健壮的网络模块需要仔细的检测网络的变化,针对性的做请求重试。
请求成功率监控
网络模块应该能监控当前App的网络请求成功率,对于失败率较高的请求,带上业务数据,手机网络环境,系统参数等等,在用户不活跃的时候能打包上报给server端,一则能找出更多需要优化的业务场景,二则能实时监控server端的健康状态,三则能从数据层面精确判断每一次网络优化是否有成效。