“图”解OKHttp系列文章三、ConnectInterceptor

1,294 阅读6分钟

话不多说,直接上代码:

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)
  }
}

乍一看这个拦截器竟然如此的短,然而crrl点击一看,里面那是大有乾坤啊。最终要的其实就一句: val exchange = realChain.call.initExchange(chain) 这个exchange是个什么东西我们暂时不看,点进去看看这句话干了些什么:

internal fun initExchange(chain: RealInterceptorChain): Exchange {
  synchronized(this) {
    check(expectMoreExchanges) { "released" }
    check(!responseBodyOpen)
    check(!requestBodyOpen)
  }

  val exchangeFinder = this.exchangeFinder!!
  val codec = exchangeFinder.find(client, chain)
  val result = Exchange(this, eventListener, exchangeFinder, codec)
  this.interceptorScopedExchange = result
  this.exchange = result
  synchronized(this) {
    this.requestBodyOpen = true
    this.responseBodyOpen = true
  }

  if (canceled) throw IOException("Canceled")
  return result
}

重点在于

val exchangeFinder = this.exchangeFinder!!
val codec = exchangeFinder.find(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)

不难看出构造Exchange最重要就是这个codec,ExchangeCodec接口的源码我这里就不放了,稍微瞅一眼就知道这个类是与编码和解码HTTP响应HTTP请求相关的。我们这里仍然聚焦于exchangeFinder.find(client, chain),codec的寻找过程,这句code正是打开潘多拉魔盒的关键性钥匙。话不多说上代码:

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,这里面那是大有乾坤,老规矩,精华部分那必须得上图:

未命名文件.jpg 注释1: 这个寻找和建立连接的过程为首先根据当前请求通过RouteSelector获得routeSelection,获取的过程图中也有画出流程,拿到routeSelection和routes之后去连接池中寻找可以复用的链接,这里我们记做routes1。如果找到了则返回做Healthy检查,如果没有的话则使用routes1【0】新建一个连接返回做Healthy检查,如果检查没有通过则继续执行find过程,这时将不会再从连接池中进行查找,而是直接通过下一个route,routes1【1】新建连接返回做Healthy检查.如果一直到routes1遍历完成都没有找到Healthy的连接,怎继续通过RouteSelector去找下一个routeSelection得到routes2重复刚刚的动作。 值得注意的是在新建连接之后并不是直接返回的,而是又一次通过 callAcquirePooledConnection去连接池中查找,这是为了防止我们在新建连接的过程中如果有其他请求建立了连接,而这个连接我们是可以复用的,则优先使用可复用的连接,这时将我们遍历到的routesX【X】保存到nextRouteToTry中,目的是如果查找到的这个可复用连接如果不是Healthy的,下面可以继续从这个route开始尝试建立新连接。

注释2: callAcquirePooledConnection这个方法通过参数合返回值不难知道就是拿着adress和routes去连接池里面找连接,这里先直接放上源码:

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
}
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
  assertThreadHoldsLock()

  // If this connection is not accepting new exchanges, we're done.
  if (calls.size >= allocationLimit || noNewExchanges) return false

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

  // If the host exactly matches, we're done: this connection can carry the address.
  if (address.url.host == 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.
  if (routes == null || !routeMatchesAny(routes)) return false

  // 3. This connection's server certificate's must cover the new host.
  if (address.hostnameVerifier !== OkHostnameVerifier) 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) {
    return false
  }

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

这里注释的很清楚,流程上就不多说了,这里说一个HTTP2上的新特性,合并连接。 我们知道因为HTTP1上存在队头阻塞的问题,为了解决这个问题HTTP1使用了“并发连接”(concurrent connections),也就是同时对一个域名发起多个长连接,用数量来解决质量的问题。

1.jpg

但是这个连接的数量也是有限制的,一般是6-8个,那怎么办呢。我如果使用多个域名不就可以搞出更多个连接了嘛

2.jpg 但是吧这么搞效率始终是不高,由于队头阻塞的原因大量的资源还是被浪费了,这个时候HTTP2就出现了,HTTP2里面有个叫多路复用的大杀器,可以解决这个队头阻塞的问题,具体的解决方法简单来说就是用“流”的方式使得请求1-请求2-响应1-响应2这种非阻塞式的请求成为可能。既然是这样那么对于之前为了解决队头阻塞的问题迫不得已建立的多条访问同一服务器的连接现在就应该合并成一条,那么浏览器在操作的时候具体是怎么弄的呢,这里举个例子: 现在有某个网站: youtiao.com,在DNS中有两个名称: A.youtiao.com B.youtiao.com 解析出的ip地址分别如下: Host A: 192.168.0.1 and 192.168.0.2 Host B: 192.168.0.2 and 192.168.0.3 现在浏览器先访问Host A,获得了Host A的两个ip地址,接着如果浏览器要访问Host B,这个时候浏览器发现虽然Host B和HostA的ip地址不完全相同,但是有重叠的部分,这个时候浏览器就会复用Host A的链接去访问Host B. 说了这么多其实就是想说一下这句Code:

// 2. The routes must share an IP address.
if (routes == null || !routeMatchesAny(routes)) return false

通过routes生成过程的解析,一个route就对应着一个ip地址,所以只要这个连接只要能匹配任意一个route对用的ip地址,我们就可以去复用这连接(HTTP2请求的情况下)。

最后在提一下连接池的回收机制,这个回收的入口为:

internal fun releaseConnectionNoEvents(): Socket? {
  val connection = this.connection!!
  connection.assertThreadHoldsLock()

  val calls = connection.calls
  val index = calls.indexOfFirst { it.get() == this@RealCall }
  check(index != -1)

  calls.removeAt(index)
  this.connection = null

  if (calls.isEmpty()) {
    connection.idleAtNs = System.nanoTime()
    if (connectionPool.connectionBecameIdle(connection)) {
      return connection.socket()
    }
  }

  return null
}
fun connectionBecameIdle(connection: RealConnection): Boolean {
  connection.assertThreadHoldsLock()

  return if (connection.noNewExchanges || maxIdleConnections == 0) {
    connection.noNewExchanges = true
    connections.remove(connection)
    if (connections.isEmpty()) cleanupQueue.cancelAll()
    true
  } else {
    cleanupQueue.schedule(cleanupTask)
    false
  }
}

当我们释放连接的时候还会创建连接池的回收任务:

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

然后按照回收策略进行连接池的回收工作。

至此我们就获取到了可用的连接,最后通过:

return resultConnection.newCodec(client, chain)
internal fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {
  val socket = this.socket!!
  val source = this.source!!
  val sink = this.sink!!
  val http2Connection = this.http2Connection

  return if (http2Connection != null) {
    Http2ExchangeCodec(client, this, chain, http2Connection)
  } else {
    socket.soTimeout = chain.readTimeoutMillis()
    source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)
    sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)
    Http1ExchangeCodec(client, this, source, sink)
  }
}

就完成了ExchangeCodec的初始化工作,拿到了这个ExchangeCodec之后后门我们就可以愉快的与网络交互了,也就是下一节,也是系列文章最后一节CallServerInterceptor的内容了。

最后在提一下,

newConnection.connect(
    connectTimeout,
    readTimeout,
    writeTimeout,
    pingIntervalMillis,
    connectionRetryEnabled,
    call,
    eventListener
)

连接建立的过程实际上内容也是非常多的,且涉及到相当多网络协议相关的内容,包括Tunnel,socket,Tls等,这里就不展开分析了。学无止境啊OVER。