OkHttp 3.7源码深入分析(五)——连接池

2019-10-18 15:01 来源:未知

OkHttp类别作品如下

OkHttp3.7源码解析文章列表如下:

  • OkHttp3源码深入分析[综述]
  • OkHttp3源码深入分析[复用连接池]
  • OkHttp3源码分析[缓存计谋]
  • OkHttp3源码深入分析[DiskLruCache]
  • OkHttp3源码深入分析[任务队列]
  • OkHttp源码解析——全体框架结构
  • OkHttp源码剖析——拦截器
  • OkHttp源码剖判——任务队列
  • OkHttp源码深入分析——缓存战术
  • OkHttp源码剖判——多路复用


1. 概述

HTTP中的keepalive连接在网络质量优化中,对于延迟降低与进度进步的有特别关键的功用。

通常性大家举办http连接时,首先进行tcp握手,然后传输数据,最后获释

图源: Nginx closed

这种艺术真的轻松,可是在复杂的网络内容中就缺乏用了,创建socket供给展开3次握手,而释放socket要求2次握手(只怕是4次)。重复的连天与自由tcp连接如同每一次唯有挤1mm的牙膏就合上牙膏盖子接着再张开接着挤同样。而每一次一而再大致是TTL壹回的光阴(也正是ping贰次),在TLS情况下消耗的时光就更加的多了。很确定,当访问复杂互联网时,延时(实际不是带宽)将改成那一个首要的元素。

本来,上面的题目早就经缓和了,在http中有一种叫做keepalive connections的机制,它能够在传输数据后依然保持延续,当顾客端要求重新获取数据时,直接利用刚刚空闲下来的连接而没有要求再行握手

图源: Nginx keep_alive

在现世浏览器中,平日同时张开6~8个keepalive connections的socket连接,并保证一定的链路生命,当无需时再关闭;而在服务器中,平日是由软件依据负荷处境(例如FD最大值、Socket内部存款和储蓄器、超时时间、栈内存、栈数量等)决定是或不是百尺竿头更上一层楼关闭。

Okhttp扶植5个并发KeepAlive,暗中认可链路生命为5分钟(链路空闲后,保持现成的时光)

当然keepalive也许有短处,在升高了单个顾客端质量的还要,复用却阻止了别的客户端的链路速度,具体来讲如下

  1. 依赖TCP的堵截机制,当总水管大小固定时,假设存在大气悠闲的keepalive connections(大家得以称作僵尸连接或者泄漏连接),另外顾客端们的符合规律连接速度也会受到震慑,那也是运行商为什么限制P2P连接数的道理
  2. 服务器/防火墙上有出现限制,比如apache服务器对各种伏乞都开线程,导致只支持1五十二个冒出连接(数据来自nginx官方网站),不过那么些瓶颈随着高并发server软硬件的发展(golang/分布式/IO多路复用)将会更少
  3. 一大波的DDOS发生的丧尸连接大概被用于恶意抨击服务器,耗尽财富

好了,以上科学普及实现,本文主假设写顾客端的,服务端不再介绍。

下文如果服务器是通过正规的运行配置好的,它默许开启了keep-alive,并不主动关闭连接

接下去讲下OkHttp的连接池管理,那也是OkHttp的为主部分。通过爱慕连接池,最大限度重用现成连接,收缩网络连接的创立费用,以此提高网络央浼功能。

2. 连接池的运用与深入分析

先是先说下源码中重要性的目的:

  • Call: 对http的哀求封装,属于程序员可以接触的上层高端代码
  • Connection: 对jdk的socket物理连接的包装,它里面有List<WeakReference<StreamAllocation>>的引用
  • StreamAllocation: 表示Connection被上层高等代码的引用次数
  • ConnectionPool: Socket连接池,对连接缓存进行回收与治本,与CommonPool有类似的宏图
  • Deque: Deque也正是双端队列,双端队列同不时间具有队列和栈性质,日常在缓存中被选用,那个是java基础

在okhttp中,连接池对顾客,以至开拓者都是晶莹的。它自动创设连接池,自动实行泄漏连接回收,自动帮你管理线程池,提供了put/get/clear的接口,以致里头调用都帮您写好了。

在原先的内部存款和储蓄器败露分析作品中笔者写到,大家清楚在socket连接中,也正是Connection中,本质是包裹好的流操作,除非手动close掉连接,基本不会被GC掉,特别轻便引发内部存款和储蓄器败露。所以当提到到并发socket编制程序时,大家就能够特不安,往往写出来的代码都是try/catch/finally的迷之缩进,却又对这么的代码无奈。

在okhttp中,在高层代码的调用中,使用了周围于引用计数的点子跟踪Socket流的调用,这里的计数对象是StreamAllocation,它被每每实行aquirerelease操作(点击函数能够进去github查看),那多少个函数其实是在转移Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的数据相当于大意socket被引用的计数(Refference Count),如若计数为0的话,说明此一连未有被运用,是悠闲的,须求经过下文的算法达成回收;若是上层代码依旧援引,就无需关闭连接。

引用计数法:给指标中增多一个引用计数器,每当有三个地点援用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象正是不恐怕再被选取。它无法管理循环援用的标题。

1. 背景

2.1. 实例化

在源码中,我们先找ConnectionPool实例化的职位,它是直接new出来的,而它的各样操作却在OkHttpClient32450新蒲京网站,的static区实现了Internal.instance接口作为ConnectionPool的包装。

关于缘何供给这么大做作品的分支包装,首若是为了让外部包的积极分子访谈非public主意,详见这里注释

1.1 keep-alive机制

在HTTP1.0中HTTP的乞请流程如下:

connectionpool_keepalive.png

这种艺术的补益是轻巧,种种供给互不忧愁。但在纷纭的网络必要场景下这种方法差相当少不可用。比如:浏览器加载四个HTML网页,HTML中大概须求加载数十三个能源,标准气象下这个财富中山大学部分源于同八个站点。遵照HTTP1.0的做法,这亟需树立数十一个TCP连接,各样连接担任八个财富央浼。创立四个TCP连接供给3次握手,而释放连接则须求2次或4次握手。重复的创立和刑满释放连接比非常的大地震慑了网络成效,同有的时候间也加进了系统开拓。

为了使得地消除这一题目,HTTP/1.1提议了Keep-Alive建制:当二个HTTP央浼的多少传输停止后,TCP连接不立刻释放,如果此刻有新的HTTP乞请,且其央浼的Host通上次央浼同样,则可以直接复用为刑释的TCP连接,进而省去了TCP的假释和重复创建的付出,减弱了网络延时:

connection_keepalive2.png

在现世浏览器中,日常同期打开6~8个keepalive connections的socket连接,并维持自然的链路生命,当无需时再关闭;而在服务器中,常常是由软件依照负荷情形(举例FD最大值、Socket内部存款和储蓄器、超时时间、栈内部存储器、栈数量等)决定是或不是继续努力关闭。

2.2. 构造

  1. 连接池内部维护了三个名称为OkHttp ConnectionPoolThreadPool,特意用来淘汰最后一位的socket,当满足以下准绳时,就能够进展倒数一位淘汰,特别像GC

    1. 并发socket空闲连接超过5个
    2. 某个socket的keepalive时间大于5分钟
    
  2. 保安着贰个Deque<Connection>,提供get/put/remove等数据结构的效率

  3. 护卫着四个RouteDatabase,它用来记录连接失利的Route的黑名单,当连接退步的时候就能够把停业的路径加进去(本文不探讨)

1.2 HTTP/2

在HTTP/1.x中,假诺顾客端想发起多少个互相央求必须树立多少个TCP连接,那确实增大了网络开拓。别的HTTP/1.x不会缩减乞求和响应报头,导致了不要求的网络流量;HTTP/1.x不扶持能源优先级导致底层TCP连接利用率低下。而这个标题都以HTTP/2要着力化解的。简单的讲HTTP/2首要消除了以下难题:

  • 报头压缩:HTTP/2使用HPACK压缩格式压缩供给和响应报头数据,减弱不必要流量花费
  • 恳请与响应复用:HTTP/2通过引进新的二进制分帧层完毕了一体化的央求和响应复用,顾客端和服务器能够将HTTP音信分解为互不相信任的帧,然后交错发送,最终再在另一端将其再度建设构造
  • 内定数量流优先级:将 HTTP 信息分解为许多单独的帧之后,大家就足以复用多少个数据流中的帧,客商端和服务器交错发送和传导那一个帧的种种就改成尤为重要的品质决定因素。为了做到这点,HTTP/2 标准允许每种数据流都有一个关乎的权重和信任关系
  • 流动调查节:HTTP/2 提供了一组轻松的创设块,这个创设块允许顾客端和服务器实现其本身的数据流和再三再四级流动调查控

HTTP/2全体质量加强的主干在于新的二进制分帧层,它定义了怎么样封装HTTP新闻并在客商端与服务器之间实行传输:

http2framing.png

还要HTTP/2引进了四个新的定义:

  • 数据流:基于TCP连接之上的逻辑双向字节流,对应二个供给及其响应。客商端每发起一个呼吁就创建二个数据流,后续该央求及其响应的兼具数据都因而该数据流传输
  • 音讯:一个伸手或响应对应的一层层数据帧
  • 帧:HTTP/2的相当的小数据切条单位

上述概念之间的逻辑关系:

  • 持有通讯都在一个 TCP 连接上做到,此一而再能够承继率性数量的双向数据流
  • 各类数据流都有二个独一的标记符和可选的优先级消息,用于承载双向新闻
  • 每条消息都以一条逻辑 HTTP 音信(举个例子诉求或响应),满含二个或七个帧
  • 帧是细微的通讯单位,承载着一定项目标数码,比如 HTTP 标头、新闻负载,等等。 来自差别数据流的帧可以交错发送,然后再依附各类帧头的多寡流标志符重新建立
  • 各样HTTP音讯被解释为三个单身的帧后得以交错发送,进而在宏观上落到实处了七个央求或响应并行传输的作用。那看似于多进度情状下的时日分片机制

http2multiplexing.png

2.3 put/get操作

在连接池中,提供如下的操作,这里能够充作是对deque的贰个简便的卷入

//从连接池中获取
get
//放入连接池
put
//线程变成空闲,并调用清理线程池
connectionBecameIdle
//关闭所有连接
evictAll

乘胜上述操作被更加尖端的靶子调用,Connection中的StreamAllocation被无休止的aquirerelease,也就是List<WeakReference<StreamAllocation>>的大小将随地随时扭转

2. 连接池的选拔与剖析

无论是HTTP/1.1的Keep-Alive编写制定依旧HTTP/2的多路复用机制,在促成上都亟待引进连接池来体贴网络连接。接下来看下OkHttp中的连接池完成。

OkHttp内部通过ConnectionPool来保管连接池,首先来看下ConnectionPool的最首要成员:

public final class ConnectionPool {
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
        ......
    }
  };

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;
  ......

    /**
    *返回符合要求的可重用连接,如果没有返回NULL
   */
  RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    ......
  }

  /*
  * 去除重复连接。主要针对多路复用场景下一个address只需要一个连接
  */
  Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    ......
    }

  /*
  * 将连接加入连接池
  */
  void put(RealConnection connection) {
      ......
  }

  /*
  * 当有连接空闲时唤起cleanup线程清洗连接池
  */
  boolean connectionBecameIdle(RealConnection connection) {
      ......
  }

  /**
   * 扫描连接池,清除空闲连接
  */
  long cleanup(long now) {
    ......
  }

  /*
   * 标记泄露连接
  */
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    ......
  }
}

连锁概念:

  • Call:对Http乞求的卷入
  • Connection/RealConnection:物理连接的包装,其里面有List<WeakReference<StreamAllocation>>的引用计数
  • StreamAllocation: okhttp中引进了StreamAllocation负担管理叁个接连上的流,同一时候在connection中也透过一个StreamAllocation的援引的列表来保管三个连连的流,进而使得连接与流之间解耦。关于StreamAllocation的定义能够看下这篇小说:okhttp源码学习笔记(二)-- 连接与连接管理
  • connections: Deque双端队列,用于维护连接的器皿
  • routeDatabase:用来记录连接失利的Route的黑名单,当连接退步的时候就能够把破产的线路加进去

2.4 Connection自动回收的达成

java内部有垃圾堆回收GC,okhttp有socket的回收;垃圾回收是根据指标的援用树完毕的,而okhttp是依附RealConnection的虚援引StreamAllocation援用计数是或不是为0落成的。大家先看代码

cleanupRunnable:

当客商socket连接成功,向连接池中put新的socket时,回收函数会被主动调用,线程池就能够进行cleanupRunnable,如下

//Socket清理的Runnable,每当put操作时,就会被主动调用
//注意put操作是在网络线程
//而Socket清理是在`OkHttp ConnectionPool`线程池中调用
while (true) {
  //执行清理并返回下场需要清理的时间
  long waitNanos = cleanup(System.nanoTime());
  if (waitNanos == -1) return;
  if (waitNanos > 0) {
    synchronized (ConnectionPool.this) {
      try {
        //在timeout内释放锁与时间片
        ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
      } catch (InterruptedException ignored) {
      }
    }
  }
}

这段死循环实际上是多个打断的清理职分,首先实行清理(clean),并赶回下次急需清理的间距时间,然后调用wait(timeout)进展等待以自由锁与时光片,当等待时间到了后,再一次开展清理,并赶回后一次要理清的间距时间...

Cleanup:

cleanup应用了近乎于GC的标记-清除算法,也便是率先标识出最不活跃的连天(大家得以称之为泄漏连接,或者空闲连接),接着举办破除,流程如下:

long cleanup(long now) {
  int inUseConnectionCount = 0;
  int idleConnectionCount = 0;
  RealConnection longestIdleConnection = null;
  long longestIdleDurationNs = Long.MIN_VALUE;

  //遍历`Deque`中所有的`RealConnection`,标记泄漏的连接
  synchronized (this) {
    for (RealConnection connection : connections) {
      // 查询此连接内部StreamAllocation的引用数量
      if (pruneAndGetAllocationCount(connection, now) > 0) {
        inUseConnectionCount++;
        continue;
      }

      idleConnectionCount++;

      //选择排序法,标记出空闲连接
      long idleDurationNs = now - connection.idleAtNanos;
      if (idleDurationNs > longestIdleDurationNs) {
        longestIdleDurationNs = idleDurationNs;
        longestIdleConnection = connection;
      }
    }

    if (longestIdleDurationNs >= this.keepAliveDurationNs
        || idleConnectionCount > this.maxIdleConnections) {
      //如果(`空闲socket连接超过5个`
      //且`keepalive时间大于5分钟`)
      //就将此泄漏连接从`Deque`中移除
      connections.remove(longestIdleConnection);
    } else if (idleConnectionCount > 0) {
      //返回此连接即将到期的时间,供下次清理
      //这里依据是在上文`connectionBecameIdle`中设定的计时
      return keepAliveDurationNs - longestIdleDurationNs;
    } else if (inUseConnectionCount > 0) {
      //全部都是活跃的连接,5分钟后再次清理
      return keepAliveDurationNs;
    } else {
      //没有任何连接,跳出循环
      cleanupRunning = false;
      return -1;
    }
  }

  //关闭连接,返回`0`,也就是立刻再次清理
  closeQuietly(longestIdleConnection.socket());
  return 0;
}

太长不想看的话,便是之类的流程:

  1. 遍历Deque中负有的RealConnection,标识泄漏的接连
  2. 如若被标识的三番五次满意(空闲socket连接超过5个&&keepalive时间大于5分钟),就将此三番五次从Deque中移除,并关闭连接,重临0,也正是将在履行wait(0),提示立时再度扫描
  3. 如果(目前还可以塞得下5个连接,但是有可能泄漏的连接(即空闲时间即将达到5分钟)),就回去此再三再四将要到期的剩余时间,供下一次清理
  4. 如果(全部都是活跃的连接),就再次回到暗中同意的keep-alive光阴,也正是5分钟后再实行清理
  5. 如果(没有任何连接),就返回-1,跳出清理的死循环

重复注意:这里的“并发”==(“空闲”+“活跃”)==5,实际不是说并发连接就决然是活跃的连年

pruneAndGetAllocationCount:

如何标识并找到最不活跃的延续呢,这里运用了pruneAndGetAllocationCount的方法,它至关心尊崇要依赖弱援用是不是为null而推断这些三番五次是不是泄漏

//类似于引用计数法,如果引用全部为空,返回立刻清理
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
  //虚引用列表
  List<Reference<StreamAllocation>> references = connection.allocations;
  //遍历弱引用列表
  for (int i = 0; i < references.size(); ) {
    Reference<StreamAllocation> reference = references.get(i);
    //如果正在被使用,跳过,接着循环
    //是否置空是在上文`connectionBecameIdle`的`release`控制的
    if (reference.get() != null) {
      //非常明显的引用计数
      i++;
      continue;
    }

    //否则移除引用
    references.remove(i);
    connection.noNewStreams = true;

    //如果所有分配的流均没了,标记为已经距离现在空闲了5分钟
    if (references.isEmpty()) {
      connection.idleAtNanos = now - keepAliveDurationNs;
      return 0;
    }
  }

  return references.size();
}
  1. 遍历RealConnection连日中的StreamAllocationList,它爱戴着多个弱引用列表
  2. 查看此StreamAllocation是不是为空(它是在线程池的put/remove手动调节的),假设为空,表明已经未有代码援引那么些指标了,要求在List中除去
  3. 遍历截至,假使List中维护的StreamAllocation删空了,就返回0,表示那个一而再已经远非代码援用了,是泄漏的连接;不然重临非0的值,表示那些依然被引述,是活跃的连年。

上述完成的过度保守,实际上用filter就足以差相当少达成,伪代码如下

return references.stream().filter(reference -> {
    return !reference.get() == null;
}).count();

2.1 实例化

率先来看下ConnectionPool的实例化进度,贰个OkHttpClient只包蕴贰个ConnectionPool,其实例化进程也在OkHttpClient的实例化进度中落实,值得说的是ConnectionPool各样艺术的调用并从未直接对外暴露,而是经过OkHttpClient的Internal接口统一对外揭露:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    static {
    Internal.instance = new Internal() {
      @Override public void addLenient(Headers.Builder builder, String line) {
        builder.addLenient(line);
      }

      @Override public void addLenient(Headers.Builder builder, String name, String value) {
        builder.addLenient(name, value);
      }

      @Override public void setCache(Builder builder, InternalCache internalCache) {
        builder.setInternalCache(internalCache);
      }

      @Override public boolean connectionBecameIdle(
          ConnectionPool pool, RealConnection connection) {
        return pool.connectionBecameIdle(connection);
      }

      @Override public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
        return pool.get(address, streamAllocation, route);
      }

      @Override public boolean equalsNonHost(Address a, Address b) {
        return a.equalsNonHost(b);
      }

      @Override public Socket deduplicate(
          ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
        return pool.deduplicate(address, streamAllocation);
      }

      @Override public void put(ConnectionPool pool, RealConnection connection) {
        pool.put(connection);
      }

      @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
        return connectionPool.routeDatabase;
      }

      @Override public int code(Response.Builder responseBuilder) {
        return responseBuilder.code;
      }

      @Override
      public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback)       {
        tlsConfiguration.apply(sslSocket, isFallback);
      }

      @Override public HttpUrl getHttpUrlChecked(String url)
          throws MalformedURLException, UnknownHostException {
        return HttpUrl.getChecked(url);
      }

      @Override public StreamAllocation streamAllocation(Call call) {
        return ((RealCall) call).streamAllocation();
      }

      @Override public Call newWebSocketCall(OkHttpClient client, Request originalRequest) {
        return new RealCall(client, originalRequest, true);
      }
    };
     ......
}

这么做的因由是:

Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation
packages. The only implementation of this interface is in {@link OkHttpClient}.

Internal的有一无二兑未来OkHttpClient中,OkHttpClient通过这种措施行强暴光其API给外界类应用。

总结

通过上面的深入分析,我们得以计算,okhttp使用了就疑似于援用计数法与标识擦除法的掺和使用,当连接空闲或许释放时,StreamAllocation的数量会稳步变成0,进而被线程池监测到并回收,那样就能够维持八个平常的keep-alive连接,Okhttp的黑科技(science and technology)就是这么落成的。

谈到底推荐一本《图解HTTP》,新加坡人写的,看起来很科学。

再引入阅读开源Redis客商端Jedis的源码,可以看下它的JedisFactory的实现。

若果你希望越多高素质的篇章,无妨关怀本人要么点赞吧!

2.2 连接池维护

ConnectionPool内部通过三个双端队列(dequeue)来维护当前有所连接,重要涉嫌到的操作包蕴:

  • put:放入新连接
  • get:从连接池中获取连接
  • evictAll:关闭全数连接
  • connectionBecameIdle:连接变空闲后调用清理线程
  • deduplicate:清除重复的多路复用线程

Ref

  1. https://www.nginx.com/blog/http-keepalives-and-web-performance/
2.2.1 StreamAllocation.findConnection

get是ConnectionPool中可是根本的方法,StreamAllocation在其findConnection方法内部通过调用get方法为其找到stream找到合适的连日,若无则新建叁个总是。首先来看下findConnection的逻辑:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                        boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // 一个StreamAllocation刻画的是一个Call的数据流动,一个Call可能存在多次请求(重定向,Authenticate等),所以当发生类似重定向等事件时优先使用原有的连接
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // 试图从连接池中找到可复用的连接
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }

    // 获取路由配置,所谓路由其实就是代理,ip地址等参数的一个组合
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      //拿到路由后可以尝试重新从连接池中获取连接,这里主要针对http2协议下清除域名碎片机制
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      //新建连接
      route = selectedRoute;
      refusedStreamCount = 0;
      result = new RealConnection(connectionPool, selectedRoute);
      //修改result连接stream计数,方便connection标记清理
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // 将新建的连接放入到连接池中
      Internal.instance.put(connectionPool, result);

      // 如果同时存在多个连向同一个地址的多路复用连接,则关闭多余连接,只保留一个
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

其主要性逻辑差不离分成以下几个步骤:

  • 查看当前streamAllocation是或不是有从前早就分配过的连年,有则一直运用
  • 从连接池中探求可复用的三番两次,有则赶回该连接
  • 配置路由,配置后重新从连接池中找找是还是不是有可复用连接,有则直接回到
  • 新建三个连接,并修改其StreamAllocation标志计数,将其放入连接池中
  • 查看连接池是不是有再一次的多路复用连接,有则清除
2.2.2 ConnectionPool.get

接下去再来看get方法的源码:

[ConnectionPool.java]
  RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
  }

其论理比较轻便,遍历当前连接池,假诺有切合条件的总是则修改器标志计数,然后回来。这里的首要逻辑在RealConnection.isEligible方法:

[RealConnection.java]
/**
   * 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, 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.
  }
  • 连日未有达成分享上限
  • 非host域必须完全等同
  • 举个例子那时host域也同样,则相符条件,能够被复用
  • 一经host不等同,在HTTP/2的域名切块场景下同样能够复用,具体细节能够参照:https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
2.2.3 deduplicate

deduplicate方法重假设指向在HTTP/2场景下八个多路复用连接清除的风貌。纵然当前连连是HTTP/2,那么具有指向该站点的乞请都应有依照同三个TCP连接:

[ConnectionPool.java]
  /**
   * Replaces the connection held by {@code streamAllocation} with a shared connection if possible.
   * This recovers when multiple multiplexed connections are created concurrently.
   */
  Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, null)
          && connection.isMultiplexed()
          && connection != streamAllocation.connection()) {
        return streamAllocation.releaseAndAcquire(connection);
      }
    }
    return null;
  }

put和evictAll相比轻巧,在这里边就不写了,大家自行看源码。

2.3 自动回收

连接池中有socket回收,而以此回收是以RealConnection的弱援用List<Reference<StreamAllocation>>是不是为0来为依照的。ConnectionPool有三个单独的线程cleanupRunnable来清理连接池,其触发机会有八个:

  • 当连接池中put新的连天时
  • 当connectionBecameIdle接口被调用时

其代码如下:

while (true) {
  //执行清理并返回下场需要清理的时间
  long waitNanos = cleanup(System.nanoTime());
  if (waitNanos == -1) return;
  if (waitNanos > 0) {
    synchronized (ConnectionPool.this) {
      try {
        //在timeout内释放锁与时间片
        ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
      } catch (InterruptedException ignored) {
      }
    }
  }
}

这段死循环实际上是一个绿灯的清理职务,首先实行清理(clean),并回到下一次内需清理的间距时间,然后调用wait(timeout)张开等待以自由锁与时间片,当等待时间到了后,再度张开清理,并回到后一次要理清的间距时间...

接下去看下cleanup函数:

[ConnectionPool.java]
long cleanup(long now) {
  int inUseConnectionCount = 0;
  int idleConnectionCount = 0;
  RealConnection longestIdleConnection = null;
  long longestIdleDurationNs = Long.MIN_VALUE;

  //遍历`Deque`中所有的`RealConnection`,标记泄漏的连接
  synchronized (this) {
    for (RealConnection connection : connections) {
      // 查询此连接内部StreamAllocation的引用数量
      if (pruneAndGetAllocationCount(connection, now) > 0) {
        inUseConnectionCount++;
        continue;
      }

      idleConnectionCount++;

      //选择排序法,标记出空闲连接
      long idleDurationNs = now - connection.idleAtNanos;
      if (idleDurationNs > longestIdleDurationNs) {
        longestIdleDurationNs = idleDurationNs;
        longestIdleConnection = connection;
      }
    }

    if (longestIdleDurationNs >= this.keepAliveDurationNs
        || idleConnectionCount > this.maxIdleConnections) {
      //如果(`空闲socket连接超过5个`
      //且`keepalive时间大于5分钟`)
      //就将此泄漏连接从`Deque`中移除
      connections.remove(longestIdleConnection);
    } else if (idleConnectionCount > 0) {
      //返回此连接即将到期的时间,供下次清理
      //这里依据是在上文`connectionBecameIdle`中设定的计时
      return keepAliveDurationNs - longestIdleDurationNs;
    } else if (inUseConnectionCount > 0) {
      //全部都是活跃的连接,5分钟后再次清理
      return keepAliveDurationNs;
    } else {
      //没有任何连接,跳出循环
      cleanupRunning = false;
      return -1;
    }
  }

  //关闭连接,返回`0`,也就是立刻再次清理
  closeQuietly(longestIdleConnection.socket());
  return 0;
}

个中央逻辑如下:

  • 遍历连接池中装有连接,标志败露连接
  • 若果被标志的总是满意(空闲socket连接超过5个&&keepalive时间大于5分钟),就将此三番五次从Deque中移除,并关闭连接,再次来到0,也正是将要施行wait(0),提醒马上再一次扫描
  • 如果(目前还可以塞得下5个连接,但是有可能泄漏的连接(即空闲时间即将达到5分钟)),就回来此接二连三将在到期的剩余时间,供下一次清理
  • 如果(全部都是活跃的连接),就回到暗许的keep-alive日子,也正是5分钟后再实行清理

pruneAndGetAllocationCount担当标识并找到不活跃接连:

[ConnnecitonPool.java]
//类似于引用计数法,如果引用全部为空,返回立刻清理
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
  //虚引用列表
  List<Reference<StreamAllocation>> references = connection.allocations;
  //遍历弱引用列表
  for (int i = 0; i < references.size(); ) {
    Reference<StreamAllocation> reference = references.get(i);
    //如果正在被使用,跳过,接着循环
    //是否置空是在上文`connectionBecameIdle`的`release`控制的
    if (reference.get() != null) {
      //非常明显的引用计数
      i++;
      continue;
    }

    //否则移除引用
    references.remove(i);
    connection.noNewStreams = true;

    //如果所有分配的流均没了,标记为已经距离现在空闲了5分钟
    if (references.isEmpty()) {
      connection.idleAtNanos = now - keepAliveDurationNs;
      return 0;
    }
  }

  return references.size();
}

OkHttp的连接池通过计数+标识清理的体制来管理连接池,使得无用连接能够被会回收,并保证四个健康的keep-alive连接。那也是OkHttp的连年池能保持快速的重大原因。

TAG标签:
版权声明:本文由32450新蒲京网站发布于葡萄游戏厅_棋牌游戏,转载请注明出处:OkHttp 3.7源码深入分析(五)——连接池