# 移动端网络层优化

当关心 App 的用户体验的时候，不得不考虑网络层相关的问题。因为一个 App 通常来说网络层的操作占据了大多数的场景。几乎每个成熟的 iOS 项目都有一个网络模块，大部分的网络请求都是基于 HTTP 完成，iOS 端采用成熟的 AFNetworking 很容易完成一个功能简单的网络模块，但是使用起来往往会有大量的问题。所以网络层优化是需要大量的经验和知识水平的。对数据的分析和调研、用户反馈，现总结网络层相关的优化手段。
优化方面：
1. 速度：网络请求速度如何进一步提升
2. 弱网：移动端网络环境随时变化，经常出现网络连接很不稳定可用性差的情况。怎样在这种情况下最大限度最快完成网络请求
3. 安全：怎样防止被第三方窃听。篡改或冒充，防止运营商劫持，同时有不影响性能
   


## 一、速度

正常一条网络请求需要经过的流程是：
1. DNS 解析。请求 DNS 服务器，获取域名对应的 IP 地址
2. 与服务器建立连接。包括 TCP 三次握手，安全协议同步流程
3. 连接建立完成，发送、接收数据，解码数据

这里存在3个优化点：
1. 直接使用 IP 地址，去除 DNS 解析步骤
2. 不要每次请求都重新建立连接，复用连接或一直使用同一条连接（长连接）
3. 压缩数据，减小传输数据的大小

### 1. DNS

DNS 完整的解析流程很长，会先从本地系统缓存读取，若没有就到最近的 DNS 服务器取，若没有再到主域名服务器取，每一层都有缓存，但为了域名解析的实时性。每一层缓存都设有过期时间，这种 DNS 解析机制有几个缺点：
- 缓存时间设置过长，域名更新不及时。设置时间短，大量 DNS 解析请求影响请求毒素
- 域名劫持。容易被中间人攻击，或者运营商劫持。把域名解析道第三方 IP 地址，据统计劫持率高达 7%
- DNS 解析过程不受控制，无法保证最快的解析速度
- 一次请求只可以借此一个域名

为了解决上述问题，就有了 HTTPDNS。原理就是代替系统的 DNS 解析工作，解决上述问题。
- 域名解析与请求分离，所有请求都直接使用 IP 地址，无需 DNS 解析，App 定时请求 HTTPDNS 服务器更新 IP 地址即可
- 通过签名等方式，保证 HTTPDNS 请求的安全，避免被劫持
- DNS 解析由自己控制。可以保证根据用户所在地返回就近的 IP 地址。或根据客户端测速结果使用最快的 IP
- 一次请求可以解析多个域名

对于 DNS 解析的情况，业界主流做法就是 HTTPDNS 或者内置 Server IP 列表。客户端直接访问 HTTPDNS 接口，获取业务在域名配置系统上配置的访问延迟最优的 IP，获取到 IP 后就直接往此 IP 发送业务协议请求，不再需要本地 DNS 服务器进行解析，从根本上解决了劫持问题。同时可以降低网络延迟，提高连接的成功率。

建立的 Server IP 列表，是在本地缓存一个 IP 映射表，可以在 App 启动时请求接口下发更新。访问其他的服务的时候根据映射拿到 IP 再发出请求。

![DNS服务器解析示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-DNSLookUP.png)

绝大多数的 App 的第一步都是 DNS 解析，解析请求回根据当时的网络情况不同而不同，各平台的 DNS 缓存策略存在差异，因此对于移动 App 网络性能会产生影响。App 网络情况跟很多因素都相关。但是 DNS 是第一步也是最重要的一环。

1. 降低 DNS 请求带来的延迟
   客户端 App 请求第一步是 DNS 解析。但是由于 Cache 的存在，使得大部分的解析请求都不会产生任何延迟。各平台都有自己的 Cache 过期策略。像 iOS 系统一般都是 24h 后过期。还有就是从飞行模式切换回来、开关机、重置网络设置等都会导致 DNS Cache 的清除。所以一般情况用户在第二天打开你的 App 都会经历一次完整的  DNS 解析请求。网络情况差的时候会明显的请求总耗时增加。如果可以直接跳过 DNS 解析这一步，就可以提高网络性能了。

2. 预防 NDS 劫持
   DNS 劫持指的是改变 DNS请求返回的结果，将目的域名对应的 IP 指向另一个地址。一般有两种方式，一种是通过病毒的方式改变本机配置的 DNS 服务器地址，二是通过攻击正常的 DNS 服务器而改变其行为。不管何种方式都会影响 App 的业务请求，如果遇到恶意的攻击还会衍生出各种安全问题。客户端自己做 DNS 到 IP 地址的映射就绕过了向 DNS 服务器请求而可能被遭到攻击的可能，让劫持者无从下手

3. 服务器动态部署
   DNS 映射实际上是模拟了 DNS 请求的解析行为。如果客户端将自己的位置信息（例如ip地址、国家码等）上传给服务器，服务器就可以根据位置信息就近推荐合适的 Server IP 地址。从而减小了整体网络请求延迟、实现了动态部署

如何设计自己的 DNS 映射机制？

DNS 服务器做的事情就是输入一个域名，输出一个 IP 地址，做自己的 DNS 映射机制就是在客户端维护一个这样的映射文件。不过这个映射文件可以根据服务器在 App 端进行更新。还需要具备一定的容错处理。

- 一个打包到 App 包里面的默认域名 IP 映射文件，这样就可以避免第一次去服务器取配置文件带来的延迟
- 一个定时器可以每隔一段时间**通过签名等方式（避免被劫持）**去服务器获取最新的域名映射文件，并保存到本地
- 每次取到最新的映射文件后，保存到本地，并将上次的映射文件保存作为备份。一旦出现线上配置错误的情况，不至于导致请求无法处理
- 如果映射文件不能处理域名映射，那么可以回滚到使用默认的 DNS 解析服务
- 如果映射后的一个 IP 持续请求失败，那么应从机制上避免这个问题。也就是需要一个无效使用的淘汰机制
- 无效的 IP 可以上报到服务器。发现问题解决问题

在 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 机制。还有上报机制。还可以对请求的总量、成功率、映射成功率等数据做侦测。

### 2. 连接

第二个问题，连接建立的耗时问题，这里主要的优化思路是复用连接，不用每次请求都重新建立连接，如何更有效率地复用连接，可以说是网络请求速度优化里最主要的点了，并且这里的优化在不断的演进中，值得关注

#### 2.1 keep-alive

HTTP 协议里有个 `keep-alive`，HTTP1.1 默认开启，一定程度上缓解了每次请求都需要进行 TCP 三次握手建立连接的耗时。原理是请求完成后不立即释放连接，而是放入**连接池**中。若此时有另一个请求要发出，如果请求的端口和域名在复用池里面有一致的，那么就直接拿出连接池中的连接进行发送和接收数据，少了建立连接的耗时。

实际上，现在无论是客户端还是浏览器都默认开启了 keep-alive，对同个域名不会再有每发一个请求就进行一次建连的情况，纯短连接已经不存在了。但是有问题，就是这个 keep-alive 的短连接一次只能发送接收一个请求，在上一个请求处理完成之前。无法接受新的请求。若同时发起多个请求，就有两种情况：

 1. 若串行发送请求，可以一直复用一个连接，但速度很慢，每个请求都需要等待上个请求完成再进行发送
 2. 若并行发送请求，那么首次每个请求都要进行 TCP 三次握手建立新的连接，虽然第二次可以复用连接池里面的这堆连接，但若连接池里面保留的过多，对服务端资源产生交大浪费，若限制了保持的连接数，并行请求超出的连接仍每次需要建立连接。对于这个问题新一代的 HTTP2.0 提出了多路复用解决方案。

#### 2.2 多路复用

HTTP2 的多路复用机制一样是复用连接。但它复用的这条连接支持同时处理多条请求，所有请求都可以在这条连接上进行，也就是解决了上面说的并发请求需要建立多条连接带来的问题。

![HTTP2多路复用](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-MultiplesRoutes.png)

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](https://github.com/Tencent/mars/issues?page=2&q=is%3Aissue+is%3Aopen)，做到一条长连接处理微信上的大部分请求，多路复用的特性上基本跟 HTTP2 一致。


#### 2.3 TCP队头阻塞

HTTP2 的多路复用看起来是完美的解决方案，但还有个问题，就是队头阻塞，这是受限于 TCP 协议，TCP 协议为了保证数据的可靠性，若传输过程中一个 TCP 包丢失，会等待这个包重传后，才会处理后续的包。HTTP2的多路复用让所有请求都在同一条连接进行，中间有一个包丢失，就会阻塞等待重传，所有请求也就被阻塞了。

对于这个问题不改变 TCP 协议就无法优化，但 TCP 协议依赖操作系统实现以及部分硬件的定制，改进缓慢，于是 GOOGLE 提出 QUIC 协议，相当于在 UDP 协议之上再定义一套可靠传输协议，解决 TCP 的一些缺陷，包括队头阻塞。具体解决原理网上资料较多，可以看看。

QUIC 处于起步阶段，少有客户端接入，QUIC 协议相对于 HTTP2 最大的优势是对TCP队头阻塞的解决，其他的像安全握手 0RTT / 证书压缩等优化 TLS1.3 已跟进，可以用于 HTTP2，并不是独有特性。TCP 队头阻塞在 HTTP2 上对性能的影响有多大，在速度上 QUIC 能带来多大提升待研究。

### 3. 数据

第三个问题，传输数据大小问题。数据对请求速度的影响分两方面，一是压缩率，二是解压序列化反序列化的速度。目前最流行的两种数据格式是 json 和 protobuf。json 是字符串，protobuf 是二进制。即使采用各种压缩算法压缩后，protobuf 仍会比 json 小。protobuf 在数据量和序列化速度上均占优势。

压缩算法多种多样，且在不断演进。最新出得 Brotli 和 [Z-standard](https://github.com/facebook/zstd) 实现了更高的压缩率。Z-standard 可以根据业务数据样本训练出适合的字典，进一步提高压缩率。是目前最好的压缩算法

除了传输数据的 body 大小，每个 HTTP 协议头的数据也不可忽视，HTTP2 里对 HTTP 协议头也进行了压缩，HTTP 头大多是重复数据，固定的字段如 method 可以用静态字典，不固定但多个请求重复的字段例如 cookie 用动态字典，可以打到非常高的压缩率。可以查看这篇[文章](https://imququ.com/post/header-compression-in-http2.html)查看介绍。


总结：通过 HTTPDNS，连接多路复用，更好的压缩算法，可以把网络请求的速度优化到不错的程度了。接下来看看弱网环境和安全方面的手段吧

## 弱网

手机无线网络环境不稳定，针对弱网的优化，微信有较多的实践和分享

1. 提升连接的成功率
   复合连接。建立连接时，阶梯式并发连接，其中一条连通后其他连接都关闭。这个方案结合串行和并发的优势。提高弱网下的连接成功率，同时又不会增加服务器资源消耗

   ![弱网复合连接](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-14-BadNetwork.png)

2. 制定最合适的超时时间
   对总读写超时（从请求到响应的超时）、首包超时、包包超时（两个数据段之间的超时）时间制定不同的计算方案，加快对超时的判断，减少等待时间，尽早重试。这里的超时时间还可以根据网络状态动态设定

3. 调优 TCP 参数，使用 TCP 优化算法
   对于服务端的 TCP 协议参数进行调优。以及开启各种优化算法使得业务特性和移动端网络环境，包括 RTO 初始值，混合慢启动，TLP、F-RTO 等

针对弱网的优化未成为标准方案，系统网络库没有内置，不过前两个客户端优化微信的开源网络库 [mars](https://github.com/Tencent/mars) 有实现。

## 安全

标准安全协议 `TLS` 保证了网络传输的安全，前身是 SSL，不断在演进。我们日常使用的 HTTPS 就是 HTTP 协议加上 TLS 安全协议。

安全协议概括性地说解决两个问题：1. 保证安全；2. 降低加密成本

在保证安全上：
1. 使用加密算法组合对传输数据加密，避免被窃听和篡改，
2. 认证对方身份，避免被第三方冒充
3. 加密算法保持灵活可更新，防止定死算法被破解后无法更换，禁用已破解的算法。

降低加密成本上：
1. 用对称加密算法加密传输数据，解决非对称加密算法的性能低以及长度限制问题
2. 缓存安全协议握手后的密钥数据，加快第二次建连的速度。
3. 加快握手过程：2RTT -> 0RTT。加快握手的思路，就是原本客户端和服务端需要协商使用什么算法后才能加密发送数据，变成通过内置的公钥和默认的算法，在握手的同时就把数据发出去，也就是不需要等待握手就开始发送数据，打到 0RTT
   
想详细看看 TLS 的可以看看这篇[文章](https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/)

目前基本主流都支持 TLS 1.2。iOS 网络库默认使用 TLS 1.2，Android 4.4 以上支持 1.2。


## 其他优化方案

- 域名合并：淘宝、美团等公司公布的解决方案中都有提到，就是将公司原来的很多域名都合并到较少的几个域名。为什么？因为 HTTP 的通道复用就是基于域名划分的。如果域名只有几个，那么多数请求都可以在长连接通道进行，这样就可以降低延迟、增加成功率
- 预热，尽早建立长连接。这样其他的业务请求就可以复用长连接通道。加快访问速度。因为每次建立连接都需要经过 DNS 域名解析、TCP 三次握手等漫长步骤。建立长连接的时机可以考虑：冷启动、前后台切换、网络切换等
- 如果情况允许，可以将网络切换到 HTTP 2.0，解决了 HTTP1.1 的 head of blocking ，降低了网络延迟，提供了更强大的多路复用技术。还加入了流量控制、新的二进制格式、Server Push、请求优先级和依赖等待等特性。
- 建立多通道。比如携程、艺龙、美团等公司都有自己的 TCP、UDP 通道。具有多域名共用通道。
- 有些超级大厂还自研了协议。比如 QUIC
- 加入 CDN 加速，动态静态资源分离
- 对于类似埋点的业务数据请求，可以合并请求，减小流量。另外结合埋点数据压缩上传
- App 网络情况诊断
- 根据网络情况，动态设置超时时间等



## 最后

网络层涉及的学问非常多，需要懂得多端的重视才可以提出靠谱的解决方案。希望不断认识不断思考。


## 参考资料

- [2016年携程App网络服务通道治理和性能优化实践](https://chuansongme.com/n/466033251461)


## 参考点：
- 移动调度
  1. DNS（DNS劫持、运营商DNS层次不齐、1RTT请求DNS、不支持LDC多中心调度、不支持自定义调度）。移动调度优势：LDC多中心调度、异地多活快速容灾、白名单问题排查
- 接口设计优化
  1. 慢逻辑监控
  2. 多次查询优化
  3. 接口 cache 等
- 静态资源、图片等相关策略
  1. 使用更快的图片格式（WebP等）
  2. 不同网络的不同图片下发
  3. 资源合并、压缩（combo）
  4. 图片压缩（webp）
- 让用户觉得快
  1. 优先级加载
  2. 异步加载
- 减小数据包大小和优化包量
  1. 推广 Protocol Buffer 等序列化方式
  2. 接入 SYNC
- 监控体系建设
  1. 全链路数据打通，问题剖析一杆子到底
  2. 多维评价模型、监控预警、数据化研发
  3. 管理决策有依据，结果有数据


## 疑难杂症

1. 有人遇到使用网络经常出现内存泄漏的情况。我觉得这是属于基础功不扎实的情况，因为 [AFHTTPSessionManager manager] 它返回的对象持有一个 session。且 session 的 delegate 对象也是强引用。AFHTTPSessionManager 的父类是 AFURLSessionManager。AFURLSessionManager initWithSessionConfiguration 底层就是 `self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];` 所以会强引用
  解决方案：
  - 每次请求完网络后需要给 [AFHTTPSessionManager manager] 这种方式初始化的 manager 释放掉。比如 AFNetWorking 提供的 *invalidateSessionCancelingTasks* 方法。
  - 将 AFHTTPSessionManager 对象做成单例获取，这样带来另一个好处，NSURLSession 不销毁，另外的请求继续发起的时候不需要初始的网络握手，达到「链路复用」的功能。

```
//AFURLSessionManager.h
@property (readonly, nonatomic, strong) NSURLSession *session;

// AFHTTPSessionManager.m



// NSURLSessionConfiguration
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
```


```Objective-C
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks {
    if (cancelPendingTasks) {
        [self.session invalidateAndCancel];
    } else {
        [self.session finishTasksAndInvalidate];
    }
}
```
2. 在 iOS 10.3 系统上存在 SSL 证书校验的问题。报错信息如下图。目前没有找到具体原因和解决方案，如果有人有解决方案请联系我。
![报错信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-29-NetworkError2.png)
![问题信息](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-08-29-NetworkError1.jpg)
