前文
对于http请求我们都知道开始于TCP链接的三次握手然后传输数据然后释放,如下图
而当我们开启连接复用keep-alive
后就是指在上一次链接不立马断开链接在超时范围内复用connection
在timeout
空闲的时间内就会复用相同的Request来减少握手大幅度提高了网络请求效率;如下图
而在Okhttp3中是怎么做到连接池复用的,本文从源码(版本v4.9.3)角度来进行探索
Okhttp3的连接池复用、清理、回收机制
连接池的代码类位于okhttp3.ConnectionPool
,该类作为默认ConnectionPool
有5个空闲状态的链接和默认5min的超时设置,如果要更改可以通OkHttpClient.Builder().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 evictAll() {
delegate.evictAll()
}
}
实际对连接池的管理代码是在okhttp3.internal.connection.RealConnectionPool
该类管理当用户发起请求时判断是否有可以复用的connection
;对connection
进行缓存、获取、清理回收的操作
// 存储RealConnection的双向队列,并在添加或者删除时持有锁可以防止同时被删除或采用
private val connections = ConcurrentLinkedQueue<RealConnection>()
缓存
添加缓存是在put方法,添加完成后并会进行一次清理操作(清理在下面说到)
fun put(connection: RealConnection) {
connection.assertThreadHoldsLock()
connections.add(connection)
// 添加到cleanupQueue循环执行,如果task已经存在则使用最早的时间执行
cleanupQueue.schedule(cleanupTask)
}
获取
fun callAcquirePooledConnection(
doExtensiveHealthChecks: Boolean,
address: Address,
call: RealCall,
routes: List<Route>?,
requireMultiplexed: Boolean
): RealConnection? {
for (connection in connections) {
// In the first synchronized block, acquire the connection if it can satisfy this call.
val acquired = synchronized(connection) {
when {
// 要求多路复用且不是HTTP2的返回false
requireMultiplexed && !connection.isMultiplexed -> false
//判断是否可以跟缓存的链接复用(ip、prxy_type等)
!connection.isEligible(address, routes) -> false
else -> {
// 可以复用
call.acquireConnectionNoEvents(connection)
true
}
}
}
if (!acquired) continue
// 检查该链接是否健康
if (connection.isHealthy(doExtensiveHealthChecks)) return connection
// 释放不健康的connection...
val toClose: Socket? = synchronized(connection) {
connection.noNewExchanges = true
call.releaseConnectionNoEvents()
}
toClose?.closeQuietly()
}
return null
}
复用判断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 // This connection is a perfect match.
}
// 1.判断是不是Http2
if (http2Connection == null) return false
// 2. 判断地址是不是同一个ip
if (routes == null || !routeMatchesAny(routes)) return false
// 3. 链接的服务器证书可以覆盖新的主机
if (address.hostnameVerifier !== OkHostnameVerifier) return false
if (!supportsUrl(address.url)) return false
// 4. 证书是否匹配
try {
address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
} catch (_: SSLPeerUnverifiedException) {
return false
}
return true
}
总结 获取复用的链接池的步骤为
- 判断:要求多路复用且不是HTTP2的返回null
- 判断:当前请求是否可以跟缓存池里的concection复用,通过isEligible() 方法来判断
- 可以复用,调用acquireConnectionNoEvents()
- 检查链接是否健康可以被使用,
- 可以则返回
- 不可以则清除,返回null
链接池的清理和回收
链接池的清理是在cleanupQueue
一个循环执行,并可以设置延时时间执行的task的线程池里操作的Queue:
cleanupTask
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}
清理cleanup
cleanup
是来执行清理的方法,该方法主要就是GC的标记清除算法,先标记后清除;判断能不能清除是通过弱引用来判断的
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 // 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 -> {
// 池内存在闲置的链接数量,在继续等待;等待时间为:保活时间-最大闲置时间
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// 有使用中的链接,再等待一个保活时间
return keepAliveDurationNs
}
else -> {
// 都不满足,没有在使用;则被清理
return -1
}
}
}
总结:cleanup的主要逻辑为
- 判断闲置的链接是否如果大于超时时间或者闲置链接的最大数量则进行清理
- 返回其下次执行清理的时间间隔,条件为
- 如果闲置的连接数大于0就返回用户设置的允许限制的时间-闲置时间最长的那个连接的闲置时间。
- 如果清理失败就返回-1
- 如果清理成功就返回0
- 如果没有闲置的链接就直接返回用户设置的最大空闲时间间隔(默认5min)
回收pruneAndGetAllocationCount
pruneAndGetAllocationCount()
方法是来判断当前链接是否在被使用,没有则进行回收;
而这个链接被创建时会被放入弱引用,就是通过判断这个弱引用来判断链接是否还在使用
private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
connection.assertThreadHoldsLock()
val references = connection.calls
var i = 0
while (i < references.size) {
val reference = references[i]
if (reference.get() != null) {
i++
continue
}
// 移除
val callReference = reference as CallReference
references.removeAt(i)
connection.noNewExchanges = true
// 没有链接,则设置可以被立即清除
if (references.isEmpty()) {
connection.idleAtNs = now - keepAliveDurationNs
return 0
}
}
return references.size
}
总结
- 获取:在RealRoutePlanner类调用RealConnectionPool.callAcquirePooledConnection方法来获取可复用的RealConnection类如果没有可复用的则返回为空并重新创建一个新的RealConnection类
- put:在一个请求成功结束后在ConnectPaln.handleSuccess方法里会把当前的RealConnection类 put到RealConnectionPool里然后也会触发清理缓存池的循环操作
- 清理:清理是在一个线程池里循环执行的,每次执行cleanup方法时会进行根据当前的空闲链接和等待时间计算下次执行的时间