OkHttp3.7源碼分析文章列表如下:
- OkHttp源碼分析——整體架構(gòu)
- OkHttp源碼分析——攔截器
- OkHttp源碼分析——任務(wù)隊(duì)列
- OkHttp源碼分析——緩存策略
- OkHttp源碼分析——多路復(fù)用
接下來講下OkHttp的連接池管理,這也是OkHttp的核心部分。通過維護(hù)連接池,最大限度重用現(xiàn)有連接,減少網(wǎng)絡(luò)連接的創(chuàng)建開銷,以此提升網(wǎng)絡(luò)請求效率。
1. 背景
1.1 keep-alive機(jī)制
在HTTP1.0中HTTP的請求流程如下:
這種方法的好處是簡單,各個請求互不干擾。但在復(fù)雜的網(wǎng)絡(luò)請求場景下這種方式幾乎不可用。例如:瀏覽器加載一個HTML網(wǎng)頁,HTML中可能需要加載數(shù)十個資源,典型場景下這些資源中大部分來自同一個站點(diǎn)。按照HTTP1.0的做法,這需要建立數(shù)十個TCP連接,每個連接負(fù)責(zé)一個資源請求。創(chuàng)建一個TCP連接需要3次握手,而釋放連接則需要2次或4次握手。重復(fù)的創(chuàng)建和釋放連接極大地影響了網(wǎng)絡(luò)效率,同時(shí)也增加了系統(tǒng)開銷。
為了有效地解決這一問題,HTTP/1.1提出了Keep-Alive
機(jī)制:當(dāng)一個HTTP請求的數(shù)據(jù)傳輸結(jié)束后,TCP連接不立即釋放,如果此時(shí)有新的HTTP請求,且其請求的Host通上次請求相同,則可以直接復(fù)用為釋放的TCP連接,從而省去了TCP的釋放和再次創(chuàng)建的開銷,減少了網(wǎng)絡(luò)延時(shí):
在現(xiàn)代瀏覽器中,一般同時(shí)開啟6~8個keepalive connections的socket連接,并保持一定的鏈路生命,當(dāng)不需要時(shí)再關(guān)閉;而在服務(wù)器中,一般是由軟件根據(jù)負(fù)載情況(比如FD最大值、Socket內(nèi)存、超時(shí)時(shí)間、棧內(nèi)存、棧數(shù)量等)決定是否主動關(guān)閉。
1.2 HTTP/2
在HTTP/1.x中,如果客戶端想發(fā)起多個并行請求必須建立多個TCP連接,這無疑增大了網(wǎng)絡(luò)開銷。另外HTTP/1.x不會壓縮請求和響應(yīng)報(bào)頭,導(dǎo)致了不必要的網(wǎng)絡(luò)流量;HTTP/1.x不支持資源優(yōu)先級導(dǎo)致底層TCP連接利用率低下。而這些問題都是HTTP/2要著力解決的。簡單來說HTTP/2主要解決了以下問題:
- 報(bào)頭壓縮:HTTP/2使用HPACK壓縮格式壓縮請求和響應(yīng)報(bào)頭數(shù)據(jù),減少不必要流量開銷
- 請求與響應(yīng)復(fù)用:HTTP/2通過引入新的二進(jìn)制分幀層實(shí)現(xiàn)了完整的請求和響應(yīng)復(fù)用,客戶端和服務(wù)器可以將HTTP消息分解為互不依賴的幀,然后交錯發(fā)送,最后再在另一端將其重新組裝
- 指定數(shù)據(jù)流優(yōu)先級:將 HTTP 消息分解為很多獨(dú)立的幀之后,我們就可以復(fù)用多個數(shù)據(jù)流中的幀,客戶端和服務(wù)器交錯發(fā)送和傳輸這些幀的順序就成為關(guān)鍵的性能決定因素。為了做到這一點(diǎn),HTTP/2 標(biāo)準(zhǔn)允許每個數(shù)據(jù)流都有一個關(guān)聯(lián)的權(quán)重和依賴關(guān)系
- 流控制:HTTP/2 提供了一組簡單的構(gòu)建塊,這些構(gòu)建塊允許客戶端和服務(wù)器實(shí)現(xiàn)其自己的數(shù)據(jù)流和連接級流控制
HTTP/2所有性能增強(qiáng)的核心在于新的二進(jìn)制分幀層,它定義了如何封裝HTTP消息并在客戶端與服務(wù)器之間進(jìn)行傳輸:
同時(shí)HTTP/2引入了三個新的概念:
- 數(shù)據(jù)流:基于TCP連接之上的邏輯雙向字節(jié)流,對應(yīng)一個請求及其響應(yīng)。客戶端每發(fā)起一個請求就建立一個數(shù)據(jù)流,后續(xù)該請求及其響應(yīng)的所有數(shù)據(jù)都通過該數(shù)據(jù)流傳輸
- 消息:一個請求或響應(yīng)對應(yīng)的一系列數(shù)據(jù)幀
- 幀:HTTP/2的最小數(shù)據(jù)切片單位
上述概念之間的邏輯關(guān)系:
- 所有通信都在一個 TCP 連接上完成,此連接可以承載任意數(shù)量的雙向數(shù)據(jù)流
- 每個數(shù)據(jù)流都有一個唯一的標(biāo)識符和可選的優(yōu)先級信息,用于承載雙向消息
- 每條消息都是一條邏輯 HTTP 消息(例如請求或響應(yīng)),包含一個或多個幀
- 幀是最小的通信單位,承載著特定類型的數(shù)據(jù),例如 HTTP 標(biāo)頭、消息負(fù)載,等等。 來自不同數(shù)據(jù)流的幀可以交錯發(fā)送,然后再根據(jù)每個幀頭的數(shù)據(jù)流標(biāo)識符重新組裝
- 每個HTTP消息被分解為多個獨(dú)立的幀后可以交錯發(fā)送,從而在宏觀上實(shí)現(xiàn)了多個請求或響應(yīng)并行傳輸?shù)男Ч_@類似于多進(jìn)程環(huán)境下的時(shí)間分片機(jī)制
2. 連接池的使用與分析
無論是HTTP/1.1的Keep-Alive
機(jī)制還是HTTP/2的多路復(fù)用機(jī)制,在實(shí)現(xiàn)上都需要引入連接池來維護(hù)網(wǎng)絡(luò)連接。接下來看下OkHttp中的連接池實(shí)現(xiàn)。
OkHttp內(nèi)部通過ConnectionPool來管理連接池,首先來看下ConnectionPool的主要成員:
public final class ConnectionPool {
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;
private final long keepAliveDurationNs;
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
......
}
};
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
......
/**
*返回符合要求的可重用連接,如果沒有返回NULL
*/
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
......
}
/*
* 去除重復(fù)連接。主要針對多路復(fù)用場景下一個address只需要一個連接
*/
Socket deduplicate(Address address, StreamAllocation streamAllocation) {
......
}
/*
* 將連接加入連接池
*/
void put(RealConnection connection) {
......
}
/*
* 當(dāng)有連接空閑時(shí)喚起cleanup線程清洗連接池
*/
boolean connectionBecameIdle(RealConnection connection) {
......
}
/**
* 掃描連接池,清除空閑連接
*/
long cleanup(long now) {
......
}
/*
* 標(biāo)記泄露連接
*/
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
......
}
}
相關(guān)概念:
-
Call
:對Http請求的封裝 -
Connection/RealConnection
:物理連接的封裝,其內(nèi)部有List<WeakReference<StreamAllocation>>
的引用計(jì)數(shù) -
StreamAllocation
: okhttp中引入了StreamAllocation負(fù)責(zé)管理一個連接上的流,同時(shí)在connection中也通過一個StreamAllocation的引用的列表來管理一個連接的流,從而使得連接與流之間解耦。關(guān)于StreamAllocation的定義可以看下這篇文章:okhttp源碼學(xué)習(xí)筆記(二)-- 連接與連接管理 -
connections
: Deque雙端隊(duì)列,用于維護(hù)連接的容器 -
routeDatabase
:用來記錄連接失敗的Route
的黑名單,當(dāng)連接失敗的時(shí)候就會把失敗的線路加進(jìn)去
2.1 實(shí)例化
首先來看下ConnectionPool的實(shí)例化過程,一個OkHttpClient只包含一個ConnectionPool,其實(shí)例化過程也在OkHttpClient的實(shí)例化過程中實(shí)現(xiàn),值得一提的是ConnectionPool各個方法的調(diào)用并沒有直接對外暴露,而是通過OkHttpClient的Internal接口統(tǒng)一對外暴露:
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
static {
Internal.instance = new Internal() {
@Override public void addLenient(Headers.Builder builder, String line) {
builder.addLenient(line);
}
@Override public void addLenient(Headers.Builder builder, String name, String value) {
builder.addLenient(name, value);
}
@Override public void setCache(Builder builder, InternalCache internalCache) {
builder.setInternalCache(internalCache);
}
@Override public boolean connectionBecameIdle(
ConnectionPool pool, RealConnection connection) {
return pool.connectionBecameIdle(connection);
}
@Override public RealConnection get(ConnectionPool pool, Address address,
StreamAllocation streamAllocation, Route route) {
return pool.get(address, streamAllocation, route);
}
@Override public boolean equalsNonHost(Address a, Address b) {
return a.equalsNonHost(b);
}
@Override public Socket deduplicate(
ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
return pool.deduplicate(address, streamAllocation);
}
@Override public void put(ConnectionPool pool, RealConnection connection) {
pool.put(connection);
}
@Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
return connectionPool.routeDatabase;
}
@Override public int code(Response.Builder responseBuilder) {
return responseBuilder.code;
}
@Override
public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) {
tlsConfiguration.apply(sslSocket, isFallback);
}
@Override public HttpUrl getHttpUrlChecked(String url)
throws MalformedURLException, UnknownHostException {
return HttpUrl.getChecked(url);
}
@Override public StreamAllocation streamAllocation(Call call) {
return ((RealCall) call).streamAllocation();
}
@Override public Call newWebSocketCall(OkHttpClient client, Request originalRequest) {
return new RealCall(client, originalRequest, true);
}
};
......
}
這樣做的原因是:
Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation
packages. The only implementation of this interface is in {@link OkHttpClient}.
Internal的唯一實(shí)現(xiàn)在OkHttpClient中,OkHttpClient通過這種方式暴露其API給外部類使用。
2.2 連接池維護(hù)
ConnectionPool內(nèi)部通過一個雙端隊(duì)列(dequeue)來維護(hù)當(dāng)前所有連接,主要涉及到的操作包括:
- put:放入新連接
- get:從連接池中獲取連接
- evictAll:關(guān)閉所有連接
- connectionBecameIdle:連接變空閑后調(diào)用清理線程
- deduplicate:清除重復(fù)的多路復(fù)用線程
2.2.1 StreamAllocation.findConnection
get是ConnectionPool中最為重要的方法,StreamAllocation
在其findConnection方法內(nèi)部通過調(diào)用get方法為其找到stream找到合適的連接,如果沒有則新建一個連接。首先來看下findConnection
的邏輯:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
// 一個StreamAllocation刻畫的是一個Call的數(shù)據(jù)流動,一個Call可能存在多次請求(重定向,Authenticate等),所以當(dāng)發(fā)生類似重定向等事件時(shí)優(yōu)先使用原有的連接
RealConnection allocatedConnection = this.connection;
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// 試圖從連接池中找到可復(fù)用的連接
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
return connection;
}
selectedRoute = route;
}
// 獲取路由配置,所謂路由其實(shí)就是代理,ip地址等參數(shù)的一個組合
if (selectedRoute == null) {
selectedRoute = routeSelector.next();
}
RealConnection result;
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
//拿到路由后可以嘗試重新從連接池中獲取連接,這里主要針對http2協(xié)議下清除域名碎片機(jī)制
Internal.instance.get(connectionPool, address, this, selectedRoute);
if (connection != null) return connection;
//新建連接
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
//修改result連接stream計(jì)數(shù),方便connection標(biāo)記清理
acquire(result);
}
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
// 將新建的連接放入到連接池中
Internal.instance.put(connectionPool, result);
// 如果同時(shí)存在多個連向同一個地址的多路復(fù)用連接,則關(guān)閉多余連接,只保留一個
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
return result;
}
其主要邏輯大致分為以下幾個步驟:
- 查看當(dāng)前streamAllocation是否有之前已經(jīng)分配過的連接,有則直接使用
- 從連接池中查找可復(fù)用的連接,有則返回該連接
- 配置路由,配置后再次從連接池中查找是否有可復(fù)用連接,有則直接返回
- 新建一個連接,并修改其StreamAllocation標(biāo)記計(jì)數(shù),將其放入連接池中
- 查看連接池是否有重復(fù)的多路復(fù)用連接,有則清除
2.2.2 ConnectionPool.get
接下來再來看get方法的源碼:
[ConnectionPool.java]
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;
}
其邏輯比較簡單,遍歷當(dāng)前連接池,如果有符合條件的連接則修改器標(biāo)記計(jì)數(shù),然后返回。這里的關(guān)鍵邏輯在RealConnection.isEligible
方法:
[RealConnection.java]
/**
* Returns true if this connection can carry a stream allocation to {@code address}. If non-null
* {@code route} is the resolved route for a connection.
*/
public boolean isEligible(Address address, 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.
}
// At this point we don't have a hostname match. But we still be able to carry the request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
// 2. The routes must share an IP address. This requires us to have a DNS address for both
// hosts, which only happens after route planning. We can't coalesce connections that use a
// proxy, since proxies don't tell us the origin server's IP address.
if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
- 連接沒有達(dá)到共享上限
- 非host域必須完全一樣
- 如果此時(shí)host域也相同,則符合條件,可以被復(fù)用
- 如果host不相同,在HTTP/2的域名切片場景下一樣可以復(fù)用,具體細(xì)節(jié)可以參考:https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
2.2.3 deduplicate
deduplicate方法主要是針對在HTTP/2場景下多個多路復(fù)用連接清除的場景。如果當(dāng)前連接是HTTP/2,那么所有指向該站點(diǎn)的請求都應(yīng)該基于同一個TCP連接:
[ConnectionPool.java]
/**
* Replaces the connection held by {@code streamAllocation} with a shared connection if possible.
* This recovers when multiple multiplexed connections are created concurrently.
*/
Socket deduplicate(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, null)
&& connection.isMultiplexed()
&& connection != streamAllocation.connection()) {
return streamAllocation.releaseAndAcquire(connection);
}
}
return null;
}
put和evictAll比較簡單,在這里就不寫了,大家自行看源碼。
2.3 自動回收
連接池中有socket回收,而這個回收是以RealConnection
的弱引用List<Reference<StreamAllocation>>
是否為0來為依據(jù)的。ConnectionPool有一個獨(dú)立的線程cleanupRunnable
來清理連接池,其觸發(fā)時(shí)機(jī)有兩個:
- 當(dāng)連接池中put新的連接時(shí)
- 當(dāng)connectionBecameIdle接口被調(diào)用時(shí)
其代碼如下:
while (true) {
//執(zhí)行清理并返回下場需要清理的時(shí)間
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
synchronized (ConnectionPool.this) {
try {
//在timeout內(nèi)釋放鎖與時(shí)間片
ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
} catch (InterruptedException ignored) {
}
}
}
}
這段死循環(huán)實(shí)際上是一個阻塞的清理任務(wù),首先進(jìn)行清理(clean),并返回下次需要清理的間隔時(shí)間,然后調(diào)用wait(timeout)
進(jìn)行等待以釋放鎖與時(shí)間片,當(dāng)?shù)却龝r(shí)間到了后,再次進(jìn)行清理,并返回下次要清理的間隔時(shí)間...
接下來看下cleanup函數(shù):
[ConnectionPool.java]
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;
//遍歷`Deque`中所有的`RealConnection`,標(biāo)記泄漏的連接
synchronized (this) {
for (RealConnection connection : connections) {
// 查詢此連接內(nèi)部StreamAllocation的引用數(shù)量
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
//選擇排序法,標(biāo)記出空閑連接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
//如果(`空閑socket連接超過5個`
//且`keepalive時(shí)間大于5分鐘`)
//就將此泄漏連接從`Deque`中移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
//返回此連接即將到期的時(shí)間,供下次清理
//這里依據(jù)是在上文`connectionBecameIdle`中設(shè)定的計(jì)時(shí)
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//全部都是活躍的連接,5分鐘后再次清理
return keepAliveDurationNs;
} else {
//沒有任何連接,跳出循環(huán)
cleanupRunning = false;
return -1;
}
}
//關(guān)閉連接,返回`0`,也就是立刻再次清理
closeQuietly(longestIdleConnection.socket());
return 0;
}
其基本邏輯如下:
- 遍歷連接池中所有連接,標(biāo)記泄露連接
- 如果被標(biāo)記的連接滿足(
空閑socket連接超過5個
&&keepalive時(shí)間大于5分鐘
),就將此連接從Deque
中移除,并關(guān)閉連接,返回0
,也就是將要執(zhí)行wait(0)
,提醒立刻再次掃描 - 如果(
目前還可以塞得下5個連接,但是有可能泄漏的連接(即空閑時(shí)間即將達(dá)到5分鐘)
),就返回此連接即將到期的剩余時(shí)間,供下次清理 - 如果(
全部都是活躍的連接
),就返回默認(rèn)的keep-alive
時(shí)間,也就是5分鐘后再執(zhí)行清理
而pruneAndGetAllocationCount
負(fù)責(zé)標(biāo)記并找到不活躍連接:
[ConnnecitonPool.java]
//類似于引用計(jì)數(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);
//如果正在被使用,跳過,接著循環(huán)
//是否置空是在上文`connectionBecameIdle`的`release`控制的
if (reference.get() != null) {
//非常明顯的引用計(jì)數(shù)
i++;
continue;
}
//否則移除引用
references.remove(i);
connection.noNewStreams = true;
//如果所有分配的流均沒了,標(biāo)記為已經(jīng)距離現(xiàn)在空閑了5分鐘
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
return references.size();
}
OkHttp的連接池通過計(jì)數(shù)+標(biāo)記清理的機(jī)制來管理連接池,使得無用連接可以被會回收,并保持多個健康的keep-alive連接。這也是OkHttp的連接池能保持高效的關(guān)鍵原因。