欢迎来到 《深入探索 Android 网络优化(三、网络优化篇)下》~
❞问题:DNS 解析慢/被劫持?
❞使用 HTTPDSN,HTTPDNS 不是使用 DNS 协议,向 DNS 服务器传统的 53 端口发送请求,而是使用 HTTP 协议向 DSN 服务器的 80 端口发送请求。
在 Awesome-WanAndroid 中已经实现了 HTTPDNS 优化,其优化代码如下所示:
// HttpModule-provideClient:httpDns 优化 builder.dns(OkHttpDns.getIns(WanAndroidApp.getAppComponent().getContext())); /** * FileName: OkHttpDNS * Date: 2020/5/8 16:08 * Description: HttpDns 优化 * @author JsonChao */ public class OkHttpDns implements Dns { private HttpDnsService dnsService; private static OkHttpDns instance = null; private OkHttpDns(Context context) { dnsService = HttpDns.getService(context, "161133"); // 1、设置预解析的 IP 使用 Https 请求。 dnsService.setHTTPSRequestEnabled(true); // 2、预先注册要使用到的域名,以便 SDK 提前解析,减少后续解析域名时请求的时延。 ArrayList<String> hostList = new ArrayList<>(Arrays.asList("www.wanandroid.com")); dnsService.setPreResolveHosts(hostList); } public static OkHttpDns getIns(Context context) { if (instance == null) { synchronized (OkHttpDns.class) { if (instance == null) { instance = new OkHttpDns(context); } } } return instance; } @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { String ip = dnsService.getIpByHostAsync(hostname); LogHelper.i("httpDns: " + ip); if(ip != null){ List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip)); return inetAddresses; } // 3、如果从阿里云 DNS 服务器获取不到 ip 地址,则走运营商域名解析的过程。 return Dns.SYSTEM.lookup(hostname); } } 复制代码重新安装 App,通过 HTTPDNS 获取到 IP 地址 log 如下所示:
2020-05-11 10:41:55.139 4036-4184/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpDns.java | 52 | lookup] httpDns: 47.104.74.169 2020-05-11 10:41:55.142 4036-4185/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpDns.java | 52 | lookup] httpDns: 47.104.74.169 复制代码利用 HTTP 协议的 keep-alive,建立连接后,会先将连接放入连接池中,如果有另一个请求的域名和端口是一样的,就直接使用连接池中对应的连接发送和接收数据。在实现网络库的连接管理时需要注意以下4点:
1)、同一个连接仅支持同一个域名。2)、后端支持 HTTP 2.0 需要改造,这里可以通过在网络平台的统一接入层将数据转换到 HTTP 1.1 后再转发到对应域名的服务器即可。3)、当所有请求都集中在一条连接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。4)、在文件下载、视频播放等场景下可能会遇到三方服务器单连接限速的问题,此时可以禁用 HTTP 2.0。TCP 连接不复用,也就是每发起一个网络请求都要重新建立连接,而刚开始连接都会经历一个慢启动的过程,可谓是慢上加慢,因此 HTTP 1.0 性能非常差。
引入了持久连接,即 TCP 连接可以复用,但数据通信必须按次序来,也就是后面的请求必须等前面的请求完成才能进行。当所有请求都集中在一条连接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。
Google 2013 实现,2018 基于 QUIC 协议的 HTTP 被确认为 HTTP3。
QUIC 简单理解为 HTTP/2.0 + TLS 1.3 + UDP。弱网环境下表现好与 TCP。
在 Awesome-WanAndroid 中已经使用 OkHttpEventListener 实现了网络请求的质量监控,其代码如下所示:
// 网络请求质量监控 builder.eventListenerFactory(OkHttpEventListener.FACTORY); /** * FileName: OkHttpEventListener * Date: 2020/5/8 16:28 * Description: OkHttp 网络请求质量监控 * @author quchao */ public class OkHttpEventListener extends EventListener { public static final Factory FACTORY = new Factory() { @Override public EventListener create(Call call) { return new OkHttpEventListener(); } }; OkHttpEvent okHttpEvent; public OkHttpEventListener() { super(); okHttpEvent = new OkHttpEvent(); } @Override public void callStart(Call call) { super.callStart(call); LogHelper.i("okHttp Call Start"); okHttpEvent.callStartTime = System.currentTimeMillis(); } /** * DNS 解析开始 * * @param call * @param domainName */ @Override public void dnsStart(Call call, String domainName) { super.dnsStart(call, domainName); okHttpEvent.dnsStartTime = System.currentTimeMillis(); } /** * DNS 解析结束 * * @param call * @param domainName * @param inetAddressList */ @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) { super.dnsEnd(call, domainName, inetAddressList); okHttpEvent.dnsEndTime = System.currentTimeMillis(); } @Override public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { super.connectStart(call, inetSocketAddress, proxy); okHttpEvent.connectStartTime = System.currentTimeMillis(); } @Override public void secureConnectStart(Call call) { super.secureConnectStart(call); okHttpEvent.secureConnectStart = System.currentTimeMillis(); } @Override public void secureConnectEnd(Call call, @Nullable Handshake handshake) { super.secureConnectEnd(call, handshake); okHttpEvent.secureConnectEnd = System.currentTimeMillis(); } @Override public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) { super.connectEnd(call, inetSocketAddress, proxy, protocol); okHttpEvent.connectEndTime = System.currentTimeMillis(); } @Override public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) { super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe); } @Override public void connectionAcquired(Call call, Connection connection) { super.connectionAcquired(call, connection); } @Override public void connectionReleased(Call call, Connection connection) { super.connectionReleased(call, connection); } @Override public void requestHeadersStart(Call call) { super.requestHeadersStart(call); } @Override public void requestHeadersEnd(Call call, Request request) { super.requestHeadersEnd(call, request); } @Override public void requestBodyStart(Call call) { super.requestBodyStart(call); } @Override public void requestBodyEnd(Call call, long byteCount) { super.requestBodyEnd(call, byteCount); } @Override public void responseHeadersStart(Call call) { super.responseHeadersStart(call); } @Override public void responseHeadersEnd(Call call, Response response) { super.responseHeadersEnd(call, response); } @Override public void responseBodyStart(Call call) { super.responseBodyStart(call); } @Override public void responseBodyEnd(Call call, long byteCount) { super.responseBodyEnd(call, byteCount); // 记录响应体的大小 okHttpEvent.responseBodySize = byteCount; } @Override public void callEnd(Call call) { super.callEnd(call); okHttpEvent.callEndTime = System.currentTimeMillis(); // 记录 API 请求成功 okHttpEvent.apiSuccess = true; LogHelper.i(okHttpEvent.toString()); } @Override public void callFailed(Call call, IOException ioe) { LogHelper.i("callFailed "); super.callFailed(call, ioe); // 记录 API 请求失败及原因 okHttpEvent.apiSuccess = false; okHttpEvent.errorReason = Log.getStackTraceString(ioe); LogHelper.i("reason " + okHttpEvent.errorReason); LogHelper.i(okHttpEvent.toString()); } } 复制代码成功 log 如下所示:
2020-05-11 11:00:42.678 6682-6847/json.chao.com.wanandroid D/OkHttp: --> GET https://www.wanandroid.com/banner/json 2020-05-11 11:00:42.687 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ Thread: RxCachedThreadScheduler-3 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ OkHttpEventListener.callStart (OkHttpEventListener.java:46) 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ LogHelper.i (LogHelper.java:37) 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpEventListener.java | 46 | callStart] okHttp Call Start 2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: └──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2020-05-11 11:00:43.485 6682-6847/json.chao.com.wanandroid D/OkHttp: <-- 200 OK https://www.wanandroid.com/banner/json (806ms, unknown-length body) 2020-05-11 11:00:43.496 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ Thread: RxCachedThreadScheduler-2 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ OkHttpEventListener.callEnd (OkHttpEventListener.java:162) 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ LogHelper.i (LogHelper.java:37) 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpEventListener.java | 162 | callEnd] NetData: [ 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ callTime: 817 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ dnsParseTime: 6 2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ connectTime: 721 2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ secureConnectTime: 269 2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ responseBodySize: 975 2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ apiSuccess: true 2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ ] 2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: └──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 复制代码见 深入探索 Android 网络优化(二、网络优化基础篇)下 - 首部压缩。
不变参数客户端只需上传以此,其它参数均在接入层进行扩展。
使用 Protocol Buffers 替代 JSON 序列化。
HTTPS 通常需要多消耗 2 RTT 的协商时延。
TLS 1.2 引入了 SHA-256 哈希算法,摒弃了 SHA-1,对增强数据完整性有着显著优势。
IETF(Internet Engineering Task Froce,互联网工程任务组)制定的 TLS 1.3 是有史以来最安全、复杂的 TLS 协议。它具有如下特点:
相比于 TLS 1.2 及之前的版本,TLS 1.3 的握手不再支持静态的 RSA 密钥交换,使用的是带有前向安全的 Diffie-Hellman 进行全面握手。因此 TLS 1.3 只需 1-RTT 握手时间。
删除了之前版本的不安全的加密算法。
1)、RSA 密钥传输:不支持前向安全性。2)、CBC 模式密码:易受 BEAST 和 Lucky 13 攻击。3)、RC4 流密码:在 HTTPS 中使用并不安全。4)、SHA-1 哈希函数:建议以 SHA-2 替代。5)、任意 Diffie-Hellman 组:CVE-2016-0701 漏洞。6)、输出密码:易受 FREAK 和 LogJam 攻击。此外,我们可以在 Google 浏览器设置 TLS 1.3。
参考 TLS 1.3 协议,合并请求,优化加密算法,使用 session-ticket 等策略,力求在安全和体验间找到一个平衡点。
在 TLS 中性能开销最大的是 TLS 握手阶段的 RSA 加解密。在 slight-ssl 中又尝试如下几种解决方案:
1)、硬件加速:使用单独的硬件加速卡处理 RSA 加解密。2)、ECDSA:ECSDSA 最底层的算法和成本对性能的消耗远低于 RSA,相差5~6倍。3)、Session Ticket 机制:将 TLS 握手从 2RTT 降低为 1RTT。基于 TLS 1.3 草案标准而实现。
类似于 TLS 协议,mmtls 协议也是位于业务层与网络连接层中间。
TLS 1.3 Handshake 协议有如下几类:
1-RTT 密钥协商方式 1-RTT ECDHE1-RTT PSK(Pre-Shared Key)0-RTT 密钥协商方式 0-RTT PSK0-RTT ECDH0-RTT PSK-ECDHE0-RTT ECDH-ECDHE而 mmtls Handshake 协议有如下几种:
1-RTT ECDHE1-RTT PSK0-RTT PSK「1-RTT ECDHE 密钥协商原理」
ECDH 密钥交换协议需要使用两个算法:
1)、密钥生成算法 ECDH_Generate_Key:生成公私钥对(ECDH_pub_key、ECDH_pri_key),其中保存私钥,将公钥互相发送给对方。2)、密钥协商算法 ECDH_compute_key:输入对方公钥与自身私钥,计算出通信双方一致的对称密钥 Key。但是 1-RTT ECDHE 算法容易被中间人攻击,中间人可以截获双方的公钥运行 ECDH_Generate_key 生成自己的公私钥对,然后将公钥发送给某一方。
❝如何解决中间人攻击?
❞中间人攻击产生的本质原因是没有经过端点认证,需要”带认证的密钥协商“。
❝数据认证的方式?
❞数据认证有对称与非对称两种方式:
1)、基于 MAC(Message Authentication Code,消息认证码)的对称认证2)、基于签名算法的非对称认证。ECDH 认证密钥协商就是 ECDH 密钥协商 + 数字签名算法 ECDSA。
双方密钥协商会对自身发出的公钥使用签名算法,由于签名算法中的公钥 ECDSA_verify_key 是公开的,中间人没有办法阻止别人获取公钥。
而 mmtls 仅对 Server 做认证,因为通信一方签名其协商数据就不会被中间人攻击。
在 TLS 中,提供了可选的双方相互认证的能力:
Client 通过选择 CipherSuite 是什么类型来决定是否要对 Server 进行认证。Server 通过是否发送 CertificateRequest 握手消息来决定是否要对 Client 进行认证。「1-RTT PSK 密钥协商原理」
在之前的 ECDH 握手下,Server 会下发加密的 PSK{key, ticket{key}},其中:
key:用来做对称加密密钥的 key 明文。ticket{key}:用 server 私密保存的 ticket_key 对 key 进行加密的密文 ticket。1)、首先,Client 将 ticket{key}、Client_Random 发送给 Server。
2)、然后,Server 使用 ticket_key 解密得到 key、Server_Random、Client_Random 计算 MAC 来认证。
3)、最后,Server 将 Server_Random、MAC 发送给 Client,Client 同 Server 使用 ticket_key 解密得到 key、Server_Random、Client_Random 去计算 MAC 来验证是否与收到的 MAC 匹配。
「0-RTT ECDH 密钥协商原理」
要想实现 0-RTT 密钥协商,就必须在协商一开始就将业务数据安全地传递到对端。
预先生成一对公私钥(static_svr_pub_key, static_svr_pri_key),并将公钥预置在 Client,私钥持久保存在 Server。
1)、首先,Client 通过 static_svr_pub_key 与 cli_pri_key 生成一个对称密钥SS(Static Secret),用 SS 衍生的密钥对业务数据加密。
2)、然后,Client cli_pub_key、Client_Random、SS 加密的 AppData 发送给 Server,Sever 通过 cli_pub_key 和 static_svr_pri_key 算出 SS,解密业务数据包。
「1-RTT PSK 密钥协商原理」
在进行 1-RTT PSK 握手之前,Client 已经有一个对称加密密钥 key 了,直接使用此 key 与 ticket{key} 一起传递给 Server 即可。
❝TLS 1.3 为什么要废除 RSA?
❞ 1)、2015年发现了 FREAK 攻击,出现了 RSA 漏洞。2)、一旦私钥泄露,中间人就可以通过私钥计算出之前所有报文的密钥,破解之前所有的密文。因此 TLS 1.3 引入了 PFS(perfect forward secrecy,前向安全性),即完全向前保密,一个密钥被破解,并不会影响其它密钥的安全性。
例如 0-RTT ECDH 密钥协商加密依赖了静态 static_svr_pri_key,不符合 PFS,我们可以使用 0-RTT ECDH-ECDHE 密钥协商,即进行 0-RTT ECDH 协商的过程中也进行 ECDHE 协商。0-RTT PSK 密钥协商的静态 ticket_key 同理也可以加入 ECDHE 协商。
❝verify_key 如何下发给客户端?
❞为避免证书链验证带来的时间消耗及传输带来的带宽消耗,直接将 verify_Key 内置客户端即可。
❝如何避免签名密钥 sign_key 泄露带来的影响?
❞因为 mmtls 内置了 verify_key 在客户端,必要时及时通过强制升级客户端的方式来撤销公钥并更新。
❝为什么要在上述密钥协商过程中都要引入 client_random、server_random、svr_pub_key 一起做签名?
❞因为 svr_pri_Key 可能会泄露,所有单独使用 svr_pub_key 时会有隐患,因为需要引入 client_random、server_random 来保证得到的签名值唯一对应一次握手。
「1、认证加密」
1)、使用对称密钥进行安全通信。2)、加密 + 消息认证码:Encrypt-then-MAC3)、TLS 1.3 只使用 AEAD(Authenticated-Encryption With Addtional data)类算法:Encrypt 与 MAC 都集成在一个算法内部,让有经验的密码专家在算法内部解决安全问题。4)、mmtls 使用 AES-GCM 这种 AEAD 类算法。「2、密钥扩展」
双方使用相同的对称密钥进行加密通信容易被某些对称密钥算法破解,因此,需要对原始对称密钥做扩展变换得到相应的对称加密参数。
密钥变长需要使用密钥延时函数(KDF,Key Derivation Function),而 TLS 1.3 与 mmtls 都使用了 HKDF 做密钥扩展。
「3、防重放」
为解决防重放,我们可以为连接上的每一个业务包都添加一个递增的序列号,只要 Server 检查到新收到的数据包的序列号小于等于之前收到的数据包的序列号,就判断为重放包,mmtls 将序列号作为构造 AES-GCM 算参数 nonce 的一部分,这样就不需要对序列号单独认证。
在 0-RTT 握手下,第一个业务数据包和握手数据包无法使用上述方案,此时需要客户端在业务框架层去协调支持防重放。
mmtls 的 「工作过程」 如下所示:
1)、使用 ECDH 做密钥协商。2)、使用 ECDSA 进行签名认证。3)、使用 AES-GCM 对称加密算法对业务数据进行加密。4)、使用 HKDF 进行密钥扩展。5)、使用的摘要算法为 SHA256。其优势具有如下4点:
1)、轻量级:去除客户端认证,内置签名公钥,减少验证时网络交换次数。2)、安全性:TLS 1.3 推荐安全性最高的基础密码组件,0-RTT 防重放由服务端、客户端框架层协同处理。3)、高性能:使用了 0-RTT 握手,优化了 TLS 1.3 中的握手方式和密钥扩展方式。4)、高可用:服务器添加了过载保护,确保其能在容灾模式下提供安全级别稍低的有损服务。最后,我们可以在统一接入层对传输数据二次加密,需要注意二次加密会增加客户端与服务器的处理耗时。
❝如果手机设置了代理,TLS 加密的数据可以被解开并被利用,如何处理?
❞可以在 客户端锁定根证书,可以同时兼容老版本与保证证书替换的灵活性。
在一线互联网公司,都会有统一的网络中台:
负责提供前后台一整套的网络解决方案。网关用于解决中间网络的通讯,为上层服务提供高质量的双向通讯能力。一个跨平台的 Socket 层解决方案,不支持完整的 HTTP 协议。
Mars 的两个核心模块如下:
SDT:网络诊断模块STN:信令传输模块,适合小数据传输。其中 STN 模块的组成图如下所示:
根据网络情况,调整其它超时的系数或绝对值。
❝Mars 是如何进行 连接优化 的?
❞每间隔几秒启动一个新的连接,只要有连接建立成功,则关闭其它连接。=> 有效提升连接成功率。
通过感知网络的状态切换到更好的网络环境下。
❝Mars 是如何进行 弱网优化 的?
❞在弱网下尽量保证下载完整的图片轮廓显示,提高用户体验。
将分包转成流式传输。
1)、分包 降低包大小增加并发包头损耗2)、流式 确认粒度策略灵活 单线程一个多机房的整体方案,在多个地区同时存在对等的多个机房,以用户维度划分,多机房共同承担全量用户的流量。
在单个机房发送故障时,故障机房的流量可以快速地被迁引到可用机房,减少故障的恢复时间。
应用一种有策略的重试机制,将网络请求以是否发送到 socket 缓冲区作为分割,将网络请求生命周期划分为”请求开始到发送到 socket 缓冲区“和”已经发送到 socket 缓冲区到请求结束“两个阶段。
这样当用户进电梯因为网络抖动的原因网络链接断了,但是数据其实已经请求到了 socket 缓冲区,使用这种有策略的重试机制,我们就可以提升客户端的网络抗抖动能力。
同步差量数据,达到节省流量,提高通信效率与请求成功率。
客户端用户不在线时,SYNC 服务端将差量数据保持在数据库中。当客户端下次连接到服务器时,再同步差量数据给用户。
核心思想是保障核心业务在体验可接受范围内做降级非核心功能和业务。从入口到业务接口总共分为四个层级,如下所示:
1)、LVS(几十亿级):多 VIP 多集群。2)、接入网关(亿级):TCP 限流、核心 RPC 限流。3)、API 网关(千万级):分级限流算法(对不同请求量的接口使用不同的策略) 高 QPS 限流:简单基数算法,超过这个值直接拒绝。中 QPS 限流:令牌桶算法,接受一定的流量并发。低 QPS 限流:分布式限流,保障限流的准确。4)、业务接口(百万级) 返回定制响应、自定义脚本。客户端静默、Alert、Toast。结合 JobScheduler 来根据实际情况做网络请求. 比方说 Splash 闪屏广告图片, 我们可以在连接到 Wifi 时下载缓存到本地; 新闻类的 App 可以在充电, Wifi 状态下做离线缓存。
app应该对网络请求划分优先级尽可能快地展示最有用的信息给用户。(高优先级的服务优先使用长连接)
立刻呈现给用户一些实质的信息是一个比较好的用户体验,相对于让用户等待那些不那么必要的信息来说。这可以减少用户不得不等待的时间,增加APP在慢速网络时的实用性。(低优先级使用短连接)
将众多请求放入等待发送队列中,待长连通道建立完毕后再将等待队列中的请求放在长连通道上依次送出。
HTTP 的请求头键值对中的的键是允许相同和重复的。例如 Set-Cookie/Cookie 字段可以包含多组相同的键名称数据。在长连通信中,如果对 header 中的键值对用不加处理的字典方式保存和传输,就会造成数据的丢失。
尽可能将问题在上线前暴露出来。
区分地域、时间段、版本、机型。
业务失败与请求失败。
以便进行针对性地优化。
RPS/TPS/QPS,每秒的请求次数,服务器最基本的性能指标,RPS 越高就说明服务器的性能越好。
反映服务器的负载能力,即服务器能够同时支持的客户端数量,越大越好。
反映服务器的处理能力,即快慢程度,响应时间越短越好。
CPU、内存、硬盘和网卡等系统资源。可以利用 top、vmstat 等工具检测相关性能。
要实现客户端监控,首先我们应该要统一网络库,而客户端需要监控的指标主要有如下三类:
1)、时延:一般我们比较关心每次请求的 DNS 时间、建连时间、首包时间、总时间等,会有类似 1 秒快开率、2 秒快开率这些指标。2)、维度:网络类型、国家、省份、城市、运营商、系统、客户端版本、机型、请求域名等,这些维度主要用于分析问题。3)、错误:DNS 失败、连接失败、超时、返回错误码等,会有 DNS 失败率、连接失败率、网络访问的失败率这些指标。为了运算简单我们可以抛弃 UV,只计算每一分钟部分维度的 PV。
关于 ArgusAPM 的网络监控切面源码分析可以参考我之前写的 深入探索编译插桩技术(二、AspectJ) - 使用 AspectJ 打造自己的性能监控框架
监控不全面,因为 App 可能不使用系统/OkHttp 网络库,或是直接使用 Native 网络请求。
需要 Hook 的方法有三类:
1)、连接相关:connect2)、发送数据相关:send 和 sendto。3)、接收数据相关:recv 和 recvfrom。不同版本 Socket 的实现逻辑会有差异,为了兼容性考虑,我们直接 PLT Hook 内存所有的 so,但是需要排除掉 Socket 函数本身所在的 libc.so。其 PLT 的 Hook 代码如下所示:
hook_plt_method_all_lib("libc.so", "connect", (hook_func) &create_hook); hook_plt_method_all_lib("libc.so, "send", (hook_func) &send_hook); hook_plt_method_all_lib("libc.so", "recvfrom", (hook_func) &recvfrom_hook); 复制代码下面,我们使用 PLT Hook 来获取网络请求信息。
项目地址
其成功 log 如下所示:
2020-05-21 15:10:37.328 27507-27507/com.dodola.socket E/HOOOOOOOOK: JNI_OnLoad 2020-05-21 15:10:37.328 27507-27507/com.dodola.socket E/HOOOOOOOOK: enableSocketHook 2020-05-21 15:10:37.415 27507-27507/com.dodola.socket E/HOOOOOOOOK: hook_plt_method 2020-05-21 15:10:58.484 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 10 2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: stack:com.dodola.socket.SocketHook.getStack(SocketHook.java:13) libcore.io.Linux.connect(Native Method) libcore.io.BlockGuardOs.connect(BlockGuardOs.java:126) libcore.io.IoBridge.connectErrno(IoBridge.java:152) libcore.io.IoBridge.connect(IoBridge.java:130) java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:129) java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:356) java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200) java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182) java.net.SocksSocketImpl.connect(SocksSocketImpl.java:357) java.net.Socket.connect(Socket.java:616) com.android.okhttp.internal.Platform.connectSocket(Platform.java:145) com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:141) com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:112) com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:184) com.android.okhttp.internal.http.Strea 2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: AF_INET6 ipv6 IP===>14.215.177.39:443 2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 1 2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: Ignore local socket connect 2020-05-21 15:10:58.523 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 1 2020-05-21 15:10:58.523 27507-27677/com.dodola.socket E/HOOOOOOOOK: Ignore local socket connect 2020-05-21 15:10:58.806 27507-27677/com.dodola.socket E/HOOOOOOOOK: respond:<!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http://www.baidu.com/?bdorz_come=1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>'); </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html> 复制代码此外,我们也可以使用爱奇艺提供的 android_plt_hook 来实现 PLT Hook。
接管了系统的 Local Socket,需要在代码中增加过滤条件。
为什么要做接入层监控?
❞ 1)、服务端更容易做到秒级的实时上报。2)、仅靠客户端的监控数据并不完全可靠。服务的入口和出口流量、服务端的处理时延、错误率等。
监控的同时如何实现准确的自动化报警呢?
❞ 1)、基于规则,例如失败率与历史数据相比暴涨、流量暴跌等。2)、基于时间序列算法或者神经网络的智能化报警,使用者不需要录入任何规则,只需有足够长的历史数据,就可以实现自动报警。通常是两种结合使用。
超限拒绝访问。
如果用户反馈 App 消耗的流量过多,或后台消耗流量较多,我们都可以具体地分析网络请求日志、以及下发命令查看具体时间段的流量、客户端线上监控 + 体系化方案建设 来实现单点问题的追查。
注意:体现演进的过程。
❞网络优化及监控我们刚开始并没有去做,因此我们在 APP 的初期并没有注意到网络的问题,并且我们通常是在 WIFI 场景下进行开发,所以并没有注意到网络方面的问题。
当 APP 增大后,用户增多,逐渐由用户反馈 界面打不开或界面显示慢,也有用户反馈我们 APP 消耗的流量比较多。在我们接受到这些反馈的时候,我们没有数据支撑,无法判断用户反馈是不是正确的。同时,我们也不知道线上用户真实的体验是怎样的。所以,我们就 「建立了线上的网络监控,主要分为 质量监控与流量监控」。
首先,最重要的是接口的请求成功率与每步的耗时,比如 DNS 的解析时间、建立连接的时间、接口失败的原因,然后在合适的时间点上报给服务器。
首先,我们获取到了精准的流量消耗情况,并且在 APM 后台,可以下发指令获取用户在具体时间段的流量消耗情况。 => 引出亮点 => 前后台流量获取方案。 关于指标 => 网络监控。
注意:结合实际案例
❞首先,我们处理了项目当中展示数据相关的接口,同时,对时效性没那么强的接口做了数据的缓存,也就是一段时间内的重复请求直接走缓存,而不走网络请求,从而避免流量浪费。对于一些数据的更新,例如省市区域、配置信息、离线包等信息,我们 「加上版本号的概念,以实现每次更新只传递变化的数据,即实现了增量更新」 => 亮点:离线包增量更新实现原理与关键细节。
然后,我们在上传流量这方面也做了处理,比如针对 POST 请求,我们对 Body 做了 GZip 压缩,而对于图片的发送,必须要经过压缩,它能够在保证清晰度的前提下极大地减少其体积。
对于图片展示,我们采用了不同场景展示不同图片的策略,比如在列表展示界面,我们只展示了缩略图,而到用户显示大图的时候,我们才去展示原图。 => 引出 webp 的使用策略。
首先,部分用户遇到流量消耗多的情况是肯定会存在的,因为线上用户非常多,每个人遇到的情况肯定是不一样的,比如有些用户他的操作路径比较诡异,可能会引发一些异常情况,因此有些用户可能会消耗比较多的流量。
我们在客户端可以精确q地获取到流量的消耗,这样就给我们排查用户的流量消耗提供了依据,我们就知道用户的流量消耗是不是很多。
此外,通过网络请求质量的监控,我们知道了用户所有网络请求的次数与大小,通过大小和次数排查,我们就能知道用户在使用过程中遇到了哪些 bug 或者是执行了一些异常的逻辑导致重复下载,处于不断重试的过程之中。
在客户端,我们发现了类似的问题之后,我们还需要配备主动预警的能力,及时地通知开发同学进行排除验证,通过以上手段,我们对待用户的反馈就能更加高效的解决,因为我们有了用户所有的网络请求数据。
如果一个 WiFi 发送过数据包,但是没有收到任何的 ACK 回包,这个时候就可以初步判断当前的 WiFi 是有问题的。
网络优化可以说是移动端性能优化领域中水最深的领域之一,要想做好网络优化必须具备非常扎实的技术功底与全链路思维。总所周知,对于一个工程师的技术评级往往是以他最深入的那一两个领域为基准,而不是计算其技术栈的平均值。因此,建议大家能找准一两个点,例如 网络、内存、NDK、Flutter,对其进行深入挖掘,以打造自身的技术壁垒。而笔者后续也会利用晚上的时间继续深入 「网络协议与安全」 的领域,开始持续不断地深入挖掘。
我的公众号 JsonChao 开通啦,欢迎关注~
欢迎关注我的微信:bcce5360
❞「由于微信群人数过多,麻烦大家想进微信群的朋友们,加我微信拉你进群。」
❞2千人QQ群,「Awesome-Android学习交流群,QQ群号:959936182」, 欢迎大家加入~
❞本文使用 mdnice 排版