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源碼解析系列



歡迎關注我的微信公衆號,和我一起每天進步一點點!
AntDream
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章