08.网络分析与优化实践
目录介绍
- 01.整体概述介绍
- 1.1 项目背景介绍
- 1.2 遇到问题
- 1.3 网络优化概念
- 1.4 网络优化维度
- 1.5 监控网络思路
- 02.网络分析流程
- 2.1 curl分析网络
- 2.2 弱网模拟思路
- 2.3 抓包拦截思路
- 2.4 流量统计分析
- 2.5 耗时统计思路
- 2.6 Glide耗时思路
- 2.7 网络速度测试
- 03.网络优化策略
- 3.1 HTTPDNS优化
- 3.2 连接复用
- 3.3 传输数据压缩
- 3.4 IP之恋
- 3.5 CDN
- 3.6 Http2
- 3.7 数据缓存
- 04.一些常见优化
- 4.1 网络数据加密
- 4.2 防止域名劫持
- 4.3 避免轮训请求攻击
- 4.4 数据劫持优化
- 4.6 图片网络优化
- 4.9 网络数据优化
- 05.如何衡量优化成果
- 5.1 流量消耗分析
- 5.2 异常率统计
- 5.3 稳定性统计
01.整体概述介绍
1.1 项目背景介绍
1.2 遇到问题
1.3 网络优化概念
- Android 网络优化方法
- 主要讲的就是线下网络测试工具、线上网络监控方案、流量优化方案和质量优化方案。
- 网络测试工具
- Network Profiler、Charles和Stetho。
- 线上网络监控方案
- 有OkHttp 的 EventListener、NetworkStatsManager 和 TrafficStats。
- 流量优化方案
- 数据缓存、数据压缩和图片压缩。
- 质量优化方案
- HttpDns 优化、协议版本优化以及资本优化。
1.4 网络优化维度
- 网络优化时要考虑的维度
- 流量维度和质量维度,然后根据这两个维度的数据减少流量消耗以及提升请求速度。
- 流量维度
- App 在一段时间内流量消耗的精准度量,流量消耗可以区分为网络类型、前后台以及请求和响应等维度。
- 质量维度
- 可以从请求时长、请求成功率和Top失败接口等数据上进行区分,以便后续能快速定位和解决问题。
1.5 监控网络思路
- 建立完善的监控体系
- 指的是做网络优化要监控网络请求成功率和网络请求异常,比如流量消耗过多、请求次数过多以及下载文件过大等异常。
02.网络基础知识
2.1 curl分析网络
- curl如何分析接口请求说明
- curl(全称Client URL)是一个在命令行下运行的网络工具,用于与服务器进行通信。
- 它支持多种协议,包括HTTP、HTTPS、FTP、SMTP、TELNET等,并提供了丰富的选项和功能,使其成为一个强大的网络调试和测试工具。
- curl命令的基本语法
- 基本语法如下:curl 【选项】 【URL】 比如,我要调试某个接口查看请求详细信息:curl -v https://www.wanandroid.com/article/list/0/json
- curl测试接口案例分析
yangchongdeMacBook-Pro:~ yangchong$ curl -v https://www.wanandroid.com/article/list/0/json * Trying 47.104.74.169:443... * Connected to www.wanandroid.com (47.104.74.169) port 443 (#0) * ALPN: offers h2,http/1.1 * (304) (OUT), TLS handshake, Client hello (1): * CAfile: /etc/ssl/cert.pem * CApath: none * (304) (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-SHA384 * ALPN: server did not agree on a protocol. Uses default. * Server certificate: * subject: CN=wanandroid.com * start date: Jan 2 00:00:00 2023 GMT * expire date: Jan 2 23:59:59 2024 GMT * subjectAltName: host "www.wanandroid.com" matched cert's "www.wanandroid.com" * issuer: C=CN; O=TrustAsia Technologies, Inc.; CN=TrustAsia RSA DV TLS CA G2 * SSL certificate verify ok. * using HTTP/1.x > GET /article/list/0/json HTTP/1.1 > Host: www.wanandroid.com > User-Agent: curl/7.88.1 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Cache-Control: private < Expires: Thu, 01 Jan 1970 08:00:00 CST < Set-Cookie: JSESSIONID=34447AAA7368A824F67DE25710B89326; Path=/; Secure; HttpOnly < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < Date: Fri, 04 Aug 2023 02:14:56 GMT < {"data":{"curPage":1,"datas":""} ……此处省略一堆的响应数据}
- 一些核心要点分析:
- Connected to www.wanandroid.com (47.104.74.169) port 443 (#0) :这段话表示解析域名后得到的ip地址和端口号port443
- (OUT),TLS handshake, Client hello (1):客户端开始新的握手( ClientHello),将自身支持的加密功能(Cipher Suites)提交给服务器。
- (IN), TLS handshake, Server hello (2):服务器( ServerHello),选择Cipher Suite。
- (IN), TLS handshake, Certificate (11):服务器发送其证书(Certificate)(当需要服务器身份验证时)。
- (IN), TLS handshake, Server key exchange (12):根据选择的密钥交换方式,服务器发送生成主密钥的额外信息。
- (IN), TLS handshake, Server finished (14):服务器通知客户端自己完成了协商过程。
- (OUT), TLS handshake, Client key exchange (16):客户端发送生成主密钥所需的额外信息(ClientKeyExchange)。
- (OUT), TLS change cipher, Change cipher spec (1):客户端切换到加密方式并通知服务器。
- (OUT), TLS handshake, Finished (20):客户端计算发送和接收到的握手消息的MAC并发送。
- (IN), TLS change cipher, Change cipher spec (1):服务器切换加密方式并通知客户端。
- (IN), TLS handshake, Finished (20):服务器计算发送和接收到的握手消息的MAC并发送。
- 详细讲解一下每个步骤,这里主要是TLS协议中握手协议流程。更多可以看:TLS协议中的握手协议
- 通过curl可以快速分析网络
//GET请求 //要发送 GET 请求,只需要在 curl 命令中指定 -X GET 参数,后面跟随请求的 URL。 curl -X GET 'https://www.example.com/api/v1/resource' curl -X GET https://www.example.com/api/v1/resource?param1=value1¶m2=value2 //下面两个作用等同 curl -X GET -v https://www.wanandroid.com/article/list/0/json curl -v https://www.wanandroid.com/article/list/0/json //POST请求 //发送表单数据。在许多情况下,你可能需要发送表单数据。使用-d或--data参数可以实现这一目标: curl -X POST -d "username=yangchong&password=123456" https://www.wanandroid.com/user/login curl -X POST -v "username=yangchong&password=123456" https://www.wanandroid.com/user/login //发送JSON数据。发送JSON数据稍微复杂一点,需要使用-H参数指定Content-Type为application/json,然后用-d参数传递JSON字符串: curl -X POST -H "Content-Type: application/json" -d '{"key1":"value1", "key2":"value2"}' http://example.com
2.2 弱网模拟思路
- 如何开发模拟网络异常工具
- 整体设计思路是,给OkHttpClient添加拦截器,然后在拦截器中拿到request和respond数据做处理即可。
- 如何模拟网络超时
- 网络超时分为两种:请求超时和响应超时。请求超时是指客户端发送request到服务端过程超时,响应超时是指客户端在接收服务端respond数据过程超时。
- 模拟请求超时需要在chain.request()代码前面sleep时间,模拟响应超时需要在chain.proceed(request)代码之后sleep时间即可。
- 如何模拟重定向或服务端异常
- 通过拦截器拿到Chain对象,先发送request,请求成功后会拿到真是respond数据。那么这个时候可以针对这个respond处理,重新设置数据和code码信息
- 如何模拟弱网环境
- 模拟弱网环境相对复杂一点,发送request和接收respond数据都会有io流读写操作。其核心思路就是对io读写速度做出限制,比如每秒读取1k数据,那么读取10k的接口数据需要10秒
- 如何对RequestBody数据限制?请求数据write的时候,第一限制字节流传输(每次写设置byteCount字节长度),第二在一秒内如果超出限制就sleep一下
- 如何对ResponseBody数据限制?
- 弱网络模拟工具可以看
- MonitorInterceptor
2.3 抓包拦截思路
- Stetho库,Facebook 开源的一个 Android 调试工具,可以测试网络,用语言来描述应该是这样子:
- 1、安装了stetho插件的app启动之后,会启动一个本地server1(LocalSocketServer),这个本地server1等待着app(client)的连接。
- 2、同时,这个本地server1会与另外一个本地server2(ChromeDevtoolsServer)连接着。
- 3、本地app一旦连接上,数据将会不停的被发送到本地server1,然后转由server2.
- 4、然后Chrome Developer Tools,想访问网站一样的,访问了ChromeDevtoolsServer,随之将数据友好的展示给了开发者,这么一个过程就此完结。
- 那么既然网络请求添加StethoInterceptor,既可以拦截网络请求和响应信息,发送给Chrome。那么能不能自己拿来用……
- StethoInterceptor大概流程,可以简化为:发送请求时,给Chrome发了条消息,收到请求时,再给Chrome发条消息(具体怎么发的可以看NetworkEventReporterImpl的实现)
- 两条消息通过EventID联系起来,它们的类型分别是OkHttpInspectorRequest 和 OkHttpInspectorResponse,两者分别继承自NetworkEventReporter.InspectorRequest和NetworkEventReporter.InspectorResponse。
- 我们只要也继承自这两个类,在自己的网络库发送和收到请求时,构造一个Request和Response并发送给Chrome即可。
- 如何拿来用
- 既然Android中使用到facebook的stetho库,可以拦截手机请求请求,然后去Chrome浏览器,在浏览器地址栏输入:chrome://inspect 。即可查看请求信息。
- 那么能不能把这个拿到的请求信息(请求信息和相应信息),放到集合中,然后在Android的页面中展示呢?这样方便开发和测试查看网络请求信息,以及请求流程中的消耗时间(比如dns解析时间,请求时间,响应时间,共耗时等等)
- 如何统计网络请求和相应信息,大概流程如下所示
- 第一步,找到入口,给OkHttpClient添加拦截器,该自定义拦截器继承Interceptor然后重写intercept拦截方法。
- 第二步,构建一个独特的eventID,一对网络事件(请求和回包)对应一个eventID(使用AtomicInteger保证异步下数据独一)。这个是在创建NetworkEventReporter对象重写nextRequestId方法获取。
- 第三步,创建一个记录请求的bean,通过eventID创建对象,然后将请求链接,链接时间,读写时间等都设置进来。这个用来记录基本信息
- 第四步,在准备发送请求时,构造一个OkHttpInspectorRequest,报告给Chrome,此时Network会显示一条请求,处于Pending状态
- 第五步,发送请求,获取数据返回包信息,在构造一个OkHttpInspectorResponse,发送给Chrome用于展示,
- 如何统计各个请求阶段的时间差,大概流程如下所示
- 第一步,找到入口,给OkHttpClient添加eventListenerFactory事件监听,这个时候创建一个继承EventListener的对象;
- 第二步,在最开始调用方法中,获取eventID(请求和回包网络事件id),然后通过这个id拿到之前创建的bean(存在Map中,key就是构建的id)。
- 第三步,在各个方法中打上时间戳,举个例子,在dnsStart记录时间戳,在dnsEnd记录时间戳,时间差就是两个的差值。
- 第四步,在执行监听最后的方法callEnd中,做出最后的计算,主要是记录每个阶段的差值,存到对应的bean中(注意一个完整的请求和回包流程id是唯一的)。
- 抓包拦截工具可以看
- MonitorNetLib
2.4 流量统计分析
- 流量监测的实现相对简单
- 利用系统提供的 TrafficStats.getUidRxBytes方法,配合Activity生命周期,即可获取每个Activity的流量消耗。
- 具体做法:在Activity start的时候记录起点,在pause的时候累加,最后在Destroyed的时候统计整个Activity的流量消耗,如果想要做到Fragment维度,就要具体业务具体分析了
- TrafficStats 缺点
- 只能统计到手机上次重启后到现在的流量消耗。
- 流量统计工具可以看
- MonitorFlow
2.5 耗时统计思路
- OkHttp如何进行各个请求环节的耗时统计呢?
- OkHttp 版本提供了EventListener接口,可以让调用者接收一系列网络请求过程中的事件,例如DNS解析、TSL/SSL连接、Response接收等。
- 通过继承此接口,调用者可以监视整个应用中网络请求次数、流量大小、耗时(比如dns解析时间,请求时间,响应时间等等)情况。
- 如何消耗记录时间
- 在OkHttp库中有一个EventListener类。该类是网络事件的侦听器。扩展这个类以监视应用程序的HTTP调用的数量、大小和持续时间。
- 所有启动/连接/获取事件最终将接收到匹配的结束/释放事件,要么成功(非空参数),要么失败(非空可抛出)。
- 比如,可以在开始链接记录时间;dns开始,结束等方法解析记录时间,可以计算dns的解析时间。
- 比如,可以在开始请求记录时间,记录connectStart,connectEnd等方法时间,则可以计算出connect连接时间。
- 遇到的问题记录说明
- 客户端请求从发起到网关实际接收到,中间有很复杂的链路,发起请求,dns解析,开始链接,发送请求头,发送请求body,返回数据等很多个阶段。
- 基于OkHttp的网络监控
- 是不是可以考虑把整个api发起到结束进行监控,从而可以方便线上去监控一个Api真实的发起到结束的状况呢?
- 基于OKHttp提供的EventListener
- 可以对于一个请求发起到最后的各个节点进行监控,之后上报日志数据,针对网络请求每个阶段的耗时情况都可以用数据说明。
- 网络请求耗时统计已经有了现成的库
- 针对网络请求每个阶段统计了消耗时间,网络请求耗时统计库;
- 关于EventListener的原理解读可以看我另一篇博客:OkHttp请求耗时统计和实践
2.6 Glide耗时思路
- EventListener 还能和 Glide 搭配使用,以获取图片请求的各个步骤的数据。
2.7 网络速度测试
03.网络优化策略
3.1 HTTPDNS优化
- DNS 的解析是我们网络请求的第一项工作,默认我们使用运营商的 LocalDNS 服务。这块耗时在 3G 网络下可能是 200~300ms,4G 网络也需要 100ms。解析慢并不是默认 LocalDNS 最大的“原罪”,它还存在一些其他问题:
- 稳定性。UDP 协议,无状态,容易域名劫持(难复现、难定位、难解决),每天至少几百万个域名被劫持,一年至少十次大规模事件。
- 准确性。LocalDNS 调度经常出现不准确,比如北京的用户调度到广东 IP,移动的运营商调度到电信的 IP,跨运营商调度会导致访问慢,甚至访问不了。
- 及时性。运营商可能会修改 DNS 的 TTL,导致 DNS 修改生效延迟。不同运营商的服务实现不一致,我们也很难保证 DNS 解析的耗时。
- 为了解决这些问题,就有了 HTTPDNS。
- 简单来说自己做域名解析的工作,通过 HTTP 请求后台去拿到域名对应的 IP 地址,直接解决上述所有问题。具体可以参考:百度DNS优化
3.2 连接复用
- 在 DNS 解析之后,我们来到了创建连接这个环节。创建连接要经过 TCP 三次握手、TLS 密钥协商,连接建立的代价是非常大的。这里我们主要的优化思路是复用连接,这样不用每次请求都重新建立连接。
- 连接管理,网络库并不会立刻把连接释放,而是放到连接池中。这时如果有另一个请求的域名和端口是一样的,就直接拿出连接池中的连接进行发送和接收数据,少了建立连接的耗时。
- 这里我们利用 HTTP 协议里的 keep-alive,而 HTTP/2.0 的多路复用则可以进一步的提升连接复用率。它复用的这条连接支持同时处理多条请求,所有请求都可以并发在这条连接上进行。
- 虽然 H2 十分强大,不过这里还有两个问题需要解决。一个是同一条 H2 连接只支持同一个域名,一个是后端支持 HTTP/2.0 需要额外的改造。这个时候我们只需要在统一接入层做改造,接入层将数据转换到 HTTP/1.1 再转发到对应域名的服务器。
- 这样所有的服务都不用做任何改造就可以享受 HTTP/2.0 的所有优化,不过这里需要注意的是 H2 的多路复用在本质上依然是同一条 TCP 连接,如果所有的域名的请求都集中在某一条连接中,在网络拥塞的时候容易出现 TCP 队首阻塞问题。
- 对于客户端网络库来说,无论 OkHttp 还是 Chromium 网络库对于 HTTP/2.0 的连接,同一个域名只会保留一条连接。对于一些第三方请求,特别是文件下载以及视频播放这些场景可能会遇到对方服务器单连接限速的问题。在这种情况下我们可以通过修改网络库实现,也可以简单的通过禁用 HTTP/2.0 协议解决。
3.3 传输数据压缩
- 传输数据压缩
- 具体有 开启 Gzip 压缩、压缩请求头以及合并请求三个方案。
- 开启 Gzip 压缩就是把内容编码 Content-Encoding 首部设为 gzip,网络框架用的是 OkHttp ,那么在没有设置 Content-Encoding 请求头的情况下,OkHttp 默认会设置为 gzip。
- 压缩请求头指的是在请求头不变的情况下,让服务端缓存请求头,在需要请求头的某些信息时,根据之前的请求头的 MD5 从之前的缓存中取。
- 合并请求指的是把可以合并的请求合并起来,因为每一个网络请求头会有冗余信息。
- 第一时间想到的还是减少传输的数据量,也就是我们常说的数据压缩。首先对于 HTTP 请求来说,数据主要包括三个部分:
- 请求 URL
- 请求 header
- 请求 body
- 具体对数据量压缩如何理解
- 对于 header 来说,如果使用 HTTP/2.0 连接本身的头部压缩技术,因此需要压缩的主要是请求 URL 和请求 body。
- 对于请求 URL 来说,一般会带很多的公共参数,这些参数大部分都是不变的。这样不变的参数客户端只需要上传一次即可,其他请求我们可以在接入层中进行参数扩展。
- 对于请求 body 来说,一方面是数据通信协议的选择,在网络传输中目前最流行的两种数据序列化方式是 JSON 和 Protocol Buffers。正如我之前所说的一样,Protocol Buffers 使用起来更加复杂一些,但在数据压缩率、序列化与反序列化速度上面都有很大的优势。
3.7 数据缓存
- 客户端数据缓存
- 可以分为 OkHttp 的 Cache 和 无网拦截器两种方案,我们项目中一般会有一些对实时性要求不高的接口,比如省市区列表,这时就可以考虑缓存这些接口的数据。
- OkHttp 默认是不会缓存响应数据的。Cache 内部使用了 DiskLruCache ,创建 Cache 只需要指定缓存目录和缓存大小,然后设置给 OkHttpClient 就可以了。使用 Cache 要注意的是,默认缓存拦截器只会缓存 GET 和 HEAD 等获取资源的请求方法的响应。
04.一些常见优化
4.1 网络数据加密
4.2 防止域名劫持
- 什么叫做域名劫持
- 指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。
- 先说下域名劫持的现象
- 广告劫持:用户正常页面指向到广告页面。这个背景估计很多人遇到过……
- 恶意劫持:域名指向IP被改变,将用户访问流量引到挂马,盗号等对用户有害页面的劫持。
- local DNS缓存:为了降低跨网流量及用户访问速度进行的一种劫持,导致域名解析结果不能按时更新。
- 域名劫持原因分析
- 缓存是DNS被劫持的根本原因,在DNS解析过程的各个缓存中均有可能被劫持。主要包括本机的hosts篡改劫持,和运营商的Local DNS劫持等。
- 解决办法该怎么做
- 方案1:HttpDNS使用HTTP协议向DNS服务器的80端口进行请求,代替传统DNS协议向DNS服务器的53端口进行请求。也就是使用Http协议去进行DNS解析请求,将服务器返回的解析结果,直接向该IP发起对应的API服务请求,代替使用域名,域名解析请求直接发送到HTTPDNS服务器,绕过运营商Local DNS,避免域名劫持问题。
- 方案2:如果知道该域名的真实IP地址,则可以直接用此IP代替域名后进行访问。比如访问谷歌网站,可以把访问改为https://216.239.XX.XX/ ,从而绕开域名劫持。
4.4 数据劫持优化
- 先说下数据劫持的现象
- 数据劫持针对明文传输的内容发生。用户发起HTTP请求,服务器返回页面内容时,经过中间的运营商网络,页面内容被篡改或加塞内容,强行插入弹窗或者广告。
- 劫持数据的大概原理
- 待完善
- 如何预防数据被劫持
- 解决方案:对内容进行HTTPS加密,实现密文传输,彻底避免劫持问题。
4.6 图片网络优化
- 比如我之前看到豆瓣接口,提供一种加载图片方式特别好。
- 接口返回图片的数据有三种,一种是高清大图,一种是正常图片,一种是缩略小图。
- 当用户处于wifi下给控件设置高清大图,当4g或者3g模式下加载正常图片,当弱网条件下加载缩略图【也称与加载图】。
- 简单来说根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。豆瓣开源接口可以参考一下!
4.7 弱网模拟优化
4.9 网络数据优化
- 连接复用:节省连接建立时间,如开启 keep-alive。
- 对于Android来说默认情况下HttpURLConnection和HttpClient都开启了keep-alive。只是2.2之前HttpURLConnection存在影响连接池的Bug,具体可见:Android HttpURLConnection及HttpClient选择
- 请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。如果某个页面内请求过多,也可以考虑做一定的请求合并。
- 减少请求数据的大小:对于post请求,body可以做gzip压缩的,header也可以做数据压缩(不过只支持http)
- 返回数据的body也可以做gzip压缩,body数据体积可以缩小到原来的30%左右。(也可以考虑压缩返回的json数据的key数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)
相关demo
- 弱网络模拟工具可以看
- MonitorInterceptor
- 抓包拦截工具可以看
- MonitorNetLib
- 流量统计工具可以看
- MonitorFlow
- 网络测速工具可以看
- MonitorSpeed
参考文章
- 探索 Android 网络优化方法
- https://www.jianshu.com/p/41bbf2f9c2a9
- 移动端网络监控实践
- https://juejin.cn/post/7018212919439523847
- Android网络性能监控方案
- https://zhuanlan.zhihu.com/p/285191490
- 货拉拉移动端网络优化——协议升级篇
- https://juejin.cn/post/7194627379656917047#heading-11
- Android进阶宝典 -- App线上网络问题优化策略
- https://juejin.cn/post/7276368438146924563
- 关于 Android 网络优化你应该了解的知识点
- https://juejin.cn/post/7130940834186264589