移动端网络优化

网络优化有很多常见的点,最早的认识来源于微信开源的 Mars,当时由于好奇把 Mars 集成进 APP,并且把 Mars 的相关 Wiki 看了,学到了很多。后来看了美团的网络库 Shark,这个还没有开源,但是获得了美团内部的大奖。两者的侧重点各有不同,不过根源还是很像的。

原生api

如果不自建网络库,大部分情况下,AFNetworking 已经成为标配,当然本质上还是基于NSURLSession。苹果建议使用 NSURLSession 这些它自己提供的 api,因为每个版本苹果都会优化,如果自建网络库,就享受不到这些优化。

举个例子,iOS11 的时候苹果就做了些优化:

  • 多路多协议的网络操作:使得移动设备的 TCP 包可以在这两个(多个)链路(WiFi,2/3/4G)上随意切换着发(同时开启两个流量链路),而不必断线重连。
  • 显式拥塞通知:因为通过丢包来发现网络拥塞(隐式拥塞通知)是一件非常消耗成本的事情。而显示拥塞通知不会丢包,而是会在包的 header 里打上一个标记。接收方拿到这个带着标记的包之后,就认为网络有拥塞现象了,于是就可以降低发包速率,从而缓解网络拥塞。
  • 网络操作在用户空间执行:linux 下,类似 socket,I/O 这些对系统资源的操作都在内核空间下进行,讲网络操作移动用户空间下执行,就不会有内核中断,上下文切换这种消耗。

自建网络库

自建网络库主要有 3 个方面可以优化,bang这篇文章讲了很详细:

  1. 速度:网络请求的速度怎样能进一步提升?
  2. 弱网:移动端网络环境随时变化,经常出现网络连接很不稳定可用性差的情况,怎样在这种情况下最大限度最快地成功请求?
  3. 安全:怎样防止被第三方窃听/篡改或冒充,防止运营商劫持,同时又不影响性能?

速度

DNS

DNS 解析耗时和 DNS 劫持问题最容易的方法就是 IP 直连,直接 Hard Code 写死几个 IP 地址。当然这样容灾的时候就无解了。一般都是用 HTTPDNS 来解决的,自己做域名解析的工作,通过 HTTP 请求后台去拿到域名对应的 IP 地址。

客户端会根据优先级去获取不同的 IP 列表,一般情况下 HTTPDNS > DNS > Hard Code。
所以本地会有很多个 IP 地址,选用哪一个就成了一个优化点:

  • 串行连接:就是一个连接一个连接的试,只要有成功的就用那个 IP。
  • 并行连接:同时发起 N 个连接,只要有一个连接成功就用它,而那个连接一定也是最快的。
  • 复合连接:综合以上两种。

串行并行各有优缺点,而复合连接是一种折中,而且实现起来会复杂点。在程序的世界里,类似的选择很多,大多数情况下,折中的那种选择都是综合性能最好的。

Shark 是用的并行连接,也就是跑马,同时发起 N 个连接,只要有一个成功了,就断开别的连接,选择最快的那个。这种方式服务器负载高,网络竞争大,不过它是最快可用的。

Mars 用的复合连接,能同时满足高性能、高可用、低负载。
mars_ip_connect
初始阶段,应用发起对 IP1 &Port1 的 connect 调用。在第4秒的时候,如果第一个 connect 还没有返回,则发起对 IP2 &Port2 的 connect 调用。以此类推,直至发起了5组 IP&Port 的 connect 调用。
和串行的不同就是它不会等待上一个连接确定不可用了才发起下一个。但也不是同时发起 N 个,这是一种折中。正因为这样,IP 顺序的选择就显得很重要了。

微信 IP&Port 排序算法的演进

连接

因为 tcp 固有的三次握手,四次挥手,所以建立/断开一次连接很耗时。所以更有效率地复用连接就很重要。大方向上,一般还是分为:

  • http: keep-alive(http1.1默认开启),多路复用(HTTP2),iOS9 以上 NSURLSession 原生支持 HTTP2,只要服务端也支持就可以直接使用。
  • tcp长连接:自己与服务端建立 tcp 长连接,这样类似于 HTTP2 的多路复用就需要自己去实现。

Mars 实现的 tcp 长连接就实现了类似 HTTP2 的多路复用。

对于连接的终止,会有四次挥手。主动关闭的一方会进入 TIME_WAIT 状态,在此状态中通常将停留2倍的 MSL 时长。TIME_WAIT 的数量太多会导致耗尽主动关闭方的 socket 端口和句柄,导致无法再发起新的连接,进而严重影响主动关闭方的并发性能,所以应该尽量的由终端来发起关闭的操作,避免服务器的大量 TIME_WAIT 状态。例如,使用长连接避免频繁的关闭;在短连接的协议设计上,务必加上终止标记(例如 http 头部加上 content-length )使得可以由终端来发起关闭的操作。

数据

数据的传输一般也分为两大类:

  • 字符串:例如 json,可读性好,但是数据量大,序列化反序列化速度慢。
  • 二进制:例如 protobuf,和上面的字符串优缺点正好相反。不过正因为没有可读性,必须依赖本地的数据格式才能反序列化,安全性上得到了保证。

Shark 自己用了一套协议 NVObject,我大概看了下实现,比 protobuf 还是要差很多,实现非常简单。
举个例子:
NVObject 的 int 都是固定 4 个字节,一点压缩算法也没用。而 protobuf 用了 Varint 编码,设置了最高有效位(msb),这一位表示还会有更多字节出现。这样对于比较小的数字,一个字节就可以了。

弱网

当时把 Mars 集成进项目的原因就是,微信真的做的好,弱网环境下别的 APP 没信号,但是微信可以把数据发出去。
除了各种 tcp 参数调优外,Mars 的传输超时设计很值得学习,信令传输超时设计

传输层的重传超时时间 (RTO)会根据数据的往返时间(RTT)动态计算,会满足一个“指数退避”的关系。也就是说重传超时时间会增加了很快。那么弱网环境下,几次丢包后,重传超时时间都 1 分钟了,这种用户体验就非常糟糕了。

所以 Mars 会在应用层断掉当前连接,重新建立连接并发送请求。一句话概括就是,尽早重试,保证可用性。

我们总结应用层超时重传,可以带来以下作用:

  1. 减少无效等待时间,增加重试次数:当 TCP 层的重传间隔已经太大的时候,断连重连,使得 TCP 层保持积极的重连间隔,提高成功率;
  2. 切换链路:当链路存在较大波动或严重拥塞时,通过更换连接(一般会顺带更换IP&Port)获得更好的性能。

安全

网络安全这个范围就很广了。首先先讲讲https,看完这张图就能明白https流程:
httpsCreat

如果倒过来推,就可以更好地理解https:

  • 首先客户端和服务端传输的数据需要用秘钥加密,对称加密就可以了。客户端每次连接都应该生成一对秘钥,然后发送一个秘钥给服务端。
  • 发送的秘钥肯定也需要加密,就需要非对称加密,服务端留有 S.pri,客户端留有 S.pub。但是这个S.pub 需要服务端发送给客户端,这个还是要加密,不然整个流程还是不安全。
  • 这样就有了鸡生蛋蛋生鸡的问题,这个时候就需要 CA 机构来打破这个环,CA 机构会生成 C.pub 和 C.pri,C.pub由浏览器内置,而 C.pri 则对服务端需要发送给客户端的 S.pub 加密生成 CA 证书发给客户端。这个 CA 证书包含 S.pub,颁发机构,有效期等。

这样就能更好地理解上面那一张图。

有一点一定要明白,https 不等于 绝对安全。

中间人攻击

https_middle_attack

比如最常用的抓包工具 Charles,即使你 APP 开启了证书校验,但是用 Charles 时会让你安装证书,这样中间人攻击依然能够发生。
iPhone 受信任的证书包括预装的受信任根证书列表和用户自己安装的证书,预装的证书中间人多数是没有的,所以关键就在于用户自己安装的证书,切记不能随便安装证书。

当然也有避免的办法,就是开启SSL Ping Mode,把证书打包进 APP,在NSURLConnectionDelegate的connection:willSendRequestForAuthenticationChallenge: 方法里检测证书是否被修改。

越狱后代码注入

利用NSURLProtocol可以拦截所有NSURLConnection请求。

@interface HttpDumpURLProtocol : NSURLProtocol
@end

@implementation HttpDumpURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
   NSLog(@"request:%@",request.URL.absoluteString);
   return NO;
}
@end

所以只要有一台越狱设备,然后注入代码就行了,都不需要高深的逆向分析。比较简单的办法是编译成动态库启动 APP 时直接挂载。

所以要切记,即使用了 https,传输的数据还是要加密。

作者:levi
comments powered by Disqus