OkHttp源码之深度解析(六)——ConnectInterceptor详解:连接机制

937 阅读15分钟

前言

OkHttp源码系列文章:

拦截器链是整个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

    //第二次
    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和执行连接被释放的回调。要是连接可用就会直接返回,不会进行后续的查找;
  • 第二次,尝试从连接池里获取连接。这里传给连接池的routes和requireMultiplexed这两个参数均为null,也就是要求不带路由信息和不带多路复用,这时如果连接池中存在跟当前address匹配的连接,则直接将其返回;
  • 第三次,拿到请求的所有路由后再次尝试从连接池里获取连接。这次由于传入了路由信息(多个route,即多个IP地址),在连接池里可能会找到因为请求合并而匹配到的不带多路复用的连接,如果能找到则直接返回这个连接;
  • 第四次,前三次都实在没找到,那就新建一个连接,并进行TCP+TLS握手与服务器建立连接,注意这个新建的连接并不会立即返回,需要根据下一次查找的结果来决定要不要用这个连接;
  • 第五次,也是最后一次查找,再一次尝试从连接池里获取连接。这次带上路由信息,并要求多路复用,这里是针对HTTP/2的操作,为了确保HTTP/2多路复用的特性防止创建多个连接,这里再次确认连接池里是否已经存在同样的连接,如果存在则直接使用已有的连接,并且释放刚刚新建的连接。如果还是没有匹配到,则将新建的连接放入连接池以用于下一次复用并返回。

以上就是OkHttp尝试连接复用的工作流程,可以看到整个查找的过程优先是复用已分配的连接,其次是连接池中的连接,实在不行才会新建连接来使用,这里体现了亨元模式池中复用的思想,将已有的连接存放在连接池中,用到的时候就在连接池里拿,没有再去新建,如此一来就可以大幅度省去TCP+TLS握手的过程以提升网络请求的效率。下面是连接复用的流程图:

graph TB
start(开始)
firstSearch(查找已分配的连接)
isUsable{连接可用?}
firstFromPool(第一次从连接池获取)
isExist1{存在可用连接?}
secondFromPool("带上路由信息,第二次从连接池获取")
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("带上路由信息,第二次从连接池获取")
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

连接复用-->|加入/释放连接|连接池
连接池-->|获取|连接复用