OkHttp3原始碼解析(三)——連線池複用
OKHttp3原始碼解析系列
本文基於OkHttp3的3.11.0版本
implementation `com.squareup.okhttp3:okhttp:3.11.0`
我們已經分析了OkHttp3的攔截器鏈和快取策略,今天我們再來看看OkHttp3的連線池複用。
客戶端和伺服器建立socket連線需要經歷TCP的三次握手和四次揮手,是一種比較消耗資源的動作。Http中有一種keepAlive connections的機制,在和客戶端通訊結束以後可以保持連線指定的時間。OkHttp3支援5個併發socket連線,預設的keepAlive時間為5分鐘。下面我們來看看OkHttp3是怎麼實現連線池複用的。
OkHttp3的連線池–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));
//最大的空閒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;
}
ConnectionPool裡的幾個重要變數:
(1)executor執行緒池,類似於CachedThreadPool,用於執行清理空閒連線的任務。
(2)Deque雙向佇列,同時具有佇列和棧的性質,經常在快取中被使用,裡面維護的RealConnection是socket物理連線的包裝
(3)RouteDatabase,用來記錄連線失敗的路線名單
下面看看ConnectionPool的建構函式
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);
}
}
從建構函式中可以看出,ConnectionPool的預設空閒連線數為5個,keepAlive時間為5分鐘。ConnectionPool是什麼時候被建立的呢?是在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;
}
}
快取操作:新增、獲取、回收連線
(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;
}
獲取連線的邏輯比較簡單,就遍歷連線池裡的連線connections,然後用RealConnection的isEligible方法找到符合條件的連線,如果有符合條件的連線則複用。需要注意的是,這裡還呼叫了streamAllocation的acquire方法。acquire方法的作用是對RealConnection引用的streamAllocation進行計數,OkHttp3是通過RealConnection的StreamAllocation的引用計數是否為0來實現自動回收連線的。
//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;
}
}
//RealConnection.class
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
每一個RealConnection中都有一個allocations變數,用於記錄對於StreamAllocation的引用。StreamAllocation中包裝有HttpCodec,而HttpCodec裡面封裝有Request和Response讀寫Socket的抽象。每一個請求Request通過Http來請求資料時都需要通過StreamAllocation來獲取HttpCodec,從而讀取響應結果,而每一個StreamAllocation都是和一個RealConnection繫結的,因為只有通過RealConnection才能建立socket連線。所以StreamAllocation可以說是RealConnection、HttpCodec和請求之間的橋樑。
當然同樣的StreamAllocation還有一個release方法,用於移除計數,也就是將當前的StreamAllocation的引用從對應的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();
}
(2)向快取中新增連線
//ConnectionPool.class
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
新增連線之前會先呼叫執行緒池執行清理空閒連線的任務,也就是回收空閒的連線。
(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) {
}
}
}
}
}
};
cleanupRunnable中執行清理任務是通過cleanup方法來完成,cleanup方法會返回下次需要清理的間隔時間,然後會呼叫wait方法釋放鎖和時間片。等時間到了就再次進行清理。下面看看具體的清理邏輯:
long cleanup(long now) {
//記錄活躍的連線數
int inUseConnectionCount = 0;
//記錄空閒的連線數
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的引用計數來判斷
//返回值大於0說明正在被使用
if (pruneAndGetAllocationCount(connection, now) > 0) {
//活躍的連線數+1
inUseConnectionCount++;
continue;
}
//說明是空閒連線,所以空閒連線數+1
idleConnectionCount++;
//找出了空閒時間最長的連線,準備移除
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
//如果空閒時間最長的連線的空閒時間超過了5分鐘
//或是空閒的連線數超過了限制,就移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
//如果存在空閒連線但是還沒有超過5分鐘
//就返回剩下的時間,便於下次進行清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//如果沒有空閒的連線,那就等5分鐘後再嘗試清理
return keepAliveDurationNs;
} else {
//當前沒有任何連線,就返回-1,跳出迴圈
cleanupRunning = false;
return -1;
}
}
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
return 0;
}
下面我們看看判斷連線是否是活躍連線的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);
//如果存在引用,就說明是活躍連線,就繼續看下一個StreamAllocation
if (reference.get() != null) {
i++;
continue;
}
// We`ve discovered a leaked allocation. This is an application bug.
//發現洩漏的引用,會列印日誌
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;
}
}
//遍歷結束後,返回引用的數量,說明當前連線是活躍連線
return references.size();
}
至此我們就分析完OkHttp3的連線池複用了。
總結
(1)OkHttp3中支援5個併發socket連線,預設的keepAlive時間為5分鐘,當然我們可以在構建OkHttpClient時設定不同的值。
(2)OkHttp3通過Deque來儲存連線,通過put、get等操作來管理連線。
(3)OkHttp3通過每個連線的引用計數物件StreamAllocation的計數來回收空閒的連線,向連線池新增新的連線時會觸發執行清理空閒連線的任務。清理空閒連線的任務通過執行緒池來執行。
OKHttp3原始碼解析系列
歡迎關注我的微信公眾號,和我一起每天進步一點點!
相關文章
- MOSN 原始碼解析 - 連線池原始碼
- 資料庫連線池-Druid資料庫連線池原始碼解析資料庫UI原始碼
- Hikari連線池原始碼解讀原始碼
- ServiceStack.Redis的原始碼分析(連線與連線池)Redis原始碼
- 《四 資料庫連線池原始碼》手寫資料庫連線池資料庫原始碼
- OkHttp 原始碼剖析系列(六)——連線複用機制及連線的建立HTTP原始碼
- OkHttp3原始碼解析(一)之請求流程HTTP原始碼
- 以太坊交易池原始碼解析原始碼
- 婚戀交友原始碼開發,採用連線複用實現效能優化原始碼優化
- netty原始碼分析之新連線接入全解析Netty原始碼
- 從原始碼分析DBCP資料庫連線池的原理原始碼資料庫
- 【JDBC】使用OracleDataSource建立連線池用於連線OracleJDBCOracle
- Spring原始碼解析之基礎應用(三)Spring原始碼
- 連線池
- Java原始碼解析 ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java原始碼解析 - ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java執行緒池ThreadPoolExecutor原始碼解析Java執行緒thread原始碼
- 雨露均沾的OkHttp—WebSocket長連線的使用&原始碼解析HTTPWeb原始碼
- Picasso-原始碼解析(三)原始碼
- slate原始碼解析(三)- 定位原始碼
- FaceBook POP原始碼解析三原始碼
- 基於RabbitMQ.Client元件實現RabbitMQ可複用的 ConnectionPool(連線池)MQclient元件
- 註冊中心 Eureka 原始碼解析 —— 應用例項註冊發現(三)之下線原始碼
- Netty原始碼解析 -- 記憶體池與PoolArenaNetty原始碼記憶體
- HTTP連線池HTTP
- django連線池Django
- 在SpringBoot中使用R2DBC連線池的原始碼和教程Spring Boot原始碼
- 【Mybatis原始碼解析】- JDBC連線資料庫的原理和操作MyBatis原始碼JDBC資料庫
- MQTT(EMQX) - SpringBoot 整合MQTT 連線池 Demo - 附原始碼 + 線上客服聊天架構圖MQQTSpring Boot原始碼架構
- http連線複用進化論HTTP
- MongoDB原始碼分析之連結池(ConnPool)GWMongoDB原始碼
- RxDownload2 原始碼解析(三)原始碼
- Spring原始碼解析之BeanFactoryPostProcessor(三)Spring原始碼Bean
- Spring原始碼解析之ConfigurationClassPostProcessor(三)Spring原始碼
- Http持久連線與HttpClient連線池HTTPclient
- 連線池和連線數詳解
- 從原始碼中分析關於phpredis中的連線池可持有數目原始碼PHPRedis
- btcpool礦池原始碼分析(3)-BlockMaker模組解析TCP原始碼BloC