OkHttp源碼之socket連接池

在整個okhttp中,相對來說最耗資源的應(yīng)該屬于socket連接了蜗帜,所以為了節(jié)省tcp的連接釋放以及TLS協(xié)議的握手等時間恋拷,socket連接池是必不可少的。研究它的連接池厅缺,我們重點關(guān)注以下兩點:

  • socket復(fù)用有何標(biāo)準(zhǔn)
  • 一個socket何時會被關(guān)閉蔬顾?

okhttp的連接池代碼在ConnectionPool中,首先看下大致的結(jié)構(gòu):

public final class ConnectionPool {
  /**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  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;
  //每個socket生存的時間
  private final long keepAliveDurationNs;
 // 用來清理socket的任務(wù)
  private final Runnable cleanupRunnable ;//具體實現(xiàn)省略
 //用來存儲socket的核心結(jié)構(gòu)
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;
}

可以看到湘捎,這里使用一個Deque<RealConnection>來保存的诀豁,至于RealConnection,可以理解成對socket的包裝窥妇。這里大家要注意到舷胜,對于連接池來說查詢從來不是什么耗時操作,所以這里其實用List也是可以的活翩,沒有什么大的影響烹骨。

get操作(連接池的復(fù)用)

@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

可以看到就只是遍歷了所有的連接,然后判斷某個連接是否可以復(fù)用材泄,我們看下復(fù)用的判斷代碼,在RealConnection中:

public boolean isEligible(Address address, @Nullable 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)){
      System.out.println("host not equal");
      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())) {
      System.out.println("host equal "+address.url().host().toString());
      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.
  }

對這整個方法沮焕,里面對好幾條復(fù)用規(guī)則作了判斷,我們將重點分析拉宗。

規(guī)則一:流數(shù)量要符合要求

一個socket如果被復(fù)用峦树,那么在多個請求并發(fā)進行的情況下,必然出現(xiàn)多個線程同時往一個socket中寫入數(shù)據(jù)簿废,那這樣做是否允許呢空入?這要分成兩種情況:

http1.x

在http 1.x協(xié)議下,所有的請求的都是順序的族檬,即使使用了管道技術(shù)(可以同時按順序連續(xù)發(fā)送請求歪赢,但消息的返回還是按照請求發(fā)送的順序返回)也是如此,因此一個socket在任何時刻只能有一個流在寫入单料,這意味著正在寫入數(shù)據(jù)的socket無法被另一個請求復(fù)用

http2.0

http2.0協(xié)議使用了多路復(fù)用技術(shù)埋凯,允許同一個socket在同一個時候?qū)懭攵鄠€流數(shù)據(jù),每個流有id扫尖,會進行組裝白对,因此,這個時候正在寫入數(shù)據(jù)的socket是可以被復(fù)用的换怖。

為了區(qū)分兩種情況甩恼,okhttp記錄了每個socket流使用情況,同時設(shè)定了每個socket能同時使用多少流,很明顯条摸,http1.x同一時間只能有一個流悦污,http2.0能有無數(shù)個:

if (allocations.size() >= allocationLimit || noNewStreams) {
      return false;
    }

現(xiàn)在再看這句if判斷就能理解了,當(dāng)然noNewStreams是在某些特殊情況下防止連接被復(fù)用時設(shè)置的钉蒲,比如服務(wù)端要求關(guān)閉這個連接切端,那當(dāng)然也不能被復(fù)用了。

小結(jié)

http1.x協(xié)議下當(dāng)前socket沒有其他流正在讀寫時可以復(fù)用顷啼,否則不行踏枣,http2.0對流數(shù)量沒有限制。

規(guī)則二 http和ssl協(xié)議配置要相同

想要復(fù)用一個http連接钙蒙,那么兩次請求的所有http配置和ssl配置都要相同:

// If the non-host fields of the address don't overlap, we're done.
    if (!Internal.instance.equalsNonHost(this.route.address(), address)){
      return false;
    }

具體的equalsNonHost方法實現(xiàn)如下:

 boolean equalsNonHost(Address that) {
    return this.dns.equals(that.dns)
        && this.proxyAuthenticator.equals(that.proxyAuthenticator)
        && this.protocols.equals(that.protocols)
        && this.connectionSpecs.equals(that.connectionSpecs)
        && this.proxySelector.equals(that.proxySelector)
        && equal(this.proxy, that.proxy)
        && equal(this.sslSocketFactory, that.sslSocketFactory)
        && equal(this.hostnameVerifier, that.hostnameVerifier)
        && equal(this.certificatePinner, that.certificatePinner)
        && this.url().port() == that.url().port();
  }

上面具體到每個配置大家可以自己研究茵瀑,篇幅有限這里不做展開。

規(guī)則三 域名要匹配

 // 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.
    }

這點不用說躬厌,滿足了以上三條規(guī)則后我們可以放心的復(fù)用這個連接了瘾婿。

規(guī)則四 特殊情況

上面的三條規(guī)則如果都符合了自然是完美復(fù)用一個連接,但其實還有一種情況也是可以復(fù)用的:多個host指向同一個ip地址的情況烤咧。
在http1.x的情況,有些網(wǎng)站為了突破瀏覽器一個域名只能建立6-8個連接的限制抢呆,會給同一個ip地址配置不同的域名煮嫌,這樣瀏覽器就能使用很多連接來訪問頁面,加快頁面打開速度抱虐,但在http2.0時代昌阿,有了多路復(fù)用,一個連接完全能滿足以前的要求恳邀,所以針對這種特殊情況我們應(yīng)該復(fù)用連接懦冰,而不是新開連接,當(dāng)然這個條件是比較苛刻的谣沸。

只有在http2.0情況下才會考慮復(fù)用

// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;

只有在沒有代理時才能復(fù)用

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

如果設(shè)置了代理刷钢,我們無法知道原始服務(wù)器的ip地址,自然無法判斷這個域名和之前的連接是否共用一個ip地址乳附,自然不能復(fù)用内地。

只有ip地址相同才能復(fù)用

   if (!this.route.socketAddress().equals(route.socketAddress())) return false;

這點是肯定的,ip地址不同socket連接肯定不可能復(fù)用赋除,無需解釋阱缓。

對不受信任的證書處理方式相同才能復(fù)用

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

第一行要求對證書的處理必須是默認的OkHostnameVerifier才行,要是自己隨便實現(xiàn)的一個該接口举农,我們無法保證它和之前的連接實現(xiàn)是否一致荆针,自然無法復(fù)用。
第二行就是要求對于這個不同的host,必須通過之前的連接的證書校驗才行:

public boolean supportsUrl(HttpUrl url) {
    if (url.port() != route.address().url().port()) {
      return false; // Port mismatch.
    }

    if (!url.host().equals(route.address().url().host())) {
      // We have a host mismatch. But if the certificate matches, we're still good.
      return handshake != null && OkHostnameVerifier.INSTANCE.verify(
          url.host(), (X509Certificate) handshake.peerCertificates().get(0));
    }

    return true; // Success. The URL is supported.
  }

這是因為復(fù)用socket連接其實就意味著跳過了https握手的過程航背,如果不通過證書校驗太危險了喉悴。、

本地證書校驗通過才能復(fù)用

https為了防止中間人攻擊可以在建立連接成功后將服務(wù)器下發(fā)的證書保存下來沃粗,這樣如果有中間人偽裝服務(wù)器粥惧,中間人下發(fā)的證書和本地保存的不一致就會校驗失敗,最后一條規(guī)則就是保證這個校驗要通過:

// 4. Certificate pinning must match the host.
    try {
      address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
    } catch (SSLPeerUnverifiedException e) {
      return false;
    }

小結(jié)

通過上面所有的校驗后最盅,這種情況下突雪,不同host同一ip地址的情況也是可以復(fù)用的。

連接池的清理

對于一個socket連接池來說必然有自己的清理機制涡贱,否則如果長期不發(fā)起網(wǎng)絡(luò)請求咏删,socket連接一直被占用就劃不來了,okhttp是通過一個單獨的線程來清理的问词。

何時開始清理工作:

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

每次put一個新連接的時候都會判斷是否需要清理督函,就是說并不會每次put都執(zhí)行,要看條件控制的激挪,然后我們看下具體清理邏輯:

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

這里似乎看不出什么辰狡,我們繼續(xù)看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<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;
  }

代碼看起來很長,其實原理很簡單垄分,就是遍歷當(dāng)前所有連接宛篇,跳過正在使用的連接馁害,其他沒有用的連接专肪,如果哪個連接超過了規(guī)定的時間蜓竹,就關(guān)掉這個socket半哟。如果都沒有超過規(guī)定時間的悄晃,就返回離規(guī)定時間最近的那個差值家淤。
拿到那個時間值后佳晶,我們再回到上面那個cleanupRunnable中抄瓦,在那里會wait線程坐求,然后醒來繼續(xù)清理蚕泽。

舉例:socket最長生存時間是30分鐘,當(dāng)前有5個連接瞻赶,c1,c2正在被使用赛糟,c3空閑了40分鐘,c4空閑了20分鐘砸逊,c5空閑了25分鐘璧南,
那么一次clean()方法會關(guān)掉c3,然后返回0,在cleanupRunnable中會立馬進行下一次循環(huán)清理师逸,這個時候檢測到離生存時間最近的是c5,那么clean()方法會返回5分鐘這個時間值司倚,cleanRunnable中會wait 5分鐘,然后5分鐘后會繼續(xù)下一次清理。

小結(jié)

理論上來說动知,只要連接池中有連接皿伺,該清理線程就一直存在,直到所有連接被釋放該線程才會停止盒粮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸵鸥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子丹皱,更是在濱河造成了極大的恐慌妒穴,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摊崭,死亡現(xiàn)場離奇詭異讼油,居然都是意外死亡,警方通過查閱死者的電腦和手機呢簸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門矮台,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人根时,你說我怎么就攤上這事瘦赫。” “怎么了蛤迎?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵耸彪,是天一觀的道長。 經(jīng)常有香客問我忘苛,道長,這世上最難降的妖魔是什么唱较? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任扎唾,我火速辦了婚禮,結(jié)果婚禮上南缓,老公的妹妹穿的比我還像新娘胸遇。我一直安慰自己,他們只是感情好汉形,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布纸镊。 她就那樣靜靜地躺著,像睡著了一般概疆。 火紅的嫁衣襯著肌膚如雪逗威。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天岔冀,我揣著相機與錄音凯旭,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛罐呼,可吹牛的內(nèi)容都是我干的鞠柄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫉柴,長吁一口氣:“原來是場噩夢啊……” “哼厌杜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起计螺,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夯尽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后危尿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呐萌,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年谊娇,在試婚紗的時候發(fā)現(xiàn)自己被綠了肺孤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡济欢,死狀恐怖赠堵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情法褥,我是刑警寧澤茫叭,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站半等,受9級特大地震影響揍愁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杀饵,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一莽囤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧切距,春花似錦朽缎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葡幸,卻和暖如春最筒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔚叨。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工是钥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掠归,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓悄泥,卻偏偏與公主長得像虏冻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弹囚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容