OkHttp2連接池復用原理分析


一、概述

  上一節講了OkHttp3的從創建HttpClient到最后調用call.enqueue(callback)來執行一個網絡請求並接收響應結果的源碼分析流程。流程分析下來能夠幫助我們理解這個框架,在理解整個執行流程的基礎上我們分析一下上一節未分析到的遺留問題。比如:OkHttp3的連接池的復用。

二、連接池原理

  多少了解點OkHttp3的同學都知道,OkHttp可以降低網絡延時加快網絡請求響應的速度。那么它是怎樣做到的呢?在說這個之前,我們先簡單回顧一下Http協議。Http協議是一個無連接的協議,客戶端(請求頭+請求體)發送請求給服務端,服務端收到(請求頭+請求體)后響應數據(響應頭和響應體)並返回。由於Http協議的底層實現是基於TCP協議的(保證數據准確到達),所以在請求+響應的過程中必然少不了Tcp的三次握手和釋放資源時的四次揮手。我們假設有這么一種情況,客戶端需要每隔10秒向服務端發送心跳包,如果按照無連接的狀態每次客戶端請求和服務端響應都需要經過Tcp的三次握手和四次揮手,這樣高頻率的發送重復的請求會嚴重影響網絡的性能,就算除去頭部字段在頻繁三次握手和四次揮手的情況下網絡性能也非常堪憂。那么有沒有一種辦法能夠讓,Http的鏈接保持一段時間,如果有形同請求時復用這個鏈接,在超時的時候把鏈接斷掉,從而減少握手次數呢?答案是肯定的,OkHttp3已經幫我們設計好了。

   OkHttp3連接池原理:OkHttp3使用ConnectionPool連接池來復用鏈接,其原理是:當用戶發起請求是,首先在鏈接池中檢查是否有符合要求的鏈接(復用就在這里發生),如果有就用該鏈接發起網絡請求,如果沒有就創建一個鏈接發起請求。這種復用機制可以極大的減少網絡延時並加快網絡的請求和響應速度。

三、源碼分析

  我們主要看下ConnectionPool連接池的源代碼,看其是怎樣實現的,我們一段一段拆分着看。

private final int maxIdleConnections;//每個地址最大的空閑連接數
private final long keepAliveDurationNs;
private final Deque<RealConnection> connections = new ArrayDeque<>();//連接池,其是一個雙端鏈表結果,支持在頭尾插入元素,且是一個后進先出的隊列
 final RouteDatabase routeDatabase = new RouteDatabase();//用來記錄鏈接失敗的路由 
boolean cleanupRunning;
  private static final Executor executor = new ThreadPoolExecutor(0 /* 核心線程數 */,
      Integer.MAX_VALUE /*線程池可容納的最大線程數量 */, 60L /* 線程池中的線程最大閑置時間 */, TimeUnit.SECONDS,/*閑置時間的單位*/
      new SynchronousQueue<Runnable>()/*線程池中的任務隊列,通過線程池的execute方法提交的runnable會放入這個隊列中*/, Util.threadFactory("OkHttp ConnectionPool", true)
/*工具類用來創建線程的,其原型是ThreadFactory*/);

  通過上面的代碼可知,ConnectionPool中會創建一個線程池,這個線程池的作用就是為了清理掉閑置的鏈接(Socket)。ConnectionPool利用自身的put方法向連接池中添加鏈接(每一個RealConnection都是一個鏈接)

 void put(RealConnection connection) {
   //java1.4中新增的關鍵字,如果為true無異常,如果為false則拋出一個異常 assert (Thread.holdsLock(this));
  //利用線程池清除空閑的Socket if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); }
  //向鏈接池中加入鏈接 connections.add(connection); }

  通過以上代碼我們發現向線程池中添加一個鏈接(RealConnection)其實是向連接池connections添加RealConnection。並且在添加之前需要調用線程池的execute方法區清理閑置的鏈接。

下面我們看下清理動作是如何實現的,直接看cleanupRunnable這個匿名內部類

 private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
   //死循環,不停的執行cleanup的清理工作 while (true) {
     //返回下次清理的時間間隔 long waitNanos = cleanup(System.nanoTime());
    //如果返回-1就直接停止 if (waitNanos == -1) return;
    //如果下次清理的時間幾個大於0 if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try {
        //根據下次返回的時間間隔來釋放wait鎖 ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } };

  在Runnable的內部會不停的執行死循環,調用cleanup來清理空閑的鏈接,並返回一個下次清理的時間間隔,根據這個時間間隔來釋放wait鎖。

  接下來看下cleanup的具體執行步驟

long cleanup(long now) {
    int inUseConnectionCount = 0;//正在使用的鏈接數量
    int idleConnectionCount = 0;//閑置的鏈接數量
  //長時間閑置的鏈接 RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // 用for循環來遍歷連接池 synchronized (this) { for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // 如果當前鏈接正在使用,就執行continue,進入下一次循環. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; }      //否則閑置鏈接+1 idleConnectionCount++; // 如果閑置時間間隔大於最大的閑置時間,那就把當前的鏈接賦值給最大閑置時間的鏈接. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } }     //如果最大閑置時間間隔大於保持鏈接的最大時間間隔或者限制連接數大於連接池允許的最大閑置連接數,就把該鏈接從連接池中移除 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // 如果閑置鏈接數大於0,則返回允許保持鏈接的最大時間間隔-最長時間間隔,也就是下次返回的時間間隔 return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // 如果所有的鏈接都在使用則直接返回保持時間間隔的最大值 return keepAliveDurationNs; } else { // 如果以上條件都不滿足,則清除事變,返回-1 cleanupRunning = false; return -1; } }   //關閉閑置時間最長的那個socket closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }  

cleanup(now)這個方法比較長,內容也比較多。我們把握大體邏輯就行。其核心邏輯是返回下次清理的時間間隔,其清理的核心是:鏈接的限制時間如果大於用戶設置的最大限制時間或者閑置鏈接的數量已經超出了用戶設置的最大數量,則就執行清除操作。其下次清理的時間間隔有四個值:

  1.如果閑置的連接數大於0就返回用戶設置的允許限制的時間-閑置時間最長的那個連接的閑置時間。

  2.如果清理失敗就返回-1,

  3.如果清理成功就返回0,

  4.如果沒有閑置的鏈接就直接返回用戶設置的最大清理時間間隔。

下面看一下系統是如何判斷當前循環到的鏈接是正在使用的鏈接

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
  //編譯StreamAllocation弱引用鏈表 List<Reference<StreamAllocation>> references = connection.allocations; for (int i = 0; i < references.size(); ) { Reference<StreamAllocation> reference = references.get(i);     //如果StreamAllocation不為空則繼續遍歷,計數器+1; if (reference.get() != null) { i++; continue; } // We've discovered a leaked allocation. This is an application bug. StreamAllocation.StreamAllocationReference streamAllocRef = (StreamAllocation.StreamAllocationReference) reference; //移除鏈表中為空的引用 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; } } return references.size(); }

  通過以上的代碼我們可以看出,其遍歷了弱引用列表,鏈表中為空的引用,最后返回一個鏈表數量。如果返回的數量>0表示RealConnection活躍,如果<=0則表示RealConnection空閑。也就是用這個來方法來判斷當前的鏈接是不是空閑的鏈接。

  我們再來看一下

closeQuietly(longestIdleConnection.socket());是如何關閉空閑時間最長的鏈接的。
public static void closeQuietly(Socket socket) {
    if (socket != null) {
      try {
        socket.close();
      } catch (AssertionError e) {
        if (!isAndroidGetsocknameError(e)) throw e;
      } catch (RuntimeException rethrown) {
        throw rethrown;
      } catch (Exception ignored) {
      }
    }
  }

  其實就一行核心代碼socket.close()。socket的使用不再介紹,大家可以看專門類的文章。

我們已經分析了從連接池清理空閑鏈接,到向連接池中加入新的鏈接。下面看看連接的使用以及連接的復用是如何實現的

  @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;
  }

  獲取連接池中的鏈接的邏輯非常的簡單,利用for循環循環遍歷連接池查看是否有符合要求的鏈接,如果有則直接返回該鏈接使用,如果沒有就發揮null,然后會在另外的地方創建一個新的RealConnection放入連接池。這里的核心代碼就是判斷是否有符合條件的鏈接:connection.isEligible(address,route)

public boolean isEligible(Address address, @Nullable Route route) {
    //如果當前鏈接的技術次數大於限制的大小,或者無法在此鏈接上創建流,則直接返回false
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // 如果地址主機字段不一致直接返回false
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // 如果主機地址完全匹配我們就重用該連接
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; // This connection is a perfect match.
    }

   ......

    return true;
  }

 分析到這里,連接池已經分析完畢了,下面來總結一下

總結:

  1.創建一個連接池

    創建連接池非常簡單只需要使用new關鍵字創建一個對象向就行了。new ConnectionPool(maxIdleConnections,keepAliveDuration,timeUnit)

    參數說明:

      1.maxIdleConnections 連接池允許的最大閑置連接數

      2.keepAliveDuration 連接池允許的保持鏈接超時時間

      3.timeUnit 保持鏈接超時的時間的時間單位

  2.向連接池中添加一個連接

    a.通過ConnectionPool的put(realConnection)方法加入鏈接,在加入鏈接之前會先調用線程池執行cleanupRunnable匿名內部類來清理空閑的鏈接,然后再把鏈接加入Deque隊列中,

    b.在cleanupRunnable匿名內部類中執行死循環不停的調用cleanup來清理空閑的連接,並返回一個下次清理的時間間隔,調用ConnectionPool.wait方法根據下次清理的時間間隔

    c.在cleanup的內部會遍歷connections連接池隊列,移除空閑時間最長的連接並返回下次清理的時間。

    d.判斷連接是否空閑是利用RealConnection內部的List<Reference<StreamAllocation> 的size。如果size>0就說明不空閑,如果size<=0就說明空閑。

  3.獲取一個鏈接

    通過ConnectionPool的get方法來獲取符合要求的RealConnection。如果有服務要求的就返回RealConnection,並用該鏈接發起請求,如果沒有符合要求的就返回null,並在外部重新創建一個RealConnection,然后再發起鏈接。判斷條件:1.如果當前鏈接的技術次數大於限制的大小,或者無法在此鏈接上創建流,則直接返回false 2.如果地址主機字段不一致直接返回false3.如果主機地址完全匹配我們就重用該連接

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM