当关心 App 的用户体验的时候,不得不考虑网络层相关的问题。因为一个 App 通常来说网络层的操作占据了大多数的场景。几乎每个成熟的 iOS 项目都有一个网络模块,大部分的网络请求都是基于 HTTP 完成,iOS 端采用成熟的 AFNetworking 很容易完成一个功能简单的网络模块,但是使用起来往往会有大量的问题。所以网络层优化是需要大量的经验和知识水平的。对数据的分析和调研、用户反馈,现总结网络层相关的优化手段。
优化方面:
正常一条网络请求需要经过的流程是:
这里存在3个优化点:
DNS 完整的解析流程很长,会先从本地系统缓存读取,若没有就到最近的 DNS 服务器取,若没有再到主域名服务器取,每一层都有缓存,但为了域名解析的实时性。每一层缓存都设有过期时间,这种 DNS 解析机制有几个缺点:
为了解决上述问题,就有了 HTTPDNS。原理就是代替系统的 DNS 解析工作,解决上述问题。
对于 DNS 解析的情况,业界主流做法就是 HTTPDNS 或者内置 Server IP 列表。客户端直接访问 HTTPDNS 接口,获取业务在域名配置系统上配置的访问延迟最优的 IP,获取到 IP 后就直接往此 IP 发送业务协议请求,不再需要本地 DNS 服务器进行解析,从根本上解决了劫持问题。同时可以降低网络延迟,提高连接的成功率。
建立的 Server IP 列表,是在本地缓存一个 IP 映射表,可以在 App 启动时请求接口下发更新。访问其他的服务的时候根据映射拿到 IP 再发出请求。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WuhypgX2-1585661949225)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-DNSLookUP.png)]
绝大多数的 App 的第一步都是 DNS 解析,解析请求回根据当时的网络情况不同而不同,各平台的 DNS 缓存策略存在差异,因此对于移动 App 网络性能会产生影响。App 网络情况跟很多因素都相关。但是 DNS 是第一步也是最重要的一环。
降低 DNS 请求带来的延迟
客户端 App 请求第一步是 DNS 解析。但是由于 Cache 的存在,使得大部分的解析请求都不会产生任何延迟。各平台都有自己的 Cache 过期策略。像 iOS 系统一般都是 24h 后过期。还有就是从飞行模式切换回来、开关机、重置网络设置等都会导致 DNS Cache 的清除。所以一般情况用户在第二天打开你的 App 都会经历一次完整的 DNS 解析请求。网络情况差的时候会明显的请求总耗时增加。如果可以直接跳过 DNS 解析这一步,就可以提高网络性能了。
预防 NDS 劫持
DNS 劫持指的是改变 DNS请求返回的结果,将目的域名对应的 IP 指向另一个地址。一般有两种方式,一种是通过病毒的方式改变本机配置的 DNS 服务器地址,二是通过攻击正常的 DNS 服务器而改变其行为。不管何种方式都会影响 App 的业务请求,如果遇到恶意的攻击还会衍生出各种安全问题。客户端自己做 DNS 到 IP 地址的映射就绕过了向 DNS 服务器请求而可能被遭到攻击的可能,让劫持者无从下手
服务器动态部署
DNS 映射实际上是模拟了 DNS 请求的解析行为。如果客户端将自己的位置信息(例如ip地址、国家码等)上传给服务器,服务器就可以根据位置信息就近推荐合适的 Server IP 地址。从而减小了整体网络请求延迟、实现了动态部署
如何设计自己的 DNS 映射机制?
DNS 服务器做的事情就是输入一个域名,输出一个 IP 地址,做自己的 DNS 映射机制就是在客户端维护一个这样的映射文件。不过这个映射文件可以根据服务器在 App 端进行更新。还需要具备一定的容错处理。
在 iOS 端实践。大致有3个角色:mapper、validator、reporter
mapper
mapper 是负责和外部交互的部分。主要负责在输入 domain name 的情况下输出 ip。同时校验来自应用层请求成功和失败的信息。失败的情况下需要将 ip 进一步验证,以确定是真的无效。如果无效则进行上报。同时还负责更新机制
validator
负责在接收到请求失败的 ip 时,对这个 ip 进行有效性验证。检测的强弱规则可以自定义。但是一般规则是在后台线程使用这个 ip 进行多次尝试,如果都不成功则告诉 mapper 这个 ip 确实无用,如果成功则说明有效。(某次失败有可能意味着当时的网络环境不稳定)
reporter
主要负责告诉 server 整个 mapping 机制的健康状况。在出现某个 ip 导致请求失败并由 validator 校验多次还是失败的情况下需要上报到服务端,让服务端去维护或验证。(有可能是在配置的时候少打了个字母 )
根据公司业务情况进行改造。比如采用服务器定时更新映射文件的这一步骤,可以更改为 socket 长链接通道在需要更新时 push,或利用 HTTP2.0 的 server push 机制。还有上报机制。还可以对请求的总量、成功率、映射成功率等数据做侦测。
第二个问题,连接建立的耗时问题,这里主要的优化思路是复用连接,不用每次请求都重新建立连接,如何更有效率地复用连接,可以说是网络请求速度优化里最主要的点了,并且这里的优化在不断的演进中,值得关注
HTTP 协议里有个 keep-alive
,HTTP1.1 默认开启,一定程度上缓解了每次请求都需要进行 TCP 三次握手建立连接的耗时。原理是请求完成后不立即释放连接,而是放入连接池中。若此时有另一个请求要发出,如果请求的端口和域名在复用池里面有一致的,那么就直接拿出连接池中的连接进行发送和接收数据,少了建立连接的耗时。
实际上,现在无论是客户端还是浏览器都默认开启了 keep-alive,对同个域名不会再有每发一个请求就进行一次建连的情况,纯短连接已经不存在了。但是有问题,就是这个 keep-alive 的短连接一次只能发送接收一个请求,在上一个请求处理完成之前。无法接受新的请求。若同时发起多个请求,就有两种情况:
HTTP2 的多路复用机制一样是复用连接。但它复用的这条连接支持同时处理多条请求,所有请求都可以在这条连接上进行,也就是解决了上面说的并发请求需要建立多条连接带来的问题。
HTTP1.1 的协议里,在一个连接里传输数据都是串行顺序传输的,必须等上一个请求全部处理完成后,下一个请求才能进行处理,导致这些请求期间这条连接并不是满带宽传输的,即使是 HTTP1.1 的 pipelining 可以同时发送多个 Request,但 response 仍是按请求的顺序串行返回,只要其中一个 response 稍微大一点或发送错误,就会阻塞住后面的请求。
HTTP2 这里的多路复用协议解决了这些问题,它把在连接里传输的数据都封装成一个个 stream,每个 stream 都有标识,stream 的发送和接收可以是乱序的,不依赖顺序,也就不会有阻塞的问题,接收端可以根据 stream 的标识去区分属于哪个请求,再进行数据拼接,得到最终数据。
解释下多路复用这个词,多路可以认为是多个连接,多个操作,复用就是 复用一条连接或一个线程。HTTP2 这里是连接的多路复用,网络相关的还有一个 I/O 的多路复用(select/epoll),指通过事件驱动的方式让多个网络请求返回的数据在同一条线程里完成读写。
客户端来说,iOS9 以上 NSURLSession 原生支持 HTTP2,只要服务端也支持就可以直接使用,Android 的 okhttp3 以上也支持了 HTTP2,国内一些大型 APP 会自建网络层,支持 HTTP2 的多路复用,避免系统的限制以及根据自身业务需要增加一些特性,例如微信的开源网络库 mars,做到一条长连接处理微信上的大部分请求,多路复用的特性上基本跟 HTTP2 一致。
HTTP2 的多路复用看起来是完美的解决方案,但还有个问题,就是队头阻塞,这是受限于 TCP 协议,TCP 协议为了保证数据的可靠性,若传输过程中一个 TCP 包丢失,会等待这个包重传后,才会处理后续的包。HTTP2的多路复用让所有请求都在同一条连接进行,中间有一个包丢失,就会阻塞等待重传,所有请求也就被阻塞了。
对于这个问题不改变 TCP 协议就无法优化,但 TCP 协议依赖操作系统实现以及部分硬件的定制,改进缓慢,于是 GOOGLE 提出 QUIC 协议,相当于在 UDP 协议之上再定义一套可靠传输协议,解决 TCP 的一些缺陷,包括队头阻塞。具体解决原理网上资料较多,可以看看。
QUIC 处于起步阶段,少有客户端接入,QUIC 协议相对于 HTTP2 最大的优势是对TCP队头阻塞的解决,其他的像安全握手 0RTT / 证书压缩等优化 TLS1.3 已跟进,可以用于 HTTP2,并不是独有特性。TCP 队头阻塞在 HTTP2 上对性能的影响有多大,在速度上 QUIC 能带来多大提升待研究。
第三个问题,传输数据大小问题。数据对请求速度的影响分两方面,一是压缩率,二是解压序列化反序列化的速度。目前最流行的两种数据格式是 json 和 protobuf。json 是字符串,protobuf 是二进制。即使采用各种压缩算法压缩后,protobuf 仍会比 json 小。protobuf 在数据量和序列化速度上均占优势。
压缩算法多种多样,且在不断演进。最新出得 Brotli 和 Z-standard 实现了更高的压缩率。Z-standard 可以根据业务数据样本训练出适合的字典,进一步提高压缩率。是目前最好的压缩算法
除了传输数据的 body 大小,每个 HTTP 协议头的数据也不可忽视,HTTP2 里对 HTTP 协议头也进行了压缩,HTTP 头大多是重复数据,固定的字段如 method 可以用静态字典,不固定但多个请求重复的字段例如 cookie 用动态字典,可以打到非常高的压缩率。可以查看这篇文章查看介绍。
总结:通过 HTTPDNS,连接多路复用,更好的压缩算法,可以把网络请求的速度优化到不错的程度了。接下来看看弱网环境和安全方面的手段吧
手机无线网络环境不稳定,针对弱网的优化,微信有较多的实践和分享
提升连接的成功率
复合连接。建立连接时,阶梯式并发连接,其中一条连通后其他连接都关闭。这个方案结合串行和并发的优势。提高弱网下的连接成功率,同时又不会增加服务器资源消耗
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S08zGtWX-1585661949226)(https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-BadNetwork.png)]
制定最合适的超时时间
对总读写超时(从请求到响应的超时)、首包超时、包包超时(两个数据段之间的超时)时间制定不同的计算方案,加快对超时的判断,减少等待时间,尽早重试。这里的超时时间还可以根据网络状态动态设定
调优 TCP 参数,使用 TCP 优化算法
对于服务端的 TCP 协议参数进行调优。以及开启各种优化算法使得业务特性和移动端网络环境,包括 RTO 初始值,混合慢启动,TLP、F-RTO 等
针对弱网的优化未成为标准方案,系统网络库没有内置,不过前两个客户端优化微信的开源网络库 mars 有实现。
标准安全协议 TLS
保证了网络传输的安全,前身是 SSL,不断在演进。我们日常使用的 HTTPS 就是 HTTP 协议加上 TLS 安全协议。
安全协议概括性地说解决两个问题:1. 保证安全;2. 降低加密成本
在保证安全上:
降低加密成本上:
想详细看看 TLS 的可以看看这篇文章
目前基本主流都支持 TLS 1.2。iOS 网络库默认使用 TLS 1.2,Android 4.4 以上支持 1.2。
网络层涉及的学问非常多,需要懂得多端的重视才可以提出靠谱的解决方案。希望不断认识不断思考。
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
所以会强引用//AFURLSessionManager.h
@property (readonly, nonatomic, strong) NSURLSession *session;
// AFHTTPSessionManager.m
// NSURLSessionConfiguration
@property (nullable, readonly, retain) id delegate;
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks {
if (cancelPendingTasks) {
[self.session invalidateAndCancel];
} else {
[self.session finishTasksAndInvalidate];
}
}