1.引子
在瞭解OkHttp的複用連線池之前,我們首先要了解幾個概念。
TCP三次握手
通常我們進行HTTP連線網路的時候我們會進行TCP的三次握手,然後傳輸資料,然後再釋放連線。
TCP三次握手的過程為:
- 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待伺服器的確認;
- 第二次握手:伺服器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認,設定Acknowledgment Number為x+1(Sequence Number+1);同時,自己自己還要傳送SYN請求資訊,將SYN位置為1,Sequence Number為y;伺服器端將上述所有資訊放到一個報文段(即SYN+ACK報文段)中,一併傳送給客戶端,此時伺服器進入SYN_RECV狀態;
- 第三次握手:客戶端收到伺服器的SYN+ACK報文段。然後將Acknowledgment Number設定為y+1,向伺服器傳送ACK報文段,這個報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態,完成TCP三次握手。
TCP四次分手
當客戶端和伺服器通過三次握手建立了TCP連線以後,當資料傳送完畢,斷開連線就需要進行TCP四次分手:
- 第一次分手:主機1(可以使客戶端,也可以是伺服器端),設定Sequence Number和Acknowledgment
Number,向主機2傳送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有資料要傳送給主機2了; - 第二次分手:主機2收到了主機1傳送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number為Sequence
- 第三次分手:主機2向主機1傳送FIN報文段,請求關閉連線,同時主機2進入LAST_ACK狀態;
- 第四次分手:主機1收到主機2傳送的FIN報文段,向主機2傳送ACK報文段,然後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段以後,就關閉連線;此時,主機1等待2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,主機1也可以關閉連線了。
來看下面的圖加強下理解:
keepalive connections
當然大量的連線每次連線關閉都要三次握手四次分手的很顯然會造成效能低下,因此http有一種叫做keepalive connections的機制,它可以在傳輸資料後仍然保持連線,當客戶端需要再次獲取資料時,直接使用剛剛空閒下來的連線而不需要再次握手。
Okhttp支援5個併發KeepAlive,預設鏈路生命為5分鐘(鏈路空閒後,保持存活的時間)。
2.連線池(ConnectionPool)分析
引用計數
在okhttp中,在高層程式碼的呼叫中,使用了類似於引用計數的方式跟蹤Socket流的呼叫,這裡的計數物件是StreamAllocation,它被反覆執行aquire與release操作,這兩個函式其實是在改變RealConnection中的List<Reference<StreamAllocation>>
的大小。(StreamAllocation.Java)
1 2 3 |
public void acquire(RealConnection connection) { connection.allocations.add(new WeakReference<>(this)); } |
1 2 3 4 5 6 7 8 9 10 |
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(); } |
RealConnection是socket物理連線的包裝,它裡面維護了List<Reference<StreamAllocation>>
的引用。List中StreamAllocation的數量也就是socket被引用的計數,如果計數為0的話,說明此連線沒有被使用就是空閒的,需要通過下文的演算法實現回收;如果計數不為0,則表示上層程式碼仍然引用,就不需要關閉連線。
主要變數
連線池的類位於okhttp3.ConnectionPool:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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. */ //空閒的socket最大連線數 private final int maxIdleConnections; //socket的keepAlive時間 private final long keepAliveDurationNs; // 雙向佇列 private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning; |
主要的變數有必要說明一下:
- executor執行緒池,類似於CachedThreadPool,需要注意的是這種執行緒池的工作佇列採用了沒有容量的SynchronousQueue,不瞭解它的請檢視Java併發程式設計(六)阻塞佇列這篇文章。
Deque<RealConnection>
,雙向佇列,雙端佇列同時具有佇列和棧性質,經常在快取中被使用,裡面維護了RealConnection也就是socket物理連線的包裝。- RouteDatabase,它用來記錄連線失敗的Route的黑名單,當連線失敗的時候就會把失敗的線路加進去。
建構函式
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public ConnectionPool() { //預設空閒的socket最大連線數為5個,socket的keepAlive時間為5秒 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); } } |
通過建構函式可以看出ConnectionPool預設的空閒的socket最大連線數為5個,socket的keepAlive時間為5秒。
例項化
ConnectionPool例項化是在OkHttpClient例項化時進行的:
1 2 3 |
public OkHttpClient() { this(new Builder()); } |
在OkHttpClient的建構函式中呼叫了new Builder():
1 2 3 4 5 6 |
public Builder() { dispatcher = new Dispatcher(); ...省略 connectionPool = new ConnectionPool(); ...省略 } |
快取操作
ConnectionPool提供對Deque<RealConnection>
進行操作的方法分別為put、get、connectionBecameIdle和evictAll幾個操作。分別對應放入連線、獲取連線、移除連線和移除所有連線操作,這裡我們舉例put和get操作。
put操作
1 2 3 4 5 6 7 8 |
void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); } |
在新增到Deque<RealConnection>
之前首先要清理空閒的執行緒,這個後面會講到。
get操作
1 2 3 4 5 6 7 8 9 10 11 12 |
RealConnection get(Address address, StreamAllocation streamAllocation) { assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address) && !connection.noNewStreams) { streamAllocation.acquire(connection); return connection; } } return null; } |
遍歷connections快取列表,當某個連線計數的次數小於限制的大小並且request的地址和快取列表中此連線的地址完全匹配。則直接複用快取列表中的connection作為request的連線。
自動回收連線
okhttp是根據StreamAllocation引用計數是否為0來實現自動回收連線的。我們在put操作前首先要呼叫executor.execute(cleanupRunnable)
來清理閒置的執行緒。我們來看看cleanupRunnable到底做了什麼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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) { } } } } } }; |
執行緒不斷的呼叫cleanup來進行清理,並返回下次需要清理的間隔時間,然後呼叫wait進行等待以釋放鎖與時間片,當等待時間到了後,再次進行清理,並返回下次要清理的間隔時間,如此迴圈下去,接下來看看cleanup方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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(); //查詢此連線的StreamAllocation的引用數量,如果大於0則inUseConnectionCount數量加1,否則idleConnectionCount加1 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //如果空閒連線keepAlive時間超過5分鐘,或者空閒連線數超過5個,則從Deque中移除此連線 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); //如果空閒連線大於0,則返回此連線即將到期的時間 } else if (idleConnectionCount > 0) { // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; //如果沒有空閒連線,並且活躍連線大於0則返回5分鐘 } 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 { //如果沒有任何連線則跳出迴圈 cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; } |
cleanup所做的簡單總結就是根據連線中的引用計數來計算空閒連線數和活躍連線數,然後標記出空閒的連線,如果空閒連線keepAlive時間超過5分鐘,或者空閒連線數超過5個,則從Deque中移除此連線。接下來根據空閒連線或者活躍連線來返回下次需要清理的時間數:如果空閒連線大於0則返回此連線即將到期的時間,如果都是活躍連線並且大於0則返回預設的keepAlive時間5分鐘,如果沒有任何連線則跳出迴圈並返回-1。在上述程式碼中的第13行,通過pruneAndGetAllocationCount方法來判斷連線是否閒置的,如果pruneAndGetAllocationCount方法返回值大於0則是空閒連線,否則就是活躍連線,讓我們來看看pruneAndGetAllocationCount方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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); //若StreamAllocation被使用則接著迴圈 if (reference.get() != null) { i++; continue; } // We've discovered a leaked allocation. This is an application bug. Internal.logger.warning("A connection to " + connection.route().address().url() + " was leaked. Did you forget to close a response body?"); //若StreamAllocation未被使用則移除引用 references.remove(i); connection.noNewStreams = true; // If this was the last allocation, the connection is eligible for immediate eviction. //如果列表為空則說明此連線沒有被引用了,則返回0,表示此連線是空閒連線 if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; } } //否則返回非0的數,表示此連線是活躍連線 return references.size(); } |
pruneAndGetAllocationCount方法首先遍歷傳進來的RealConnection的StreamAllocation列表,如果StreamAllocation被使用則接著遍歷下一個StreamAllocation,如果StreamAllocation未被使用則從列表中移除。如果列表為空則說明此連線沒有引用了,則返回0,表示此連線是空閒連線,否則就返回非0的數表示此連線是活躍連線。
總結
可以看出連線池複用的核心就是用Deque<RealConnection>
來儲存連線,通過put、get、connectionBecameIdle和evictAll幾個操作來對Deque進行操作,另外通過判斷連線中的計數物件StreamAllocation來進行自動回收連線。
參考資料
okhttp3原始碼
簡析TCP的三次握手與四次分手
TCP三次握手過程
短連線、長連線與keep-alive
OkHttp3原始碼分析[複用連線池]
okhttp連線池複用機制