OkOne-高级功能之OkHttp预建连以及原理剖析

2,516 阅读10分钟

简介

OkOne是一款基于okhttp库的网络性能优化框架,但不同于其他框架对okhttp的使用调用进行封装,而是从不一样的方面,以对开发者无侵入的方式进行优化。

更多介绍见《OkOne-基于okhttp的网络性能优化框架》

GitHub地址

github.com/chidehang/O…

预建连

开发者可以在合适的时机提前建立连接,若连接成功,则会将其添加进okhttp连接池。

OkOne.preBuildConnection(okHttpClient, url, new PreConnectCallback() {
    @Override
    public void connectCompleted(String url) {
        Log.d(TAG"预建连成功: " + url);
    }

    @Override
    public void connectFailed(Throwable t) {
        Log.e(TAG"预建连失败", t);
    }
});

效果演示

  • 首次尝试预建连
s1
s1

预连接 "https://stackoverflow.com/" ,连接成功。

  • 重复预建连
s2
s2

尝试再次对 "https://stackoverflow.com/" 域名预建连,会直接返回错误,避免重复预建连。

  • 接口请求
s3
s3

请求 "https://stackoverflow.com/" 响应成功,可以看到从callStart直接到connectionAcquired,省去了中间的DNS和握手建连过程。

原理剖析

OkHttp框架本身不对开发者开放预建连功能,要实现预建连功能必须了解OkHttp中的Connection(连接)创建和ConnectionPool(连接池)机制。接下来深入OkHttp框架源码中进行分析,找到实现预建连的插入点。

源码基于OkHttp当前最新版本4.9.0

大家都知道OkHttp在发起请求后,会经过拦截链层层处理,其中ConnectInterceptor拦截器负责查找或新建Connection。

ConnectInterceptor#intercept -> RealCall#initExchange -> ExchangeFinder#find

ConnectInterceptor中是通过ExchangeFinderfind方法来获取Connection,直接来看ExchangeFinder#find方法。

  fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      // 进一步获取可用Connection
      val resultConnection = findHealthyConnection(
          // ···限于篇幅省略参数代码
      )
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      // ···
    } catch (e: IOException) {
      // ···
    }
  }

接着看ExchangeFinder#findHealthyConnection:

  @Throws(IOException::class)
  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      // 进一步获取Connection
      val candidate = findConnection(
          // ···限于篇幅省略参数代码
      )
      
      // 检查Connection是否可用,若检查通过则返回该Connection。
      // 否则,切换下一个Route用于尝试。
      // 若再无可用Route,则抛异常退出。
      // ···
    }
  }

关键看ExchangeFinder#findConnection方法:

@Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    // ···
    // 检查上一次请求的Connection是否可以复用
    // ···

    // 一些标记清零
    // ···

    // Attempt to get a connection from the pool.
    // 检查连接池中是否存在相同地址的Connection
    if (connectionPool.callAcquirePooledConnection(address, call, nullfalse)) {
      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.
      // 使用来自先前合并连接的路由(连接成功后会检查连接池中是否有一样的连接,有则合并,并保存Route)。
      // 初次连接不走这个case。
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      // Use a route from an existing route selection.
      // 切换Route继续尝试时,初次连接不走这个case。
    } else {
      // Compute a new route selection. This is a blocking operation!
      // 初次连接走这个case。
      var localRouteSelector = routeSelector
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      // 获取一组Route。
      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.
      // 再次检查连接池。
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }

      // 获取一个Route。
      route = localRouteSelection.next()
    }

    // Connect. Tell the call about the connecting call so async cancels work.
    // 新建Connection。
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      // 尝试连接,若连接失败,方法内部会抛出异常
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    // 记录当前Route(从失败Route集合中移除该Route)。
    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!
    // 再次检查连接池。
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }

    synchronized(newConnection) {
      // 将新建的Connection添加进连接池(此方法也将触发连接池启动清理任务)。
      connectionPool.put(newConnection)
      call.acquireConnectionNoEvents(newConnection)
    }

    eventListener.connectionAcquired(call, newConnection)
    // 返回新建的Connection。
    return newConnection
  }

在该方法中我们只关心新建Connection的关键步骤:

  • 1.选取Route
  • 2.新建RealConnection
  • 3.connect进行建连
  • 4.连接成功后加入ConnectionPool

一.如何获取Route?

通过分析前面findConnection方法,发现可以通过RouteSelector来获取。而创建RouteSelector又需要Address(作为构造方法的第一个参数),因此先来获取Address。

获取Address可以参考RealCall#createAddress方法:

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

构造RouteSelector的第二个参数RouteDatabase可以通过okHttpClient实例获取,第三个参数Call可以使用一个Call空实现,第四个参数EventListener可以使用EventListener.NONE。

有了RouteSelector便可以获取Route,预建连作为一个辅助优化功能,不强制必须成功,不必循环尝试所有Route,只需要通过next方法获取一次当前的Route。

拿到Address和Route后先通过RealConnectionPool#callAcquirePooledConnection方法检查一次连接池。RealConnectionPool可以通过反射从okHttpClient中获取。

在RealConnectionPool#callAcquirePooledConnection方法中会遍历连接池中的RealConnection,通过RealConnection#isMultiplexed和RealConnection#isEligible方法进行比较(除此之外还有其他操作)。预建连时只需要进行比较比较即可,因此参考该方法中的实现,自行通过反射来调用isMultiplexed、isEligible方法进行比较。

二.如何新建RealConnection?

使用前面获取的RealConnectionPool和Route直接new一个。

三.如何connect?

调用新建的RealConnection的connect方法。connect前四个参数通过okHttpClient获取。第五个参数connectionRetryEnabled传false,失败不重试,预建连不强求成功。第五、六个参数使用空实现。

            connection.connect(
                    mClient.connectTimeoutMillis(),
                    mClient.readTimeoutMillis(),
                    mClient.writeTimeoutMillis(),
                    mClient.pingIntervalMillis(),
                    false,
                    BuildConnectionProcessor.NONE_CALL,
                    EventListener.NONE
            );

若connect方法抛出异常,则视为预建连失败,不将其添加进连接池。连接成功后,再检查一次连接池,若已存在,则关闭当前新建的RealConnection的Socket。

四.如何添加进ConnectionPool?

调用RealConnectionPool的put方法把新建的RealConnection传入,同时会触发RealConnectionPool启动清理闲置RealConnection的任务(若未启动)。

            synchronized (connection) {
                realConnectionPool.put(connection);
            }

至此完成预建连,完整实现见GitHub上源码。