天天看點

OkHttp連接配接是怎麼回事

OkHttp作為常用的網絡通訊元件,其中大部分的功能點需要我們深入了解,本系列的文章将以源碼角度解析元件背後的運作原理,避免踩坑。

文章中提到的okHttp版本為4.10.0,源碼的git分支是4.10.x,源碼git位址:https://square.github.io/okhttp/

文章概要

前一篇文章我們了解了如果通過提高參數去優化我們現有的連接配接,這一篇文章我們将深入了解OkHttp連接配接從建立、複用、銷毀的整個生命周期。

一、OkHttp連接配接池概述

OkHttp連接配接是怎麼回事

構造函數

  • maxIdleConnections 最大常駐連接配接數量(預設5個)
  • keepAliveDuration 連接配接存活時間(預設5)
  • timeUnit 上面存活時間的時間機關(預設分鐘)

連接配接池存在的意義

  • 避免連接配接建立(TCP三向交握)和斷開(TCP四次揮手)帶來的資源損耗以及耗時的增加
  • 複用和管理連接配接
OkHttp連接配接是怎麼回事

二、源碼分析

1.1、連接配接池初始化

我們都知道OkHttp在建立client時使用的是建造者模式,如果我們沒有提供額外的參數設定,連接配接池将由Builder對象建立初始化。

【OkHttpClient.kt】 連接配接池的建立

open class OkHttpClient internal constructor(
  builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {
  // 省略部分代碼
  @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool
    
}

// okhttpclient builder的預設建立方式
class Builder constructor() {
    // 省略部分代碼
    internal var 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)
  // 省略部分代碼
}

           

所有連接配接通過ConcurrentLinkedQueue線程安全隊列存儲

private val connections = ConcurrentLinkedQueue<RealConnection>()

           

常用的方法有

// 非空閑的連接配接數量
idleConnectionCount()

// 所有連接配接數量(包含空閑的連接配接數量)
connectionCount()

// 從池中取出一個連接配接
callAcquirePooledConnection(...)

// 把連接配接放到連接配接池中
put(...)

// 通知目前connection已變為空閑。如果連接配接已從池中删除并應關閉,則傳回 true
connectionBecameIdle(...)

// 放棄所有連接配接
evictAll()

// 清理溢出的連接配接
cleanup()

// 清理失效的連接配接并統計目前連接配接的引用個數
pruneAndGetAllocationCount(...)
           

1.2、連接配接複用

為了友善了解,這裡的源碼分析将從一個同步請求開始分析,異步任務相關我們會在後續的文章中提到。
OkHttp連接配接是怎麼回事

建立同步請求開始執行

httpClient.newCall(request).execute();
           

【OkHttpClient.kt#newCall】 建立RealCall對象

override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
           

【RealCall.kt#execute】 execute()開始執行調用

override fun execute(): Response {
   /// 省略部分代碼
      return getResponseWithInterceptorChain()
   /// 省略部分代碼
  }
  
           

【RealCall.kt#getResponseWithInterceptorChain】 建構攔截處理鍊,連接配接處理的代碼主要在ConnectInterceptor類中

internal fun getResponseWithInterceptorChain(): Response {
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    // 連接配接處理
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )

    // 省略部分代碼
      val response = chain.proceed(originalRequest)
    // 省略部分代碼 
  }

           

【ConnectInterceptor.kt#intercept】 為目前請求配置設定或申請連接配接

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

【RealCall.kt#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
  }
           

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

【ExchangeFinder.kt#findConnection】 findHealthConnection()->findConnection() 找連接配接

private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    // 嘗試重用RealCall中的連接配接
    val callConnection = call.connection
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        // connection的noNewExchanges = true(noNewExchanges标記=true 表示連接配接永不可用) 或 host port 不同
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          toClose = call.releaseConnectionNoEvents()
        }
      }
      // 複用連接配接
      if (call.connection != null) {
        return callConnection
      }

      // 連接配接不可用則釋放掉
      // The call's connection was released.
      // 省略部分代碼
    }
    // 省略部分代碼

    // 從連接配接池裡撈一個連接配接出來
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      return result
    }

    // 省略部分代碼
  }

           

【RealConnectionPool.kt#callAcquirePooledConnection】 選取複用連接配接的邏輯就比較簡單,周遊所有的連接配接,通過isEligible()找到符合條件的連接配接

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
  }
  
           

【RealConnection.kt#isEligible】 連接配接是否達到可以複用的條件

internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    // 正在請求的數量是否超過限制
    // allocationLimit http 1.1 等于預設值1,http2多路複用可以有多個
    if (calls.size >= allocationLimit || noNewExchanges) return false
    // 位址的代理、協定版本、port等資訊是否相等
    if (!this.route.address.equalsNonHost(address)) return false
    // host是否相等
    if (address.url.host == this.route().address.url.host) {
      return true // This connection is a perfect match.
    }
    // 剩下是http2的連接配接判斷
    // 省略部分代碼
  }
           

【RealCall.kt#acquireConnectionNoEvents】目前call加入連接配接的calls中,标記這個連接配接被占用

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

    check(this.connection == null)
    this.connection = connection
    connection.calls.add(CallReference(this, callStackTrace))
  }
           

1.4、建立新連接配接

連接配接的建立要從上面的 【ExchangeFinder.kt#findConnection】 開始說,當連接配接池中沒有連接配接,或連接配接都不符合複用的條件時,則會建立一個新連接配接。

private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    
    // 省略部分代碼

    // 建立新連接配接對象
    // 還沒有發起socket連接配接
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      // 發起socket新連接配接
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())

    // 這裡又去連接配接池中取一次,因為http2連接配接多路複用的,再去取一次有可能上面的流程走完池裡有可複用的連接配接了
    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)
    }

    // ...
    return newConnection
  }
           

1.5、連接配接銷毀

OkHttp連接配接是怎麼回事

清理連接配接主要是cleanup方法,cleanup方法的執行時機有三個

  1. 建立新連接配接後,新連接配接入池(put()方法) 【RealConnectionPool.kt#put】
fun put(connection: RealConnection) {
    connection.assertThreadHoldsLock()

    connections.add(connection)
    // 打開清理任務
    cleanupQueue.schedule(cleanupTask)
}
           
  1. 請求完成時(messageDone() -> connectionBecameIdle()方法)
  2. 重用RealCall中連接配接無效時(connectionBecameIdle()方法)

【RealConnectionPool.kt#connectionBecameIdle】

fun connectionBecameIdle(connection: RealConnection): Boolean {
    connection.assertThreadHoldsLock()

    // 如果傳入的連接配接時無效的,或 maxIdleConnections == 0(不允許有空閑連接配接)
    return if (connection.noNewExchanges || maxIdleConnections == 0) {
      // 标記目前連接配接失效
      connection.noNewExchanges = true
      // 從池裡删除
      connections.remove(connection)
      
      if (connections.isEmpty()) cleanupQueue.cancelAll()
      true
    } else {
      cleanupQueue.schedule(cleanupTask)
      false
    }
}
           

【RealConnectionPool.kt#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 -> {
        // We've chosen a connection to evict. Confirm it's still okay to be evict, then close it.
        val connection = longestIdleConnection!!
        synchronized(connection) {
          if (connection.calls.isNotEmpty()) return 0L // No longer idle.
          if (connection.idleAtNs + longestIdleDurationNs != now) return 0L // No longer oldest.
          connection.noNewExchanges = true
          connections.remove(longestIdleConnection)
        }

        connection.socket().closeQuietly()
        if (connections.isEmpty()) cleanupQueue.cancelAll()

        // Clean up again immediately.
        return 0L
      }

      idleConnectionCount > 0 -> {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs
      }

      inUseConnectionCount > 0 -> {
        // All connections are in use. It'll be at least the keep alive duration 'til we run
        // again.
        return keepAliveDurationNs
      }

      else -> {
        // No connections, idle or in use.
        return -1
      }
    }
}
           

結尾

至此,我們了解了Okhttp連接配接的建立、複用、以及銷毀流程。 後面我們将深入分析OkHttp 4.10.0事件,感興趣的同學歡迎繼續關注!

繼續閱讀