OkHttp 3.7原始碼分析(五)——連線池

yangxi_001發表於2017-06-19

接下來講下OkHttp的連線池管理,這也是OkHttp的核心部分。通過維護連線池,最大限度重用現有連線,減少網路連線的建立開銷,以此提升網路請求效率。

1. 背景

1.1 keep-alive機制

在HTTP1.0中HTTP的請求流程如下:

connectionpool_keepalive.png

這種方法的好處是簡單,各個請求互不干擾。但在複雜的網路請求場景下這種方式幾乎不可用。例如:瀏覽器載入一個HTML網頁,HTML中可能需要載入數十個資源,典型場景下這些資源中大部分來自同一個站點。按照HTTP1.0的做法,這需要建立數十個TCP連線,每個連線負責一個資源請求。建立一個TCP連線需要3次握手,而釋放連線則需要2次或4次握手。重複的建立和釋放連線極大地影響了網路效率,同時也增加了系統開銷。

為了有效地解決這一問題,HTTP/1.1提出了Keep-Alive機制:當一個HTTP請求的資料傳輸結束後,TCP連線不立即釋放,如果此時有新的HTTP請求,且其請求的Host通上次請求相同,則可以直接複用為釋放的TCP連線,從而省去了TCP的釋放和再次建立的開銷,減少了網路延時:

connection_keepalive2.png

在現代瀏覽器中,一般同時開啟6~8個keepalive connections的socket連線,並保持一定的鏈路生命,當不需要時再關閉;而在伺服器中,一般是由軟體根據負載情況(比如FD最大值、Socket記憶體、超時時間、棧記憶體、棧數量等)決定是否主動關閉。

1.2 HTTP/2

在HTTP/1.x中,如果客戶端想發起多個並行請求必須建立多個TCP連線,這無疑增大了網路開銷。另外HTTP/1.x不會壓縮請求和響應報頭,導致了不必要的網路流量;HTTP/1.x不支援資源優先順序導致底層TCP連線利用率低下。而這些問題都是HTTP/2要著力解決的。簡單來說HTTP/2主要解決了以下問題:

  • 報頭壓縮:HTTP/2使用HPACK壓縮格式壓縮請求和響應報頭資料,減少不必要流量開銷
  • 請求與響應複用:HTTP/2通過引入新的二進位制分幀層實現了完整的請求和響應複用,客戶端和伺服器可以將HTTP訊息分解為互不依賴的幀,然後交錯傳送,最後再在另一端將其重新組裝
  • 指定資料流優先順序:將 HTTP 訊息分解為很多獨立的幀之後,我們就可以複用多個資料流中的幀,客戶端和伺服器交錯傳送和傳輸這些幀的順序就成為關鍵的效能決定因素。為了做到這一點,HTTP/2 標準允許每個資料流都有一個關聯的權重和依賴關係
  • 流控制:HTTP/2 提供了一組簡單的構建塊,這些構建塊允許客戶端和伺服器實現其自己的資料流和連線級流控制

HTTP/2所有效能增強的核心在於新的二進位制分幀層,它定義瞭如何封裝HTTP訊息並在客戶端與伺服器之間進行傳輸:

http2framing.png

同時HTTP/2引入了三個新的概念:

  • 資料流:基於TCP連線之上的邏輯雙向位元組流,對應一個請求及其響應。客戶端每發起一個請求就建立一個資料流,後續該請求及其響應的所有資料都通過該資料流傳輸
  • 訊息:一個請求或響應對應的一系列資料幀
  • 幀:HTTP/2的最小資料切片單位

上述概念之間的邏輯關係:

  • 所有通訊都在一個 TCP 連線上完成,此連線可以承載任意數量的雙向資料流
  • 每個資料流都有一個唯一的識別符號和可選的優先順序資訊,用於承載雙向訊息
  • 每條訊息都是一條邏輯 HTTP 訊息(例如請求或響應),包含一個或多個幀
  • 幀是最小的通訊單位,承載著特定型別的資料,例如 HTTP 標頭、訊息負載,等等。 來自不同資料流的幀可以交錯傳送,然後再根據每個幀頭的資料流識別符號重新組裝
  • 每個HTTP訊息被分解為多個獨立的幀後可以交錯傳送,從而在巨集觀上實現了多個請求或響應並行傳輸的效果。這類似於多程式環境下的時間分片機制

http2multiplexing.png

2. 連線池的使用與分析

無論是HTTP/1.1的Keep-Alive機制還是HTTP/2的多路複用機制,在實現上都需要引入連線池來維護網路連線。接下來看下OkHttp中的連線池實現。

OkHttp內部通過ConnectionPool來管理連線池,首先來看下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));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
        ......
    }
  };

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;
  ......

    /**
    *返回符合要求的可重用連線,如果沒有返回NULL
   */
  RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    ......
  }

  /*
  * 去除重複連線。主要針對多路複用場景下一個address只需要一個連線
  */
  Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    ......
    }

  /*
  * 將連線加入連線池
  */
  void put(RealConnection connection) {
      ......
  }

  /*
  * 當有連線空閒時喚起cleanup執行緒清洗連線池
  */
  boolean connectionBecameIdle(RealConnection connection) {
      ......
  }

  /**
   * 掃描連線池,清除空閒連線
  */
  long cleanup(long now) {
    ......
  }

  /*
   * 標記洩露連線
  */
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    ......
  }
}
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

相關概念:

  • Call:對Http請求的封裝
  • Connection/RealConnection:物理連線的封裝,其內部有List<WeakReference<StreamAllocation>>的引用計數
  • StreamAllocation: okhttp中引入了StreamAllocation負責管理一個連線上的流,同時在connection中也通過一個StreamAllocation的引用的列表來管理一個連線的流,從而使得連線與流之間解耦。關於StreamAllocation的定義可以看下這篇文章:okhttp原始碼學習筆記(二)– 連線與連線管理
  • connections: Deque雙端佇列,用於維護連線的容器
  • routeDatabase:用來記錄連線失敗的Route的黑名單,當連線失敗的時候就會把失敗的線路加進去

2.1 例項化

首先來看下ConnectionPool的例項化過程,一個OkHttpClient只包含一個ConnectionPool,其例項化過程也在OkHttpClient的例項化過程中實現,值得一提的是ConnectionPool各個方法的呼叫並沒有直接對外暴露,而是通過OkHttpClient的Internal介面統一對外暴露:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    static {
    Internal.instance = new Internal() {
      @Override public void addLenient(Headers.Builder builder, String line) {
        builder.addLenient(line);
      }

      @Override public void addLenient(Headers.Builder builder, String name, String value) {
        builder.addLenient(name, value);
      }

      @Override public void setCache(Builder builder, InternalCache internalCache) {
        builder.setInternalCache(internalCache);
      }

      @Override public boolean connectionBecameIdle(
          ConnectionPool pool, RealConnection connection) {
        return pool.connectionBecameIdle(connection);
      }

      @Override public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
        return pool.get(address, streamAllocation, route);
      }

      @Override public boolean equalsNonHost(Address a, Address b) {
        return a.equalsNonHost(b);
      }

      @Override public Socket deduplicate(
          ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
        return pool.deduplicate(address, streamAllocation);
      }

      @Override public void put(ConnectionPool pool, RealConnection connection) {
        pool.put(connection);
      }

      @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
        return connectionPool.routeDatabase;
      }

      @Override public int code(Response.Builder responseBuilder) {
        return responseBuilder.code;
      }

      @Override
      public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback)       {
        tlsConfiguration.apply(sslSocket, isFallback);
      }

      @Override public HttpUrl getHttpUrlChecked(String url)
          throws MalformedURLException, UnknownHostException {
        return HttpUrl.getChecked(url);
      }

      @Override public StreamAllocation streamAllocation(Call call) {
        return ((RealCall) call).streamAllocation();
      }

      @Override public Call newWebSocketCall(OkHttpClient client, Request originalRequest) {
        return new RealCall(client, originalRequest, true);
      }
    };
     ......
}
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

這樣做的原因是:

Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation
packages. The only implementation of this interface is in {@link OkHttpClient}.
  • 1
  • 2
  • 1
  • 2

Internal的唯一實現在OkHttpClient中,OkHttpClient通過這種方式暴露其API給外部類使用。

2.2 連線池維護

ConnectionPool內部通過一個雙端佇列(dequeue)來維護當前所有連線,主要涉及到的操作包括:

  • put:放入新連線
  • get:從連線池中獲取連線
  • evictAll:關閉所有連線
  • connectionBecameIdle:連線變空閒後呼叫清理執行緒
  • deduplicate:清除重複的多路複用執行緒
2.2.1 StreamAllocation.findConnection

get是ConnectionPool中最為重要的方法,StreamAllocation在其findConnection方法內部通過呼叫get方法為其找到stream找到合適的連線,如果沒有則新建一個連線。首先來看下findConnection的邏輯:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                        boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // 一個StreamAllocation刻畫的是一個Call的資料流動,一個Call可能存在多次請求(重定向,Authenticate等),所以當發生類似重定向等事件時優先使用原有的連線
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // 試圖從連線池中找到可複用的連線
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }

    // 獲取路由配置,所謂路由其實就是代理,ip地址等引數的一個組合
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      //拿到路由後可以嘗試重新從連線池中獲取連線,這裡主要針對http2協議下清除域名碎片機制
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      //新建連線
      route = selectedRoute;
      refusedStreamCount = 0;
      result = new RealConnection(connectionPool, selectedRoute);
      //修改result連線stream計數,方便connection標記清理
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // 將新建的連線放入到連線池中
      Internal.instance.put(connectionPool, result);

      // 如果同時存在多個連向同一個地址的多路複用連線,則關閉多餘連線,只保留一個
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 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
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

其主要邏輯大致分為以下幾個步驟:

  • 檢視當前streamAllocation是否有之前已經分配過的連線,有則直接使用
  • 從連線池中查詢可複用的連線,有則返回該連線
  • 配置路由,配置後再次從連線池中查詢是否有可複用連線,有則直接返回
  • 新建一個連線,並修改其StreamAllocation標記計數,將其放入連線池中
  • 檢視連線池是否有重複的多路複用連線,有則清除
2.2.2 ConnectionPool.get

接下來再來看get方法的原始碼:

[ConnectionPool.java]
  RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

其邏輯比較簡單,遍歷當前連線池,如果有符合條件的連線則修改器標記計數,然後返回。這裡的關鍵邏輯在RealConnection.isEligible方法:

[RealConnection.java]
/**
   * Returns true if this connection can carry a stream allocation to {@code address}. If non-null
   * {@code route} is the resolved route for a connection.
   */
  public boolean isEligible(Address address, 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)) 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())) {
      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.
  }
  • 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
  • 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
2.2.3 deduplicate

deduplicate方法主要是針對在HTTP/2場景下多個多路複用連線清除的場景。如果當前連線是HTTP/2,那麼所有指向該站點的請求都應該基於同一個TCP連線:

[ConnectionPool.java]
  /**
   * Replaces the connection held by {@code streamAllocation} with a shared connection if possible.
   * This recovers when multiple multiplexed connections are created concurrently.
   */
  Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, null)
          && connection.isMultiplexed()
          && connection != streamAllocation.connection()) {
        return streamAllocation.releaseAndAcquire(connection);
      }
    }
    return null;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

put和evictAll比較簡單,在這裡就不寫了,大家自行看原始碼。

2.3 自動回收

連線池中有socket回收,而這個回收是以RealConnection的弱引用List<Reference<StreamAllocation>>是否為0來為依據的。ConnectionPool有一個獨立的執行緒cleanupRunnable來清理連線池,其觸發時機有兩個:

  • 當連線池中put新的連線時
  • 當connectionBecameIdle介面被呼叫時

其程式碼如下:

while (true) {
  //執行清理並返回下場需要清理的時間
  long waitNanos = cleanup(System.nanoTime());
  if (waitNanos == -1) return;
  if (waitNanos > 0) {
    synchronized (ConnectionPool.this) {
      try {
        //在timeout內釋放鎖與時間片
        ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
      } catch (InterruptedException ignored) {
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

這段死迴圈實際上是一個阻塞的清理任務,首先進行清理(clean),並返回下次需要清理的間隔時間,然後呼叫wait(timeout)進行等待以釋放鎖與時間片,當等待時間到了後,再次進行清理,並返回下次要清理的間隔時間…

接下來看下cleanup函式:

[ConnectionPool.java]
long cleanup(long now) {
  int inUseConnectionCount = 0;
  int idleConnectionCount = 0;
  RealConnection longestIdleConnection = null;
  long longestIdleDurationNs = Long.MIN_VALUE;

  //遍歷`Deque`中所有的`RealConnection`,標記洩漏的連線
  synchronized (this) {
    for (RealConnection connection : connections) {
      // 查詢此連線內部StreamAllocation的引用數量
      if (pruneAndGetAllocationCount(connection, now) > 0) {
        inUseConnectionCount++;
        continue;
      }

      idleConnectionCount++;

      //選擇排序法,標記出空閒連線
      long idleDurationNs = now - connection.idleAtNanos;
      if (idleDurationNs > longestIdleDurationNs) {
        longestIdleDurationNs = idleDurationNs;
        longestIdleConnection = connection;
      }
    }

    if (longestIdleDurationNs >= this.keepAliveDurationNs
        || idleConnectionCount > this.maxIdleConnections) {
      //如果(`空閒socket連線超過5個`
      //且`keepalive時間大於5分鐘`)
      //就將此洩漏連線從`Deque`中移除
      connections.remove(longestIdleConnection);
    } else if (idleConnectionCount > 0) {
      //返回此連線即將到期的時間,供下次清理
      //這裡依據是在上文`connectionBecameIdle`中設定的計時
      return keepAliveDurationNs - longestIdleDurationNs;
    } else if (inUseConnectionCount > 0) {
      //全部都是活躍的連線,5分鐘後再次清理
      return keepAliveDurationNs;
    } else {
      //沒有任何連線,跳出迴圈
      cleanupRunning = false;
      return -1;
    }
  }

  //關閉連線,返回`0`,也就是立刻再次清理
  closeQuietly(longestIdleConnection.socket());
  return 0;
}
  • 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
  • 50
  • 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
  • 50

其基本邏輯如下:

  • 遍歷連線池中所有連線,標記洩露連線
  • 如果被標記的連線滿足(空閒socket連線超過5個&&keepalive時間大於5分鐘),就將此連線從Deque中移除,並關閉連線,返回0,也就是將要執行wait(0),提醒立刻再次掃描
  • 如果(目前還可以塞得下5個連線,但是有可能洩漏的連線(即空閒時間即將達到5分鐘)),就返回此連線即將到期的剩餘時間,供下次清理
  • 如果(全部都是活躍的連線),就返回預設的keep-alive時間,也就是5分鐘後再執行清理

pruneAndGetAllocationCount負責標記並找到不活躍連線:

[ConnnecitonPool.java]
//類似於引用計數法,如果引用全部為空,返回立刻清理
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);
    //如果正在被使用,跳過,接著迴圈
    //是否置空是在上文`connectionBecameIdle`的`release`控制的
    if (reference.get() != null) {
      //非常明顯的引用計數
      i++;
      continue;
    }

    //否則移除引用
    references.remove(i);
    connection.noNewStreams = true;

    //如果所有分配的流均沒了,標記為已經距離現在空閒了5分鐘
    if (references.isEmpty()) {
      connection.idleAtNanos = now - keepAliveDurationNs;
      return 0;
    }
  }

  return references.size();
}
  • 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
  • 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

OkHttp的連線池通過計數+標記清理的機制來管理連線池,使得無用連線可以被會回收,並保持多個健康的keep-alive連線。這也是OkHttp的連線池能保持高效的關鍵原因。

轉自:http://blog.csdn.net/asialiyazhou/article/details/72598365

相關文章