科技美学OkHttp3源码分析[复用连接池]

OkHttp系列文章如下

  • OkHttp3源码分析[综述]
  • OkHttp3源码分析[复用连接池]
  • OkHttp3源码分析[缓存策略]
  • OkHttp3源码分析[DiskLruCache]
  • OkHttp3源码分析[职责队列]

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服务器对每个请求都从头线程,导致只支持150个冒出连接(数据来源nginx官网),不过这瓶颈随着高并发server软硬件的升华(golang/分布式/IO多路复用)将会越来越少
  3. 恢宏之DDOS产生的僵尸连接可能为用来恶意抨击服务器,耗尽资源

吓了,以上大了,本文主要是形容客户端的,服务端不再介绍。

下文假设服务器是经过专业的运维配置好的,它默认开启了keep-alive,并无主动关闭连接

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的靶子就是未可能再也被运用。它不能够处理循环引用的题目。

2.1. 实例化

于源码中,我们事先物色ConnectionPool实例化的岗位,它是一直new出来的,而她的各种操作却在OkHttpClient的static区实现了Internal.instance接口作为ConnectionPool的包装。

至于何以要如此多是一举的旁包装,主要是为了吃外部包的积极分子访问非public主意,详见此注释

2.2. 构造

  1. 连接池内部维护了一个名为OkHttp ConnectionPoolThreadPool,专门用来淘汰末位的socket,当满足以下标准时,就会进展末位淘汰,非常像GC

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

  3. 护在一个RouteDatabase,它因此来记录连接失败的Route的私自名单,当连接失败的下即便见面将黄的路加进去(本文不讨论)

2.3 put/get操作

于连续池中,提供如下的操作,这里可以作为是指向deque的一个概括的包装

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

随着上述操作为还高级的靶子调用,Connection中的StreamAllocation给连的aquirerelease,也就是List<WeakReference<StreamAllocation>>的大小将无时无刻变化

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();

总结

通过上面的分析,我们得以总结,okhttp使用了看似于引用计数法与标记擦除法的鱼龙混杂使用,当连接空闲或者释放时,StreamAllocation的多少会日趋变成0,从而被线程池监测到连回收,这样虽好保障多单正常的keep-alive连接,Okhttp的野鸡科技就是这么实现之。

最后推荐一遵照《图解HTTP》,日本丁写的,看起颇不利。

再度引进阅读开源Redis客户端Jedis的源码,可以看下其的JedisFactory的实现。

若是您期望再次多高质量之篇章,不妨关心我要点赞吧!

Ref

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