蠻久之前寫過一篇博客OkHttp源碼解析,相信大多數同學也看過或者瞭解過OkHttp的整體架構使用的是基於責任鏈模式的攔截器鏈。
其實這個庫的其他設計也是蠻有意思的,這篇筆記我們就來看看它的Http連接是怎麼實現的。
這部分的代碼我們從ConnectInterceptor這個攔截器看起,它主要就是負責http的連接。新版本的OkHttp已經用kotlin重寫了,所以下面的代碼都是kotlin的:
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)
}
}
class RealCall(...) : Call {
internal fun initExchange(chain: RealInterceptorChain): Exchange {
...
val codec = exchangeFinder.find(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)
...
return result
}
}
ConnectInterceptor的代碼很簡單,主要的功能就是初始化RealCall的Exchange。這個Exchange的功能就是基於http的Connection進行數據交換。
Connect緩存
在代碼裏面我們可以看到這個Exchange的codec是find出來的,codec的功能就算http報文的編解碼。一般來講用find的話就意味着它可能存在緩存:
class ExchangeFinder(
private val connectionPool: RealConnectionPool,
internal val address: Address,
private val call: RealCall,
private val eventListener: EventListener
) {
...
fun find(client: OkHttpClient, chain: RealInterceptorChain ): ExchangeCodec {
...
val resultConnection = findHealthyConnection(...)
return resultConnection.newCodec(client, chain)
...
}
private fun findHealthyConnection(...): RealConnection {
...
val candidate = findConnection(...)
...
}
private fun findConnection(...): RealConnection {
...
// 從連接池裏面查找可用連接
if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
val result = call.connection!!
eventListener.connectionAcquired(call, result)
return result
}
...
// 找不到的話創建新的連接
val newConnection = RealConnection(connectionPool, route)
...
// 連接socket
newConnection.connect(...)
...
// 將新連接丟到連接池
connectionPool.put(newConnection)
// 綁定RealCall和連接
call.acquireConnectionNoEvents(newConnection)
...
return newConnection
}
...
}
class RealCall(...) : Call {
fun acquireConnectionNoEvents(connection: RealConnection) {
...
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
}
從上面的代碼我們可以看出來,實際上並不是codec有緩存,而是http的Connection有緩存。codec是通過這個緩存的Connection創建出來的。
Connection實際上就維護着一條socket連接,我們可以看newConnection.connect的具體實現:
fun connect(...) {
...
connectSocket(connectTimeout, readTimeout, call, eventListener)
...
}
private fun connectSocket(...) {
val proxy = route.proxy
val address = route.address
val rawSocket = when (proxy.type()) {
Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
else -> Socket(proxy)
}
...
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
...
}
也就是並不是每次請求都會創建一條新的socket連接:
請求計數
HTTP/1.0中,每次http請求都要創建一個tcp連接,而tcp連接的創建會消耗時間和資源(需要三次握手)。HTTP/1.1中引入了重用連接的機制,就是在http請求頭中加入Connection: keep-alive
來告訴對方這個請求響應完成後不要關閉,下次請求可以繼續使用這條鏈接。
HTTP 1.X中一條連接同時只能進行一次請求,也就是說必須一個將上次Request的Response完全讀取之後才能發送下一次Request,而HTTP 2中添加了鏈路複用的機制同時可以發送多個Request。
於是這裏就存在了請求計數和請求數量計算的問題,那麼OkHttp是如何實現的呢?
前面章節中創建或者複用Connect的時候都會調用到RealCall.acquireConnectionNoEvents,將RealCall的弱引用丟到connection.calls裏面,於是就完成了請求的計數:
class RealCall(...) : Call {
fun acquireConnectionNoEvents(connection: RealConnection) {
...
this.connection = connection
connection.calls.add(CallReference(this, callStackTrace))
}
}
internal class CallReference(...) : WeakReference<RealCall>(referent)
有add就有remove,正如我們上面所說,一次請求實際上就是發送一個Request並將它的Response完全讀取。我們用Response.string舉例,它最終是通過Exchange使用socket從服務端讀取的數據:
abstract class ResponseBody : Closeable {
@Throws(IOException::class)
fun string(): String = source().use { source ->
source.readString(charset = source.readBomAsCharset(charset()))
}
}
class Exchange(...) {
override fun read(sink: Buffer, byteCount: Long): Long {
...
val read = delegate.read(sink, byteCount)
...
if (read == -1L) {
complete(null)
return -1L
}
val newBytesReceived = bytesReceived + read
...
if (newBytesReceived == contentLength) {
complete(null)
}
return read
...
}
}
中間的過程過於曲折我就不一步步跟蹤了,大家只要知道最終會調到Exchange.read方法。裏面有兩種情況讀取到-1代表與服務器已經斷開連接,讀取的長度等於Response header裏面的Content-Length字段,代表本次Response的全部數據已經讀取完成。
這兩者都代表這這次請求已經完成,會調用complete方法,最終調到RealCall.releaseConnectionNoEvents將它從connection.calls裏面刪掉:
class Exchange(...) {
...
fun <E : IOException?> complete(e: E): E {
...
return bodyComplete(bytesReceived, responseDone = true, requestDone = false, e = e)
}
...
fun <E : IOException?> bodyComplete(...): E {
...
return call.messageDone(this, requestDone, responseDone, e)
}
...
}
class RealCall(...) : Call {
...
internal fun <E : IOException?> messageDone(...): E {
return callDone(e)
}
...
private fun <E : IOException?> callDone(e: E): E {
releaseConnectionNoEvents()
}
...
internal fun releaseConnectionNoEvents(): Socket? {
...
val calls = connection.calls
val index = calls.indexOfFirst { it.get() == this@RealCall }
...
calls.removeAt(index)
...
if (calls.isEmpty()) {
connection.idleAtNs = System.nanoTime()
...
}
...
}
...
}
這裏就涉及到兩個使用OkHttp容易不小心出現的錯誤:
- Response.string只能調用一次
- Response必須被讀取
由於Response.string讀取完成之後這次請求其實就已經結束了,而且OkHttp並沒有對這個結果做緩存,所以下次再讀取就會出現java.lang.IllegalStateException: closed異常。
而我們從上面的流程知道,connection.calls的remove要Response讀取完成後執行,如果我們得到一個Response之後一直不去讀取的話實際上它會一直佔中這這個Connect,下次HTTP 1.X的請求就不能複用這套鏈接,要新建一條Connect。
請求限制
通過connection.calls我們能知道當前有多少個請求在佔用這條connection,所以在連接池裏面就能對次數進行限制。
從前面篇幅我們知道ExchangeFinder是通過RealConnectionPool.callAcquirePooledConnection從連接緩存池查找的Connection:
fun callAcquirePooledConnection(...): 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
}
connection.isEligible裏面除了判斷address是否相等之外還會判斷請求數量是否已滿:
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
...
// 連接次數是否已滿,在HTTP 1.X的情況下allocationLimit總是爲1
if (calls.size >= allocationLimit || noNewExchanges) return false
// 判斷地址是否線條
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.
}
...
}
allocationLimit在HTTP 1.X的情況下allocationLimit總是爲1就保證了HTTP 1.X的情況下每次只能跑一個請求。
連接的斷開
從上面的流從我們看到,連接在請求完成之後是不會斷開的,等待下次請求複用。如果一直不去斷開的話,就會有一個資源佔用的問題。那麼OkHttp是在什麼時候斷開連接的呢?
其實RealConnectionPool內部會有個cleanupTask專門用於連接的清理
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}
它會在RealConnectionPool的put(加入新連接)、connectionBecameIdle(有連接空閒)裏面被調用:
fun put(connection: RealConnection) {
connection.assertThreadHoldsLock()
connections.add(connection)
cleanupQueue.schedule(cleanupTask)
}
fun connectionBecameIdle(connection: RealConnection): Boolean {
connection.assertThreadHoldsLock()
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connection.noNewExchanges = true
connections.remove(connection)
if (connections.isEmpty()) cleanupQueue.cancelAll()
true
} else {
cleanupQueue.schedule(cleanupTask)
false
}
}
cleanupQueue會根據Task.runOnce的返回值等待一段時間再次調用runOnce:
abstract class Task(...) {
...
/** Returns the delay in nanoseconds until the next execution, or -1L to not reschedule. */
abstract fun runOnce(): Long
...
}
這裏的runOnce實際就是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) {
// 如果這個connection還在使用(Response還沒有讀完),就計數然後繼續搜索
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 -> {
// 如果空閒最久的連接比keepAliveDurationNs這個值要大就回收
val connection = longestIdleConnection!!
...
// 關閉socket
connection.socket().closeQuietly()
if (connections.isEmpty()) cleanupQueue.cancelAll()
// 我們只回收了空閒超時最久的連接,可能還會有其他連接也超時了,返回0讓它立馬進行下一次清理
return 0L
}
idleConnectionCount > 0 -> {
// 如果有空閒連接,就計算最近的一次空閒超時的時間,去等待
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// 如果所有連接都在使用,就等待這個超時時間去重新檢查清理
return keepAliveDurationNs
}
else -> {
// 如果沒有連接,就不需要再檢查了
return -1
}
}
}
也就是說這裏面會查找空閒過久的連接,然後關閉它的socket。然後計算下一次進行cleanup的等待時長。
pruneAndGetAllocationCount返回的是正在佔用的請求數,用於檢測連接是否空閒。但是其實它內部還會去回收泄露的Response:
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
}
// We've discovered a leaked call. This is an application bug.
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)
references.removeAt(i)
connection.noNewExchanges = true
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNs = now - keepAliveDurationNs
return 0
}
}
return references.size
}
這裏的"A connection to ${connection.route().address.url} was leaked. Did you forget to close a response body?"指定就是前面請求計數那裏講的容易出現問題的第二點:得到一個Response之後一直不去讀取的話實際上它會一直佔中這這個Connect,具體可能是下面的樣子:
client.newCall(getRequest()).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
// 啥都不幹
}
});
onResponse傳入的response沒有人去讀取數據,就會一直佔用連接,但是由於它在後面又沒有人引用就會被GC回收導致這條連接再也不能斷開。
pruneAndGetAllocationCount裏面就通過弱引用get返回null的方式去檢查到這樣的異常,進行清理動作。
低版本的清理流程
上面講的是最新版本的清理流程,低版本的流程稍微有點差異但是原理大致相同:
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
會專門爲連接的清理開一條線程用while true的方式不斷檢查,當然類似的會使用wait方法等待cleanup返回的時間,減少cpu佔用。