❝欢迎来到 《深入探索 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 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 lookup(String hostname) throws UnknownHostException {
String ip = dnsService.getIpByHostAsync(hostname);
LogHelper.i("httpDns: " + ip);
if(ip != null){
List 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点:
TCP 连接不复用,也就是每发起一个网络请求都要重新建立连接,而刚开始连接都会经历一个慢启动的过程,可谓是慢上加慢,因此 HTTP 1.0 性能非常差。
引入了持久连接,即 TCP 连接可以复用,但数据通信必须按次序来,也就是后面的请求必须等前面的请求完成才能进行。当所有请求都集中在一条连接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。
Google 2013 实现,2018 基于 QUIC 协议的 HTTP 被确认为 HTTP3。
QUIC 简单理解为 HTTP/2.0 + TLS 1.3 + UDP。弱网环境下表现好与 TCP。
优势
目前的缺点
使用场景
QUIC 加密协议原理
在 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 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 的协商时延。
1、提高连接复用率
2、减少握手次数(TLS 1.3 实现 0 RTT 协商)
TLS 1.2 引入了 SHA-256 哈希算法,摒弃了 SHA-1,对增强数据完整性有着显著优势。
IETF(Internet Engineering Task Froce,互联网工程任务组)制定的 TLS 1.3 是有史以来最安全、复杂的 TLS 协议。它具有如下特点:
1)、更快的访问速度
相比于 TLS 1.2 及之前的版本,TLS 1.3 的握手不再支持静态的 RSA 密钥交换,使用的是带有前向安全的 Diffie-Hellman 进行全面握手。因此 TLS 1.3 只需 1-RTT 握手时间。
2)、更强的安全性
删除了之前版本的不安全的加密算法。
此外,我们可以在 Google 浏览器设置 TLS 1.3。
3、slight-ssl
参考 TLS 1.3 协议,合并请求,优化加密算法,使用 session-ticket 等策略,力求在安全和体验间找到一个平衡点。
在 TLS 中性能开销最大的是 TLS 握手阶段的 RSA 加解密。在 slight-ssl 中又尝试如下几种解决方案:
4、微信 mmtls 原理
基于 TLS 1.3 草案标准而实现。
类似于 TLS 协议,mmtls 协议也是位于业务层与网络连接层中间。
mmtls 协议组成图
Handshake 协议
TLS 1.3 Handshake 协议有如下几类:
而 mmtls Handshake 协议有如下几种:
「1-RTT ECDHE 密钥协商原理」
ECDH 密钥交换协议需要使用两个算法:
但是 1-RTT ECDHE 算法容易被中间人攻击,中间人可以截获双方的公钥运行 ECDH_Generate_key 生成自己的公私钥对,然后将公钥发送给某一方。
❝如何解决中间人攻击?
❞
中间人攻击产生的本质原因是没有经过端点认证,需要”带认证的密钥协商“。
❝数据认证的方式?
❞
数据认证有对称与非对称两种方式:
ECDH 认证密钥协商就是 ECDH 密钥协商 + 数字签名算法 ECDSA。
双方密钥协商会对自身发出的公钥使用签名算法,由于签名算法中的公钥 ECDSA_verify_key 是公开的,中间人没有办法阻止别人获取公钥。
而 mmtls 仅对 Server 做认证,因为通信一方签名其协商数据就不会被中间人攻击。
在 TLS 中,提供了可选的双方相互认证的能力:
「1-RTT PSK 密钥协商原理」
在之前的 ECDH 握手下,Server 会下发加密的 PSK{key, ticket{key}},其中:
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?
❞
因此 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 来保证得到的签名值唯一对应一次握手。
Record 协议
「1、认证加密」
「2、密钥扩展」
双方使用相同的对称密钥进行加密通信容易被某些对称密钥算法破解,因此,需要对原始对称密钥做扩展变换得到相应的对称加密参数。
密钥变长需要使用密钥延时函数(KDF,Key Derivation Function),而 TLS 1.3 与 mmtls 都使用了 HKDF 做密钥扩展。
「3、防重放」
为解决防重放,我们可以为连接上的每一个业务包都添加一个递增的序列号,只要 Server 检查到新收到的数据包的序列号小于等于之前收到的数据包的序列号,就判断为重放包,mmtls 将序列号作为构造 AES-GCM 算参数 nonce 的一部分,这样就不需要对序列号单独认证。
在 0-RTT 握手下,第一个业务数据包和握手数据包无法使用上述方案,此时需要客户端在业务框架层去协调支持防重放。
小结
mmtls 的 「工作过程」 如下所示:
其优势具有如下4点:
3)、复用 Session Ticket 会话,节省一个 RTT 耗时。
最后,我们可以在统一接入层对传输数据二次加密,需要注意二次加密会增加客户端与服务器的处理耗时。
❝如果手机设置了代理,TLS 加密的数据可以被解开并被利用,如何处理?
❞
可以在 客户端锁定根证书,可以同时兼容老版本与保证证书替换的灵活性。
在一线互联网公司,都会有统一的网络中台:
一个跨平台的 Socket 层解决方案,不支持完整的 HTTP 协议。
Mars 的两个核心模块如下:
其中 STN 模块的组成图如下所示:
包包超时
动态超时
根据网络情况,调整其它超时的系数或绝对值。
❝Mars 是如何进行 连接优化 的?
❞
复合连接
每间隔几秒启动一个新的连接,只要有连接建立成功,则关闭其它连接。=> 有效提升连接成功率。
自动重连优化
网络切换
通过感知网络的状态切换到更好的网络环境下。
❝Mars 是如何进行 弱网优化 的?
❞
常规方案
1)、快速重传
2)、HARQ(Hybrid Automatic Repeat reQuest)
进阶方案
TCP 丢包的恢复方式 TLP
发图-有损下载
在弱网下尽量保证下载完整的图片轮廓显示,提高用户体验。
发图-有损上传数据
有损上传数据的流程,有损下载流程同理
发图-低成本重传
将分包转成流式传输。
一个多机房的整体方案,在多个地区同时存在对等的多个机房,以用户维度划分,多机房共同承担全量用户的流量。
在单个机房发送故障时,故障机房的流量可以快速地被迁引到可用机房,减少故障的恢复时间。
应用一种有策略的重试机制,将网络请求以是否发送到 socket 缓冲区作为分割,将网络请求生命周期划分为”请求开始到发送到 socket 缓冲区“和”已经发送到 socket 缓冲区到请求结束“两个阶段。
这样当用户进电梯因为网络抖动的原因网络链接断了,但是数据其实已经请求到了 socket 缓冲区,使用这种有策略的重试机制,我们就可以提升客户端的网络抗抖动能力。
同步差量数据,达到节省流量,提高通信效率与请求成功率。
客户端用户不在线时,SYNC 服务端将差量数据保持在数据库中。当客户端下次连接到服务器时,再同步差量数据给用户。
核心思想是保障核心业务在体验可接受范围内做降级非核心功能和业务。从入口到业务接口总共分为四个层级,如下所示:
结合 JobScheduler 来根据实际情况做网络请求. 比方说 Splash 闪屏广告图片, 我们可以在连接到 Wifi 时下载缓存到本地; 新闻类的 App 可以在充电, Wifi 状态下做离线缓存。
app应该对网络请求划分优先级尽可能快地展示最有用的信息给用户。(高优先级的服务优先使用长连接)
立刻呈现给用户一些实质的信息是一个比较好的用户体验,相对于让用户等待那些不那么必要的信息来说。这可以减少用户不得不等待的时间,增加APP在慢速网络时的实用性。(低优先级使用短连接)
将众多请求放入等待发送队列中,待长连通道建立完毕后再将等待队列中的请求放在长连通道上依次送出。
HTTP 的请求头键值对中的的键是允许相同和重复的。例如 Set-Cookie/Cookie 字段可以包含多组相同的键名称数据。在长连通信中,如果对 header 中的键值对用不加处理的字典方式保存和传输,就会造成数据的丢失。
尽可能将问题在上线前暴露出来。
宏观监控维度
1)、请求耗时
区分地域、时间段、版本、机型。
2)、失败率
业务失败与请求失败。
3)、Top 失败接口、异常接口
以便进行针对性地优化。
微观监控维度
1)、吞吐量(requests per second)
RPS/TPS/QPS,每秒的请求次数,服务器最基本的性能指标,RPS 越高就说明服务器的性能越好。
2)、并发数(concurrency)
反映服务器的负载能力,即服务器能够同时支持的客户端数量,越大越好。
3)、响应时间(time per request)
反映服务器的处理能力,即快慢程度,响应时间越短越好。
4)、操作系统资源
CPU、内存、硬盘和网卡等系统资源。可以利用 top、vmstat 等工具检测相关性能。
优化方针
要实现客户端监控,首先我们应该要统一网络库,而客户端需要监控的指标主要有如下三类:
为了运算简单我们可以抛弃 UV,只计算每一分钟部分维度的 PV。
1、Aspect 插桩 — ArgusAPM
关于 ArgusAPM 的网络监控切面源码分析可以参考我之前写的 深入探索编译插桩技术(二、AspectJ) - 使用 AspectJ 打造自己的性能监控框架
缺点
监控不全面,因为 App 可能不使用系统/OkHttp 网络库,或是直接使用 Native 网络请求。
2、Native Hook
需要 Hook 的方法有三类:
不同版本 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:
百度一下,你就知道
复制代码
此外,我们也可以使用爱奇艺提供的 android_plt_hook 来实现 PLT Hook。
缺点
接管了系统的 Local Socket,需要在代码中增加过滤条件。
❝为什么要做接入层监控?
❞
监控维度
服务的入口和出口流量、服务端的处理时延、错误率等。
❝监控的同时如何实现准确的自动化报警呢?
❞
通常是两种结合使用。
超限拒绝访问。
如果用户反馈 App 消耗的流量过多,或后台消耗流量较多,我们都可以具体地分析网络请求日志、以及下发命令查看具体时间段的流量、客户端线上监控 + 体系化方案建设 来实现单点问题的追查。
❝注意:体现演进的过程。
❞
网络优化及监控我们刚开始并没有去做,因此我们在 APP 的初期并没有注意到网络的问题,并且我们通常是在 WIFI 场景下进行开发,所以并没有注意到网络方面的问题。
当 APP 增大后,用户增多,逐渐由用户反馈 界面打不开或界面显示慢,也有用户反馈我们 APP 消耗的流量比较多。在我们接受到这些反馈的时候,我们没有数据支撑,无法判断用户反馈是不是正确的。同时,我们也不知道线上用户真实的体验是怎样的。所以,我们就 「建立了线上的网络监控,主要分为 质量监控与流量监控」。
首先,最重要的是接口的请求成功率与每步的耗时,比如 DNS 的解析时间、建立连接的时间、接口失败的原因,然后在合适的时间点上报给服务器。
首先,我们获取到了精准的流量消耗情况,并且在 APM 后台,可以下发指令获取用户在具体时间段的流量消耗情况。 => 引出亮点 => 前后台流量获取方案。 关于指标 => 网络监控。
❝注意:结合实际案例
❞
首先,我们处理了项目当中展示数据相关的接口,同时,对时效性没那么强的接口做了数据的缓存,也就是一段时间内的重复请求直接走缓存,而不走网络请求,从而避免流量浪费。对于一些数据的更新,例如省市区域、配置信息、离线包等信息,我们 「加上版本号的概念,以实现每次更新只传递变化的数据,即实现了增量更新」 => 亮点:离线包增量更新实现原理与关键细节。
然后,我们在上传流量这方面也做了处理,比如针对 POST 请求,我们对 Body 做了 GZip 压缩,而对于图片的发送,必须要经过压缩,它能够在保证清晰度的前提下极大地减少其体积。
对于图片展示,我们采用了不同场景展示不同图片的策略,比如在列表展示界面,我们只展示了缩略图,而到用户显示大图的时候,我们才去展示原图。 => 引出 webp 的使用策略。
首先,部分用户遇到流量消耗多的情况是肯定会存在的,因为线上用户非常多,每个人遇到的情况肯定是不一样的,比如有些用户他的操作路径比较诡异,可能会引发一些异常情况,因此有些用户可能会消耗比较多的流量。
我们在客户端可以精确q地获取到流量的消耗,这样就给我们排查用户的流量消耗提供了依据,我们就知道用户的流量消耗是不是很多。
此外,通过网络请求质量的监控,我们知道了用户所有网络请求的次数与大小,通过大小和次数排查,我们就能知道用户在使用过程中遇到了哪些 bug 或者是执行了一些异常的逻辑导致重复下载,处于不断重试的过程之中。
在客户端,我们发现了类似的问题之后,我们还需要配备主动预警的能力,及时地通知开发同学进行排除验证,通过以上手段,我们对待用户的反馈就能更加高效的解决,因为我们有了用户所有的网络请求数据。
如果一个 WiFi 发送过数据包,但是没有收到任何的 ACK 回包,这个时候就可以初步判断当前的 WiFi 是有问题的。
网络优化可以说是移动端性能优化领域中水最深的领域之一,要想做好网络优化必须具备非常扎实的技术功底与全链路思维。总所周知,对于一个工程师的技术评级往往是以他最深入的那一两个领域为基准,而不是计算其技术栈的平均值。因此,建议大家能找准一两个点,例如 网络、内存、NDK、Flutter,对其进行深入挖掘,以打造自身的技术壁垒。而笔者后续也会利用晚上的时间继续深入 「网络协议与安全」 的领域,开始持续不断地深入挖掘。
我的公众号 JsonChao 开通啦,欢迎关注~
现如今,Android 行业人才已逐渐饱和化,但高级人才依旧很稀缺,我们经常遇到的情况是,100份简历里只有2、3个比较合适的候选人,大部分的人都是疲于业务,没有花时间来好好学习,或是完全不知道学什么来提高自己的技术。对于 Android 开发者来说,尽早建立起一个完整的 Android 知识框架,了解目前大厂高频出现的常考知识点,掌握面试技巧,是一件非常需要重视的事情。
去年,为了进入一线大厂去做更有挑战的事情,拿到更高的薪资,我提前准备了半年的时间,沉淀了一份 「两年磨一剑」 的体系化精品面试题,而后的半年,我都在不断地进行面试,总共面试了二三十家公司,每一场面试完之后,我都将对应的面试题和详细的答案进行了系统化的总结,并更新到了我的面试项目里,现在,在每一个模块之下,我都已经精心整理出了 超高频和高频的常考 知识点。
在我近一年的大厂实战面试复盘中逐渐对原本的内容进行了大幅度的优化,并且新增了很多新的内容。它可以说是一线互联网大厂的面试精华总结,同时后续还会包含如何写简历和面试技巧的内容,能够帮你省时省力地准备面试,大大降低找到一个好工作的难度。
这份面试项目不同于我 Github 上的 Awesome-Android-Interview 面试项目:https://github.com/JsonChao/Awesome-Android-Interview,Awesome-Android-Interview 已经在 2 年前(2020年 10 月停止更新),内容稍显陈旧,里面也有不少点表述不严谨,总体含金量较低。而我今天要分享的这份面试题库,是我在这两年持续总结、细化、沉淀出来的体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高。
在分享之前,有一点要注意的是,一定不要将资料泄露出去!细想一下就明白了:
1、如果暴露出去,拿到手的人比你更快掌握,更早进入大厂,拿到高薪,你进大厂的机会就会变小,毕竟现在好公司就那么多,一个萝卜一个坑。
2、两年前我公开分享的简陋版 Awesome-Android-Interview 面试题库现在还在被各个培训机构当做引流资料,加大了现在 Android 内卷。。
所以,这一点一定要切记。
现在,我已经在我的成长社群里修订好了 《体系化高频核心 Android 面试题库》 中的 ”计算机基础高频核心面试题“ 和 ”Java 和 kotlin 高频核心面试题“ 部分,后续还会为你带来我核心题库中的:
“Android基础 高频核心面试题”
“基础架构 高频核心面试题”
“跨平台 高频核心面试题”
“性能优化 高频核心面试题”
”Framework 高频核心面试题“
”NDK 高频核心面试题“
获取方法:扫描下方的二维码。
出身普通的人,如何真正改变命运?
这是我过去五、六年一直研究的命题。首先,是为自己研究,因为我是从小城镇出来的,通过持续不断地逆袭立足深圳。越是出身普通的人,就越需要有耐心,去进行系统性地全面提升,这方面,我有非常丰富的实践经验和方法论。因此,我开启了 “JsonChao” 的成长社群,希望和你一起完成系统性地蜕变。
每周会提供一份让 个人增值,避免踩坑 的硬干货。
每日以文字或语音的形式分享我个人学习和实践中的 思考精华或复盘记录。
提供 每月 三 次成长、技术或面试指导的咨询服务。
更多服务正在研发中...
如果你希望持续提升自己,获得更高的薪资或是想加入大厂,那么超哥的知识星球会对你有很大的帮助。
如果你既努力,又焦虑,特别适合加入超哥的知识星球,因为我经历过同样的阶段,而且最后找到了走出焦虑,靠近梦想的地方。
如果你希望改变自己的生活状态,欢迎加入超哥的知识星球,和我一起每日迭代,持续精进。
365元每年
每天一元,给自己的成长持续加油
为了回馈 JsonChao 的 CSDN 忠实用户,我申请了少量优惠券,先到者先得,错过再无