簡介
上一篇文章(OkHttp 源碼解析(二):建立連接)分析了 OkHttp 建立連接的過程,主要涉及到的幾個類包括 StreamAllocation
、RealConnection
以及 HttpCodec
,其中 RealConnection
封裝了底層的 Socket。Socket 建立了 TCP 連接,這是需要消耗時間和資源的,而 OkHttp 則使用連接池來管理這里連接,進(jìn)行連接的重用,提高請求的效率。OkHttp 中的連接池由 ConnectionPool
實現(xiàn),本文主要是對這個類進(jìn)行分析。
get 和 put
在 StreamAllocation
的 findConnection
方法中,有這樣一段代碼:
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
Internal.instance.get
最終是從 ConnectionPool
取得一個RealConnection
, 如果有了則直接返回。下面是 ConnectionPool
中的代碼:
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}
connections
是 ConnectionPool
中的一個隊列:
private final Deque<RealConnection> connections = new ArrayDeque<>();
從隊列中取出一個 Connection
之后,判斷其是否能滿足重用的要求:
public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// 省略 http2 相關(guān)代碼
...
}
boolean equalsNonHost(Address that) {
return this.dns.equals(that.dns)
&& this.proxyAuthenticator.equals(that.proxyAuthenticator)
&& this.protocols.equals(that.protocols)
&& this.connectionSpecs.equals(that.connectionSpecs)
&& this.proxySelector.equals(that.proxySelector)
&& equal(this.proxy, that.proxy)
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
&& equal(this.certificatePinner, that.certificatePinner)
&& this.url().port() == that.url().port();
}
如果這個 Connection
已經(jīng)分配的數(shù)量超過了分配限制或者被標(biāo)記為不能再分配,則直接返回 false
,否則調(diào)用 equalsNonHost
,主要是判斷 Address
中除了 host
以外的變量是否相同,如果有不同的,那么這個連接也不能重用。最后就是判斷 host
是否相同,如果相同那么對于當(dāng)前的 Address
來說, 這個 Connection
便是可重用的。從上面的代碼看來,get
邏輯還是比較簡單明了的。
接下來看一下 put
,在 StreamAllocation
的 findConnection
方法中,如果新創(chuàng)建了 Connection
,則將其放到連接池中。
Internal.instance.put(connectionPool, result);
最終調(diào)用的是 ConnectionPool#put
:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
首先判斷其否啟動了清理線程,如果沒有則將 cleanupRunnable
放到線程池中。最后是將 RealConnection
放到隊列中。
cleanup
線程池需要對閑置的或者超時的連接進(jìn)行清理,CleanupRunnable
就是做這件事的:
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) {
}
}
}
}
}
};
run
里面有個無限循環(huán),調(diào)用 cleanup
之后,得到一個時間 waitNano
,如果不為 -1 則表示線程的睡眠時間,接下來調(diào)用 wait
進(jìn)入睡眠。如果是 -1,則表示當(dāng)前沒有需要清理的連接,直接返回即可。
清理的主要實現(xiàn)在 cleanup
方法中,下面是其代碼:
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
// 1. 判斷是否是空閑連接
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
// 2. 判斷是否是最長空閑時間的連接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
// 3. 如果最長空閑的時間超過了設(shè)定的最大值,或者空閑鏈接數(shù)量超過了最大數(shù)量,則進(jìn)行清理,否則計算下一次需要清理的等待時間
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (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.
cleanupRunning = false;
return -1;
}
}
// 3. 關(guān)閉連接的socket
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
清理的邏輯大致是以下幾步:
遍歷所有的連接,對每個連接調(diào)用
pruneAndGetAllocationCount
判斷其是否閑置的連接。如果是正在使用中,則直接遍歷一下個。對于閑置的連接,判斷是否是當(dāng)前空閑時間最長的。
對于當(dāng)前空閑時間最長的連接,如果其超過了設(shè)定的最長空閑時間(5分鐘)或者是最大的空閑連接的數(shù)量(5個),則清理此連接。否則計算下次需要清理的時間,這樣
cleanupRunnable
中的循環(huán)變會睡眠相應(yīng)的時間,醒來后繼續(xù)清理。
pruneAndGetAllocationCount
用于清理可能泄露的 StreamAllocation
并返回正在使用此連接的 StreamAllocation
的數(shù)量,代碼如下:
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
List<Reference<StreamAllocation>> references = connection.allocations;
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
if (reference.get() != null) {
i++;
continue;
}
// We've discovered a leaked allocation. This is an application bug.
// 如果 StreamAlloction 引用被回收,但是 connection 的引用列表中扔持有,那么可能發(fā)生了內(nèi)存泄露
StreamAllocation.StreamAllocationReference streamAllocRef =
(StreamAllocation.StreamAllocationReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
references.remove(i);
connection.noNewStreams = true;
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
return references.size();
}
如果 StreamAllocation
已經(jīng)被回收,說明應(yīng)用層的代碼已經(jīng)不需要這個連接,但是 Connection
仍持有 StreamAllocation
的引用,則表示StreamAllocation
中 release(RealConnection connection)
方法未被調(diào)用,可能是讀取 ResponseBody
沒有關(guān)閉 I/O 導(dǎo)致的。
總結(jié)
OkHttp 中的連接池主要就是保存一個正在使用的連接的隊列,對于滿足條件的同一個 host 的多個連接復(fù)用同一個 RealConnection
,提高請求效率。此外,還會啟動線程對閑置超時或者超出閑置數(shù)量的 RealConnection
進(jìn)行清理。