前语

拦截器链是整个OkHttp结构的核心所在,而衔接拦截器ConnectInterceptor则是核心中的核心。衔接拦截器ConnectInterceptor的首要职责是树立与服务器的衔接,了解HTTP的朋友应该清楚,HTTP是依据TCP协议的,TCP衔接的树立和断开需求经过三次握手四次挥手等操作,假如说每次恳求的时分都树立一个TCP衔接,那么同一客户端频频地发起网络恳求将会耗费很多的网络资源,导致功能低下。为了寻求极致的体验,OkHttp在ConnectInterceptor这个节点完结了一套衔接机制对此问题进行了优化,支撑衔接复用,大幅度提高了网络恳求的功率,本文将具体解析ConnectInterceptor的源码,探究衔接机制的完结原理。

PS:本文依据OkHttp3版别4.9.3

ConnectInterceptor源码

object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.call.initExchange(chain)
    val connectedChain = realChain.copy(exchange = exchange)
    return connectedChain.proceed(realChain.request)
  }
}

ConnectInterceptor的源码看上去极端简单,首要便是经过RealCall的initExchange办法拿到一个Exchange目标,然后依据这个exchange去创立一条新的拦截器链履行后面的拦截器。代码虽然只有短短几行,而实践上衔接查找复用等要害操作都是封装在了其他类中,只有搞清楚这儿的exchange是怎样来的才能了解到整个衔接机制的原理,那么initExchange办法也便是后续剖析的进口。

衔接复用

RealCall.initExchange办法

  internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ......//省掉
    val exchangeFinder = this.exchangeFinder!!
    val codec = exchangeFinder.find(client, chain)
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    ......//省掉
    return result
  }

initExchange办法里最首要的逻辑便是由上面这几行代码完结的,这儿涉及到三个重要的类:

  • ExchangeFinder类:衔接复用的要害完结类,上面代码中的ExchangeFinder目标是在重试和重定向拦截器RetryAndFollowUpInterceptor中创立的,至于是怎样创立的不必深究,只需知道ExchangeFinder的首要职责是查找和创立衔接。
  • ExchangeCodec类:编解码器,首要负责对恳求编码和对响应解码。
  • Exchange类:交换器,用于传输单个HTTP恳求的Request和Response。

可以看到initExchange办法回来的Exchange是依据ExchangeFinder和ExchangeCodec来创立的,这儿的要害在于编解码器ExchangeCodec的生成,持续追寻ExchangeFinder.find办法看看里边做了什么。

ExchangeFinder.find办法

  fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      val resultConnection = findHealthyConnection(
          connectTimeout = chain.connectTimeoutMillis,
          readTimeout = chain.readTimeoutMillis,
          writeTimeout = chain.writeTimeoutMillis,
          pingIntervalMillis = client.pingIntervalMillis,
          connectionRetryEnabled = client.retryOnConnectionFailure,
          doExtensiveHealthChecks = chain.request.method != "GET"
      )
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure(e.lastConnectException)
      throw e
    } catch (e: IOException) {
      trackFailure(e)
      throw RouteException(e)
    }
  }

这段代码块首要做了以下两件事:

  • 调用findHealthyConnection办法查找可用的衔接,并回来resultConnection,即一个衔接RealConnection目标;
  • 依据拿到的衔接构建并回来对应的编解码器,HTTP/1.1协议则回来Http1ExchangeCodec,HTTP/2协议则回来Http2ExchangeCodec。

在这儿编解码器是怎样拿到的并不是要点,要害是要搞清楚衔接resultConnection是怎样来的,具体看findHealthyConnection办法。

ExchangeFinder.findHealthyConnection办法

  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )
      if (candidate.isHealthy(doExtensiveHealthChecks)) {
        return candidate
      }
      candidate.noNewExchanges()
      ......//省掉
    }
  }

findHealthyConnection办法里边开启了一个while循环,在循环体内会先获取衔接candidate,并查看这个衔接的合法性,检测Socket是否被封闭等等,合规律跳出循环并回来这个衔接,不合规律会将这个衔接标记为不可用并将其从衔接池里移除,然后便是进入下一轮的循环直到拿到一个合法的衔接停止。同样的,这儿查找衔接的作业也是封装在其他办法里边,那就持续追寻findConnection办法。

ExchangeFinder.findConnection办法

好了现在终于来到了真实完结衔接查找复用的当地,获取衔接的实践调用链路为:

graph LR
ExchangeFinder.find-->ExchangeFinder.findHealthyConnection-->ExchangeFinder.findConnection

下面是findConnection办法的源码:

  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    if (call.isCanceled()) throw IOException("Canceled")
    //第一次
    val callConnection = call.connection
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          toClose = call.releaseConnectionNoEvents()
        }
      }
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }
    //在拿新衔接之前重置一些状态
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0
    //第2次
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      return result
    }
    //第三次
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      ......//省掉获取路由的逻辑
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }
      route = localRouteSelection.next()
    }
    //第四次
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())
    //第五次
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }
    synchronized(newConnection) {
      connectionPool.put(newConnection)
      call.acquireConnectionNoEvents(newConnection)
    }
    eventListener.connectionAcquired(call, newConnection)
    return newConnection
  }

依据上面的代码可以知道,ExchangeFinder为了获取衔接最多会测验五次,直到拿到RealConnection(也便是衔接):

  • 第一次,测验运用已经给当时恳求分配到的衔接。首次发起恳求的话是没有衔接的,这种状况拿到的衔接会是null,需求树立衔接;假如这个恳求在进行重定向就或许复用前次的衔接。假如存在衔接,则先查看这个衔接的noNewExchanges标志位和主机跟端口自创立时起有没有更改正,当noNewExchanges标志位为true(即代表无法在当时衔接上创立新的流)或许主机端口变了,阐明这个衔接不可复用,则开释当时衔接,最终会封闭Socket和履行衔接被开释的回调。要是衔接可用就会直接回来,不会进行后续的查找;
  • 第2次,测验从衔接池里获取衔接。这儿传给衔接池的routes和requireMultiplexed这两个参数均为null,也便是要求不带路由信息和不带多路复用,这时假如衔接池中存在跟当时address匹配的衔接,则直接将其回来;
  • 第三次,拿到恳求的一切路由后再次测验从衔接池里获取衔接。这次由于传入了路由信息(多个route,即多个IP地址),在衔接池里或许会找到由于恳求合并而匹配到的不带多路复用的衔接,假如能找到则直接回来这个衔接;
  • 第四次,前三次都真实没找到,那就新建一个衔接,并进行TCP+TLS握手与服务器树立衔接,注意这个新建的衔接并不会立即回来,需求依据下一次查找的结果来决议要不要用这个衔接;
  • 第五次,也是最终一次查找,再一次测验从衔接池里获取衔接。这次带上路由信息,并要求多路复用,这儿是针对HTTP/2的操作,为了确保HTTP/2多路复用的特性避免创立多个衔接,这儿再次承认衔接池里是否已经存在同样的衔接,假如存在则直接运用已有的衔接,而且开释刚刚新建的衔接。假如仍是没有匹配到,则将新建的衔接放入衔接池以用于下一次复用并回来。

以上便是OkHttp测验衔接复用的作业流程,可以看到整个查找的进程优先是复用已分配的衔接,其次是衔接池中的衔接,真实不可才会新建衔接来运用,这儿表现了亨元模式池中复用的思维,将已有的衔接寄存在衔接池中,用到的时分就在衔接池里拿,没有再去新建,如此一来就可以大幅度省去TCP+TLS握手的进程以提升网络恳求的功率。下面是衔接复用的流程图:

graph TB
start(开端)
firstSearch(查找已分配的衔接)
isUsable{衔接可用?}
firstFromPool(第一次从衔接池获取)
isExist1{存在可用衔接?}
secondFromPool("带上路由信息,第2次从衔接池获取")
isExist2{存在可用衔接?}
newConnection("新建衔接,进行TCP+TLS握手,衔接服务器")
thirdFromPool("带上路由信息,确保HTTP/2的多路复用性,<br/>第三次从衔接池获取")
isExist3{存在可用衔接?}
releaseConnection("开释新建的衔接,<br/>运用已有衔接")
useNewConnection("运用新建的衔接,<br/>并将其放入衔接池")
return(回来衔接)
start-->firstSearch-->isUsable-->|否|firstFromPool
isUsable-->|是|return
firstFromPool-->isExist1-->|否|secondFromPool
isExist1-->|是|return
secondFromPool-->isExist2-->|否|newConnection-->thirdFromPool-->isExist3-->|否|useNewConnection-->return
isExist2-->|是|return
isExist3-->|是|releaseConnection-->return

到这儿其实OkHttp的衔接机制从大体上就已经剖析得八九不离十了,不过估计大家还会有许多问号:衔接到底是怎样树立起来的?在衔接复用的五次测验中,其间有三次是从衔接池里获取的,可见这个衔接池也是个重要人物,那衔接池到底是个什么东东?三次从衔接池里获取衔接又有什么不同?别慌,咱们持续扒下去。

衔接树立

依据上文的剖析,咱们可以知道所谓的衔接其实是一个RealConnection,在进行TCP+TLS握手衔接服务器时调用的是RealConnection的connect办法,下面是connect办法的源码:

  fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) {
    check(protocol == null) { "already connected" }
    var routeException: RouteException? = null
    val connectionSpecs = route.address.connectionSpecs
    val connectionSpecSelector = ConnectionSpecSelector(connectionSpecs)
    ......//一些协议装备相关的反常处理
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
          if (rawSocket == null) {
            break
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
        break
      } catch (e: IOException) {
        ......//反常处理
      }
    }
    if (route.requiresTunnel() && rawSocket == null) {
      throw RouteException(ProtocolException(
          "Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))
    }
    idleAtNs = System.nanoTime()
  }

上面精简了部分代码,在connect办法内部最首要的逻辑在while循环体中,循环履行以下步骤直到树立起衔接:

  • 假如是设置了HTTP署理,则调用connectTunnel办法走署理地道树立衔接;
  • 假如没有设置署理,则调用connectSocket办法走默许逻辑树立Socket衔接;
  • 最终调用establishProtocol办法树立协议,这儿会触发TLS握手。

其间在connectTunnel办法内部会先经过connectSocket办法去树立Socket衔接,然后再创立地道,也便是说无论有没有设置署理,实践上底层都是经过Socket去完结的,都会调用以下的链路去树立Socket衔接:

graph LR
RealConnection.connectSocket-->Platform.connectSocket-->Socket.connect

这块没什么好剖析的,想要具体了解这块的朋友可以自行查看源码,下面咱们进入establishProtocol办法看树立协议的逻辑。

树立协议:RealConnection.establishProtocol

  private fun establishProtocol(
    connectionSpecSelector: ConnectionSpecSelector,
    pingIntervalMillis: Int,
    call: Call,
    eventListener: EventListener
  ) {
    if (route.address.sslSocketFactory == null) {
      if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
        socket = rawSocket
        protocol = Protocol.H2_PRIOR_KNOWLEDGE
        startHttp2(pingIntervalMillis)
        return
      }
      socket = rawSocket
      protocol = Protocol.HTTP_1_1
      return
    }
    eventListener.secureConnectStart(call)
    connectTls(connectionSpecSelector)
    eventListener.secureConnectEnd(call, handshake)
    if (protocol === Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis)
    }
  }

代码很简单,在前面树立好了Socket衔接之后,就会履行establishProtocol办法对各个协议进行支撑,分非HTTPS和HTTPS两种状况:

  • 非HTTPS:优先走HTTP/2协议,假如不支撑HTTP/2则走HTTP/1.1协议;
  • HTTPS:先进行TLS握手,然后判别是不是HTTP/2协议,是则走HTTP/2衔接办法。

TLS握手:RealConnection.connectTls

  private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
    val address = route.address
    val sslSocketFactory = address.sslSocketFactory
    var success = false
    var sslSocket: SSLSocket? = null
    try {
      //构建包装目标
      sslSocket = sslSocketFactory!!.createSocket(
          rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket
      //对SSLSocket进行相关装备
      val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
      if (connectionSpec.supportsTlsExtensions) {
        Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
      }
      //进行TLS握手
      sslSocket.startHandshake()
      val sslSocketSession = sslSocket.session
      val unverifiedHandshake = sslSocketSession.handshake()
      //证书查验
      if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
        val peerCertificates = unverifiedHandshake.peerCertificates
        if (peerCertificates.isNotEmpty()) {
          val cert = peerCertificates[0] as X509Certificate
          throw SSLPeerUnverifiedException("""
              |Hostname ${address.url.host} not verified:
              |    certificate: ${CertificatePinner.pin(cert)}
              |    DN: ${cert.subjectDN.name}
              |    subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)}
              """.trimMargin())
        } else {
          throw SSLPeerUnverifiedException(
              "Hostname ${address.url.host} not verified (no certificates)")
        }
      }
      val certificatePinner = address.certificatePinner!!
      handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,
          unverifiedHandshake.localCertificates) {
        certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,
            address.url.host)
      }
      //查看服务器供给的证书是否包含在固定证书里
      certificatePinner.check(address.url.host) {
        handshake!!.peerCertificates.map { it as X509Certificate }
      }
      //握手成功
      val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
        Platform.get().getSelectedProtocol(sslSocket)
      } else {
        null
      }
      socket = sslSocket
      source = sslSocket.source().buffer()
      sink = sslSocket.sink().buffer()
      protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
      success = true
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket)
      }
      if (!success) {
        sslSocket?.closeQuietly()
      }
    }
  }

进行TLS握手的进程首要干了以下的事:

  • 依据之前树立好的Socket构建一个包装目标SSLSocket;
  • 依据SSLSocket装备Socket密码、TLS版别及扩展信息等等;
  • 进行TLS握手,并查验证书对目标主机是否有用;
  • 查看服务器供给的证书是否包含在固定证书里;
  • 握手成功,记录握手信息和ALPN协议。

到这儿衔接树立剖析结束,衔接树立的底层原理是依据Socket去完结的,整个进程大致如下:

flowchart LR
start(开端)
isRequiresTunnel{是否运用HTTP署理}
connectSocket(直接树立Socket衔接)
subgraph 经过地道衔接
connectSocket2(树立Socket衔接)-->createTunnel(创立地道)
end
isNotHTTPS{"非HTTPS?"}
isSupportHTTP2{"支撑HTTP/2?"}
startHttp1(走HTTP/1.1衔接办法)
startHttp2(走HTTP/2衔接办法)
connectTls(进行TLS握手)
finish(衔接树立完结)
start-->isRequiresTunnel-->|否|connectSocket-->isNotHTTPS
isRequiresTunnel-->|是|经过地道衔接-->isNotHTTPS
isNotHTTPS-->|是|isSupportHTTP2-->|否|startHttp1-->finish
isSupportHTTP2-->|是|startHttp2
isNotHTTPS-->|否|connectTls-->startHttp2-->finish

衔接池:RealConnectionPool

衔接池的含义

OkHttp之所以可以完结衔接复用,衔接池起到很要害的作用。频频地树立和断开TCP衔接(要进行三次握手四次挥手)会耗费很多的网络资源和时刻,而HTTP中的KeepAlive机制使得衔接可以被复用,减少了频频进行网络恳求带来的功能问题。要复用衔接就得完结衔接的维护管理,由此也就引入了衔接池的概念,完结对衔接进行缓存、获取、铲除等操作。

衔接池的初始化

在创立OkHttpClient的时分,会供给一个默许的衔接池:

class Builder constructor() {
    internal var connectionPool: ConnectionPool = ConnectionPool()
    ......
}

咱们先来看看ConnectionPool类的源码:

class ConnectionPool internal constructor(
  internal val delegate: RealConnectionPool
) {
  constructor(
    maxIdleConnections: Int,
    keepAliveDuration: Long,
    timeUnit: TimeUnit
  ) : this(RealConnectionPool(
      taskRunner = TaskRunner.INSTANCE,
      maxIdleConnections = maxIdleConnections,
      keepAliveDuration = keepAliveDuration,
      timeUnit = timeUnit
  ))
  constructor() : this(5, 5, TimeUnit.MINUTES)
  fun idleConnectionCount(): Int = delegate.idleConnectionCount()
  fun connectionCount(): Int = delegate.connectionCount()
  fun evictAll() {
    delegate.evictAll()
  }
}

可以看到,假如用户没有手动装备衔接池的话,OkHttp默许支撑的最大闲暇衔接数是5个,衔接默许保活时长是5分钟。另外不难发现,ConnectionPool类中持有了一个RealConnectionPool目标,一切的作业都是操控RealConnectionPool去干的,ConnectionPool类仅仅一个署理,而RealConnectionPool才是衔接池的真实完结类,所以RealConnectionPool便是咱们下面要剖析的目标。衔接池RealConnectionPool对衔接的管理最首要的无非便是存储、获取和铲除,本文也要点从这三个方面打开剖析。

衔接存储

衔接存储调用的是RealConnectionPool的put办法:

fun put(connection: RealConnection) {
  connection.assertThreadHoldsLock()
  connections.add(connection)
  cleanupQueue.schedule(cleanupTask)
}

代码里的connections是衔接池中维护的一个用来寄存衔接的双端行列,衔接存储的逻辑很简单,将衔接刺进到双端行列connections里,然后调用使命行列履行整理使命。这儿的整理使命cleanupTask是一个Task实例:

private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
  override fun runOnce() = cleanup(System.nanoTime())
}

履行cleanupTask的时分会调用cleanup办法去整理衔接,cleanup办法的含义和具体逻辑会在后文具体剖析。

衔接获取

在衔接复用的流程里会三次从衔接池里获取衔接,调用到的便是RealConnectionPool的callAcquirePooledConnection办法:

  fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    for (connection in connections) {
      synchronized(connection) {
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        if (!connection.isEligible(address, routes)) return@synchronized
        call.acquireConnectionNoEvents(connection)
        return true
      }
    }
    return false
  }

获取衔接的时分会遍历衔接池里一切的衔接去判别是否有匹配的衔接:

  • 假如要求多路复用而当时的衔接不是HTTP/2衔接时,阐明当时衔接不适用,则越过当时衔接,持续遍历;
  • 判别当时衔接是否可以承载指向对应address的数据流,假如不能则越过当时衔接持续遍历,假如能则将这个衔接设置到call里去,为这个衔接所承载的call调集添加一个对当时call的弱引用,最终回来这个衔接。

可以看到,RealConnection.isEligible办法对于衔接能不能匹配成功起了很重要的作用,那就点进去看看是怎样匹配的:

  //RealConnection.isEligible办法
  internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    assertThreadHoldsLock()
    if (calls.size >= allocationLimit || noNewExchanges) return false
    if (!this.route.address.equalsNonHost(address)) return false
    if (address.url.host == this.route().address.url.host) {
      return true
    }
    //流程走到这儿阐明主机名不匹配,不过在后续判定中有或许由于衔接合并而匹配到
    if (http2Connection == null) return false
    if (routes == null || !routeMatchesAny(routes)) return false
    if (address.hostnameVerifier !== OkHostnameVerifier) return false
    if (!supportsUrl(address.url)) return false
    try {
      address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
    } catch (_: SSLPeerUnverifiedException) {
      return false
    }
    return true
  }

匹配逻辑如下:

  • 假如衔接已到达可以承载的最大并发流数或许不允许在此衔接上创立新的流,则匹配失利;
  • 假如address中非host部分的字段匹配不上,则匹配失利;
  • 假如address的host完全匹配,则匹配成功,直接回来true;
  • 前面的判定都没有回来结果的话,阐明主机名不匹配,不过在后续判定中有或许由于衔接合并而匹配到,而后续判定是针对HTTP/2的,假如不是HTTP/2则直接回来false,匹配失利;
  • 假如没有路由信息或许IP地址不匹配,则匹配失利;
  • 假如证书不匹配,则匹配失利;
  • 假如url不合法,则匹配失利;
  • 进行证书pinning匹配,能匹配上就回来true。

衔接铲除

衔接池铲除衔接的办法有两种,一种是守时铲除,经过履行cleanup办法来完结;另一种是用户手动铲除,经过履行evictAll办法来完结,下面分别来剖析这两种办法的具体流程。

守时铲除:cleanup

  fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE
    for (connection in connections) {
      synchronized(connection) {
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++
        } else {
          idleConnectionCount++
          val idleDurationNs = now - connection.idleAtNs
          if (idleDurationNs > longestIdleDurationNs) {
            longestIdleDurationNs = idleDurationNs
            longestIdleConnection = connection
          } else {
            Unit
          }
        }
      }
    }
    when {
      longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections -> {
        val connection = longestIdleConnection!!
        synchronized(connection) {
          if (connection.calls.isNotEmpty()) return 0L
          if (connection.idleAtNs + longestIdleDurationNs != now) return 0L
          connection.noNewExchanges = true
          connections.remove(longestIdleConnection)
        }
        connection.socket().closeQuietly()
        if (connections.isEmpty()) cleanupQueue.cancelAll()
        return 0L
      }
      idleConnectionCount > 0 -> {
        return keepAliveDurationNs - longestIdleDurationNs
      }
      inUseConnectionCount > 0 -> {
        return keepAliveDurationNs
      }
      else -> {
        return -1
      }
    }
  }

处理的思路很清晰:

  • 遍历衔接池中的一切衔接,假如当时衔接正在被运用,则正在运用的衔接计数+1,不然的话闲暇衔接计数+1,一起在遍历的进程中找出搁置时刻最长的衔接及其搁置时长;
  • 依据不同状况测验整理闲暇衔接:
    1. 当衔接的最长闲暇时长不小于设置的保活时长(默许是5分钟)或许闲暇衔接数大于设置支撑的最大闲暇衔接数(默许是5个),则从搁置最久的衔接下手,先检测这个衔接有没有承载恳求,假如有的话就阐明这个衔接行将会被用到,这时直接回来0,不会铲除;然后检测这个衔接的idleAtNs时刻戳与最长搁置时长的和是否等于当时程序履行的时长,个人理解这是为了双重确保要被处理的衔接是搁置最久的衔接,要是还有其他衔接比当时衔接搁置更久,则直接回来0,不会铲除;最终要是这个衔接可以铲除,则将其标记为不接受新的数据流,并从衔接行列里边移除它,封闭Socket并回来0。
    2. 当没有超出上面所述的限制,而且闲暇衔接数大于0,则回来最长闲暇时刻距离设置的保活时长的时刻差,也便是履行下一次铲除要等候的时刻;
    3. 走到这儿阐明现在没有闲暇的衔接,当正在运用的衔接数大于0,则直接回来设置的保活时长,过了这段时刻之后再来测验整理;
    4. 其他状况,也便是衔接池里没有衔接,则直接回来-1,不需求整理。

其间,cleanup办法回来的Long型值代表下一次履行守时铲除使命的周期,等时刻到了就会主动履行cleanup从头测验铲除闲暇衔接,假如回来的是-1就阐明不需求从头安排使命了。追寻cleanup办法的调用途,可以发现在往衔接池存入新衔接和外部告诉衔接池要开释池中某个衔接时会履行cleanup办法,这两种状况都会触发守时铲除使命移除池中长时刻用不到的衔接,以到达衔接池主动维护衔接的意图。

flowchart TB
start(加入新衔接/开释某衔接)
realConnectionPool(衔接池)
cleanupTask(启动守时整理使命)
longestIdle(找出搁置最久的衔接及其搁置时长)
cleanup(测验铲除闲暇最久的衔接)
continue{"回来-1?"}
nextCycle(等候下一个整理周期)
finish(守时使命结束)
start-->realConnectionPool-->cleanupTask-->longestIdle-->cleanup-->continue-->|否|finish
continue-->|是|nextCycle-->cleanupTask

手动铲除:evictAll

  fun evictAll() {
    val i = connections.iterator()
    while (i.hasNext()) {
      val connection = i.next()
      val socketToClose = synchronized(connection) {
        if (connection.calls.isEmpty()) {
          i.remove()
          connection.noNewExchanges = true
          return@synchronized connection.socket()
        } else {
          return@synchronized null
        }
      }
      socketToClose?.closeQuietly()
    }
    if (connections.isEmpty()) cleanupQueue.cancelAll()
  }

这段代码块的逻辑很简单,首要便是遍历衔接池中的衔接,找到闲暇衔接并移除。evictAll办法是供给给OkHttpClient去调用的,运用户可以手动铲除衔接池中的一切闲暇衔接。

作业流程

到这儿本文对OkHttp的衔接机制也剖析完了,也是自己OkHttp结构源码解析系列的收官之作,最终来看个衔接机制的大致流程吧:

flowchart TB
request(Request)
ConnectInterceptor(ConnectInterceptor)
subgraph 衔接复用
firstSearch(查找已分配的衔接)
isUsable{衔接可用?}
firstFromPool(第一次从衔接池获取)
isExist1{存在可用衔接?}
secondFromPool("带上路由信息,第2次从衔接池获取")
isExist2{存在可用衔接?}
newConnection("新建衔接,进行TCP+TLS握手,衔接服务器")
thirdFromPool("带上路由信息,确保HTTP/2的多路复用性,<br/>第三次从衔接池获取")
isExist3{存在可用衔接?}
releaseConnection("开释新建的衔接,<br/>运用已有衔接")
useNewConnection("运用新建的衔接,<br/>并将其放入衔接池")
return(回来衔接)
RealConnection
end
subgraph 衔接池
callAcquirePooledConnection("callAcquirePooledConnection,获取衔接")
put("put,加入衔接")-->
cleanupTask("cleanup,启动守时整理使命")
end
subgraph RealConnection
connectSocket(树立Socket衔接)
establishProtocol(树立协议)
connectTls(TLS握手)
end
request-->ConnectInterceptor-->firstSearch-->isUsable-->|否|firstFromPool
isUsable-->|是|return
firstFromPool-->isExist1-->|否|secondFromPool
isExist1-->|是|return
secondFromPool-->isExist2-->|否|RealConnection-->newConnection
-->thirdFromPool-->isExist3-->|否|useNewConnection-->return
isExist2-->|是|return
isExist3-->|是|releaseConnection-->return
衔接复用-->|加入/开释衔接|衔接池
衔接池-->|获取|衔接复用