深刻探索 Android 网络优化(3、网络优化篇)下

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

本文思惟导图

欢迎来到 《深刻探索 Android 网络优化(3、网络优化篇)下》~css

5、网络请求质量优化(🔥)

一、Http 请求过程

  • 1)、请求到达运营商的 DNS 服务器并* 解析* 成对应的 IP 地址。
    • HTTPDNS
  • 2)、根据 IP 地址找到相应的服务器,进行 TCP 三次握手, 建立链接
    • 链接复用
    • 网络库的链接管理
  • 3)、发送/接收数据。
    • 压缩
    • 加密
  • 4)、关闭链接。

二、HTTPDNS

问题:DNS 解析慢/被劫持?html

使用 HTTPDSN,HTTPDNS 不是使用 DNS 协议,向 DNS 服务器传统的 53 端口发送请求,而是使用 HTTP 协议向 DSN 服务器的 80 端口发送请求。java

1)、HTTPDNS 优点

  • 一、绕过运营商域名解析的过程,避免 Local DNS 的劫持。
  • 二、下降平均访问时延,提供链接成功率。
  • 三、HTTPDNS 服务器会增长流量调度、网络拨测/灰度、网络容灾等功能。

2)、HTTPDNS + OKHttp 实践

在 Awesome-WanAndroid 中已经实现了 HTTPDNS 优化,其优化代码以下所示:android

// 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");  // 一、设置预解析的 IP 使用 Https 请求。  dnsService.setHTTPSRequestEnabled(true);  // 二、预先注册要使用到的域名,以便 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;  }  // 三、若是从阿里云 DNS 服务器获取不到 ip 地址,则走运营商域名解析的过程。  return Dns.SYSTEM.lookup(hostname);  } } 复制代码

从新安装 App,经过 HTTPDNS 获取到 IP 地址 log 以下所示:git

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点:github

  • 1)、同一个链接仅支持同一个域名。
  • 2)、后端支持 HTTP 2.0 须要改造,这里能够经过在网络平台的统一接入层将数据转换到 HTTP 1.1 后再转发到对应域名的服务器便可。
  • 3)、当全部请求都集中在一条链接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。
  • 4)、在文件下载、视频播放等场景下可能会遇到三方服务器单链接限速的问题,此时能够禁用 HTTP 2.0。

四、协议版本升级

HTTP 1.0

TCP 链接不复用,也就是每发起一个网络请求都要从新创建链接,而刚开始链接都会经历一个慢启动的过程,可谓是慢上加慢,所以 HTTP 1.0 性能很是差。web

HTTP 1.1

引入了持久链接,即 TCP 链接能够复用,但数据通讯必须按次序来,也就是后面的请求必须等前面的请求完成才能进行。当全部请求都集中在一条链接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。算法

HTTP 2

  • 二进制协议
  • 多工
  • 服务端与客户端能够双向实时通讯。

QUIC

Google 2013 实现,2018 基于 QUIC 协议的 HTTP 被确认为 HTTP3。chrome

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 共享,并由此派生出一组 前向安全密钥去进行数据的加密通讯。

五、网络请求质量监控

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。

六、压缩

1)、header(HTTP 2.0 头部压缩)

深刻探索 Android 网络优化(2、网络优化基础篇)下 - 首部压缩

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%的数据来训练字典,而字典的下发与更新由统一接入层负责。

七、加密

HTTPS 一般须要多消耗 2 RTT 的协商时延。

1)、HTTPS 优化

一、提升链接复用率

  • 1)、多个域名共用同一个 HTTP2 链接。
  • 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

三、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。

四、微信 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)、使用对称密钥进行安全通讯。
  • 2)、加密 + 消息认证码:Encrypt-then-MAC
  • 3)、TLS 1.3 只使用 AEAD(Authenticated-Encryption With Addtional data)类算法:Encrypt 与 MAC 都集成在一个算法内部,让有经验的密码专家在算法内部解决安全问题。
  • 4)、mmtls 使用 AES-GCM 这种 AEAD 类算法。

二、密钥扩展

双方使用相同的对称密钥进行加密通讯容易被某些对称密钥算法破解,所以,须要对原始对称密钥作扩展变换获得相应的对称加密参数。

密钥变长须要使用密钥延时函数(KDF,Key Derivation Function),而 TLS 1.3 与 mmtls 都使用了 HKDF 作密钥扩展。

三、防重放

为解决防重放,咱们能够为链接上的每个业务包都添加一个递增的序列号,只要 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 加密的数据能够被解开并被利用,如何处理?

能够在 客户端锁定根证书,能够同时兼容老版本与保证证书替换的灵活性。

八、网络容灾机制

  • 1)、备用服务器分流。
  • 2)、屡次失败后必定时间内不进行请求,避免雪崩效应。

九、资本手段优化

  • 1)、CDN 加速,更新后须要记住清理缓存
  • 2)、提升带宽
  • 3)、动静资源分离
  • 4)、部署跨国的专线、加速点
  • 5)、多 IDC 就进接入
  • 6)、P2P 技术

6、网络库设计

一、统一的网络中台

在一线互联网公司,都会有统一的网络中台:

  • 负责提供先后台一整套的网络解决方案。
  • 网关用于解决中间网络的通信,为上层服务提供高质量的双向通信能力。

二、如何设计一个优秀的统一网络库?

  • 1)、统一 API:统一的策略管理、流解析(兼容JSON、XML、Protocol Buffers)等
  • 2)、全局网络控制:统一的网络调度、流量监控、容灾管理等
  • 3)、高性能:速度、CPU、内存、I/O、失败率、崩溃率、协议兼容性等

三、统一网络库的核心模块有哪些?

  • 1)、DNS 管理
  • 2)、链接管理
  • 3)、协议处理
  • 4)、并发模型
  • 5)、IO 模型
  • 6)、预链接
  • 7)、错误兼容处理
  • 8)、数据解析
  • 9)、网络质量监控
  • 10)流量监控
  • 11)、代理 WebView 网络请求

四、高质量网络库

1)、Chromium 网络库

  • Google 出品,咱们能够基于 Chromium 网络库二次开发本身的网络库, 以便享受 Google 后续网络优化的成果,例如 TLS 1.三、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
  • 一、PTO 触发尾包重传。
  • 二、尾包的 ACK 带上 SACK 信息。
  • 三、SACK 触发 FACK 快速重传和恢复。
  • 四、避免了 RTO 致使的慢启动和延迟。
发图-有损下载

在弱网下尽可能保证下载完整的图片轮廓显示,提升用户体验。

发图-有损上传数据

  • 在弱网下尽可能保证上传完整的图片轮廓显示,提升用户体验。
  • 可以下降客户端上传失败率 10% 以上。

有损上传数据的流程,有损下载流程同理

  • 1)、发送渐进式图片(例如 JPG 等)。
  • 2)、服务器接收数据且回复数据确认包。
  • 3)、当数据足够时(50%),回复发送成功确认包。
  • 4)、发送方继续补充数据
    • 网络正常,数据完整。
    • 网络异常,认为已发送成功。
  • 5)、服务器通知发送者。

发图-低成本重传

将分包转成流式传输。

  • 1)、分包
    • 下降包大小
    • 增长并发
    • 包头损耗
  • 2)、流式 确认粒度策略灵活 单线程

7、其它优化方案

一、异地多活

一个多机房的总体方案,在多个地区同时存在对等的多个机房,以用户维度划分,多机房共同承担全量用户的流量。

在单个机房发送故障时,故障机房的流量能够快速地被迁引到可用机房,减小故障的恢复时间。

二、抗抖动优化

应用一种有策略的重试机制,将网络请求以是否发送到 socket 缓冲区做为分割,将网络请求生命周期划分为”请求开始到发送到 socket 缓冲区“和”已经发送到 socket 缓冲区到请求结束“两个阶段。

这样当用户进电梯由于网络抖动的缘由网络连接断了,可是数据其实已经请求到了 socket 缓冲区,使用这种有策略的重试机制,咱们就能够提高客户端的网络抗抖动能力。

三、SYNC 机制

同步差量数据,达到节省流量,提升通讯效率与请求成功率。

客户端用户不在线时,SYNC 服务端将差量数据保持在数据库中。当客户端下次链接到服务器时,再同步差量数据给用户。

四、高并发流量处理:服务端接入层多级限流

核心思想是保障核心业务在体验可接受范围内作降级非核心功能和业务。从入口到业务接口总共分为四个层级,以下所示:

  • 1)、LVS(几十亿级):多 VIP 多集群。
  • 2)、接入网关(亿级):TCP 限流、核心 RPC 限流。
  • 3)、API 网关(千万级):分级限流算法(对不一样请求量的接口使用不一样的策略)
    • 高 QPS 限流:简单基数算法,超过这个值直接拒绝。
    • 中 QPS 限流:令牌桶算法,接受必定的流量并发。
    • 低 QPS 限流:分布式限流,保障限流的准确。
  • 4)、业务接口(百万级)
    • 返回定制响应、自定义脚本。
    • 客户端静默、Alert、Toast。

五、JobScheduler

结合 JobScheduler 来根据实际状况作网络请求. 比方说 Splash 闪屏广告图片, 咱们能够在链接到 Wifi 时下载缓存到本地; 新闻类的 App 能够在充电, Wifi 状态下作离线缓存。

六、网络请求优先级排序

app应该对网络请求划分优先级尽量快地展现最有用的信息给用户。(高优先级的服务优先使用长链接)

马上呈现给用户一些实质的信息是一个比较好的用户体验,相对于让用户等待那些不那么必要的信息来讲。这能够减小用户不得不等待的时间,增长APP在慢速网络时的实用性。(低优先级使用短链接)

七、创建长连通道

实现原理

将众多请求放入等待发送队列中,待长连通道创建完毕后再将等待队列中的请求放在长连通道上依次送出。

关键细节

HTTP 的请求头键值对中的的键是容许相同和重复的。例如 Set-Cookie/Cookie 字段能够包含多组相同的键名称数据。在长连通讯中,若是对 header 中的键值对用不加处理的字典方式保存和传输,就会形成数据的丢失。

八、减小域名和避免重定向。

九、没有请求的请求,才是最快的请求。

7、网络体系化方案建设

一、线下测试

1)、正确认识

尽量将问题在上线前暴露出来。

2)、侧重点

  • 1)、请求有误、多余
  • 2)、网络切换
  • 3)、弱网
  • 4)、无网

二、线上监控

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。

一、Aspect 插桩 — ArgusAPM

关于 ArgusAPM 的网络监控切面源码分析能够参考我以前写的 深刻探索编译插桩技术(2、AspectJ) - 使用 AspectJ 打造本身的性能监控框架

缺点

监控不全面,由于 App 可能不使用系统/OkHttp 网络库,或是直接使用 Native 网络请求。

二、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)、基于时间序列算法或者神经网络的智能化报警,使用者不须要录入任何规则,只需有足够长的历史数据,就能够实现自动报警。

一般是两种结合使用。

三、异常监控体系搭建

1)、服务器防刷

超限拒绝访问。

2)、客户端

  • 1)、大文件预警
  • 2)、异常兜底策略:例如客户端超过5次链接失败,则设置更长的重试时间。

3)、单点问题追查

若是用户反馈 App 消耗的流量过多,或后台消耗流量较多,咱们均可以具体地分析网络请求日志、以及下发命令查看具体时间段的流量、客户端线上监控 + 体系化方案建设 来实现单点问题的追查。

8、网络优化常见问题

一、在网络方面大家作了哪些监控,创建了哪些指标?

注意:体现演进的过程。

网络优化及监控咱们刚开始并无去作,所以咱们在 APP 的初期并无注意到网络的问题,而且咱们一般是在 WIFI 场景下进行开发,因此并无注意到网络方面的问题。

当 APP 增大后,用户增多,逐渐由用户反馈 界面打不开或界面显示慢,也有用户反馈咱们 APP 消耗的流量比较多。在咱们接受到这些反馈的时候,咱们没有数据支撑,没法判断用户反馈是否是正确的。同时,咱们也不知道线上用户真实的体验是怎样的。因此,咱们就 创建了线上的网络监控,主要分为 质量监控与流量监控

1)、质量监控

首先,最重要的是接口的请求成功率与每步的耗时,好比 DNS 的解析时间、创建链接的时间、接口失败的缘由,而后在合适的时间点上报给服务器。

2)、流量监控

首先,咱们获取到了精准的流量消耗状况,而且在 APM 后台,能够下发指令获取用户在具体时间段的流量消耗状况。 => 引出亮点 => 先后台流量获取方案。 关于指标 => 网络监控。

二、怎么有效地下降用户的流量消耗?

注意:结合实际案例

1)、数据:缓存、增量更新(这一步减小了很是多的流量消耗)

首先,咱们处理了项目当中展现数据相关的接口,同时,对时效性没那么强的接口作了数据的缓存,也就是一段时间内的重复请求直接走缓存,而不走网络请求,从而避免流量浪费。对于一些数据的更新,例如省市区域、配置信息、离线包等信息,咱们 加上版本号的概念,以实现每次更新只传递变化的数据,即实现了增量更新 => 亮点:离线包增量更新实现原理与关键细节。

2)、上传:压缩

而后,咱们在上传流量这方面也作了处理,好比针对 POST 请求,咱们对 Body 作了 GZip 压缩,而对于图片的发送,必需要通过压缩,它可以在保证清晰度的前提下极大地减小其体积。

3)、图片:缩略图、webp

对于图片展现,咱们采用了不一样场景展现不一样图片的策略,好比在列表展现界面,咱们只展现了缩略图,而到用户显示大图的时候,咱们才去展现原图。 => 引出 webp 的使用策略。

三、用户反馈消耗流量多这种问题怎么排查?

首先,部分用户遇到流量消耗多的状况是确定会存在的,由于线上用户很是多,每一个人遇到的状况确定是不同的,好比有些用户他的操做路径比较诡异,可能会引起一些异常状况,所以有些用户可能会消耗比较多的流量。

1)、精准获取流量的能力

咱们在客户端能够精确q地获取到流量的消耗,这样就给咱们排查用户的流量消耗提供了依据,咱们就知道用户的流量消耗是否是不少。

2)、全部请求大小及次数的监控

此外,经过网络请求质量的监控,咱们知道了用户全部网络请求的次数与大小,经过大小和次数排查,咱们就能知道用户在使用过程当中遇到了哪些 bug 或者是执行了一些异常的逻辑致使重复下载,处于不断重试的过程之中。

3)、主动预警的能力

在客户端,咱们发现了相似的问题以后,咱们还须要配备主动预警的能力,及时地通知开发同窗进行排除验证,经过以上手段,咱们对待用户的反馈就能更加高效的解决,由于咱们有了用户全部的网络请求数据。

四、系统如何知道当前 WiFi 有问题?

若是一个 WiFi 发送过数据包,可是没有收到任何的 ACK 回包,这个时候就能够初步判断当前的 WiFi 是有问题的。

9、总结

网络优化能够说是移动端性能优化领域中水最深的领域之一,要想作好网络优化必须具有很是扎实的技术功底与全链路思惟。总所周知,对于一个工程师的技术评级每每是以他最深刻的那一两个领域为基准,而不是计算其技术栈的平均值。所以,建议你们能找准一两个点,例如 网络、内存、NDK、Flutter,对其进行深刻挖掘,以打造自身的技术壁垒。而笔者后续也会利用晚上的时间继续深刻 网络协议与安全 的领域,开始持续不断地深刻挖掘。

公众号

个人公众号 JsonChao 开通啦,欢迎关注~

参考连接:


Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

因为微信群人数过多,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。

本文使用 mdnice 排版

相关文章
相关标签/搜索