在整個okhttp中,相對來說最耗資源的應(yīng)該屬于socket連接了蜗帜,所以為了節(jié)省tcp的連接釋放以及TLS協(xié)議的握手等時間恋拷,socket連接池是必不可少的。研究它的連接池厅缺,我們重點關(guān)注以下兩點:
- socket復(fù)用有何標(biāo)準(zhǔn)
- 一個socket何時會被關(guān)閉蔬顾?
okhttp的連接池代碼在ConnectionPool中,首先看下大致的結(jié)構(gòu):
public final class ConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
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;
//每個socket生存的時間
private final long keepAliveDurationNs;
// 用來清理socket的任務(wù)
private final Runnable cleanupRunnable ;//具體實現(xiàn)省略
//用來存儲socket的核心結(jié)構(gòu)
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
}
可以看到湘捎,這里使用一個Deque<RealConnection>來保存的诀豁,至于RealConnection,可以理解成對socket的包裝窥妇。這里大家要注意到舷胜,對于連接池來說查詢從來不是什么耗時操作,所以這里其實用List也是可以的活翩,沒有什么大的影響烹骨。
get操作(連接池的復(fù)用)
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
可以看到就只是遍歷了所有的連接,然后判斷某個連接是否可以復(fù)用材泄,我們看下復(fù)用的判斷代碼,在RealConnection中:
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)){
System.out.println("host not equal");
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())) {
System.out.println("host equal "+address.url().host().toString());
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.
}
對這整個方法沮焕,里面對好幾條復(fù)用規(guī)則作了判斷,我們將重點分析拉宗。
規(guī)則一:流數(shù)量要符合要求
一個socket如果被復(fù)用峦树,那么在多個請求并發(fā)進行的情況下,必然出現(xiàn)多個線程同時往一個socket中寫入數(shù)據(jù)簿废,那這樣做是否允許呢空入?這要分成兩種情況:
http1.x
在http 1.x協(xié)議下,所有的請求的都是順序的族檬,即使使用了管道技術(shù)(可以同時按順序連續(xù)發(fā)送請求歪赢,但消息的返回還是按照請求發(fā)送的順序返回)也是如此,因此一個socket在任何時刻只能有一個流在寫入单料,這意味著正在寫入數(shù)據(jù)的socket無法被另一個請求復(fù)用
http2.0
http2.0協(xié)議使用了多路復(fù)用技術(shù)埋凯,允許同一個socket在同一個時候?qū)懭攵鄠€流數(shù)據(jù),每個流有id扫尖,會進行組裝白对,因此,這個時候正在寫入數(shù)據(jù)的socket是可以被復(fù)用的换怖。
為了區(qū)分兩種情況甩恼,okhttp記錄了每個socket流使用情況,同時設(shè)定了每個socket能同時使用多少流,很明顯条摸,http1.x同一時間只能有一個流悦污,http2.0能有無數(shù)個:
if (allocations.size() >= allocationLimit || noNewStreams) {
return false;
}
現(xiàn)在再看這句if判斷就能理解了,當(dāng)然noNewStreams是在某些特殊情況下防止連接被復(fù)用時設(shè)置的钉蒲,比如服務(wù)端要求關(guān)閉這個連接切端,那當(dāng)然也不能被復(fù)用了。
小結(jié)
http1.x協(xié)議下當(dāng)前socket沒有其他流正在讀寫時可以復(fù)用顷啼,否則不行踏枣,http2.0對流數(shù)量沒有限制。
規(guī)則二 http和ssl協(xié)議配置要相同
想要復(fù)用一個http連接钙蒙,那么兩次請求的所有http配置和ssl配置都要相同:
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)){
return false;
}
具體的equalsNonHost方法實現(xià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();
}
上面具體到每個配置大家可以自己研究茵瀑,篇幅有限這里不做展開。
規(guī)則三 域名要匹配
// 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.
}
這點不用說躬厌,滿足了以上三條規(guī)則后我們可以放心的復(fù)用這個連接了瘾婿。
規(guī)則四 特殊情況
上面的三條規(guī)則如果都符合了自然是完美復(fù)用一個連接,但其實還有一種情況也是可以復(fù)用的:多個host指向同一個ip地址的情況烤咧。
在http1.x的情況,有些網(wǎng)站為了突破瀏覽器一個域名只能建立6-8個連接的限制抢呆,會給同一個ip地址配置不同的域名煮嫌,這樣瀏覽器就能使用很多連接來訪問頁面,加快頁面打開速度抱虐,但在http2.0時代昌阿,有了多路復(fù)用,一個連接完全能滿足以前的要求恳邀,所以針對這種特殊情況我們應(yīng)該復(fù)用連接懦冰,而不是新開連接,當(dāng)然這個條件是比較苛刻的谣沸。
只有在http2.0情況下才會考慮復(fù)用
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
只有在沒有代理時才能復(fù)用
// 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;
如果設(shè)置了代理刷钢,我們無法知道原始服務(wù)器的ip地址,自然無法判斷這個域名和之前的連接是否共用一個ip地址乳附,自然不能復(fù)用内地。
只有ip地址相同才能復(fù)用
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
這點是肯定的,ip地址不同socket連接肯定不可能復(fù)用赋除,無需解釋阱缓。
對不受信任的證書處理方式相同才能復(fù)用
// 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;
第一行要求對證書的處理必須是默認的OkHostnameVerifier才行,要是自己隨便實現(xiàn)的一個該接口举农,我們無法保證它和之前的連接實現(xiàn)是否一致荆针,自然無法復(fù)用。
第二行就是要求對于這個不同的host,必須通過之前的連接的證書校驗才行:
public boolean supportsUrl(HttpUrl url) {
if (url.port() != route.address().url().port()) {
return false; // Port mismatch.
}
if (!url.host().equals(route.address().url().host())) {
// We have a host mismatch. But if the certificate matches, we're still good.
return handshake != null && OkHostnameVerifier.INSTANCE.verify(
url.host(), (X509Certificate) handshake.peerCertificates().get(0));
}
return true; // Success. The URL is supported.
}
這是因為復(fù)用socket連接其實就意味著跳過了https握手的過程航背,如果不通過證書校驗太危險了喉悴。、
本地證書校驗通過才能復(fù)用
https為了防止中間人攻擊可以在建立連接成功后將服務(wù)器下發(fā)的證書保存下來沃粗,這樣如果有中間人偽裝服務(wù)器粥惧,中間人下發(fā)的證書和本地保存的不一致就會校驗失敗,最后一條規(guī)則就是保證這個校驗要通過:
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
小結(jié)
通過上面所有的校驗后最盅,這種情況下突雪,不同host同一ip地址的情況也是可以復(fù)用的。
連接池的清理
對于一個socket連接池來說必然有自己的清理機制涡贱,否則如果長期不發(fā)起網(wǎng)絡(luò)請求咏删,socket連接一直被占用就劃不來了,okhttp是通過一個單獨的線程來清理的问词。
何時開始清理工作:
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
每次put一個新連接的時候都會判斷是否需要清理督函,就是說并不會每次put都執(zhí)行,要看條件控制的激挪,然后我們看下具體清理邏輯:
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) {
}
}
}
}
}
};
這里似乎看不出什么辰狡,我們繼續(xù)看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.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
idleConnectionCount++;
// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
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;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
代碼看起來很長,其實原理很簡單垄分,就是遍歷當(dāng)前所有連接宛篇,跳過正在使用的連接馁害,其他沒有用的連接专肪,如果哪個連接超過了規(guī)定的時間蜓竹,就關(guān)掉這個socket半哟。如果都沒有超過規(guī)定時間的悄晃,就返回離規(guī)定時間最近的那個差值家淤。
拿到那個時間值后佳晶,我們再回到上面那個cleanupRunnable中抄瓦,在那里會wait線程坐求,然后醒來繼續(xù)清理蚕泽。
舉例:socket最長生存時間是30分鐘,當(dāng)前有5個連接瞻赶,c1,c2正在被使用赛糟,c3空閑了40分鐘,c4空閑了20分鐘砸逊,c5空閑了25分鐘璧南,
那么一次clean()方法會關(guān)掉c3,然后返回0,在cleanupRunnable中會立馬進行下一次循環(huán)清理师逸,這個時候檢測到離生存時間最近的是c5,那么clean()方法會返回5分鐘這個時間值司倚,cleanRunnable中會wait 5分鐘,然后5分鐘后會繼續(xù)下一次清理。
小結(jié)
理論上來說动知,只要連接池中有連接皿伺,該清理線程就一直存在,直到所有連接被釋放該線程才會停止盒粮。