========================================================================
连接池,就是请求通过复用存在的连接,达到节省开辟新连接所需开销的结果。这也是一种设计模式,是一种浅学设计模式之享元模式(19/23)。
因为连接的场景有多种(Spdy、SSL、WebSocket等),所以Socket的种类也有多种,连接池的分类可以参照下图:
上图展示了连接池的多种不同类型,可以看到最根基的的TCPSocket
连接,分别来看下每个连接池的作用:
- SSL连接池
管理SSLSocket,但SSLSocket又依赖于TCP连接池提供的TCPSocket
- HTTP代理连接池
如果走HTTP协议,那么就需要TCP连接池提供TCPSocket,如果走HTTPS协议,那么就需要SSL连接池提供SSLSocket;
- SpdySession池
依赖SSL连接池提供SSLSocket,这里需要说明下,虽然HTTP/2协议没有强制绑定HTTPS,但是在实际开发中确实都是绑定HTTPS
- SOCKS连接池
管理的SOCKSSocket和SOCKS5Socket都需要依赖TCP连接池提供的TCPSocket
- WebSocket连接池
依赖TCP连接池提供的TCPSocket,声明下这里没有说明WSS(Web Socket Secure)的情况
=========================================================================
这里参考的是OkHttp4的代码
连接池的类位于okhttp3.ConnectionPool
。我们需要了解到如何在timeout时间内复用connection,并且有效的对其进行回收清理操作。我们先来看看该类的作用,因为有文档注释,我们来看看官方是如何描述该类的
翻译:该类管理 Http/Http2 的连接复用,用来减少网络的消耗。有着相同ip地址的Http请求可以共享一个连接通道。该类实现了一种长连接的策略。
构造函数创造了一个新的连接池和附带参数,这些参数可能会在未来的OkHttp版本中被更改(也就是说不建议我们直接使用)。目前这个连接池可以最多同时持有5个闲置的连接,如果有多的连接,将会被移除掉。
// ConnectionPool.kt
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
))
// 1
constructor() : this(5, 5, TimeUnit.MINUTES)
…
}
这是构造函数,注释1中可以看出,默认就的最多闲置连接是5个,保持时间是5分钟,taskRunner
是一个线程管理器,用来检测闲置socket并对其进行清理,在3.x版本中,它是一个Executor
。然后这个类就没别的东西了,其他的都在它的父类RealConnectionPool
里面了
RealConnectionPool
是真正的连接池,ConnectionPool
是其子类,他除了刚刚那几个子类传来的参数之外,还有一个很重要的参数:
/**
- 使用线程安全的双向队列来管理所有的 [RealConnection]—Socket连接
*/
private val connections = ConcurrentLinkedQueue()
连接池可以通过 connections
来管理连接的添加、删除、复用。
3.2.1 put操作
fun put(connection: RealConnection) {
connection.assertThreadHoldsLock()
// 1
connections.add(connection)
// 2
cleanupQueue.schedule(cleanupTask)
}
注释1: 在连接池connections中添加一个连接。
注释2: 需要整理一遍connections里的连接,比如说多出来的连接需要删除掉,超过保持时长的连接要去掉。
3.2.2 判断连接是否可以复用
在3.x版本,该类提供了一个方法来返回一个可复用的连接,主要逻辑是遍历connections的所有连接,判断是否有连接可复用。而4.x的版本稍微的更改逻辑,先来看下这个方法:
// 1
fun callAcquirePooledConnection(
address: Address,
call: RealCall,
routes: List?,
requireMultiplexed: Boolean
): Boolean {
for (connection in connections) {
synchronized(connection) {
// 2
if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
// 3
if (!connection.isEligible(address, routes)) return@synchronized
// 4
call.acquireConnectionNoEvents(connection)
return true
}
}
return false
}
注释1: 传入一个ip地址,该方法就是判断是否已经存在该ip打通的socket,如果有返回true,说明可以复用,否则返回false
注释2: 判断连接的多路复用,这个属性是给Http2用的
注释3: 检查ip地址和路由列表是否合法
注释4: 调用 RealCall.acquireConnectionNoEvents()
方法,将RealCall的 connection指向该连接,表明存在可以复用的连接,并且返回true。那么调用者就可以通过它的RealCall来获取到复用的连接了。
可以看下 RealCall的方法:
// RealCall.kt
fun acquireConnectionNoEvents(connection: RealConnection) {
connection.assertThreadHoldsLock()
check(this.connection == null)
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
3.2.3 清除和回收连接
在刚刚put方法里面,我们看到了该类会实现一个方法来check连接池里的连接,它的作用是清除和回收超时和多出来的连接,我们来看看这个方法,因为方法比较长,所以分成两个部分来看,下面是上半部分:
// RealConnectionPool.kt
/**
-
作用是维护连接池,删除那些超时的连接、或者超出最大数量限制的连接
-
返回的值是睡眠到下次执行该方法的时间,
-
如果不需要进一步清理,则返回-1
*/
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
// 1
for (connection in connections) {
synchronized(connection) {
// 2
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
} else {
idleConnectionCount++
// 3
val idleDurationNs = now - connection.idleAtNs
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
} else {
Unit
}
}
}
}
…
}
注释1: 遍历连接池内的所有连接
注释2: 调用 pruneAndGetAllocationCount()
方法,查看该连接是否正在被使用。如果正在使用,则工作连接+1
,否则 闲置连接+1
注释3: 计算该连接的闲置时间。遍历一圈,记录下闲置时间最久的连接。
再来看下cleanup()
的下半部分:
// RealConnectionPool.kt
…
when {
// 1
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
(longestIdleConnection)
}
connection.socket().closeQuietly()
if (connections.isEmpty()) cleanupQueue.cancelAll()
// Clean up again immediately.
return 0L
}
// 2
idleConnectionCount > 0 -> {
return keepAliveDurationNs - longestIdleDurationNs
}
// 3
inUseConnectionCount > 0 -> {
return keepAliveDurationNs
}
// 4
else -> {
return -1
}
}
这里是根据上半部分的统计结果进行处理:
注释1:闲置最久的连接时间已经超过5分钟或者当前空闲的连接数超过了5个,则通过 connections.remove()
和 connection.socket().closeQuietly()
移除掉闲置最久的连接,
注释2:当前存在闲置连接,则返回 闲置最久的连接还需要等待多少时间就到5分钟 的时间间隔
注释3:当前没有闲置连接,有工作连接, 则返回 5分钟
注释4:既没有工作连接又没有闲置连接,返回-1
这个方法主要就是通过计算有无超时的限制连接或则超过容量的连接进行删除,其中它使用了一个方法 pruneAndGetAllocationCount()
来查看一个连接是否正在被使用,我们可以看看这个方法的逻辑。
3.2.4 查看连接是否闲置
// RealConnectionPool.kt
/**
-
删除所有的发生泄漏的回调,然后返回[Connection]剩余的实时的被调用的数量
-
如果一个回调正在被引用但是实际上已经被代码不使用他们了,这个回调就是泄漏的,
-
这种泄漏检测是不靠谱的,而且依赖于 GC回收
*/
private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
connection.assertThreadHoldsLock()
// 1
val references = connection.calls
var i = 0
// 2
while (i < references.size) {
val reference = references[i]
// 3
if (reference.get() != null) {
i++
continue
}
// 4
val callReference = reference as CallReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
“Did you forget to close a response body?”
Platform.get().logCloseableLeak(message, callReference.callStackTrace)
// 5
references.removeAt(i)
connection.noNewExchanges = true
if (references.isEmpty()) {
connection.idleAtNs = now - keepAliveDurationNs
return 0
}
}
return references.size
}