Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android網絡編程(八)源碼解析OkHttp中篇[復用連接池]

Android網絡編程(八)源碼解析OkHttp中篇[復用連接池]

編輯:關於Android編程

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狀態;<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPrXayP20zs7Vytajur/Nu6e2y8rVtb23/s7xxve1xFNZTitBQ0uxqM7Ets6ho8i7uvO9q0Fja25vd2xlZGdtZW50IE51bWJlcsno1sPOqnkrMaOsz/K3/s7xxve3osvNQUNLsajOxLbOo6zV4rj2sajOxLbOt6LLzc3qsc/S1Lrzo6y/zbuntsu6zbf+zvHG97bLtry9+MjrRVNUQUJMSVNIRUTXtMyso6zN6rPJVENQyP20zs7VytahozwvcD4NCjxoNCBpZD0="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> 的大小。(StreamAllocation.java)

  public void acquire(RealConnection connection) {
    connection.allocations.add(new WeakReference<>(this));
  }
  private void release(RealConnection connection) {
    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
      Reference reference = connection.allocations.get(i);
      if (reference.get() == this) {
        connection.allocations.remove(i);
        return;
      }
    }
    throw new IllegalStateException();
  }

RealConnection是socket物理連接的包裝,它裡面維護了List>的引用。List中StreamAllocation的數量也就是socket被引用的計數,如果計數為0的話,說明此連接沒有被使用就是空閒的,需要通過下文的算法實現回收;如果計數不為0,則表示上層代碼仍然引用,就不需要關閉連接。

主要變量

連接池的類位於okhttp3.ConnectionPool:

 private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue(), 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 connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;

主要的變量有必要說明一下:

executor線程池,類似於CachedThreadPool,需要注意的是這種線程池的工作隊列采用了沒有容量的SynchronousQueue,不了解它的請查看Java並發編程(六)阻塞隊列這篇文章。 Deque,雙向隊列,雙端隊列同時具有隊列和棧性質,經常在緩存中被使用,裡面維護了RealConnection也就是socket物理連接的包裝。 RouteDatabase,它用來記錄連接失敗的Route的黑名單,當連接失敗的時候就會把失敗的線路加進去。

構造函數

  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實例化時進行的:

  public OkHttpClient() {
    this(new Builder());
  }

在OkHttpClient的構造函數中調用了new Builder():

  public Builder() {
      dispatcher = new Dispatcher();
     ...省略
      connectionPool = new ConnectionPool();
     ...省略
    }

緩存操作

ConnectionPool提供對Deque進行操作的方法分別為put、get、connectionBecameIdle和evictAll幾個操作。分別對應放入連接、獲取連接、移除連接和移除所有連接操作,這裡我們舉例put和get操作。

put操作

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

在添加到Deque之前首先要清理空閒的線程,這個後面會講到。

get操作

  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到底做了什麼:

  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方法:

  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 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方法:

  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List> references = connection.allocations;
    //遍歷弱引用列表
    for (int i = 0; i < references.size(); ) {
      Reference 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來存儲連接,通過put、get、connectionBecameIdle和evictAll幾個操作來對Deque進行操作,另外通過判斷連接中的計數對象StreamAllocation來進行自動回收連接。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved