OkHttp3源码解析(三)——连接池复用
OKHttp3源碼解析系列
- OkHttp3源碼解析(一)之請求流程
- OkHttp3源碼解析(二)——攔截器鏈和緩存策略
本文基于OkHttp3的3.11.0版本
implementation 'com.squareup.okhttp3:okhttp:3.11.0' 復(fù)制代碼我們已經(jīng)分析了OkHttp3的攔截器鏈和緩存策略,今天我們再來看看OkHttp3的連接池復(fù)用。
客戶端和服務(wù)器建立socket連接需要經(jīng)歷TCP的三次握手和四次揮手,是一種比較消耗資源的動作。Http中有一種keepAlive connections的機(jī)制,在和客戶端通信結(jié)束以后可以保持連接指定的時間。OkHttp3支持5個并發(fā)socket連接,默認(rèn)的keepAlive時間為5分鐘。下面我們來看看OkHttp3是怎么實現(xiàn)連接池復(fù)用的。
OkHttp3的連接池--ConnectionPool
public final class ConnectionPool {//線程池,用于執(zhí)行清理空閑連接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));//最大的空閑socket連接數(shù)private final int maxIdleConnections;//socket的keepAlive時間private final long keepAliveDurationNs;private final Deque<RealConnection> connections = new ArrayDeque<>();final RouteDatabase routeDatabase = new RouteDatabase();boolean cleanupRunning; } 復(fù)制代碼ConnectionPool里的幾個重要變量:
(1)executor線程池,類似于CachedThreadPool,用于執(zhí)行清理空閑連接的任務(wù)。
(2)Deque雙向隊列,同時具有隊列和棧的性質(zhì),經(jīng)常在緩存中被使用,里面維護(hù)的RealConnection是socket物理連接的包裝
(3)RouteDatabase,用來記錄連接失敗的路線名單
下面看看ConnectionPool的構(gòu)造函數(shù)
public ConnectionPool() {this(5, 5, TimeUnit.MINUTES); }public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {this.maxIdleConnections = maxIdleConnections;this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);// Put a floor on the keep alive duration, otherwise cleanup will spin loop.if (keepAliveDuration <= 0) {throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);} } 復(fù)制代碼從構(gòu)造函數(shù)中可以看出,ConnectionPool的默認(rèn)空閑連接數(shù)為5個,keepAlive時間為5分鐘。ConnectionPool是什么時候被創(chuàng)建的呢?是在OkHttpClient的builder中:
public static final class Builder {...ConnectionPool connectionPool;...public Builder() {...connectionPool = new ConnectionPool();...}//我們也可以定制連接池public Builder connectionPool(ConnectionPool connectionPool) {if (connectionPool == null) throw new NullPointerException("connectionPool == null");this.connectionPool = connectionPool;return this;} } 復(fù)制代碼緩存操作:添加、獲取、回收連接
(1)從緩存中獲取連接
//ConnectionPool.class @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, true);return connection;}}return null; } 復(fù)制代碼獲取連接的邏輯比較簡單,就遍歷連接池里的連接connections,然后用RealConnection的isEligible方法找到符合條件的連接,如果有符合條件的連接則復(fù)用。需要注意的是,這里還調(diào)用了streamAllocation的acquire方法。acquire方法的作用是對RealConnection引用的streamAllocation進(jìn)行計數(shù),OkHttp3是通過RealConnection的StreamAllocation的引用計數(shù)是否為0來實現(xiàn)自動回收連接的。
//StreamAllocation.class public void acquire(RealConnection connection, boolean reportedAcquired) {assert (Thread.holdsLock(connectionPool));if (this.connection != null) throw new IllegalStateException();this.connection = connection;this.reportedAcquired = reportedAcquired;connection.allocations.add(new StreamAllocationReference(this, callStackTrace)); }public static final class StreamAllocationReference extends WeakReference<StreamAllocation> {public final Object callStackTrace;StreamAllocationReference(StreamAllocation referent, Object callStackTrace) {super(referent);this.callStackTrace = callStackTrace;} } 復(fù)制代碼//RealConnection.class public final List<Reference<StreamAllocation>> allocations = new ArrayList<>(); 復(fù)制代碼每一個RealConnection中都有一個allocations變量,用于記錄對于StreamAllocation的引用。StreamAllocation中包裝有HttpCodec,而HttpCodec里面封裝有Request和Response讀寫Socket的抽象。每一個請求Request通過Http來請求數(shù)據(jù)時都需要通過StreamAllocation來獲取HttpCodec,從而讀取響應(yīng)結(jié)果,而每一個StreamAllocation都是和一個RealConnection綁定的,因為只有通過RealConnection才能建立socket連接。所以StreamAllocation可以說是RealConnection、HttpCodec和請求之間的橋梁。
當(dāng)然同樣的StreamAllocation還有一個release方法,用于移除計數(shù),也就是將當(dāng)前的StreamAllocation的引用從對應(yīng)的RealConnection的引用列表中移除。
private void release(RealConnection connection) {for (int i = 0, size = connection.allocations.size(); i < size; i++) {Reference<StreamAllocation> reference = connection.allocations.get(i);if (reference.get() == this) {connection.allocations.remove(i);return;}}throw new IllegalStateException(); } 復(fù)制代碼(2)向緩存中添加連接
//ConnectionPool.class void put(RealConnection connection) {assert (Thread.holdsLock(this));if (!cleanupRunning) {cleanupRunning = true;executor.execute(cleanupRunnable);}connections.add(connection);} 復(fù)制代碼添加連接之前會先調(diào)用線程池執(zhí)行清理空閑連接的任務(wù),也就是回收空閑的連接。
(3)空閑連接的回收
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) {}}}}} }; 復(fù)制代碼cleanupRunnable中執(zhí)行清理任務(wù)是通過cleanup方法來完成,cleanup方法會返回下次需要清理的間隔時間,然后會調(diào)用wait方法釋放鎖和時間片。等時間到了就再次進(jìn)行清理。下面看看具體的清理邏輯:
long cleanup(long now) {//記錄活躍的連接數(shù)int inUseConnectionCount = 0;//記錄空閑的連接數(shù)int idleConnectionCount = 0;//空閑時間最長的連接RealConnection longestIdleConnection = null;long longestIdleDurationNs = Long.MIN_VALUE;synchronized (this) {for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {RealConnection connection = i.next();//判斷連接是否在使用,也就是通過StreamAllocation的引用計數(shù)來判斷//返回值大于0說明正在被使用if (pruneAndGetAllocationCount(connection, now) > 0) {//活躍的連接數(shù)+1inUseConnectionCount++;continue;}//說明是空閑連接,所以空閑連接數(shù)+1idleConnectionCount++;//找出了空閑時間最長的連接,準(zhǔn)備移除long idleDurationNs = now - connection.idleAtNanos;if (idleDurationNs > longestIdleDurationNs) {longestIdleDurationNs = idleDurationNs;longestIdleConnection = connection;}}if (longestIdleDurationNs >= this.keepAliveDurationNs|| idleConnectionCount > this.maxIdleConnections) {//如果空閑時間最長的連接的空閑時間超過了5分鐘//或是空閑的連接數(shù)超過了限制,就移除connections.remove(longestIdleConnection);} else if (idleConnectionCount > 0) {//如果存在空閑連接但是還沒有超過5分鐘//就返回剩下的時間,便于下次進(jìn)行清理return keepAliveDurationNs - longestIdleDurationNs;} else if (inUseConnectionCount > 0) {//如果沒有空閑的連接,那就等5分鐘后再嘗試清理return keepAliveDurationNs;} else {//當(dāng)前沒有任何連接,就返回-1,跳出循環(huán)cleanupRunning = false;return -1;}}closeQuietly(longestIdleConnection.socket());// Cleanup again immediately.return 0; } 復(fù)制代碼下面我們看看判斷連接是否是活躍連接的pruneAndGetAllocationCount方法
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);//如果存在引用,就說明是活躍連接,就繼續(xù)看下一個StreamAllocationif (reference.get() != null) {i++;continue;}// We've discovered a leaked allocation. This is an application bug.//發(fā)現(xiàn)泄漏的引用,會打印日志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;//如果列表為空,就說明此連接上沒有StreamAllocation引用了,就返回0,表示是空閑的連接if (references.isEmpty()) {connection.idleAtNanos = now - keepAliveDurationNs;return 0;}}//遍歷結(jié)束后,返回引用的數(shù)量,說明當(dāng)前連接是活躍連接return references.size(); } 復(fù)制代碼至此我們就分析完OkHttp3的連接池復(fù)用了。
總結(jié)
(1)OkHttp3中支持5個并發(fā)socket連接,默認(rèn)的keepAlive時間為5分鐘,當(dāng)然我們可以在構(gòu)建OkHttpClient時設(shè)置不同的值。
(2)OkHttp3通過Deque來存儲連接,通過put、get等操作來管理連接。
(3)OkHttp3通過每個連接的引用計數(shù)對象StreamAllocation的計數(shù)來回收空閑的連接,向連接池添加新的連接時會觸發(fā)執(zhí)行清理空閑連接的任務(wù)。清理空閑連接的任務(wù)通過線程池來執(zhí)行。
歡迎關(guān)注我的微信公眾號,和我一起每天進(jìn)步一點點! 復(fù)制代碼
總結(jié)
以上是生活随笔為你收集整理的OkHttp3源码解析(三)——连接池复用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python+OpenCV图像处理(十五
- 下一篇: 人生苦短:Python里的17个“超赞操