探索OkHttp系列 (五) 连接建立与复用

前言

上一篇文章我们介绍了CacheInterceptor拦截器,这篇文章我们要介绍的拦截器是ConnectInterceptor,该拦截器的作用是获得一个健康可用的与目标服务器的连接,然后就将请求交给下一个拦截器处理。

该拦截器的内部实现非常的复杂,涉及到OkHttp许多的机制,例如路由选择机制、连接的建立与复用机制,我们在下面的分析中,先对其大体流程进行分析,然后再一个一个点地深入分析,逐个突破。

ConnectInterceptor::intercept

ConnectInterceptor::intercept

/**
 * Opens a connection to the target server and proceeds to the next interceptor. The network might
 * be used for the returned response, or to validate a cached response with a conditional GET.
 */
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)
  }
}

从该方法的注释可以看出,该拦截器的主要作用是:打开一个到目标服务器的连接,然后将请求交给下一个拦截器处理。这个连接其实就是TCP连接,用于Http的请求和响应

获取连接的大体流程

intercept方法的关键代码是RealCall::initExchange,该方法会返回一个Exchange对象,Exchange是什么呢?我们后面会提到。

RealCall::initExchange

  /** Finds a new or pooled connection to carry a forthcoming request and response. */
  internal fun initExchange(chain: RealInterceptorChain): Exchange {
    synchronized(this) {
      check(expectMoreExchanges) { "released" }
      check(!responseBodyOpen)
      check(!requestBodyOpen)
    }

    val exchangeFinder = this.exchangeFinder!!
    // 创建ExchangeCodec对象  
    val codec = exchangeFinder.find(client, chain)
    // 利用ExchangeCodec实例,创建了一个Exchange对象  
    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")
    // 返回创建的Exchange对象
    return result
  }

从该方法的注释可以看出:该方法用于找到一个新的、或者连接池里一个健康可用的连接,来承载一个即将到来的请求和响应。上面出现了ExchangeCodecExchange两个类,分别查看它们的注释。

ExchangeCodec

ExchangeCodec

/** Encodes HTTP requests and decodes HTTP responses. */
interface ExchangeCodec {
	...
}

它是一个接口,负责对Http请求进行编码和对Http响应进行解码,它有两个实现类,分别是Http1ExchangeCodecHttp2ExchangeCodec。以Http1ExchangeCodec为例,查看其中的writeRequestHeaders方法

  override fun writeRequestHeaders(request: Request) {
    val requestLine = RequestLine.get(request, connection.route().proxy.type())
    writeRequest(request.headers, requestLine)
  }
  ...	
  fun writeRequest(headers: Headers, requestLine: String) {
    check(state == STATE_IDLE) { "state: $state" }
    sink.writeUtf8(requestLine).writeUtf8("\r\n")
    for (i in 0 until headers.size) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n")
    }
    sink.writeUtf8("\r\n")
    state = STATE_OPEN_REQUEST_BODY
  }  	

这就是其中的一个编码操作,负责将Request转化为报文的格式,以便发送给服务端。另外,ExchangeCodec包含了一个RealConnection对象。

Exchange

Exchange

/**
 * Transmits a single HTTP request and a response pair. This layers connection management and events
 * on [ExchangeCodec], which handles the actual I/O.
 */
class Exchange(...){
    ...
}

该类传输单个的Http请求和响应对,负责连接管理的工作。Exchange类的内部包含了一个ExchangeCodec对象,我们查看Exchange类内部的writeRequestHeaders方法

  fun writeRequestHeaders(request: Request) {
    try {
      eventListener.requestHeadersStart(call)
      // 调用了ExchangeCodec对象的方法  
      codec.writeRequestHeaders(request)
      eventListener.requestHeadersEnd(call, request)
    } catch (e: IOException) {
      eventListener.requestFailed(call, e)
      trackFailure(e)
      throw e
    }
  }

可以看到,它内部的I/O操作,实际上是调用了ExchangeCodec对象的方法来实现的,我们可以认为Exchange是封装ExchangeCodec的一个工具类,Exchange负责连接管理,而ExchangeCodec负责处理实际的I/O

ExchangeFinder::find

RealCall::initExchange调用了ExchangeFinder::find,获取一个ExchangeCodec对象,我们查看该方法

  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"
      )
      // 利用获取的连接,创建了一个ExchangeCodec对象  
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure(e.lastConnectException)
      throw e
    } catch (e: IOException) {
      trackFailure(e)
      throw RouteException(e)
    }
  }

上面的方法中,获取了一个健康可用的连接,并且利用该连接,创建了一个编码解码的ExchangeCodec对象,有了与服务器的连接处理I/O的ExchangeCodec对象,我们其实就可以和服务器进行通信了。

ExchangeFinder::findHealthyConnection

上面调用了ExchangeFinder::findHealthyConnection方法,获取了一个健康可用的连接,该方法如下

  /**
   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
   * until a healthy connection is found.
   */
  @Throws(IOException::class)
  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      // 获取一个可用的连接,RealConnection对象代表一个连接  
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )

      // Confirm that the connection is good.
      // 检查连接是否健康,该连接是否已准备好去承载新的流  
      if (candidate.isHealthy(doExtensiveHealthChecks)) {
        return candidate
      }

      // If it isn't, take it out of the pool.
      // 如果该连接不健康,就给该连接做一个标记,不再使用该连接  
      candidate.noNewExchanges()

      // Make sure we have some routes left to try. One example where we may exhaust all the routes
      // would happen if we made a new connection and it immediately is detected as unhealthy.
      // 确保我们还有可尝试的路由  
      if (nextRouteToTry != null) continue

      val routesLeft = routeSelection?.hasNext() ?: true
      if (routesLeft) continue

      val routesSelectionLeft = routeSelector?.hasNext() ?: true
      if (routesSelectionLeft) continue

      // 已耗尽所有的路由  
      throw IOException("exhausted all routes")
    }
  }

从该方法的注释可以看出:该方法会查找到一个可用且健康的连接并将其返回,如果找到的可用连接是不健康的,那么会一直重复查找可用连接的这个过程,直到一个可用且健康的连接被找到

该方法内部有一个while(true)的循环,在循环代码里面,会不断地去获取一个可用连接,并检查该连接是否健康,如果该连接健康,就将其返回,如果连接不健康,在有可尝试的路由的前提下,会重复前面查找可用连接的过程。注意,这里说的可用和健康是两个不同的指标

判断连接是否健康使用了RealConnection::isHealthy方法,如下

  // 如果该连接准备好去承载新的流,就返回true
  fun isHealthy(doExtensiveChecks: Boolean): Boolean {
    assertThreadDoesntHoldLock()

    val nowNs = System.nanoTime()

    val rawSocket = this.rawSocket!!
    val socket = this.socket!!
    val source = this.source!!
    // 判断socket是否可用  
    if (rawSocket.isClosed || socket.isClosed || socket.isInputShutdown ||
            socket.isOutputShutdown) {
      return false
    }

    // 如果是HTTP/2连接,检测该HTTP/2连接是否是健康的  
    val http2Connection = this.http2Connection
    if (http2Connection != null) {
      return http2Connection.isHealthy(nowNs)
    }
	  
    val idleDurationNs = synchronized(this) { nowNs - idleAtNs }
    // 若空闲时间达到某个值,则检测socket是否是健康的
    if (idleDurationNs >= IDLE_CONNECTION_HEALTHY_NS && doExtensiveChecks) {
      return socket.isHealthy(source)
    }

    return true
  }

ExchangeFinder::findConnection

上面调用了findConnection方法,该方法用于获取一个可用连接。该方法的实现逻辑较为复杂,我们先介绍它的大体执行流程,后面再对它的各个点进行详细地分析

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   *
   * This checks for cancellation before each blocking operation.
   */
  // 返回一个连接去承载新的流,优先使用现有连接,接着是连接池中的连接,最后是创建一个新的连接
  @Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    // 检查取消事件  
    if (call.isCanceled()) throw IOException("Canceled")

    // 1.尝试去重用call的连接  
    val callConnection = call.connection 
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        // 检查这个连接是否可用和可复用  
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          // 连接不可用,在call中移除该连接,并返回该连接对应的Socket,随后要关闭它  
          toClose = call.releaseConnectionNoEvents()
        }
      }

      // If the call's connection wasn't released, reuse it. We don't call connectionAcquired() here
      // because we already acquired it.
      // 如果连接可以使用,那么就返回该连接  
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      // The call's connection was released.
      // 关闭Socket  
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }

    // We need a new connection. Give it fresh stats.
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0

    // 2.尝试从连接池中获取连接(第一次)
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      // 返回连接
      return result
    }

    // Nothing in the pool. Figure out what route we'll try next.
    // 连接池里没有东西,计算下一条要尝试的路由  
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      // Use a route from a preceding coalesced connection.
      routes = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      // 从现有的routeSelection中获取一个路由  
      routes = null
      route = routeSelection!!.next()
    } else {
      // 计算一个新的routeSelector,这是一个阻塞操作  
      var localRouteSelector = routeSelector
      // 如果routeSelector为null,那么就先创建一个RouteSelector  
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      // 从routeSelector中获取一个新的routeSelection  
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      // 获取routeSelection中的路由列表  
      routes = localRouteSelection.routes

      if (call.isCanceled()) throw IOException("Canceled")

      // Now that we have a set of IP addresses, make another attempt at getting a connection from
      // the pool. We have a better chance of matching thanks to connection coalescing.
      // 3.现在我们有了一组IP地址,再次尝试从连接池中获取连接,由于连接合并,这次我们有更大的希望
      // 从连接池里获取一个连接(第二次)  
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        // 返回连接
        return result
      }

      // 从routeSelection中获取一个路由,用于新连接的创建  
      route = localRouteSelection.next()
    }

    // Connect. Tell the call about the connecting call so async cancels work.
    // 4.创建新连接  
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      // 执行TCP+TLS握手(Https请求才会做TLS握手),这是一个阻塞的操作    
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())

    // If we raced another call connecting to this host, coalesce the connections. This makes for 3
    // different lookups in the connection pool!
    // 5.如果有另一个调用也是连接到相同的主机,并且该调用已经创建了新连接,将连接放到了连接池里,
    // 那么就使用连接池里的连接(第三次)  
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      // 保存路由  
      nextRouteToTry = route
      // 将前面新创建的连接的Socket关闭  
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      // 返回连接池中的连接
      return result
    }

    /* 第三次在连接池中获取连接,依然没找到,意味着要使用新创建的连接 */  
        
    synchronized(newConnection) {
      // 6.将先创建的连接放进连接池里面
      connectionPool.put(newConnection)
      call.acquireConnectionNoEvents(newConnection)
    }

    eventListener.connectionAcquired(call, newConnection)
    // 返回新创建的连接  
    return newConnection
  }

获取一个可用的连接,分为了5步

  1. 重用call当中的连接
  2. 第一次尝试从连接池获取连接
  3. 第二次尝试从连接池获取连接
  4. 自己新创建一个连接
  5. 第三次尝试从连接池获取连接

重用call的连接的逻辑:在获取了call的连接之后,对该连接做了两个判断,分别是

  • 判断是否不再接受新的连接
  • 判断和当前请求是否有相同的主机名和端口号

关于第二点,既然要复用连接,那么就需要「该请求需要连接到的地方」和「已有连接指向的地方」是同一个地方,同一个地方怎么判断?通过主机名和端口号判断。

如果call中的连接无法复用,就会调用callreleaseConnectionNoEvents方法,释放该连接,如下

  # RealCall
  internal fun releaseConnectionNoEvents(): Socket? {
    val connection = this.connection!!
	...
    // 将RealCall的connection属性置空  
    this.connection = null
	...
  }

还有一个问题,为什么有可能在call中就已经存在了一个连接呢,我们才刚开始寻找连接呢?还记得我们前面提到的RetryAndFollowUpInterceptor拦截器吗?当请求失败需要重试或者重定向的时候,这时候连接还在呢,是可以直接进行复用的

在上面获取连接的时候,还存在的问题是

  • 为什么要三次从连接池当中获取连接?它们之间有什么区别?
  • 出现了routerouteSelectionrouteSelector的概念,它们分别是什么?

要解答上面的问题,我们需要一些前置知识:OkHttp中的代理与路由、Http2的合并连接机制。

代理与路由

代理即代理服务器(Proxy Server),代理服务器是介于客户端和服务器之间的一台服务器,客户端发送给服务器的请求都由代理服务器进行转发,如果没有代理,则客户端直接与服务器进行交互。通过代理服务器,客户端可以隐藏身份,防止受到外来攻击。

OkHttp中出现两种代理类型:

  • HTTP代理:能够代理客户端进行HTTP访问,主要是代理浏览器访问网页,它的端口号一般为808080
  • SOCKS代理:SOCKS代理与其他类型的代理不同,它只是简单地传递数据包,并不关心是何种应用协议,因此SOCKS代理服务器比其他类型的代理服务器速度要快得多。

其中SOCKS4只支持TCP协议,而SOCKS5既支持TCP协议又支持UDP协议。

OkHttp中,对于SOCKS代理,代理服务器完成TCP数据包的转发工作,而HTTP代理,除了转发数据之外,还会解析HTTP的请求及响应,并根据请求及响应的内容做一些处理。

代理

Proxy

Java中,通过Java.net.Proxy类来描述一个代理服务器:

public class Proxy {
    public enum Type {
        // 不使用代理    
        DIRECT,
        // HTTP代理    
        HTTP,
        // SOCKS代理    
        SOCKS
    };
	
    // 代理类型
    private Type type;
    // Socket地址
    private SocketAddress sa;
    ...
}

该类主要包含了代理类型以及代理服务器对应的SocketAddress,其中代理类型有三种:

  • DIRECT:不使用代理
  • HTTP:使用HTTP代理
  • SOCKS:使用SOCKS代理

ProxySelector

ProxySelector可以根据用户传入的URI,返回该URI对应的代理服务器列表,即List<Proxy>

public abstract class ProxySelector {
    private static ProxySelector theProxySelector;

    static {
        try {
            Class<?> c = Class.forName("sun.net.spi.DefaultProxySelector");
            if (c != null && ProxySelector.class.isAssignableFrom(c)) {
                // 默认的代理选择器
                theProxySelector = (ProxySelector) c.newInstance();
            }
        } catch (Exception e) {
            theProxySelector = null;
        }
    }

    // getDefault方法用于获取当前已注册的代理选择器,若用户没有注册代理选择器,那么该方法返回一个
    // 一个系统提供的代理选择器
    public static ProxySelector getDefault() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);
        }
        return theProxySelector;
    }

    // setDefault方法用于注册一个代理选择器
    public static void setDefault(ProxySelector ps) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
        }
        theProxySelector = ps;
    }

    // 子类实现该方法,外界通过调用select方法,可以获取该URI所有适用的代理
    public abstract List<Proxy> select(URI uri);

    // 子类实现该方法,外界调用该方法,通知代理服务器不可用
    public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe);
}

ProxySelector是一个抽象类,在OkHttp中只有一个实现类NullProxySelector

object NullProxySelector : ProxySelector() {
  override fun select(uri: URI?): List<Proxy> {
    requireNotNull(uri) { "uri must not be null" }
    return listOf(Proxy.NO_PROXY)
  }

  override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
  }
}

无论外界传入什么URI,该代理选择器都会返回一个包含Proxy.NO_PROXY的列表,Proxy.NO_PROXYProxy内部已经预定义好的,如下

public class Proxy {
    ...
    public final static Proxy NO_PROXY = new Proxy();
    
    private Proxy() {
        type = Type.DIRECT;
        sa = null;
    }
    ...    
}

可以看出,Proxy.NO_PROXY表示的代理类型为Type.DIRECT,也就是不使用代理。

所以,对于NullProxySelector,外界调用它的select方法,无论传入的URI是什么,它都会返回一个不使用代理的Proxy NO_PROXY

用户可以在初始化OkHttpClient的时候,对proxyproxySelector进行配置

  @get:JvmName("proxy") val proxy: Proxy? = builder.proxy

  @get:JvmName("proxySelector") val proxySelector: ProxySelector =
      when {
        // Defer calls to ProxySelector.getDefault() because it can throw a SecurityException.
        builder.proxy != null -> NullProxySelector
        else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector
      }
用户设置的Proxy 用户设置的ProxySelector OkHttpClient中的Proxy OkHttpClient中的ProxySelector
null null null 系统提供的ProxySelectorNullProxySelector
null ProxySelector null 用户设置的ProxySelector
Proxy null Proxy NullProxySelector
Proxy ProxySelector Proxy NullProxySelector

路由

Route

OkHttp中抽象出Route来描述网络数据包的传输路径,最主要还是描述直接与其建立TCP连接的目标端点,它表示一个路由信息

class Route(
  // 记录请求url相关信息,包括请求的源服务器主机名、端口等信息  
  @get:JvmName("address") val address: Address,
  // 此路由的代理服务器信息  
  @get:JvmName("proxy") val proxy: Proxy,
  // 连接目标地址  
  @get:JvmName("socketAddress") val socketAddress: InetSocketAddress
) {
	...
}

Route中主要记录了这条路由通过的代理服务器信息Proxy、连接目标地址InetSocketAddress,根据代理协议的不同,这里的InetSocketAddress会有不同的含义:

  • 不使用代理:它包含的信息是HTTP服务器经过了DNS解析的IP地址以及协议的端口号。
  • HTTP代理:它包含的信息是代理服务器经过DNS解析的IP地址以及端口号。
  • SOCKS代理:它包含的信息是HTTP服务器的域名和协议端口号。

RouteSelector

OkHttp中通过RouteSelector类来管理所有的路由信息,并选择可用路由。RouteSelector的主要工作:

  1. 收集所有可用的路由
  2. 选择可用的路由
  3. 维护连接失败的路由信息

收集所有可用的路由

收集路由分为两个步骤:

  1. 收集所有的代理

  2. 收集特定代理服务器选择情况下的所有路由

    a. 收集该代理对应的连接目标地址

    b. 利用address、代理、连接目标地址等信息,构建路由

先看第一步如何收集所有的代理

class RouteSelector(
  private val address: Address,
  private val routeDatabase: RouteDatabase,
  private val call: Call,
  private val eventListener: EventListener
) {
  /* State for negotiating the next proxy to use. */
  private var proxies = emptyList<Proxy>()
  private var nextProxyIndex: Int = 0

  /* State for negotiating the next socket address to use. */
  private var inetSocketAddresses = emptyList<InetSocketAddress>()

  /* State for negotiating failed routes */
  private val postponedRoutes = mutableListOf<Route>()

  init {
    resetNextProxy(address.url, address.proxy)
  }
  ...  
}

RouteSelector的构造方法中,调用到了resetNextProxy,该方法就是用于收集所有代理,该方法传入了两个参数,分别是address.urladdress.proxy,其实就是用户要请求的url和用户手动设置的代理,后面会提到这个address的构造时机

  private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
    fun selectProxies(): List<Proxy> {
      // 如果用户指定了代理,则使用用户指定的代理
      if (proxy != null) return listOf(proxy)

      // 如果URI缺少了host,那么就不使用代理  
      val uri = url.toUri()
      if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)

      // address的proxySelector其实就是OkHttpClient的proxySelector,它的类型在上面已经
      // 提过了,这里使用proxySelector寻找URI可用的代理  
      val proxiesOrNull = address.proxySelector.select(uri)
      // 代理为空,则不使用代理  
      if (proxiesOrNull.isNullOrEmpty()) return immutableListOf(Proxy.NO_PROXY)
	  // 返回代理列表
      return proxiesOrNull.toImmutableList()
    }

    eventListener.proxySelectStart(call, url)
    // 记录代理  
    proxies = selectProxies()
    // 代理下标  
    nextProxyIndex = 0
    eventListener.proxySelectEnd(call, url, proxies)
  }

在这个方法中,就完成了代理的收集

当我们调用RouteSelectornext方法的时候,就可以获取一个Selection,这个方法其实就是收集一个特定代理服务器选择情况下的所有路由

  @Throws(IOException::class)
  operator fun next(): Selection {
    // 没有下一个代理,并且没有被延迟的路由,则抛出异常  
    if (!hasNext()) throw NoSuchElementException()

    // Compute the next set of routes to attempt.
    // 计算下一组要尝试的路由  
    val routes = mutableListOf<Route>()
    // 存在需要尝试的代理  
    while (hasNextProxy()) {
      // 延迟路由总是最后被尝试  
      val proxy = nextProxy()
      // 遍历某个代理下的连接目标地址  
      for (inetSocketAddress in inetSocketAddresses) {
        // 根据连接目标地址创建路由 
        val route = Route(address, proxy, inetSocketAddress)
        // 如果该路由最近连接失败,则延迟它  
        if (routeDatabase.shouldPostpone(route)) {
          postponedRoutes += route
        } else {
          routes += route
        }
      }

      // 如果没有可用路由,继续尝试下一个代理服务器  
      if (routes.isNotEmpty()) {
        break
      }
    }

    // 遍历完所有代理服务器,依然没有找到可用路由,使用之前记录的延迟路由(最近连接失败的路由)  
    if (routes.isEmpty()) {
      // We've exhausted all Proxies so fallback to the postponed routes.
      routes += postponedRoutes
      postponedRoutes.clear()
    }

    // 返回可用的路由集合
    return Selection(routes)
  }

我们查看nextProxy方法,该方法用于获取下一个代理,并且计算该代理对应的连接目标地址

  // 返回下一个尝试的代理,可能是PROXY.NO_PROXY但不可能为null
  @Throws(IOException::class)
  private fun nextProxy(): Proxy {
    if (!hasNextProxy()) {
      throw SocketException(
          "No route to ${address.url.host}; exhausted proxy configurations: $proxies")
    }
    // 获取下一个proxy  
    val result = proxies[nextProxyIndex++]
    // 计算该proxy对应的连接目标地址
    resetNextInetSocketAddress(result)
    // 返回proxy
    return result
  }

我们查看resetNextInetSocketAddress方法,该方法用于计算某个代理对应的连接目标地址

  // 为当前的proxy收集socket addresses
  @Throws(IOException::class)
  private fun resetNextInetSocketAddress(proxy: Proxy) {
    // 通过新建一个列表,清空上一个代理服务器的连接地址  
    val mutableInetSocketAddresses = mutableListOf<InetSocketAddress>()
    inetSocketAddresses = mutableInetSocketAddresses

    // 记录主机名和端口号  
    val socketHost: String
    val socketPort: Int
    // 不使用代理或者使用SOCKS代理,记录HTTP服务器主机名以及协议端口号  
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
      socketHost = address.url.host
      socketPort = address.url.port
    } else {
      // 使用HTTP代理,记录代理服务器的主机名以及端口号  
      val proxyAddress = proxy.address()
      require(proxyAddress is InetSocketAddress) {
        "Proxy.address() is not an InetSocketAddress: ${proxyAddress.javaClass}"
      }
      socketHost = proxyAddress.socketHost
      socketPort = proxyAddress.port
    }

    // 端口号不合法  
    if (socketPort !in 1..65535) {
      throw SocketException("No route to $socketHost:$socketPort; port is out of range")
    }

    // 使用SOCKS代理  
    if (proxy.type() == Proxy.Type.SOCKS) {
      // 根据HTTP服务器的主机名和协议端口号,创建连接目标地址,此时创建的InetSocketAddress
      // 实例是未经解析的  
      mutableInetSocketAddresses += InetSocketAddress.createUnresolved(socketHost, socketPort)
    } else {
      // 不使用代理或者使用HTTP代理,则会进行DNS解析  
      eventListener.dnsStart(call, socketHost)

      // Try each address for best behavior in mixed IPv4/IPv6 environments.
      // 对Http服务器的主机名进行DNS解析  
      val addresses = address.dns.lookup(socketHost)
      if (addresses.isEmpty()) {
        throw UnknownHostException("${address.dns} returned no addresses for $socketHost")
      }

      eventListener.dnsEnd(call, socketHost, addresses)
      // 为每个解析到的IP地址创建连接目标地址  
      for (inetAddress in addresses) {
        mutableInetSocketAddresses += InetSocketAddress(inetAddress, socketPort)
      }
    }
  }

从上面的代码可以看出,一个特定代理服务器选择下的连接目标地址因代理类型不同而不同

  • 直接连接:对HTTP服务器域名进行解析,并为每个解析到的IP地址创建连接目标地址
  • 使用SOCKS代理:直接以HTTP服务器域名以及协议端口号创建连接目标地址
  • 使用HTTP代理:对代理服务器的域名进行解析,并为每个解析到的IP地址创建连接目标地址

通过以上对RouteSelector的分析,我们可以知道

  1. 在创建RouteSelector的时候,它会先在构造函数中收集所有的代理
  2. 当调用RouteSelectornext方法的时候,它会返回一个Selection对象,Selection对象代表的是某个代理服务器下可用的路由集合,代理服务器有三种类型:不使用代理、使用HTTP代理、使用SOCKS代理
  3. 在获取Selection对象的时候,会先获取该代理对应的连接目标地址列表,再根据连接目标地址列表去构造该代理对应的路由Route列表
  4. RouteSelector会先尝试某个代理对应的Selection,如果所有代理下都找不到可用的路由,那么就使用「延迟路由列表」去构建一个Selection对象,也就是说,延迟路由总是最后被尝试

SelectionRouteSelector的一个内部类,它的实现很简单

  class Selection(val routes: List<Route>) {
    private var nextRouteIndex = 0

    operator fun hasNext(): Boolean = nextRouteIndex < routes.size

    operator fun next(): Route {
      if (!hasNext()) throw NoSuchElementException()
      return routes[nextRouteIndex++]
    }
  }

调用Selectionnext方法,即可获取该Selection的下一个路由Route

另外,一个域名是可以对应多个IP地址的,可以参考 一个域名最多能对应几个IP地址?。结合上面的分析,我们可以画出下面的这个图

image-20211130094719937

当然,前面提到过,Proxy有三种类型,每种类型的连接目标地址InetSocketAddress会有不同,也就是上面的IPPort会有不同,根据Proxy类型的不同,InetSocketAddress会有不同的含义:

  • 不使用代理:它包含的信息是HTTP服务器经过了DNS解析的IP地址以及协议的端口号。
  • HTTP代理:它包含的信息是代理服务器经过DNS解析的IP地址以及端口号。
  • SOCKS代理:它包含的信息是HTTP服务器的域名和协议端口号。

现在还有一个问题,ExchangeFinderAddressRouteSelector类分别是什么时候创建的呢?ExchangeFinder是在RetryAndFollowUpInterceptor当中被创建的

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
	@Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
    	...
        var newExchangeFinder = true
        ...
        while (true) {
        	call.enterNetworkInterceptorExchange(request, newExchangeFinder)
            ...
        }
    }
    ...    
}

这里调用了RealCallenterNetworkInterceptorExchange方法,该方法会根据newExchangeFinder的值,判断是否要创建一个ExchangeFindernewExchangeFinder表示是否要创建一个ExchangeFinder

  fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
	...
    if (newExchangeFinder) {
      this.exchangeFinder = ExchangeFinder(
          connectionPool,
          createAddress(request.url),
          this,
          eventListener
      )
    }
  }

这里创建了一个ExchangeFinder,并将其引用存储在RealCall当中,另外createAddress方法会去创建一个Address对象,如下

  private fun createAddress(url: HttpUrl): Address {
    var sslSocketFactory: SSLSocketFactory? = null
    var hostnameVerifier: HostnameVerifier? = null
    var certificatePinner: CertificatePinner? = null
    if (url.isHttps) {
      sslSocketFactory = client.sslSocketFactory
      hostnameVerifier = client.hostnameVerifier
      certificatePinner = client.certificatePinner
    }

    return Address(
        uriHost = url.host,
        uriPort = url.port,
        dns = client.dns,
        socketFactory = client.socketFactory,
        sslSocketFactory = sslSocketFactory,
        hostnameVerifier = hostnameVerifier,
        certificatePinner = certificatePinner,
        proxyAuthenticator = client.proxyAuthenticator,
        proxy = client.proxy,
        protocols = client.protocols,
        connectionSpecs = client.connectionSpecs,
        proxySelector = client.proxySelector
    )
  }

其中url参数就是请求的url。从上面可以看出,我们创建的Address对象会存储urlhostport信息,另外Address对象还有一些其它的信息,这些信息均取自OkHttpClient

另外,一个RouteSelector对象,可能会在ExchangeFinderfindConnection方法中被创建

  @Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
  	...
    if (nextRouteToTry != null) {
    	...
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
    	...
    } else {
        // Compute a new route selection. This is a blocking operation!
        var localRouteSelector = routeSelector
        if (localRouteSelector == null) {
            localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
            this.routeSelector = localRouteSelector
        }
        val localRouteSelection = localRouteSelector.next()
        routeSelection = localRouteSelection
        routes = localRouteSelection.routes
        ...  
    }
  }

插叙:逻辑梳理

在介绍了上面的知识后,我们将目前逻辑梳理一下。

首先,在RetryAndFollowUpInterceptorintercept方法里面,会调用RealCallenterNetworkInterceptorExchange方法,在该方法里面,会根据newExchangeFinder的值,去判断是否要创建一个新的ExchangeFinder对象,若创建,则将ExchangeFinder对象的引用存于RealCall

class RetryAndFollowUpInterceptor(private val client: OkHttpClient) : Interceptor {
	@Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        ...
		call.enterNetworkInterceptorExchange(request, newExchangeFinder)
    	...
    }
    ...    
}
class RealCall(
  val client: OkHttpClient,
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {
	...
    fun enterNetworkInterceptorExchange(request: Request, newExchangeFinder: Boolean) {
    	...
        if (newExchangeFinder) {
          this.exchangeFinder = ExchangeFinder(
              connectionPool,
              createAddress(request.url),
              this,
              eventListener
          )
        }
    }
    ...
}

创建ExchangeFinder对象的同时,也会调用createAddress方法,去创建一个Address对象,Address会记录请求urlhostport

接着在获取一个健康可用连接的时候,调用链为 RealCall::initExchange -> ExchangeFinder::find -> ExchangeFinder::findHealthyConnection -> ExchangeFinder::findConnection,在ExchangeFinderfindConnection方法里面,若RouteSelector还没创建,就会去创建RouteSelector,逻辑如下

  @Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
  	...
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      ...
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      routes = null
      route = routeSelection!!.next()
    } else {
      var localRouteSelector = routeSelector
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      routes = localRouteSelection.routes
	  
      ...
      route = localRouteSelection.next()
    }  
    ...  
  }

我们重点关注If的这大段语句,nextRouteToTry先不去管它,暂时认为它为null。上面routeSelectorrouteSelection的类型分别是RouteSelectorRouteSelector.Selection,它们的含义在上面已经介绍过了。

ExchangeFinderfindHealthyConnection方法我们可以知道,findConnection方法是有可能多次被调用的,在第一次被调用的时候,routeSelectorrouteSelection均为null,因此会在else分支下创建RouteSelector对象,并且会调用routeSelectornext方法,获取一个Selection并将其赋值给routeSelection,然后再调用routeSelectionnext方法,获取一个路由route。在第二次进入findConnection方法的时候,会先在else if语句判断routeSelection是否还有可用的路由,如果有,则直接获取routeSelection的下一个路由,如果当前routeSelection的路由已经耗尽,则会在else分支中,使用routeSelectornext方法获取下一个routeSelection,然后再从routeSelection中获取一个路由route

选择可用路由

RouteSelector选择可用路由的逻辑主要在next()方法中,上面已经对next()方法进行了分析:

  1. 如果当前有可被尝试的代理服务器,获取该代理服务器对应的所有连接目标地址。
  2. 根据连接目标地址创建对应路由,如果该路由最近连接失败,将该路由加入延迟路由集合,否则加入正常路由集合。
  3. 如果当前有正常路由,返回正常路由集合,否则继续1、2步骤。
  4. 遍历完所有可尝试代理服务器仍然没有正常路由,则返回延迟路由集合(最近连接失败的路由)。

维护路由连接失败信息

RouteSelector借助RouteDatabase维护失败的路由信息,避免浪费时间去连接一些不可用的路由:

class RouteDatabase {
  private val failedRoutes = mutableSetOf<Route>()

  /** Records a failure connecting to [failedRoute]. */
  @Synchronized fun failed(failedRoute: Route) {
    failedRoutes.add(failedRoute)
  }

  /** Records success connecting to [route]. */
  @Synchronized fun connected(route: Route) {
    failedRoutes.remove(route)
  }

  /** Returns true if [route] has failed recently and should be avoided. */
  @Synchronized fun shouldPostpone(route: Route): Boolean = route in failedRoutes
}

该类实现非常简单,里面维护了一个连接失败路由容器,当路由连接失败,调用failed方法将该路由加入容器中,当路由连接成功,调用connected方法将该路由从容器中删除,通过调用shouldPostpone方法判断该路由是否在连接失败路由容器中,从而知道该路由最近是否连接失败。

HTTP中的连接复用机制

OkHttp中出现了「连接合并」的概念,它和HTTP/2的多路复用机制有关。我们先介绍HTTP中的连接复用机制,然后再介绍OkHttp中的连接复用机制。

虽然说 HTTP 是一个无连接、无状态的协议,但由于它基于 TCP,因此也存在着管理 TCP 连接的管理问题。

HTTP/1.0 及之前

使用TCP协议往往存在粘包问题,出现粘包问题的核心原因是:TCP 协议是面向字节流的,它的数据并没有边界。

由于 TCP 连接的数据之间没有数据边界,从而导致 HTTP 数据包粘连,无法处理,因此 HTTP/1.0 及之前,采用了一种非常暴力的方式进行解决:

每进行一次 HTTP 通信,就要先建立一条 TCP 连接,在通信结束后,就需要断开一次 TCP 连接,而每次TCP 连接的建立需要经过三次握手,而 TCP 连接的关闭需要四次挥手,这显然非常浪费资源。

image-20211201093318325

其实 HTTP/1.0 中还引入了 Connection:Keep-Alive 这个 Header,它就是用来支持 TCP 连接的维持的,但在 大部分基于 HTTP/1.0 的服务器中并没有实现。

HTTP/1.1

HTTP/1.1真正引入Keep-Alive机制,默认开启,可以通过Connection:close关闭。在 HTTP 通信结束时,若启用了 Keep-Alive 机制,则该连接并不会立即关闭,此时如果有新的请求到来,且 host 和 port 相同,则会复用这条 TCP 连接进行请求,减少了 TCP 连接的频繁建立与关闭的资源消耗。

image-20211201093637597

这样就避免了每次HTTP通信都要建立和关闭 TCP 连接,从而也就避免了频繁的握手和挥手(三次握手和四次挥手),使得 HTTP 请求的效率大大提高

image-20211201093810241

粘包问题

现在多个HTTP请求可以复用同一条TCP连接,而HTTP数据包也没有通过分隔符确定数据的头和尾,那么它是如何解决粘包的问题的呢?

实际上它是通过 Content-Length 这个 Header 解决的,它标明了数据部分所占用的大小,从而可以通过它来确定这个数据包的边界,避免粘包。这也是 HTTP/1.1 引入 Content-Length 的原因。

同时,HTTP/1.1 中还有一个 Keep-Alive 请求头,可以对 timeoutmax 进行设置,用来指定空闲连接保持打开的时间以及连接关闭前这条连接可以发送请求数的最大值。

HTTP/2.0

HTTP1.1 存在的问题

HTTP/1.1 中,虽然实现了 TCP 连接的复用,但仍有如下几个缺陷:

  1. 如果客户端想要发起并行的请求,则必须建立多个 TCP 连接,这对网络资源的消耗也是十分严重的。
  2. 不会读对请求及响应的 Header 进行压缩,造成了网络流量的浪费。

关于第一点:例如,打开一个网页,浏览器会先向服务器请求一份HTML文件,在拿到HTML文件后,就需要向服务器请求HTML中的图片等资源。若是使用HTTP 1.1协议,虽然TCP连接是可以复用的,但是TCP连接上无法并行请求,需要等到上一个请求结束后,才能复用该 TCP 连接进行下一个请求,如果想并行请求,只能建立多个TCP连接,在每个TCP连接上都进行一个HTTP请求;若是使用HTTP2.0协议,则可以在同一个TCP连接上并行地发送多个HTTP请求,同时去请求HTML文件中的多份资源,然后等待服务端的响应,这就是HTTP2.0的多路复用机制

多路复用

HTTP/2.0 引入了一种多路复用机制,同时引入了几个新的概念:

  • 数据流:基于 TCP 连接上的一个双向的字节流,每发起一个请求,就会建立一个数据流,后续的请求过程的数据传递都通过该流进行
  • 数据帧:HTTP/2 中的数据最小切片单位,其中又分为了 Header FrameData Frame 等等。
  • 消息:一个请求或响应对应的一系列数据帧。

引入了这些概念之后,在 HTTP 请求的过程中,服务端/客户端首先会将我们的请求/响应切分为不同的数据帧,当另一方接收到后再将其组装从而形成完整的请求/响应,如下所示

image-20211201095807774

这样,就实现了对 TCP 连接的多路复用,将一个请求或响应分为了一个个的数据帧,使得多个请求可以并行地进行。

多路复用与 Keep-Alive 的区别

  1. Keep-Alive 机制虽然解决了复用 TCP 连接问题,但没有解决请求阻塞的问题,需要等到上一个请求结束后,才能复用该 TCP 连接进行下一个请求。
  2. HTTP/1.x 对数据的传递仍然是以一个整体进行传递,而在 HTTP/2 中引入了数据帧的概念,使得多个请求可以同时在流中进行传递。
  3. HTTP/2 采用了 HPACK 压缩算法对 Header 进行压缩,降低了请求的流量消耗。
image-20211201100007939

连接合并

英文名为:connection coalescing。

「连接合并」出现的背景:在 HTTP/1.1 中,浏览器为了更快地获取数据,对于每个「主机名+端口号」会使用 6 个连接。有些网站站点还会启用更多的主机名,这些主机名往往被解析为相同的 IP 地址或者 IP 地址集合,网站站点这么做的目的是为了触发浏览器使用更多的连接。在 HTTP/2 引入了多路复用后,它允许多个流在一个连接上同时进行传输,那么原先为了让浏览器使用更多连接而使用多个主机名的网站站点,与现在 HTTP/2 浏览器使用一个 TCP 连接进行多路复用的意思,就背道而驰了。那些网站站点不希望切换回单一的主机名,主要是基于下面的考虑

  • 这会是一个重大的架构变化。
  • 还有一些浏览器仍然是在使用 HTTP/1.1。

「连接合并」:对于连接合并,不同的浏览器有不同的做法,一些浏览器甚至不去处理它。下面是个例子:假设某个站点 "example.com" 在 DNS 中有两个主机名,分别是 "A.example.com" 和 "B.example.com",每个主机名都对应着一个 IP 列表。另外,HTTP/2 是基于 HTTPS 的,使用 HTTP/2 的浏览器,在 TLS 握手过程中可以获取服务端的两个主机名 "A.example.com"、"B.example.com"。假设两个主机名 DNS 解析后的 IP 列表为:

# A.example.com
192.168.0.1 and 192.168.0.2
# B.example.com
192.168.0.2 and 192.168.0.3

下面是 Chrome 浏览器的连接合并的做法

  • 假设一开始浏览器连接到了A站点的 192.168.0.1,现在有个要连接 "B.example.com" 的请求,浏览器会先去 DNS 解析获取它的 IP 列表,发现其中并没有 192.168.0.1,于是会为 "B.example.com" 创建一个新的连接。
  • 假设一开始浏览器连接到了A站点的 192.168.0.2,现在有个要连接 "B.example.com" 的请求,浏览器会先去 DNS 解析获取它的 IP 列表,发现B对应的 IP 地址也有 192.168.0.2,于是浏览器会重用到A站点的 TCP 连接,去请求到B站点的数据。

OkHttp的连接复用机制

OkHttp使用连接池实现了连接复用机制,我们看下连接池ConnectionPool的实现。

ConnectionPool

ConnectionPool只是连接池的入口,真正的实现其实是RealConnectionPool

初始化

OkHttpClient.Builder中默认创建一个ConnectionPool对象,用户也可以自己传入一个ConnectionPool对象

open class OkHttpClient internal constructor(
  builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {
	...
    val connectionPool: ConnectionPool = builder.connectionPool
    ...
    class Builder constructor() {
        ...
        internal var connectionPool: ConnectionPool = ConnectionPool()
        ...
        fun connectionPool(connectionPool: ConnectionPool) = apply {
      		this.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)

  /** Returns the number of idle connections in the pool. */
  fun idleConnectionCount(): Int = delegate.idleConnectionCount()

  /** Returns total number of connections in the pool. */
  fun connectionCount(): Int = delegate.connectionCount()

  /** Close and remove all idle connections in the pool. */
  fun evictAll() {
    delegate.evictAll()
  }
}

可以看出,其内部依靠了RealConnectionPool实现。

RealConnectionPool

我们查看RealConnectionPool的构造参数和成员变量

class RealConnectionPool(
  taskRunner: TaskRunner,
  /** The maximum number of idle connections for each address. */
  private val maxIdleConnections: Int,
  keepAliveDuration: Long,
  timeUnit: TimeUnit
) {
  private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)

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

  private val connections = ConcurrentLinkedQueue<RealConnection>()
  ...
}

ConnectionPoolRealConnectionPool对象的构造,我们可以知道默认情况下,OkHttp中的连接池最大空闲连接的数量为5,并且最大的空闲时间为5分钟,这里的最大空闲连接数量是相对于一个address而言。

keepAliveDurationNs也就是将空闲时间使用纳秒来表示。

connections用于存放连接。

上面还出现了Task cleanupTaskTaskQueue cleanupQueue这两个对象。

Task类:从Task类的注释可以得知,它代表一个可以执行一次或多次的任务。

  • 复发性:Task是一个抽象类,它有一个抽象方法runOnce,子类需要去实现该方法,在该方法中定义要执行的任务,runOnce返回一个Long值,该值表示该Task下一次被调度执行的延时,特别地,若该值为-1,则表示该Task不再被调度执行。
  • 可取消:Task存放于TaskQueue当中,当Task在队列中等待被执行或者正在执行时,是可以被取消的,取消一个在等待被执行的Task,那么该Task就不会被执行了,取消一个正在执行的Task,不会影响该Task的本次执行,但之后该Task就不会再被调度执行了。
  • TaskQueue关系:一个Task会被绑定到一个TaskQueue,一个TaskQueue中可以有多个Task,但一个Task只可以对应一个TaskQueueTaskQueue中的Task是有顺序的,队列中的Task是不会并发执行的。

TaskQueue类:里面存放着一组任务,这组任务按顺序执行,不是并发执行。

Task类的runOnce方法中,调用了cleanup方法,该方法执行「清理空闲连接」的任务,后面会提到。

RealConnectionPool中几个比较重要的操作方法为:putcleanupcallAcquirePooledConnectionconnectionBecameIdle,分别对应放入连接清理空闲连接获取可用连接通知连接空闲操作。

放入连接

放入连接对应put方法,RealConnectionPool::put如下

  fun put(connection: RealConnection) {
    connection.assertThreadHoldsLock()
	// 存入队列
    connections.add(connection)
    // 启动清理空闲连接的任务  
    cleanupQueue.schedule(cleanupTask)
  }

该方法会将connection存入连接缓存池中,并且启动清理空闲连接的任务。

清理空闲连接

cleanupTask表示一个清理空闲连接的任务,该任务会执行cleanup方法

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

cleanup方法用于清理空闲连接,如下

  // 该方法用于维护连接池,如果有「空闲时间」超过「允许的最长空闲时间」的连接,或者「空闲连接的数量」
  // 超过「允许的最大空闲连接数量」,那么就从连接缓存池中移除「空闲时间最长的连接」
  fun cleanup(now: Long): Long {
    // 正在使用的连接数量  
    var inUseConnectionCount = 0
    // 空闲的连接数量  
    var idleConnectionCount = 0
    // 空闲时间最长的连接  
    var longestIdleConnection: RealConnection? = null
    // 空闲时间最长的连接 的 空闲时间
    var longestIdleDurationNs = Long.MIN_VALUE

    // Find either a connection to evict, or the time that the next eviction is due.
    // 找到需要移除的连接,或者计算下一次需要清理的时间  
    for (connection in connections) {
      synchronized(connection) {
        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          // 如果当前连接正在使用  
          inUseConnectionCount++
        } else {
          // 如果当前连接是空闲的  
          idleConnectionCount++

          // If the connection is ready to be evicted, we're done.
          // 计算连接的空闲时间  
          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()
        // 若当前的连接池为空,那么就没有必要再清理连接了,所以调用TaskQueue的cancelAll方法,
        // 取消所有Task,也就是取消「所有清理连接的」任务      
        if (connections.isEmpty()) cleanupQueue.cancelAll()

        // 立即执行下次清理任务 
        return 0L
      }

      // 有空闲的连接,但是还没有达到清理的标准(空闲连接 || 空闲连接数)  
      idleConnectionCount > 0 -> {
        // 返回下次执行清理任务的时间  
        return keepAliveDurationNs - longestIdleDurationNs
      }

      // 没有连接是空闲的,所有连接都在使用  
      inUseConnectionCount > 0 -> {
        // 待「允许的最长空闲时间」之后,再次执行清理连接的任务
        return keepAliveDurationNs
      }

      // 没有任何的连接  
      else -> {          
        return -1
      }
    }
  }

由于该方法在TaskrunOnce方法中调用,并且其返回值作为runOnce方法的返回值,前面我们提到过,runOnce方法的返回值表示该Task下一次被调度执行的延时,或者返回-1表示该Task不再被调度执行,因此这里cleanup方法的返回值意味着下一次执行「清理空闲连接」任务的延时,或者返回-1,表示不再执行该「清理空闲连接」的任务。

我们再总结一下cleanup方法所做的事情:

  1. 遍历连接缓存池中的连接,记录当前空闲连接数量、正在使用连接数量、当前空闲时间最长的连接及其空闲时间。

  2. 若最长空闲时间 超过 允许的最长空闲时间,或者,空闲连接的数量 超过 允许的最大空闲数量 ,这时候会从连接缓存池中移除 空闲时间最久 的连接,同时检测当前连接缓存池是否为空,如果为空则取消所有「清理空闲连接」的任务。立即执行下次「清理空闲连接」的任务。 否则往下走。

  3. 若 有空闲的连接,但是还没有达到清理的标准(空闲连接 || 空闲连接数) ,返回下次执行「清理空闲连接」任务的延时。否则往下走。

  4. 若 没有连接是空闲的,所有连接都在使用 ,那么就返回keepAliveDurationNs,表示下次执行「清理空闲连接」任务的延时。否则往下走。

  5. 当前连接缓存池中没有任何连接,该「清理空闲连接」任务不需要再执行。

获取可用连接

从连接池获取可用连接对应着callAcquirePooledConnection方法,RealConnectionPoolcallAcquirePooledConnection如下

  fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?, // 请求方路由选择后得到的路由列表
    requireMultiplexed: Boolean // 是否要求要「Http/2.0多路复用的连接」
  ): Boolean { 
    // 遍历连接缓存池中的连接  
    for (connection in connections) {
      synchronized(connection) {
        // 请求方要求要多路复用连接,但该连接不是多路复用的连接,则跳过该连接  
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        // 判断该连接是否符合条件,若不符合则跳过该连接  
        if (!connection.isEligible(address, routes)) return@synchronized
        // RealCall使用该连接  
        call.acquireConnectionNoEvents(connection)
        return true
      }
    }
    return false
  }

该方法会遍历连接缓存池中的连接,为请求者寻找一个满足条件的连接,找到了则返回true,未找到则返回false。如果该连接满足请求者的条件,则会调用RealCallacquireConnectionNoEvents方法,表示RealCall获取该连接

  fun acquireConnectionNoEvents(connection: RealConnection) {
    connection.assertThreadHoldsLock()

    check(this.connection == null)
    // RealCall中记录该连接  
    this.connection = connection
    connection.calls.add(CallReference(this, callStackTrace))
  }

我们还要关注RealConnectionisEligible方法,该方法用于判断连接是否符合请求者的条件

  internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    assertThreadHoldsLock()

    // 该connection承载的流数量已经到达最大值,或者该连接不可用,那么返回false  
    if (calls.size >= allocationLimit || noNewExchanges) return false

    // 比较Address的非host字段,如果有不相同的,那么返回false  
    if (!this.route.address.equalsNonHost(address)) return false

    // 如果主机名host也匹配,那么意味着该连接可以复用,返回true
    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/
	
    // 此时 该连接对应的地址 和 请求者的目标地址,虽然它们的host没有匹配上,但是如果它们满足
    // 「连接合并」的要求,该连接仍然可以被复用,承载请求  
      
    // 若该连接不是Http/2(多路复用)连接,则返回false  
    if (http2Connection == null) return false

    // 2. The routes must share an IP address.
    // 用户需要传入routes,并且传入的routes至少要有一个与该连接的路由相匹配  
    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
    // 是否支持该url  
    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.
  }

上面的routeMatchesAny方法如下:

  private fun routeMatchesAny(candidates: List<Route>): Boolean {
    return candidates.any {
      it.proxy.type() == Proxy.Type.DIRECT &&
          route.proxy.type() == Proxy.Type.DIRECT &&
          route.socketAddress == it.socketAddress
    }
  }

可以看出,它返回true的条件:该coneection对应的路由是直连,并且要求用户传入的routes中至少要有一条路由为直连,并且该路由和coneection的路由的目标地址要相同。

supportsUrl方法:

  private fun supportsUrl(url: HttpUrl): Boolean {
    assertThreadHoldsLock()

    val routeUrl = route.address.url

    if (url.port != routeUrl.port) {
      return false // Port mismatch.
    }

    if (url.host == routeUrl.host) {
      return true // Host match. The URL is supported.
    }

    // We have a host mismatch. But if the certificate matches, we're still good.
    return !noCoalescedConnections && handshake != null && certificateSupportHost(url, handshake!!)
  }

我们再总结一下获取可用连接的callAcquirePooledConnection方法:

  1. 遍历缓存连接池中的连接,对于每个连接,处理如下
  2. 若用户要求"多路复用"的连接,但该连接不是"多路复用"的连接,则跳过该连接
  3. 若该连接不满足条件,则跳过该连接
    1. 用户要求的addressconnectionroute,它们hostport等信息都匹配,则满足条件
    2. 若满足Http/2.0的「连接合并」的要求,也认为该连接满足条件
    3. 否则该连接不满足条件
  4. 该连接满足用户的要求,则RealCall获取该连接,并返回true,表示连接池获取连接成功
  5. 若所有连接都不符合用户要求,则返回false,表示连接池获取连接失败

三次连接池的获取的区别

回到ExchangeFinder::findConnection方法中,它三次从连接池获取连接,它们之间有什么区别呢?

// 第一次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, null, false)
// 第二次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, routes, false)
// 第三次从连接池获取连接
connectionPool.callAcquirePooledConnection(address, call, routes, true)

它们的区别在于第三个参数和第四个参数,刚刚我们提到了RealConnectionPoolcallAcquirePooledConnection方法,它用于获取可用的连接

  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
  }
  • 第三个参数routes:客户端到目的地(有可能是源服务器,也有可能是代理服务器)的路由列表,它由我们前面介绍的RouteSelector生成的

  • 第四个参数requireMultiplexed:表示客户端是否需要「多路复用」的连接,多路复用连接是HTTP/2.0的特性

三次从连接池获取连接的区别:

  • 第一次:requireMultiplexed参数为false,说明先不去管连接池中的连接是不是HTTP/2的连接(多路复用),只要在isEligible方法中,判断目标请求地址和connection对应路由的主机名、端口等信息都匹配,就使用该连接,若不匹配,则不使用该连接(由于routes参数为null,因此不会去使用「连接合并」的特性)
  • 第二次:requireMultiplexed参数为false,说明先不去管连接池中的连接是不是HTTP/2的连接(多路复用),只要在isEligible方法中,判断目标请求地址和connection对应路由的主机名、端口等信息都匹配,就使用该连接,若只有主机名不匹配,由于routes的参数不为null,也就是请求方传入经过了路由选择的路由列表,这时候会去判断是否满足「连接合并」的特性,满足则使用该连接,否则不使用
  • 第三次:requireMultiplexed参数为true,表明只需要多路复用的连接,在callAcquirePooledConnection方法中会筛去不是多路复用的连接,对于连接池的每一个多路复用的连接,会先去看主机名、端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,则判断是否满足「连接合并」的特性,满足则使用该连接,否则不使用

简单总结:

  • 第一次:不使用「连接合并」的特性,只有目标请求地址和connection对应路由的主机名、端口等信息都匹配,才使用该连接
  • 第二次:使用「连接合并」的特性,不管是不是多路复用连接,优先判断主机名和端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,判断是否符合「连接合并」的要求,满足则使用,否则不使用
  • 第三次:使用「连接合并」的特性,这一次只使用多路复用的连接。也是优先判断主机名和端口等信息是否匹配,匹配则直接使用,不匹配则判断是否符合「连接合并」的要求,满足则使用,否则不使用

通知连接空闲

这对应RealConnectionPoolconnectionBecameIdle方法

  /**
   * Notify this pool that [connection] has become idle. Returns true if the connection has been
   * removed from the pool and should be closed.
   */
  // 通知连接池该connection已空闲,如果该connection从连接池中移除则返回true	
  fun connectionBecameIdle(connection: RealConnection): Boolean {
    connection.assertThreadHoldsLock()

    // 如果该连接无法承载新的stream,或者连接池允许的最大空闲连接数为0,则从连接池中移除该连接  
    return if (connection.noNewExchanges || maxIdleConnections == 0) {
      connection.noNewExchanges = true
      connections.remove(connection)
      if (connections.isEmpty()) cleanupQueue.cancelAll()
      true
    } else {
      // 否则执行清理空闲连接的任务  
      cleanupQueue.schedule(cleanupTask)
      false
    }
  }

PS:当调用RealConnectionPoolput方法或者connectionBecameIdle方法时都立即执行空闲连接清理任务。

建立新连接

ExchangeFinder::findConnection中,如果call中没有可复用的连接,并且前两次从连接池获取连接的时候,也没能成功地获取到连接,那么这时候就会创建一个新的连接

    ...
	val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } 
	...

我们查看RealConnectionconnect方法

  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) {
            // We were unable to connect the tunnel but properly closed down our resources.
            // 我们无法连接隧道,但妥善关闭了我们的资源。  
            break
          }
        } else {
          // 不需要使用隧道技术  
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
        // 建立协议  
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
        break
      } catch (e: IOException) {
        // 出现异常,释放一些资源
        ...
      }
    }
      
	// 默认尝试建立隧道的次数为21,超过该最大次数则抛异常
    if (route.requiresTunnel() && rawSocket == null) {
      throw RouteException(ProtocolException(
          "Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))
    }

    idleAtNs = System.nanoTime()
  }

这里使用了一个while(true)循环,直到成功建立连接,主要步骤:

  1. 判断是否使用隧道技术

    • 是 : 调用connectTunnel方法

    • 否 : 调用connectSocket方法

  2. 调用establishProtocol方法建立协议

不使用隧道(直接连接)

直接连接,调用的是RealConnectionconnectSocket方法

  /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  // 在原始套接字上,构建一个完整的HTTP或HTTPS连接
  @Throws(IOException::class)
  private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
  ) {
    val proxy = route.proxy
    val address = route.address

    // 根据代理类型,初始化rawSocket  
    val rawSocket = when (proxy.type()) {
      Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
      else -> Socket(proxy)
    }
    this.rawSocket = rawSocket

    eventListener.connectStart(call, route.socketAddress, proxy)
    rawSocket.soTimeout = readTimeout
    try {
      // 连接socket,之所以这样写是因为支持不同的平台 
      // 里面的实现调用了 socket.connect(address, connectTimeout) 方法  
      Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
    } catch (e: ConnectException) {
      throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
        initCause(e)
      }
    }

    // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
    try {
      // 获取source和sink,分别用于输入和输出  
      source = rawSocket.source().buffer()
      sink = rawSocket.sink().buffer()
    } catch (npe: NullPointerException) {
      if (npe.message == NPE_THROW_WITH_NULL) {
        throw IOException(npe)
      }
    }
  }

该方法的大致流程如下:

  1. 根据代理类型创建Socket对象。
  2. 调用socketconnect方法进行socket连接,这是 Java 原生的方法,在这里进行了TCP三次握手
  3. 调用Okio库提供的方法获取输入输出流source以及sink

通过隧道连接

没有看懂 okhttp 在这一部分的操作,这里就简单记录下隧道的概念。

在从IPv4向IPv6迁移的过程中,我们就有提到过建隧道的方法,我们回顾下当时的应用。首先,要明确的一点是IPv6是向后兼容(backward)的,也就是可以发送、路由和接收IPv4数据报,但已部署的具有IPv4能力的系统却不能够处理IPv6数据报。

我们看下面这个图

image-20211206130622802

假定图中的B和E结点要使用IPv6数据报进行交互,但它们是经由中间IPv4路由器互联的,我们将两台IPv6路由器之间的中间IPv4路由器的集合称为一个隧道,借助于隧道,在隧道发送端的B结点可将整个IPv6数据报放到一个IPv4数据报的有效载荷中,该IPv4数据报的地址设为指向隧道接收端的E结点,再发送给隧道中的第一个节点(在此例中为C)。隧道中的中间IPv4路由器在它们之间为该数据报提供路由,就像对待其他数据报一样,完全不知道该IPv4数据报自身就含有一个完整的IPv6数据报。隧道接收端的E节点最终收到该IPv4数据报(它是该IPv4数据报的目的地),并确定该IPv4数据报含有一个IPv6数据报(通过观察在IPv4数据报中的协议号字段是41,指示该IPv4有效载荷是IPv6数据报),从中取出IPv6数据报,然后再为该IPv6数据报提供路由,就好像它是从一个直接相连的IPv6邻居那里接收到该IPv6数据报一样。

说明:IPv4向IPv6迁移只是建隧道其中的一个应用场景,在其它的一些地方也有建隧道的概念与应用

Http中如何打开隧道呢

HTTP 提供了一个特殊的 method—— CONNECT,它是 HTTP/1.1 协议中预留的方法,客户端发送一个 CONNECT 请求给隧道网关请求打开一条 TCP 连接,当隧道打通之后,客户端通过 HTTP 隧道发送的所有数据会转发给 TCP 连接,服务器响应的所有数据会通过隧道发给客户端。

建立协议

无论是直接连接,还是通过隧道连接,在建立了Socket连接之后,都需要调用establishProtocol方法,建立协议。这一步发生在TCP连接建立之后,在TCP连接上发送数据之前,这一步会做TLS握手(使得数据可以加密传输)、HTTP/2的协议协商等。

RealConnection::establishProtocol

  @Throws(IOException::class)
  private fun establishProtocol(
    connectionSpecSelector: ConnectionSpecSelector,
    pingIntervalMillis: Int,
    call: Call,
    eventListener: EventListener
  ) {
    // 普通的HTTP请求,而非HTTPS请求,则协议版本定义为HTTP/1.1  
    if (route.address.sslSocketFactory == null) {
      // 协议中包含h2_prior_knowledge  
      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)
    // TLS握手  
    connectTls(connectionSpecSelector)
    eventListener.secureConnectEnd(call, handshake)
    // 若同时为HTTPS请求和HTTP/2
    if (protocol === Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis)
    }
  }

该方法主要进行以下工作:

  1. 如果是HTTP请求,就将协议protocol定义为HTTP/1.1,如果协议中包含了h2_prior_knowledge,则采用HTTP/2进行请求,调用startHttp2方法。
  2. 如果是HTTPS请求,就调用connectTls方法进行TLS握手,再判断是不是HTTP/2协议,从而决定是否调用startHttp2方法开启HTTP2连接

开启HTTP2连接

查看RealConnection::startHttp2

  @Throws(IOException::class)
  private fun startHttp2(pingIntervalMillis: Int) {
    val socket = this.socket!!
    val source = this.source!!
    val sink = this.sink!!
    socket.soTimeout = 0 // HTTP/2 connection timeouts are set per-stream.
    // 初始化一个Http2连接  
    val http2Connection = Http2Connection.Builder(client = true, taskRunner = TaskRunner.INSTANCE)
        .socket(socket, route.address.url.host, source, sink)
        .listener(this)
        .pingIntervalMillis(pingIntervalMillis)
        .build()
    this.http2Connection = http2Connection
    // 最大并发流数  
    this.allocationLimit = Http2Connection.DEFAULT_SETTINGS.getMaxConcurrentStreams()
    // 开启Http2连接
    http2Connection.start()
  }

我们在RealConnectionisEligible方法中曾见过allocationLimit的身影,

  internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
	...
    if (calls.size >= allocationLimit || noNewExchanges) return false
    ...
  }

当时主要用于判断connection承载的流数量是否已经到达最大值,从而决定该connection是否可用,allocationLimit的初始值是1,也就是说,在HTTP/1中,一个connection同时最多承载一个流,但是在HTTP/2中,由于有多路复用的机制,它的allocationLimit值就不再是1了。

startHttp2的主要工作就是初始化一个Http2Connection,将Http2Connectionlistener设置为RealConnection本身,接着设置allocationLimit的值,最后调用Http2Connection.start方法开启Http2连接。我们查看Http2Connection::start方法

  // Sends any initial frames and starts reading frames from the remote peer.
  // This should be called after Builder.build for all new connections.
  // Params:sendConnectionPreface - true to send connection preface frames.
  // 该方法必须在Builder.build方法之后调用
  @Throws(IOException::class) @JvmOverloads
  fun start(sendConnectionPreface: Boolean = true, taskRunner: TaskRunner = TaskRunner.INSTANCE) {
    if (sendConnectionPreface) {
      // 发送初始帧  
      writer.connectionPreface()
      // 将OkHttp的设置以设置帧的形式发送给对等方  
      writer.settings(okHttpSettings)
      // 窗口更新  
      val windowSize = okHttpSettings.initialWindowSize
      if (windowSize != DEFAULT_INITIAL_WINDOW_SIZE) {
        writer.windowUpdate(0, (windowSize - DEFAULT_INITIAL_WINDOW_SIZE).toLong())
      }
    }
   
    // 开启一个线程,用于读取对等方发送的初始帧和设置帧  
    taskRunner.newQueue().execute(name = connectionName, block = readerRunnable)
  }

该方法主要内容如下:

  1. 发送初始帧给服务器
  2. OkHttp的设置以设置帧的形式发送给服务器
  3. 开启一个线程,读取服务器发送的初始帧和设置帧

HTTP/2中,每个终端在使用HTTP/2协议之前都要发送一个初始帧作为最终的确认,同时初始帧后面还需跟着一个设置帧,作为HTTP/2连接建立的初始化设置。

HTTP/2协议中的基本单位是帧,每个帧都有不同的类型和用途, 例如,报头(HEADERS)和数据(DATA)帧组成了基本的HTTP 请求和响应,其他帧例如 设置(SETTINGS)、窗口更新(WINDOW_UPDATE)和推送承诺(PUSH_PROMISE) 是用来实现HTTP/2的其他功能。

TLS握手

如果是HTTPS请求,则需要调用connectTls方法进行TLS握手,RealConnection::connectTls

  @Throws(IOException::class)
  private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
    val address = route.address
    val sslSocketFactory = address.sslSocketFactory
    var success = false
    var sslSocket: SSLSocket? = null
    try {
      // Create the wrapper over the connected socket.
      // 基于前面TCP连接的Socket包装一个SSLSocket对象  
      sslSocket = sslSocketFactory!!.createSocket(
          rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket

      // Configure the socket's ciphers, TLS versions, and extensions.
      // 配置TLS相关信息  
      val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
      if (connectionSpec.supportsTlsExtensions) {
        Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
      }

      // Force handshake. This can throw!
      // 进行TLS握手  
      sslSocket.startHandshake()
      // block for session establishment
      // 获取SSLSession  
      val sslSocketSession = sslSocket.session
      val unverifiedHandshake = sslSocketSession.handshake()

      // Verify that the socket's certificates are acceptable for the target host.
      // 验证socket证书对该主机是否有效  
      if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
        ...
      }

      ...

      // Success! Save the handshake and the ALPN protocol.
      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()
      }
    }
  }

connectTls方法主要进行以下工作:

  1. 基于前面TCP连接创建的Socket包装为一个SSLSocket对象。
  2. 配置TLS相关信息。
  3. 进行TLS握手。
  4. 验证证书对该主机是否有效。
  5. 获取输入输出流source以及sink

连接获取流程梳理

我们再梳理一下ExchangeFinder::findConnection获取连接的流程,它一共有五步:

  1. 重用call当中的连接
  2. 第一次尝试从连接池获取连接
  3. 第二次尝试从连接池获取连接
  4. 自己新创建一个连接
  5. 第三次尝试从连接池获取连接

重用call当中的连接:存在RetryAndFollowUpInterceptor拦截器,当请求失败需要重试或者重定向的时候,这时候call中的连接还在,如果该连接满足条件,则可以进行复用

第一次尝试从连接池获取连接:不使用「连接合并」的特性,只有目标请求地址和connection对应路由的主机名、端口等信息都匹配,才使用该连接;

第二次尝试从连接池获取连接:使用「连接合并」的特性,不管是不是多路复用连接,优先判断主机名和端口等信息是否匹配,匹配则直接使用,若只有主机名不匹配,判断是否符合「连接合并」的要求,满足则使用,否则不使用;

自己新创建一个连接:首先创建一个RealConnection对象,然后调用该对象的connect方法,在该方法中,会进行TCP连接(三次握手),并建立协议(根据情况,判断是否要进行TLS握手、开启HTTP2连接);

第三次尝试从连接池获取连接:使用「连接合并」的特性,这一次只使用多路复用的连接。也是优先判断主机名和端口等信息是否匹配,匹配则直接使用,不匹配则判断是否符合「连接合并」的要求,满足则使用,否则不使用

为什么在新创建了一个连接后,还要第三次尝试从连接池获取连接呢

ExchangeFinder::findConnection方法中对于第三次从连接池获取连接的注释如下:

    // If we raced another call connecting to this host, coalesce the connections. This 
    // makes for 3 different lookups in the connection pool!

有可能另外一个call也创建了一个连接到hostconnection,并且那个call先于当前的call将该连接放到了连接池当中,这时候我们这个call就尝试从连接池中去获取连接,如果满足「连接合并」的特性,则复用另外一个call的连接,当前call新建的连接则将其关闭,节省资源。

另外,如果在新建了连接后,第三次从连接池又成功获取了连接,那么会将成功新建的连接的路由 route 记录在 nextRouteToTry 变量里面,如果连接池获取的连接被判定为不健康的话,那么重新调用 ExchangeFinder::findConnection 方法获取连接的时候,可以直接使用 nextRouteToTry 记录的路由去新建一个 TCP 连接,而不用再去路由列表里面找一个路由。

整个流程梳理

ConnectInterceptor拦截器的作用是获得一个健康可用的与目标服务器的连接,然后就交给下一个拦截器处理。获取健康可用的连接的调用链如下:

ConnectInterceptor::intercept
->RealCall::initExchange
->ExchangeFinder::find
->ExchangeFinder::findHealthyConnection
->ExchangeFinder::findConnection
  • ExchangeFinder::findConnection中,会去获取一个可用的连接,里面又分为了5步。

  • ExchangeFinder::findHealthyConnection中,会判断获取到的连接是否健康,若健康则返回,若不健康,在有可以尝试的路由的前提下,继续调用findConnection方法获取一个可用的连接。

  • ExchangeFinder::find方法中,会通过已获取的健康可用的连接,去创建一个ExchangeCodec对象,该对象负责对Http请求进行编码和对Http响应进行解码,此时,有了与服务器的连接处理I/O的ExchangeCodec对象,我们其实就可以和服务器进行通信了。

  • RealCall::initExchange方法中,会利用ExchangeCodec实例,创建了一个Exchange对象,我们可以认为Exchange是封装ExchangeCodec的一个工具类,Exchange负责连接管理,而ExchangeCodec负责处理实际的I/O

  • 接着在ConnectInterceptor::intercept方法中,将获取到的Exchange对象放到拦截器链RealInterceptorChain里面,此时已经获得了一个健康可用的与目标服务器的连接,然后将请求交给下一个拦截器处理。

大致流程如下:

image-20211209161223068

参考

  1. OkHttp 源码剖析系列(六)——连接复用机制及连接的建立 - 掘金.
  2. 一条链上七个娃—从网络请求过程看OkHttp拦截器.
  3. HTTP/2 connection coalescing | daniel.haxx.se
posted @ 2021-12-19 00:34  Giagor  阅读(2444)  评论(1编辑  收藏  举报