OkHttp鏈接復用

OkHttp是一個高效的HTTP庫

  ?  支持 SPDY 橡淑,共享同一個Socket來處理同一個服務器的所有請求相艇;

  ?  如果SPDY不可用稿静,則通過連接池來減少請求延時;

  ?  無縫的支持GZIP來減少數據流量沸枯;

  ?  緩存響應數據來減少重復的網絡請求到忽。

源碼地址:
https://github.com/square/okhttp


核心類ConnectionPool.java

** * Manages reuse of HTTP and HTTP/2 connections for reduced 
network latency. HTTP requests that * share the same {@link Address} 
may share a {@link Connection}. This class implements the policy * of 
which connections to keep open for future use. */

關鍵支持HTTP和HTTP/2協議

HTTP 2.0 相比 1.1 的更新大部分集中于: 
  
   ? 多路復用 
   ? HEAD 壓縮 
   ? 服務器推送 
   ? 優(yōu)先級請求 

1.默認構造方法,無參和有參

/**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  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);
    }
  }

現在的okhttp,默認是保持了5個空閑鏈接,空閑鏈接超過5分鐘將被移除.

private final Deque<RealConnection> connections = new ArrayDeque<>();

2.OkHttp采用了一個雙端隊列去緩存所有鏈接對象.

3.如果一個請求來了,看看是怎么返回一個可用鏈接的

/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
  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;
  }

可以看到遍歷隊列,先判斷connection.allocations.size() < connection.allocationLimit,這個鏈接的當前流數量是否超過了最大并發(fā)流數,然后再匹配地址Address,最后檢測noNewStreams這個值.

這個值,是什么時候被設置為true的呢?
答案是在這個鏈接被移除(remove)的時候.
詳見2個方法:
evictAll()
pruneAndGetAllocationCount(RealConnection connection, long now)

4.回收空閑的鏈接,優(yōu)化鏈接

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

每次吧,創(chuàng)建一個鏈接的時候,再將它添加到隊列的時候,都會執(zhí)行一下clean操作

 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,等待之后釋放鎖,繼續(xù)執(zhí)行下一次的清理忌堂。
那么怎么找到閑置的連接是主要解決的問題。

/**
   * Performs maintenance on this pool, evicting the connection that has been idle the longest if
   * either it has exceeded the keep alive limit or the idle connections limit.
   *
   * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
   * -1 if no further cleanups are required.
   */
  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<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      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);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } 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 {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

在遍歷緩存列表的過程中酗洒,這里涉及2個數目:連接數目inUseConnectionCount和空閑連接數目idleConnectionCount, 它們的值跟隨著pruneAndGetAllocationCount()返回值而變化士修。那么很顯然pruneAndGetAllocationCount()方法就是用來識別對應連接是否閑置的。>0則不閑置樱衷。否則就是閑置的連接棋嘲。

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);

      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;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }

原先存放在RealConnection 中的allocations 派上用場了。遍歷StreamAllocation 弱引用鏈表矩桂,移除為空的引用沸移,遍歷結束后返回鏈表中弱引用的數量。所以可以看出List<Reference<StreamAllocation>> 就是一個記錄connection活躍情況的 >0表示活躍, =0 表示空閑侄榴。StreamAllocation 在列表中的數量就是就是物理socket被引用的次數
StreamAllocation.java

  public void acquire(RealConnection connection) {
    assert (Thread.holdsLock(connectionPool));
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

  /** Remove this allocation from the connection's list of allocations. */
  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();
  }

ok,通過pruneAndGetAllocationCount方法,咱們獲得了某個鏈接的并發(fā)流數量,返回到cleanup,

// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
    longestIdleDurationNs = idleDurationNs;
    longestIdleConnection = connection;
}

很簡單的幾行代碼,但是很關鍵,目的是記錄,哪個鏈接是空閑時間最長的,閑了這么長時間,被我逮著了吧,哈哈哈...
繼續(xù)向下走,找組織

if (longestIdleDurationNs >= this.keepAliveDurationNs    || 
      idleConnectionCount > this.maxIdleConnections)

這就是溢出空閑鏈接的原因,滿足2個條件中的任意一個,你就被開除了,要么你這個RealConnection已經閑得發(fā)慌,要么當前閑的人太多了,你就悲劇了,拜拜吧...
最關鍵的怎么知道下一次掃蕩的時間呢?

'你問我,我問誰?'
'好吧,看代碼!'

跟著組織繼續(xù)走
第一個case

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

return 0;

發(fā)現有人頂風作案,會不會有同伙,那好,立即再次掃蕩,所以時間緊迫,return后立即再次清理.

第二個case

if (idleConnectionCount > 0) {
   // A connection will be ready to evict soon.
   return keepAliveDurationNs - longestIdleDurationNs;
} 

哈哈,你敢不老實,你已經透支了longestIdleDurationNs這么長時間,如果再過 keepAliveDurationNs - longestIdleDurationNs這么長時間,你還在透支,那么對不起,你被標記了,拉黑.

第三個case

if (inUseConnectionCount > 0) {
  // All connections are in use. It'll be at least the keep alive duration 'til we run again.
      return keepAliveDurationNs;
} 

大家都在干活,貌似都挺乖,好吧,正常運轉,下次過keepAliveDurationNs(默認5分鐘)正常時間,再看看.

第四個case
也就是前3個情況都不成立.inUseConnectionCount == 0

    // No connections, idle or in use.
    cleanupRunning = false;
    return -1;

哈哈,沒人干活,放假吧.
cleanupRunnable的run方法

while (true) {
    long waitNanos = cleanup(System.nanoTime());
    if (waitNanos == -1) return;
    ...   
}

自此,okhttp最核心的思想,連接復用就梳理完了.


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末雹锣,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子癞蚕,更是在濱河造成了極大的恐慌蕊爵,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桦山,死亡現場離奇詭異攒射,居然都是意外死亡醋旦,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門会放,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浑度,“玉大人,你說我怎么就攤上這事鸦概÷嵴牛” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵窗市,是天一觀的道長先慷。 經常有香客問我,道長咨察,這世上最難降的妖魔是什么论熙? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮摄狱,結果婚禮上脓诡,老公的妹妹穿的比我還像新娘。我一直安慰自己媒役,他們只是感情好祝谚,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酣衷,像睡著了一般交惯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上穿仪,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天席爽,我揣著相機與錄音,去河邊找鬼啊片。 笑死只锻,一個胖子當著我的面吹牛,可吹牛的內容都是我干的紫谷。 我是一名探鬼主播齐饮,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼碴里!你這毒婦竟也來了沈矿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤咬腋,失蹤者是張志新(化名)和其女友劉穎羹膳,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體根竿,經...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡陵像,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年就珠,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醒颖。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡妻怎,死狀恐怖,靈堂內的尸體忽然破棺而出泞歉,到底是詐尸還是另有隱情逼侦,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布腰耙,位于F島的核電站榛丢,受9級特大地震影響,放射性物質發(fā)生泄漏挺庞。R本人自食惡果不足惜晰赞,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望选侨。 院中可真熱鬧掖鱼,春花似錦、人聲如沸援制。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隘谣。三九已至增拥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寻歧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工秩仆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留码泛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓澄耍,卻偏偏與公主長得像噪珊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子齐莲,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內容