深入探索 Android 網絡優化(三、網絡優化篇)下

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

本文思維導圖

歡迎來到 《深入探索 Android 網絡優化(三、網絡優化篇)下》~

五、網絡請求質量優化(🔥)

1、Http 請求過程

  • 1)、請求到達運營商的 DNS 服務器並* 解析* 成對應的 IP 地址。
    • HTTPDNS
  • 2)、根據 IP 地址找到相應的服務器,進行 TCP 三次握手,創建連接
    • 連接複用
    • 網絡庫的連接管理
  • 3)、發送/接收數據。
    • 壓縮
    • 加密
  • 4)、關閉連接。

2、HTTPDNS

問題:DNS 解析慢/被劫持?

使用 HTTPDSN,HTTPDNS 不是使用 DNS 協議,向 DNS 服務器傳統的 53 端口發送請求,而是使用 HTTP 協議向 DSN 服務器的 80 端口發送請求。

1)、HTTPDNS 優勢

  • 1、繞過運營商域名解析的過程,避免 Local DNS 的劫持。
  • 2、降低平均訪問時延,提供連接成功率。
  • 3、HTTPDNS 服務器會增加流量調度、網絡撥測/灰度、網絡容災等功能。

2)、HTTPDNS + OKHttp 實踐

在 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
複製代碼

3、網絡庫的連接管理

利用 HTTP 協議的 keep-alive,建立連接後,會先將連接放入連接池中,如果有另一個請求的域名和端口是一樣的,就直接使用連接池中對應的連接發送和接收數據。在實現網絡庫的連接管理時需要注意以下4點:

  • 1)、同一個連接僅支持同一個域名。
  • 2)、後端支持 HTTP 2.0 需要改造,這裏可以通過在網絡平臺的統一接入層將數據轉換到 HTTP 1.1 後再轉發到對應域名的服務器即可。
  • 3)、當所有請求都集中在一條連接中時,在網絡擁塞時容易出現 TCP 隊首阻塞問題。
  • 4)、在文件下載、視頻播放等場景下可能會遇到三方服務器單連接限速的問題,此時可以禁用 HTTP 2.0。

4、協議版本升級

HTTP 1.0

TCP 連接不復用,也就是每發起一個網絡請求都要重新建立連接,而剛開始連接都會經歷一個慢啓動的過程,可謂是慢上加慢,因此 HTTP 1.0 性能非常差。

HTTP 1.1

引入了持久連接,即 TCP 連接可以複用,但數據通信必須按次序來,也就是後面的請求必須等前面的請求完成才能進行。當所有請求都集中在一條連接中時,在網絡擁塞時容易出現 TCP 隊首阻塞問題。

HTTP 2

  • 二進制協議
  • 多工
  • 服務端與客戶端可以雙向實時通信。

QUIC

Google 2013 實現,2018 基於 QUIC 協議的 HTTP 被確認爲 HTTP3。

QUIC 簡單理解爲 HTTP/2.0 + TLS 1.3 + UDP。弱網環境下表現好與 TCP。

優勢

  • 1)、解決了在連接複用中 HTTP2 + TCP 存在的隊首阻塞問題,
  • 2)、由於是基於 UDP,所以可以靈活控制擁塞協議。例如 Client 端可以直接使用 Google 的 BBR 算法
  • 3)、連接遷:由於 UDP 通過類似connection id 的特性,使得客戶端網絡切換的時候不需要重連,用戶使用 App 的體驗會更加流暢。

目前的缺點

  • 1)、NAT 局域網路由、交換機、防火牆等會禁止 UDP 443 通行,因此 QUIC 創建連接成功率只有95%。
  • 2)、運營商針對 UDP 通道不支持/支持不足。
  • 3)、使用 UDP 不一定會比 TCP 更快,客戶端可同時使用 TCP 和 QUIC 競速,從而選擇更優鏈路。

使用場景

  • 1)、實時性
  • 2)、可丟棄
  • 3)、請求互相依賴
  • 4)、可同時使用 TCP & QUIC

QUIC 加密協議原理

  • 1)、當 Client 與 Server 第一次通信時,會發送 Inchoate Client Hello 消息下載 Server Config(SCFG) 暫存消息。
  • 2)、SCFG 中包含一個 Diffie-Hellman 共享,下一次 Client 將使用它派生初始密鑰(即 0-RTT 密鑰)並利用其加密數據給 Server。
  • 3)、之後,Server 將發出一個新的暫存 Diffie-Hellman 共享,並由此派生出一組 前向安全密鑰去進行數據的加密通信。

5、網絡請求質量監控

1)、接口請求耗時、成功率、錯誤碼

在 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: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
複製代碼

2)、根據網絡質量來動態設定網絡服務的重要參數(超時、併發線程數)

  • 根據用戶 2G/3G/4G/WIFI 的網絡環境。
  • 根據用戶當前網絡的 RTT。

6、壓縮

1)、header(HTTP 2.0 頭部壓縮)

深入探索 Android 網絡優化(二、網絡優化基礎篇)下 - 首部壓縮

2)、URL

不變參數客戶端只需上傳以此,其它參數均在接入層進行擴展。

3)、body

使用 Protocol Buffers 替代 JSON 序列化。

4)、圖片

  • 1)、webp
  • 2)、hevc
  • 3)、SharpP
  • 4)、基於 AI 的圖片超清化
    • 深度學習 CNN(Convolutional Neural Network,卷積神經網絡)。
    • CNN 學習到的是高分辨率圖像和低分辨率圖像的差。

5)、壓縮算法

  • 1)、GZIP
  • 2)、Google Brotli
  • 3)、Facebook Z-standard(推薦):通過業務數據樣本訓練處合適的字典,因此是壓縮率最好的算法,由於各業務線維護字典成本較大,可以在網絡平臺的統一接入層進行壓縮與解壓。我們可以抽樣1%的數據來訓練字典,而字典的下發與更新由統一接入層負責。

7、加密

HTTPS 通常需要多消耗 2 RTT 的協商時延。

1)、HTTPS 優化

1、提高連接複用率

  • 1)、多個域名共用同一個 HTTP2 連接。
  • 2)、長連接。

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)、更強的安全性

刪除了之前版本的不安全的加密算法。

  • 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

3、slight-ssl

參考 TLS 1.3 協議,合併請求,優化加密算法,使用 session-ticket 等策略,力求在安全和體驗間找到一個平衡點。

在 TLS 中性能開銷最大的是 TLS 握手階段的 RSA 加解密。在 slight-ssl 中又嘗試如下幾種解決方案:

  • 1)、硬件加速:使用單獨的硬件加速卡處理 RSA 加解密。
  • 2)、ECDSA:ECSDSA 最底層的算法和成本對性能的消耗遠低於 RSA,相差5~6倍。
  • 3)、Session Ticket 機制:將 TLS 握手從 2RTT 降低爲 1RTT。

4、微信 mmtls 原理

基於 TLS 1.3 草案標準而實現。

類似於 TLS 協議,mmtls 協議也是位於業務層與網絡連接層中間。

mmtls 協議組成圖
  • 1)、Handshake、Alert 和 Application Protocol 都是 record 協議的上層協議。
  • 2)、Record 協議包中有字段用於區分器上層協議是上述3種任一協議。
  • 3)、在 mmtls/TLS 中Handshake 子協議負責密鑰協商, Record 子協議負責數據對稱加密傳輸。除了性能與效率的因素之外,更利於隔離複雜性。
Handshake 協議

TLS 1.3 Handshake 協議有如下幾類:

  • 1-RTT 密鑰協商方式
    • 1-RTT ECDHE
    • 1-RTT PSK(Pre-Shared Key)
  • 0-RTT 密鑰協商方式
    • 0-RTT PSK
    • 0-RTT ECDH
    • 0-RTT PSK-ECDHE
    • 0-RTT ECDH-ECDHE

而 mmtls Handshake 協議有如下幾種:

  • 1-RTT ECDHE
  • 1-RTT PSK
  • 0-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 來保證得到的簽名值唯一對應一次握手。

Record 協議

1、認證加密

  • 1)、使用對稱密鑰進行安全通信。
  • 2)、加密 + 消息認證碼:Encrypt-then-MAC
  • 3)、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)、高可用:服務器添加了過載保護,確保其能在容災模式下提供安全級別稍低的有損服務。

3)、複用 Session Ticket 會話,節省一個 RTT 耗時。

最後,我們可以在統一接入層對傳輸數據二次加密,需要注意二次加密會增加客戶端與服務器的處理耗時。

如果手機設置了代理,TLS 加密的數據可以被解開並被利用,如何處理?

可以在 客戶端鎖定根證書,可以同時兼容老版本與保證證書替換的靈活性。

8、網絡容災機制

  • 1)、備用服務器分流。
  • 2)、多次失敗後一定時間內不進行請求,避免雪崩效應。

9、資本手段優化

  • 1)、CDN 加速,更新後需要記住清理緩存
  • 2)、提高帶寬
  • 3)、動靜資源分離
  • 4)、部署跨國的專線、加速點
  • 5)、多 IDC 就進接入
  • 6)、P2P 技術

六、網絡庫設計

1、統一的網絡中臺

在一線互聯網公司,都會有統一的網絡中臺:

  • 負責提供前後臺一整套的網絡解決方案。
  • 網關用於解決中間網絡的通訊,爲上層服務提供高質量的雙向通訊能力。

2、如何設計一個優秀的統一網絡庫?

  • 1)、統一 API:統一的策略管理、流解析(兼容JSON、XML、Protocol Buffers)等
  • 2)、全局網絡控制:統一的網絡調度、流量監控、容災管理等
  • 3)、高性能:速度、CPU、內存、I/O、失敗率、崩潰率、協議兼容性等

3、統一網絡庫的核心模塊有哪些?

  • 1)、DNS 管理
  • 2)、連接管理
  • 3)、協議處理
  • 4)、併發模型
  • 5)、IO 模型
  • 6)、預連接
  • 7)、錯誤兼容處理
  • 8)、數據解析
  • 9)、網絡質量監控
  • 10)流量監控
  • 11)、代理 WebView 網絡請求

4、高質量網絡庫

1)、Chromium 網絡庫

  • Google 出品,我們可以基於 Chromium 網絡庫二次開發自己的網絡庫, 以便享受 Google 後續網絡優化的成果,例如 TLS 1.3、QUIC 支持等等。
  • 跨平臺。
  • 需要補足 Mars 的 弱網/連接優化 功能。
  • 自定義協議:改造 TLS,將 RSA 更換爲 ECDHE,以提升加解密速度。

2)、微信 Mars

一個跨平臺的 Socket 層解決方案,不支持完整的 HTTP 協議。

Mars 的兩個核心模塊如下:

  • SDT:網絡診斷模塊
  • STN:信令傳輸模塊,適合小數據傳輸。

其中 STN 模塊的組成圖如下所示:

包包超時

  • 每次讀取或發送的間隔。
  • 獲取 sock snd buf 內未發送的數據。
  • Android:ioctl 讀取 SIOCOUTQ。
  • iOS:getsockopt 讀取 SO_NWRITE。

動態超時

根據網絡情況,調整其它超時的係數或絕對值。

Mars 是如何進行 連接優化 的?

複合連接

每間隔幾秒啓動一個新的連接,只要有連接建立成功,則關閉其它連接。=> 有效提升連接成功率。

自動重連優化

  • 1)、減少無效等待時間,增加重試次數。
  • 2)、但 TCP 層的重傳間隔過大時,此時斷連重連,能夠讓 TCP 層保持積極的重連間隔,以提高成功率。
  • 3)、當鏈路存在較大波動或嚴重擁塞時,通過更換連接以獲得更好的性能。

網絡切換

通過感知網絡的狀態切換到更好的網絡環境下。

Mars 是如何進行 弱網優化 的?

常規方案

1)、快速重傳
  • 減小重傳成本(SACK、FEC)
  • 儘早發現重傳(DUP ACK、FACK、RTO、NACK)
2)、HARQ(Hybrid Automatic Repeat reQuest)
  • 3 GPP 標準方案。
  • 增加併發度。
  • 儘量準確避免擁堵(丟包和擁堵的區別)。

進階方案

TCP 丟包的恢復方式 TLP
  • 1、PTO 觸發尾包重傳。
  • 2、尾包的 ACK 帶上 SACK 信息。
  • 3、SACK 觸發 FACK 快速重傳和恢復。
  • 4、避免了 RTO 導致的慢啓動和延遲。
發圖-有損下載

在弱網下儘量保證下載完整的圖片輪廓顯示,提高用戶體驗。

發圖-有損上傳數據

  • 在弱網下儘量保證上傳完整的圖片輪廓顯示,提高用戶體驗。
  • 能夠降低客戶端上傳失敗率 10% 以上。

有損上傳數據的流程,有損下載流程同理

  • 1)、發送漸進式圖片(例如 JPG 等)。
  • 2)、服務器接收數據且回覆數據確認包。
  • 3)、當數據足夠時(50%),回覆發送成功確認包。
  • 4)、發送方繼續補充數據
    • 網絡正常,數據完整。
    • 網絡異常,認爲已發送成功。
  • 5)、服務器通知發送者。

發圖-低成本重傳

將分包轉成流式傳輸。

  • 1)、分包
    • 降低包大小
    • 增加併發
    • 包頭損耗
  • 2)、流式 確認粒度策略靈活 單線程

七、其它優化方案

1、異地多活

一個多機房的整體方案,在多個地區同時存在對等的多個機房,以用戶維度劃分,多機房共同承擔全量用戶的流量。

在單個機房發送故障時,故障機房的流量可以快速地被遷引到可用機房,減少故障的恢復時間。

2、抗抖動優化

應用一種有策略的重試機制,將網絡請求以是否發送到 socket 緩衝區作爲分割,將網絡請求生命週期劃分爲”請求開始到發送到 socket 緩衝區“和”已經發送到 socket 緩衝區到請求結束“兩個階段。

這樣當用戶進電梯因爲網絡抖動的原因網絡鏈接斷了,但是數據其實已經請求到了 socket 緩衝區,使用這種有策略的重試機制,我們就可以提升客戶端的網絡抗抖動能力。

3、SYNC 機制

同步差量數據,達到節省流量,提高通信效率與請求成功率。

客戶端用戶不在線時,SYNC 服務端將差量數據保持在數據庫中。當客戶端下次連接到服務器時,再同步差量數據給用戶。

4、高併發流量處理:服務端接入層多級限流

核心思想是保障核心業務在體驗可接受範圍內做降級非核心功能和業務。從入口到業務接口總共分爲四個層級,如下所示:

  • 1)、LVS(幾十億級):多 VIP 多集羣。
  • 2)、接入網關(億級):TCP 限流、核心 RPC 限流。
  • 3)、API 網關(千萬級):分級限流算法(對不同請求量的接口使用不同的策略)
    • 高 QPS 限流:簡單基數算法,超過這個值直接拒絕。
    • 中 QPS 限流:令牌桶算法,接受一定的流量併發。
    • 低 QPS 限流:分佈式限流,保障限流的準確。
  • 4)、業務接口(百萬級)
    • 返回定製響應、自定義腳本。
    • 客戶端靜默、Alert、Toast。

5、JobScheduler

結合 JobScheduler 來根據實際情況做網絡請求. 比方說 Splash 閃屏廣告圖片, 我們可以在連接到 Wifi 時下載緩存到本地; 新聞類的 App 可以在充電, Wifi 狀態下做離線緩存。

6、網絡請求優先級排序

app應該對網絡請求劃分優先級儘可能快地展示最有用的信息給用戶。(高優先級的服務優先使用長連接)

立刻呈現給用戶一些實質的信息是一個比較好的用戶體驗,相對於讓用戶等待那些不那麼必要的信息來說。這可以減少用戶不得不等待的時間,增加APP在慢速網絡時的實用性。(低優先級使用短連接)

7、建立長連通道

實現原理

將衆多請求放入等待發送隊列中,待長連通道建立完畢後再將等待隊列中的請求放在長連通道上依次送出。

關鍵細節

HTTP 的請求頭鍵值對中的的鍵是允許相同和重複的。例如 Set-Cookie/Cookie 字段可以包含多組相同的鍵名稱數據。在長連通信中,如果對 header 中的鍵值對用不加處理的字典方式保存和傳輸,就會造成數據的丟失。

8、減少域名和避免重定向。

9、沒有請求的請求,纔是最快的請求。

七、網絡體系化方案建設

1、線下測試

1)、正確認識

儘可能將問題在上線前暴露出來。

2)、側重點

  • 1)、請求有誤、多餘
  • 2)、網絡切換
  • 3)、弱網
  • 4)、無網

2、線上監控

1)、服務端監控

宏觀監控維度

1)、請求耗時

區分地域、時間段、版本、機型。

2)、失敗率

業務失敗與請求失敗。

3)、Top 失敗接口、異常接口

以便進行鍼對性地優化。

微觀監控維度

1)、吞吐量(requests per second)

RPS/TPS/QPS,每秒的請求次數,服務器最基本的性能指標,RPS 越高就說明服務器的性能越好。

2)、併發數(concurrency)

反映服務器的負載能力,即服務器能夠同時支持的客戶端數量,越大越好。

3)、響應時間(time per request)

反映服務器的處理能力,即快慢程度,響應時間越短越好。

4)、操作系統資源

CPU、內存、硬盤和網卡等系統資源。可以利用 top、vmstat 等工具檢測相關性能。

優化方針

  • 1)、合理利用系統資源,提高服務器的吞吐量和併發數,降低響應時間。
  • 2)、選用高性能的 Web 服務器,開啓長連接,提升 TCP 的傳輸效率。

2)、客戶端監控

要實現客戶端監控,首先我們應該要統一網絡庫,而客戶端需要監控的指標主要有如下三類:

  • 1)、時延:一般我們比較關心每次請求的 DNS 時間、建連時間、首包時間、總時間等,會有類似 1 秒快開率、2 秒快開率這些指標。
  • 2)、維度:網絡類型、國家、省份、城市、運營商、系統、客戶端版本、機型、請求域名等,這些維度主要用於分析問題。
  • 3)、錯誤:DNS 失敗、連接失敗、超時、返回錯誤碼等,會有 DNS 失敗率、連接失敗率、網絡訪問的失敗率這些指標。

爲了運算簡單我們可以拋棄 UV,只計算每一分鐘部分維度的 PV。

1、Aspect 插樁 — ArgusAPM

關於 ArgusAPM 的網絡監控切面源碼分析可以參考我之前寫的 深入探索編譯插樁技術(二、AspectJ) - 使用 AspectJ 打造自己的性能監控框架

缺點

監控不全面,因爲 App 可能不使用系統/OkHttp 網絡庫,或是直接使用 Native 網絡請求。

2、Native Hook

需要 Hook 的方法有三類:

  • 1)、連接相關:connect
  • 2)、發送數據相關: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%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 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,需要在代碼中增加過濾條件。

3)、接入層監控

爲什麼要做接入層監控?

  • 1)、服務端更容易做到秒級的實時上報。
  • 2)、僅靠客戶端的監控數據並不完全可靠。

監控維度

服務的入口和出口流量、服務端的處理時延、錯誤率等。

4)、監控報警

  • 1)、秒級或者分鐘級別的實時監控只有訪問量(PV)、錯誤率等幾個維度:最快速度發現問題。
  • 2)、小時或者天級別的監控可以監控全部的維度:更好地定位出問題的區域。

監控的同時如何實現準確的自動化報警呢?

  • 1)、基於規則,例如失敗率與歷史數據相比暴漲、流量暴跌等。
  • 2)、基於時間序列算法或者神經網絡的智能化報警,使用者不需要錄入任何規則,只需有足夠長的歷史數據,就可以實現自動報警。

通常是兩種結合使用。

3、異常監控體系搭建

1)、服務器防刷

超限拒絕訪問。

2)、客戶端

  • 1)、大文件預警
  • 2)、異常兜底策略:例如客戶端超過5次連接失敗,則設置更長的重試時間。

3)、單點問題追查

如果用戶反饋 App 消耗的流量過多,或後臺消耗流量較多,我們都可以具體地分析網絡請求日誌、以及下發命令查看具體時間段的流量、客戶端線上監控 + 體系化方案建設 來實現單點問題的追查。

八、網絡優化常見問題

1、在網絡方面你們做了哪些監控,建立了哪些指標?

注意:體現演進的過程。

網絡優化及監控我們剛開始並沒有去做,因此我們在 APP 的初期並沒有注意到網絡的問題,並且我們通常是在 WIFI 場景下進行開發,所以並沒有注意到網絡方面的問題。

當 APP 增大後,用戶增多,逐漸由用戶反饋 界面打不開或界面顯示慢,也有用戶反饋我們 APP 消耗的流量比較多。在我們接受到這些反饋的時候,我們沒有數據支撐,無法判斷用戶反饋是不是正確的。同時,我們也不知道線上用戶真實的體驗是怎樣的。所以,我們就 建立了線上的網絡監控,主要分爲 質量監控與流量監控

1)、質量監控

首先,最重要的是接口的請求成功率與每步的耗時,比如 DNS 的解析時間、建立連接的時間、接口失敗的原因,然後在合適的時間點上報給服務器。

2)、流量監控

首先,我們獲取到了精準的流量消耗情況,並且在 APM 後臺,可以下發指令獲取用戶在具體時間段的流量消耗情況。 => 引出亮點 => 前後臺流量獲取方案。 關於指標 => 網絡監控。

2、怎麼有效地降低用戶的流量消耗?

注意:結合實際案例

1)、數據:緩存、增量更新(這一步減少了非常多的流量消耗)

首先,我們處理了項目當中展示數據相關的接口,同時,對時效性沒那麼強的接口做了數據的緩存,也就是一段時間內的重複請求直接走緩存,而不走網絡請求,從而避免流量浪費。對於一些數據的更新,例如省市區域、配置信息、離線包等信息,我們 加上版本號的概念,以實現每次更新只傳遞變化的數據,即實現了增量更新 => 亮點:離線包增量更新實現原理與關鍵細節。

2)、上傳:壓縮

然後,我們在上傳流量這方面也做了處理,比如針對 POST 請求,我們對 Body 做了 GZip 壓縮,而對於圖片的發送,必須要經過壓縮,它能夠在保證清晰度的前提下極大地減少其體積。

3)、圖片:縮略圖、webp

對於圖片展示,我們採用了不同場景展示不同圖片的策略,比如在列表展示界面,我們只展示了縮略圖,而到用戶顯示大圖的時候,我們纔去展示原圖。 => 引出 webp 的使用策略。

3、用戶反饋消耗流量多這種問題怎麼排查?

首先,部分用戶遇到流量消耗多的情況是肯定會存在的,因爲線上用戶非常多,每個人遇到的情況肯定是不一樣的,比如有些用戶他的操作路徑比較詭異,可能會引發一些異常情況,因此有些用戶可能會消耗比較多的流量。

1)、精準獲取流量的能力

我們在客戶端可以精確q地獲取到流量的消耗,這樣就給我們排查用戶的流量消耗提供了依據,我們就知道用戶的流量消耗是不是很多。

2)、所有請求大小及次數的監控

此外,通過網絡請求質量的監控,我們知道了用戶所有網絡請求的次數與大小,通過大小和次數排查,我們就能知道用戶在使用過程中遇到了哪些 bug 或者是執行了一些異常的邏輯導致重複下載,處於不斷重試的過程之中。

3)、主動預警的能力

在客戶端,我們發現了類似的問題之後,我們還需要配備主動預警的能力,及時地通知開發同學進行排除驗證,通過以上手段,我們對待用戶的反饋就能更加高效的解決,因爲我們有了用戶所有的網絡請求數據。

4、系統如何知道當前 WiFi 有問題?

如果一個 WiFi 發送過數據包,但是沒有收到任何的 ACK 回包,這個時候就可以初步判斷當前的 WiFi 是有問題的。

九、總結

網絡優化可以說是移動端性能優化領域中水最深的領域之一,要想做好網絡優化必須具備非常紮實的技術功底與全鏈路思維。總所周知,對於一個工程師的技術評級往往是以他最深入的那一兩個領域爲基準,而不是計算其技術棧的平均值。因此,建議大家能找準一兩個點,例如 網絡、內存、NDK、Flutter,對其進行深入挖掘,以打造自身的技術壁壘。而筆者後續也會利用晚上的時間繼續深入 網絡協議與安全 的領域,開始持續不斷地深入挖掘。

公衆號

我的公衆號 JsonChao 開通啦,歡迎關注~

參考鏈接:


Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

由於微信羣人數過多,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

本文使用 mdnice 排版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章