探究Android客户端网络预连接优化机制

目录
  • 一、连接复用
  • 二、预连接实现
  • 三、源码分析
  • 四、优化
  • 五、问答

一、连接复用

对于一个普通的接口请求,通过charles抓包,查看网络请求Timing栏信息,我们可以看到类似如下请求时长信息:

  • Duration 175 ms
  • DNS 6 ms
  • Connect 50 msTLS Handshake 75 ms
  • Request 1 ms
  • Response 1 ms
  • Latency 42 ms

同样的请求,再来一次,时长信息如下所示:

  • Duration 39 ms
  • DNS -
  • Connect -
  • TLS Handshake -
  • Request 0 ms
  • Response 0 ms
  • Latency 39 ms

我们发现,整体网络请求时间从175ms降低到了39ms。其中DNS,Connect,TLS Handshake 后面是个横线,表示没有时长信息,于是整体请求时长极大的降低了。这就是Http(s)的连接复用的效果。那么问题来了,什么是连接复用,为什么它能降低请求时间?

在解决这个疑问之前,我们先来看看一个网络请求发起,到收到返回的数据,这中间发生了什么?

  • 客户端发起网络请求
  • 通过DNS服务解析域名,获取服务器IP (基于UDP协议的DNS解析)
  • 建立TCP连接(3次握手)
  • 建立TLS连接(https才会用到)
  • 发送网络请求request
  • 服务器接收request,构造并返回response
  • TCP连接关闭(4次挥手)

上面的连接复用直接让上面2,3,4步都不需要走了。这中间省掉的时长应该怎么算?如果我们定义网络请求一次发起与收到响应的一个来回(一次通信来回)作为一个RTT(Round-trip delay time)。

1)DNS默认基于UDP协议,解析最少需要1-RTT;

2)建立TCP连接,3次握手,需要2-RTT;

3)建立TLS连接,根据TLS版本不同有区别,常见的TLS1.2需要2-RTT。

Client                                               Server

ClientHello                  -------->

                                                ServerHello

                                               Certificate*

                                         ServerKeyExchange*

                                        CertificateRequest*

                             <--------      ServerHelloDone

Certificate*

ClientKeyExchange

CertificateVerify*

[ChangeCipherSpec]

Finished                     -------->

                                         [ChangeCipherSpec]

                             <--------             Finished

Application Data             <------->     Application Data

                   TLS 1.2握手流程(来自 RFC 5246)

注:TLS1.3版本相比TLS1.2,支持0-RTT数据传输(可选,一般是1-RTT),但目前支持率比较低,用的很少。

http1.0版本,每次http请求都需要建立一个tcp socket连接,请求完成后关闭连接。前置建立连接过程可能就会额外花费4-RTT,性能低下。

http1.1版本开始,http连接默认就是持久连接,可以复用,通过在报文头部中加上Connection:Close来关闭连接 。如果并行有多个请求,可能还是需要建立多个连接,当然我们也可以在同一个TCP连接上传输,这种情况下,服务端必须按照客户端请求的先后顺序依次回送结果。

注:http1.1默认所有的连接都进行了复用。然而空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。

http2 更进一步,支持二进制分帧,实现TCP连接的多路复用,不再需要与服务端建立多个TCP连接了,同域名的多个请求可以并行进行。

还有个容易被忽视的是,TCP有拥塞控制,建立连接后有慢启动过程(根据网络情况一点一点的提高发送数据包的数量,前面是指数级增长,后面变成线性),复用连接可以避免这个慢启动过程,快速发包。

二、预连接实现

客户端常用的网络请求框架如OkHttp等,都能完整支持http1.1与HTTP2的功能,也就支持连接复用。了解了这个连接复用机制优势,那我们就可以利用起来,比如在APP闪屏等待的时候,就预先建立首页详情页等关键页面多个域名的连接,这样我们进入相应页面后可以更快的获取到网络请求结果,给予用户更好体验。在网络环境偏差的情况下,这种预连接理论上会有更好的效果。

具体如何实现?

第一反应,我们可以简单的对域名链接提前发起一个HEAD请求(没有body可以省流量),这样就能提前建立好连接,下次同域名的请求就可以直接复用,实现起来也是简单方便。于是写了个demo,试了个简单接口,完美,粗略统计首次请求速度可以提升40%以上。

于是在游戏中心App启动Activity中加入了预连接相关逻辑,跑起来试了下,竟然没效果...

抓包分析,发现连接并没有复用,每次进去详情页后都重新创建了连接,预连接可能只是省掉了DNS解析时间,demo上的效果无法复现。看样子分析OkHttp连接复用相关源码是跑不掉了。

三、源码分析

OKHttp通过几个默认的Interceptor用于处理网络请求相关逻辑,建立连接在ConnectInterceptor类中;

public final class ConnectInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
​
    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
​
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

RealConnection即为后面使用的connection,connection生成相关逻辑在StreamAllocation类中;

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  ...
    RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
        writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
    HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
  ...
}
​
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);
    ...
      return candidate;
    }
}

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    ...

    // 尝试从connectionPool中获取可用connection
    Internal.instance.acquire(connectionPool, address, this, null);
    if (connection != null) {
    foundPooledConnection = true;
    result = connection;
    } else {
    selectedRoute = route;
    }

   ...

    if (!foundPooledConnection) {
      ...
      // 如果最终没有可复用的connection,则创建一个新的
        result = new RealConnection(connectionPool, selectedRoute);
    }
  ...
}

这些源码都是基于okhttp3.13版本的代码,3.14版本开始这些逻辑有修改。

StreamAllocation类中最终获取connection是在findConnection方法中,优先复用已有连接,没可用的才新建立连接。获取可复用的连接是在ConnectionPool类中;

/**
 * Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
 * share the same {@link Address} may share a {@link Connection}. This class implements the policy
 * of which connections to keep open for future use.
 */
public final class ConnectionPool {

  private final Runnable cleanupRunnable = () -> {
    while (true) {
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (ConnectionPool.this) {
          try {
            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

  // 用一个队列保存当前的连接
  private final Deque<RealConnection> connections = new ArrayDeque<>();

  /**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
  ...
  }

  void acquire(Address address, StreamAllocation streamAllocation, @Nullable Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return;
      }
    }
  }

由上面源码可知,ConnectionPool默认最大维持5个空闲的connection,每个空闲connection5分钟后自动释放。如果connection数量超过最大数5个,则会移除最旧的空闲connection。

最终判断空闲的connection是否匹配,是在RealConnection的isEligible方法中;

/**
   * Returns true if this connection can carry a stream allocation to {@code address}. If non-null
   * {@code route} is the resolved route for a connection.
   */
  public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    if (route == null) return false;
    if (route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
    if (!this.route.socketAddress().equals(route.socketAddress())) return false;

    // 3. This connection's server certificate's must cover the new host.
    if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
    if (!supportsUrl(address.url())) return false;

    // 4. Certificate pinning must match the host.
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

    return true; // The caller's address can be carried by this connection.
  }

这块代码比较直白,简单解释下比较条件:

如果该connection已达到承载的流上限(即一个connection可以承载几个请求,http1默认是1个,http2默认是Int最大值)则不符合;

如果2个Address除Host之外的属性有不匹配,则不符合(如果2个请求用的okhttpClient不同,复写了某些重要属性,或者服务端端口等属性不一样,那都不允许复用);

如果host相同,则符合,直接返回true(其它字段已经在上一条比较了);

如果是http2,则判断无代理、服务器IP相同、证书相同等条件,如果都符合也返回true;

整体看下来,出问题的地方应该就是ConnectionPool 的队列容量太小导致的。游戏中心业务复杂,进入首页后,触发了很多接口请求,导致连接池直接被占满,于是在启动页做好的预连接被释放了。通过调试验证了下,进入详情页时,ConnectionPool中的确已经没有之前预连接的connection了。

四、优化

在http1.1中,浏览器一般都是限定一个域名最多保留5个左右的空闲连接。然而okhttp的连接池并没有区分域名,整体只做了默认最大5个空闲连接,如果APP中不同功能模块涉及到了多个域名,那这默认的5个空闲连接肯定是不够用的。有2个修改思路:

  • 重写ConnectionPool,将连接池改为根据域名来限定数量,这样可以完美解决问题。然而OkHttp的ConnectionPool是final类型的,无法直接重写里面逻辑,另外OkHttp不同版本上,ConnectionPool逻辑也有区别,如果考虑在编译过程中使用ASM等字节码编写技术来实现,成本很大,风险很高。
  • 直接调大连接池数量和超时时间。这个简单有效,可以根据自己业务情况适当调大这个连接池最大数量,在构建OkHttpClient的时候就可以传入这个自定义的ConnectionPool对象。

我们直接选定了方案2。

五、问答

1、如何确认连接池最大数量值?

这个数量值有2个参数作为参考:页面最大同时请求数,App总的域名数。也可以简单设定一个很大的值,然后进入APP后,将各个主要页面都点一遍,看看当前ConnectionPool中留存的connection数量,适当做一下调整即可。

2、调大了连接池会不会导致内存占用过多?

经测试:将connectionPool最大值调成50,在一个页面上,用了13个域名链接,总共重复4次,也就是一次发起52个请求之后,ConnectionPool中留存的空闲connection平均22.5个,占用内存为97Kb,ConnectionPool中平均每多一个connection会占用4.3Kb内存。

3、调大了连接池会影响到服务器吗?

理论上是不会的。连接是双向的,即使客户端将connection一直保留,服务端也会根据实际连接数量和时长调整,自动关闭连接的。比如服务端常用的nginx就可以自行设定最大保留的connection数量,超时也会自动关闭旧连接。因此如果服务器定义的最大连接数和超时时间比较小,可能我们的预连接会无效,因为连接被服务端关闭了。

用charles可以看到这种连接被服务端关闭的效果:TLS大类中Session Resumed里面看到复用信息。

这种情况下,客户端会重新建立连接,会有tcp和tls连接时长信息。

4、预连接会不会导致服务器压力过大?

由于进入启动页就发起了网络请求进行预连接,接口请求数增多了,服务器肯定会有影响,具体需要根据自己业务以及服务器压力来判断是否进行预连接。

5、如何最大化预连接效果?

由上面第3点问题可知,我们的效果实际是和服务器配置息息相关,此问题涉及到服务器的调优。

服务器如果将连接超时设置的很小或关闭,那可能每次请求都需要重新建立连接,这样服务器在高并发的时候会因为不断创建和销毁TCP连接而消耗很多资源,造成大量资源浪费。

服务器如果将连接超时设置的很大,那会由于连接长时间未释放,导致服务器服务的并发数受到影响,如果超过最大连接数,新的请求可能会失败。

可以考虑根据客户端用户访问到预连接接口平均用时来调节。比如游戏中心详情页接口预连接,那可以统计一下用户从首页平均浏览多长时间才会进入到详情页,根据这个时长和服务器负载情况来适当调节。

以上就是探究Android客户端网络预连接优化机制的详细内容,更多关于Android客户端网络 预连接优化的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android获取网络图片并显示的方法

    本文实例为大家分享了Android获取网络图片并显示的具体代码,供大家参考,具体内容如下 使用 HttpURLConnection 获得连接,再使用 InputStream 获得图片的数据流,通过 BitmapFactory 将数据流转换为 Bitmap,再将 Bitmap 通过线程的 Message 发送出去,Handler 接收到消息就会通知 ImageView 显示出来. 记得要在manifest文件中添加 < uses-permission android:name="androi

  • Android使用网络获取定位的方法

    本文实例为大家分享了Android使用网络获取定位的具体代码,供大家参考,具体内容如下 目标效果: 程序运行弹出权限选择,选择运行网络定位后会查询位置,然后在TextView上显示当前国家和城市. 1.activity_main.xml页面定义TextView显示城市名. activity_main.xml页面: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:too

  • Android使用OkHttp进行网络同步异步操作

    OkHttp是一个Java和Android的HTTP和HTTP/2的客户端,负责发送HTTP请求以及接受HTTP响应. 一.使用OkHttp OkHttp发送请求后,可以通过同步或异步地方式获取响应.下面就同步和异步两种方式进行介绍. 1.1.同步方式 发送请求后,就会进入阻塞状态,知道收到响应.下面看一个下载百度首页的例子: OkHttpClient client = new OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).bui

  • Android8.1原生系统网络感叹号消除的方法

    原生系统Android8.1上,WiFi上出现感叹号,此时WiFi可正常访问. 原因 这是Android 5.0引入的网络评估机制:就是当你连上网络后,会给目标产生204响应的服务器发送给一个请求,如果服务器返回的是状态码为204的响应,那么就被认为网络可以访问:否则,如返回的是其他状态码,那么将被视为网络访问需要登录操作等:没有响应的话,就被认为是网络不可访问.这里的情况就是,目标服务器不能正常访问 产生204响应的服务器 加粗网址亲测可行,其余未测试,但可作为一个参考 http://conn

  • android实现okHttp的get和post请求的简单封装与使用

    由于Android课程项目需要,特地查阅了okHttp的使用,发现网上找的大多和自己的需求不一样.所以就着团队项目需要,自己简单封装了一个okHttp的get和post请求. 话不多说,直接看代码吧! 一.前期需要用到的属性封装 private static Request request = null; private static Call call = null; private static int TimeOut = 120; //单例获取ohttp3对象 private static

  • Android中okhttp3使用详解

    一.引入包 在项目module下的build.gradle添加okhttp3依赖 compile 'com.squareup.okhttp3:okhttp:3.3.1' 二.基本使用 1.okhttp3 Get 方法 1.1 .okhttp3 同步 Get方法 /** * 同步Get方法 */ private void okHttp_synchronousGet() { new Thread(new Runnable() { @Override public void run() { try {

  • 详解Android 利用Iptables实现网络黑白名单(防火墙)

    一.概述 为了使读此简笔的人对Iptables有一个简单的了解,此处强行百度了一波概念,如果想深入的了解Iptables的各种配置规则和内核对其的管理运行机制请自行www.baidu.com,这些并不是本简笔的目的所在. 闲言少叙,开始正文 ---->以下概述来自baidu,读者可酌情跳过 iptables的前身叫ipfirewall (内核1.x时代),是从freeBSD上移植过来的,能够工作在内核当中的,对数据包进行检测的一款简易访问控制工具.但是ipfirewall工作功能极其有限(它需要

  • 详解Android中OkHttp3的例子和在子线程更新UI线程的方法

    okHttp用于android的http请求.据说很厉害,我们来一起尝尝鲜.但是使用okHttp也会有一些小坑,后面会讲到如何掉进坑里并爬出来. 首先需要了解一点,这里说的UI线程和主线程是一回事儿.就是唯一可以更新UI的线程.这个只是点会在给okHttp填坑的时候用到.而且,这个内容本身在日常的开发中也经常用到,值得好好学一学. okHttp发起同步请求 第一个列子是一个同步请求的例子. private void performSyncHttpRequest() { OkHttpClient

  • 探究Android客户端网络预连接优化机制

    目录 一.连接复用 二.预连接实现 三.源码分析 四.优化 五.问答 一.连接复用 对于一个普通的接口请求,通过charles抓包,查看网络请求Timing栏信息,我们可以看到类似如下请求时长信息: Duration 175 ms DNS 6 ms Connect 50 msTLS Handshake 75 ms Request 1 ms Response 1 ms Latency 42 ms 同样的请求,再来一次,时长信息如下所示: Duration 39 ms DNS - Connect -

  • Android端TCP长连接的性能优化教程分享

    前言 大家应该都知道,在Android端实现TCP长连接场景其实不多,我们最熟悉的不过推送和HTTP协议的实现(OkHttp),本文讨论的是在实现推送长连接的情况下怎么来做性能优化,下文只是我的一点拙见,有不妥之处还望指出,下面话不多说了,来一起看看详细的介绍吧. 推送长连接 可以说大部分APP是离不开推送(push)这个功能的,不过平常我们都是接入第三方SDK(极光.个推等)居多,因为要做一个推送服务,不光客户端要编写相应的Socket通信代码,服务器端更是麻烦,要处理大规模的长连接服务,消息

  • Android三种网络通讯方式及Android的网络通讯机制

    Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口).Org.apache接口和Android.net.*(Android网络接口).下面分别介绍这些接口的功能和作用. 1.标准Java接口 java.net.*提供与联网有关的类,包括流.数据包套接字(socket).Internet协议.常见Http处理等.比如:创建URL,以及URLConnection/HttpURLConnection对象.设置链接参数.链接到服务器.向服务器写数据.从服务器读取

  • android 检查网络连接状态实现步骤

    获取网络信息需要在AndroidManifest.xml文件中加入相应的权限. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 1)判断是否有网络连接 复制代码 代码如下: public boolean isNetworkConnected(Context context) { if (context != null) { ConnectivityManager mConn

  • Android编程判断是否连接网络的方法【WiFi及3G判断】

    本文实例讲述了Android编程判断是否连接网络的方法.分享给大家供大家参考,具体如下: 判断wifi网络是否链接: public static boolean isWiFiActive(Context inContext) { WifiManager mWifiManager = (WifiManager) inContext .getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = mWifiManager.getConnect

  • Android中判断网络是否连接实例详解

    Android中判断网络是否连接实例详解 在android中,如何监测网络的状态呢,这个有的时候也是十分重要的,方法如下: public class ConnectionDetector { private Context _context; public ConnectionDetector(Context context){ this._context = context; } public boolean isConnectingToInternet(){ ConnectivityMana

  • android 判断网络是否可用与连接的网络是否能上网

    网络状态获取 上传与下载都需要先查看当前手机的网络状态,需要获取ConnectionManager /** * 判断当前是否有网络连接,但是如果该连接的网络无法上网,也会返回true * @param mContext * @return */ public static boolean isNetConnection(Context mContext) { if (mContext!=null){ ConnectivityManager connectivityManager = (Conne

  • Android 深入探究自定义view之事件的分发机制与处理详解

    目录 题引 Activity对事件的分发过程 父布局拦截的分发处理过程 ACTION_DOWN 事件 ACTION_MOVE 事件 父布局不拦截时的分发处理过程 ACTION_DOWN ACTION_MOVE 解决冲突方案 外部拦截 内部拦截 本文主要探讨下面几个问题: 学习事件分发机制是为了解决什么问题 Activity对事件的分发过程 父布局拦截的分发处理过程 父布局不拦截时的分发处理过程 冲突解决方案 题引 事件只有一个,多个人想要处理,处理的对象不是我们想给的对象就是事件冲突. 如上图,

  • Android高级开发之性能优化典范

    本章介绍android高级开发中,对于性能方面的处理.主要包括电量,视图,内存三个性能方面的知识点. 1.视图性能 (1)Overdraw简介 Overdraw就是过度绘制,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布 局导致一些像素会被多次绘制,而每次绘制都会对应到CPU的一组绘图命令和GPU的一些操作,当这个操作耗时超过16.67ms时,就会出现掉帧现象,表现为应用卡顿,所以对重叠不可见元素的重复绘制会产生额外的开销,需要尽量减

  • 详解Android客户端与服务器交互方式

    最近的Android项目开发过程中一个问题困扰自己很长时间,Android客户端与服务器交互有几种方式,最常见的就是webservices和json.要在Android手机客户端与pc服务器交互,需要满足下面几种条件:跨平台.传输数据格式标准.交互方便. 为了与服务器通讯其实无非就两种协议HTTP和TCP,TCP的学习Socket,HTTP的话熟悉一下HTTP协议和相关Java API.而下面的几种方式就是从这两种协议扩展出来的:webservices soap.SSH的JSON(可参考:该链接

随机推荐