OkHttp源碼閱讀(七) —— 攔截器之ConnectInterceptor

??Duang!~ Duang!~ Duang潜慎!~重磅來(lái)襲,OkHttp里個(gè)人覺(jué)得最重要也是最有特色的角色出現(xiàn)了(因?yàn)榭吹淖钽卤?,這個(gè)攔截器里邊的子角色很多屏鳍,也同樣都很重要,最最最最重要的是代碼量太TM多了,讀這塊代碼的時(shí)候有一句特別經(jīng)典的話常常出現(xiàn)在我腦海里局服,那就是"臥槽钓瞭!這TM什么玩應(yīng)兒!R肌I轿小!唆迁!"鸭丛,不過(guò)硬骨頭也得啃啊,一口啃不動(dòng)那就一點(diǎn)一點(diǎn)的啃L圃稹鳞溉!

前言

??在分析ConnectInterceptor之前,先聲明幾點(diǎn)注意事項(xiàng),首先這個(gè)攔截器里邊涉及到知識(shí)點(diǎn)和代碼太多了鼠哥,沒(méi)必要每一行代碼都要知道是什么意思熟菲,有些類和方法只需要知道它的作用是什么就可以了,可以忽略細(xì)節(jié)朴恳,主要還是把重點(diǎn)放到核心邏輯上抄罕,所以下面先預(yù)熱一些類的定義和作用。

  1. RealConnetion
  2. ConnectionPool
  3. StreamAllocation

RealConnection

??從字面上來(lái)看RealConnection的意思是真正的鏈接于颖,沒(méi)錯(cuò)!RealConnection所做的事情也是與服務(wù)器建立通信鏈接呆贿,Realconnection是Connection接口的實(shí)現(xiàn)類,在Connction接口定義中森渐,有幾個(gè)方法

public interface Connection {
  /** Returns the route used by this connection. */
  Route route();

  /**
   * Returns the socket that this connection is using. Returns an {@linkplain
   * javax.net.ssl.SSLSocket SSL socket} if this connection is HTTPS. If this is an HTTP/2
   * connection the socket may be shared by multiple concurrent calls.
   */
  Socket socket();

  /**
   * Returns the TLS handshake used to establish this connection, or null if the connection is not
   * HTTPS.
   */
  @Nullable Handshake handshake();

  /**
   * Returns the protocol negotiated by this connection, or {@link Protocol#HTTP_1_1} if no protocol
   * has been negotiated. This method returns {@link Protocol#HTTP_1_1} even if the remote peer is
   * using {@link Protocol#HTTP_1_0}.
   */
  Protocol protocol();
}

上邊代碼可以看出RealConnection實(shí)現(xiàn)該接口后做入, 就意味著客戶端和服務(wù)器有了一條通信鏈路,RealConnetion做了好多事情同衣,包括三次握手母蛛、建立普通鏈接建立隧道鏈接乳怎、TLS鏈接彩郊、代理等等前弯,而對(duì)于Android開(kāi)發(fā)人員來(lái)說(shuō),這些具體實(shí)現(xiàn)細(xì)節(jié)沒(méi)有必要研究的很細(xì)秫逝,如果感興趣的話可以進(jìn)一步深入的了解恕出,本人能力有限,暫時(shí)放棄深入了解具體實(shí)現(xiàn)违帆。不過(guò)RealConnection中有些屬性和方法的含義是非常有必要了解的浙巫。
首先先看一下RealConnection這個(gè)類的重要屬性定義

/**連接池*/
  private final ConnectionPool connectionPool;
  private final Route route;

  // The fields below are initialized by connect() and never reassigned.
  //下面這些字段,通過(guò)connect()方法開(kāi)始初始化刷后,并且絕對(duì)不會(huì)再次賦值
  /** The low-level TCP socket. */
  /**底層socket*/
  private Socket rawSocket;

  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  /**應(yīng)用層socket*/
  private Socket socket;
  /**握手*/
  private Handshake handshake;
  /**協(xié)議*/
  private Protocol protocol;
  /**http2鏈接*/
  private Http2Connection http2Connection;
  /**通過(guò)source和sink的畴,與服務(wù)器進(jìn)行輸入輸出流交互*/
  private BufferedSource source;
  private BufferedSink sink;

  // The fields below track connection state and are guarded by connectionPool.

  /** If true, no new streams can be created on this connection. Once true this is always true. */

  /**下面這個(gè)字段是 屬于表示鏈接狀態(tài)的字段,并且有connectPool統(tǒng)一管理
   * 如果noNewStreams被設(shè)為true尝胆,則noNewStreams一直為true丧裁,不會(huì)被改變,并且表示這個(gè)鏈接不會(huì)再創(chuàng)新的stream流*/

  public boolean noNewStreams;

  public int successCount;

  /**
   * The maximum number of concurrent streams that can be carried by this connection. If {@code
   * allocations.size() < allocationLimit} then new streams can be created on this connection.
   */
  /**并發(fā)流上限,此鏈接可以承載最大并發(fā)流的限制含衔,如果不超過(guò)限制煎娇,可以隨意增加*/
  public int allocationLimit = 1;

  /** Current streams carried by this connection. */
  /**統(tǒng)計(jì)當(dāng)前鏈接上創(chuàng)建了哪些流,相當(dāng)于計(jì)數(shù)器*/
  public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;

通過(guò)上面的代碼基本上可以得出一個(gè)結(jié)論贪染,

  1. 除了route字段缓呛,部分字段都是在connect()方法里邊賦值的,并且不會(huì)再次賦值
  2. 鏈接時(shí)通過(guò)source和sink以流的方式和服務(wù)器交互
  3. noNewStream可以簡(jiǎn)單理解為它表示該連接不可用杭隙。這個(gè)值一旦被設(shè)為true,則這個(gè)conncetion則不會(huì)再創(chuàng)建stream.
  4. allocationLimit是分配流的數(shù)量上限哟绊,一個(gè)connection最大只能支持一個(gè)并發(fā)
  5. allocations是關(guān)聯(lián)StreamAllocation,它用來(lái)統(tǒng)計(jì)在一個(gè)連接上建立了哪些流,通過(guò)StreamAllocation的acquire方法和release方法可以將一個(gè)allcation對(duì)象添加到鏈表或者移除鏈表痰憎,

對(duì)RealConnection的屬性有了簡(jiǎn)單的了解之后匿情,我們來(lái)分析分析它的方法,像什么建立三次握手信殊,建立普通鏈接炬称,建立隧道鏈接方法什么的,我就不分析涡拘,因?yàn)闆](méi)必要研究這么底層玲躯,何況我也不怎么明白。主要分析對(duì)整體邏輯比較重要的對(duì)后邊的分析有影響的方法鳄乏。

isHealthy方法

 /** Returns true if this connection is ready to host new streams. */
  public boolean isHealthy(boolean doExtensiveChecks) {
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }

    if (http2Connection != null) {
      return !http2Connection.isShutdown();
    }

    if (doExtensiveChecks) {
      try {
        int readTimeout = socket.getSoTimeout();
        try {
          socket.setSoTimeout(1);
          if (source.exhausted()) {
            return false; // Stream is exhausted; socket is closed.
          }
          return true;
        } finally {
          socket.setSoTimeout(readTimeout);
        }
      } catch (SocketTimeoutException ignored) {
        // Read timed out; socket is good.
      } catch (IOException e) {
        return false; // Couldn't read; socket is closed.
      }
    }

    return true;
  }

如代碼所示,這個(gè)方法是用來(lái)判斷一個(gè)鏈接是否是健康鏈接跷车,只有健康的鏈接才能被復(fù)用,那么怎么判斷一個(gè)鏈接是健康的鏈接呢橱野?如代碼所示要同時(shí)滿足一下幾個(gè)要求朽缴,該鏈接才是健康的鏈接

  • socket必須是關(guān)閉的狀態(tài)(socket.isClosed())
  • socket輸入流是關(guān)閉狀態(tài)(socket.isInputShutdown())
  • socket輸出流是關(guān)閉狀態(tài)(socket.isOutputShutdown())
  • 如果是http2鏈接,http2也要處于關(guān)閉狀態(tài)(http2Connection.isShutdown())

剩下的是鏈接超時(shí)時(shí)socket的狀態(tài)判斷

isEligible方法

 /**
   * 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, @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)) 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.
  }

如上面代碼所示水援,isEligible方法的作用是通過(guò)address和route判斷當(dāng)前realConnection是否可以被復(fù)用密强,代碼邏輯比較簡(jiǎn)單茅郎,都是一些if-else的判斷,大致邏輯判斷可以總結(jié)以下幾點(diǎn):

  • 如果鏈接達(dá)到了并發(fā)上限或渤,則該鏈接不能被復(fù)用
  • 通過(guò)host域還有route的address邏輯關(guān)系系冗,判斷該鏈接是否可以被復(fù)用(有些地方不是很清楚)

最后簡(jiǎn)單的了解下newCodec方法

public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

主要是通過(guò)當(dāng)前是不是HTTP/2來(lái)創(chuàng)建一個(gè)HttpCodec。HttpCodec的作用其實(shí)就是讀寫(xiě)流薪鹦,有時(shí)間的童鞋可以仔細(xì)研究下掌敬。

ConnectionPool

??連接池在okhttp中連接池的作用可謂是重中之重,其復(fù)用機(jī)制很大程度上提高了網(wǎng)絡(luò)性能池磁,降低了延遲和提升了響應(yīng)速度奔害,連接池的精髓當(dāng)然也是在于鏈接的復(fù)用,究竟ConnectionPool怎么復(fù)用的鏈接地熄,在這之前先鋪墊一些內(nèi)容华临。
??我們常常在瀏覽器開(kāi)發(fā)者工具中或者是客戶端網(wǎng)絡(luò)代碼編寫(xiě)的過(guò)程經(jīng)常會(huì)看到這樣的代碼"keep-alive",keep-alive的作用就是讓客戶端可服務(wù)器保持長(zhǎng)連接,并且這個(gè)鏈接時(shí)可復(fù)用的离斩,可為什么鏈接的復(fù)用會(huì)提高性能呢银舱?這就要從一個(gè)網(wǎng)絡(luò)請(qǐng)求響應(yīng)的過(guò)程說(shuō)起了.
??首先通常情況下瘪匿,我們發(fā)起一個(gè)HTTP請(qǐng)求的時(shí)候跛梗,開(kāi)始階段要先完成TCP的三次握手,然后再傳輸數(shù)據(jù)棋弥,最后再釋放鏈接核偿,我們知道,在高并發(fā)下顽染,如果請(qǐng)求數(shù)量很多漾岳,或者是同一個(gè)客戶端發(fā)起多次頻繁的請(qǐng)求,那么每次的請(qǐng)求都要進(jìn)行三次握手粉寞,最后釋放鏈接這是一個(gè)非常消耗性能的操作尼荆,所有就引申出了長(zhǎng)連接,如圖所示

1.jpeg

如圖所示 在timeout空閑時(shí)間內(nèi)唧垦,鏈接時(shí)不會(huì)關(guān)閉的捅儒,相同重復(fù)的request將復(fù)用原先的connection,減少握手的次數(shù)振亮,從而提升性能巧还,那么OkHttp是如何做到鏈接復(fù)用的呢?主角登場(chǎng)ConnectionPool

首先簡(jiǎn)單的看一下ConnectionPool類的注釋

/**
 * 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的鏈接坊秸,以便減少網(wǎng)絡(luò)請(qǐng)求延遲麸祷。同一個(gè)address將共享同一個(gè)connection。該類實(shí)現(xiàn)了復(fù)用連接的目標(biāo)褒搔。

/**
   * 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.
   */
  /**這是一個(gè)后臺(tái)線程用于清除過(guò)期的鏈接阶牍,一個(gè)線程池里最多只能運(yùn)行一個(gè)線程喷面,這個(gè)線程池允許被回收*/
  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. */
  /**每個(gè)address的最大空閑連接數(shù)*/
  private final int maxIdleConnections;
  /**鏈接保活時(shí)間*/
  private final long keepAliveDurationNs;
  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) {
            }
          }
        }
      }
    }
  };

  /**鏈接容器荸恕,存放鏈接*/
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  /**路由庫(kù)*/
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;

從上邊代碼可以了解到乖酬,ConnectionPool的核心還是RealConnection的容器,控制RealConnection的讀取,該容器是順序容器融求,不是關(guān)聯(lián)容器咬像,ConnectionPool中使用雙端隊(duì)列來(lái)存放RealConnection,而且ConnectionPool對(duì)連接池中的最大空閑連接數(shù)和保活時(shí)間做了控制生宛,maxIdleConnectionskeepAliveDurationNs成員分別體現(xiàn)對(duì)最大空閑連接數(shù)及連接毕匕海活時(shí)間的控制。這種控制通過(guò)匿名的Runnable cleanupRunnable在線程池executor中執(zhí)行,cleanupRunning是清理的標(biāo)記位兆解,并在向連接池中添加新的RealConnection觸發(fā)责嚷。

??ConnectionPool的對(duì)象創(chuàng)建是可以O(shè)kHttp初始化時(shí)用戶自己創(chuàng)建,可以指定一些需要的自定義參數(shù)待讳,這里就分析下默認(rèn)情況下ConnectionPool的參數(shù)配置,首先構(gòu)造方法:

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

方法注釋大概的意思是創(chuàng)建一個(gè)適用于單個(gè)應(yīng)用程序的新連接池仰剿。該連接池的參數(shù)將在未來(lái)的okhttp中發(fā)生改變创淡,目前最多可容乃5個(gè)空閑的連接,存活期是5分鐘南吮,maxIdleConnectionskeepAliveDurationNs是清理淘汰連接的的指標(biāo)琳彩,這里需要說(shuō)明的是maxIdleConnections是值每個(gè)地址上最大的空閑連接數(shù)。所以O(shè)kHttp只是限制與同一個(gè)遠(yuǎn)程服務(wù)器的空閑連接數(shù)量部凑,對(duì)整體的空閑連接并沒(méi)有限制露乏。

接下來(lái)就是分析ConnectionPool如何通過(guò)雙端隊(duì)列進(jìn)行RealConnection的管理
首先看一下put方法:

void put(RealConnection connection) {
//斷言表達(dá)式,put的基本條件是涂邀,當(dāng)前線程是否持有該對(duì)象的鎖
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

put方法比較簡(jiǎn)單瘟仿,就是異步觸發(fā)清理任務(wù),然后將連接添加到隊(duì)列中比勉。重點(diǎn)是怎么清理任務(wù)

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

如圖開(kāi)啟一個(gè)線程循環(huán)去cleanup清理劳较,代碼如下

long cleanup(long now) {
    //正在使用connection數(shù)量
    int inUseConnectionCount = 0;
    //空閑的connection數(shù)量
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    //最長(zhǎng)空閑時(shí)間
    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.
        /**找出空閑時(shí)間最長(zhǎng)的鏈接*/
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      /**最長(zhǎng)空閑時(shí)間如果超出了保活時(shí)間敷搪,或者空閑鏈接個(gè)數(shù)超過(guò)了最大空閑鏈接個(gè)數(shù)的話兴想,就要將該鏈接從隊(duì)列中移除*/
      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.
        /**不符合清理?xiàng)l件,則返回下次需要執(zhí)行清理的等待時(shí)間赡勘,也就是此連接即將到期的時(shí)間*/
        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.
        /**執(zhí)行到這說(shuō)明已經(jīng)沒(méi)有空閑鏈接了嫂便,那么沒(méi)有空閑的連接,則隔keepAliveDuration(分鐘)之后再次執(zhí)行*/
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        //清理結(jié)束
        cleanupRunning = false;
        return -1;
      }
    }
    //關(guān)閉socket鏈接
    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    //這里是在清理一個(gè)空閑時(shí)間最長(zhǎng)的連接以后會(huì)執(zhí)行到這里闸与,需要立即再次執(zhí)行清理
    return 0;
  }

以上代碼流程大致這樣毙替,通過(guò)for循環(huán)查找最長(zhǎng)空閑時(shí)間的連接以及對(duì)應(yīng)空閑時(shí)長(zhǎng)岸售,然后判斷是否超出最大空閑連接數(shù)(maxIdleConnections)或者或者超過(guò)最大空閑時(shí)間(keepAliveDurationNs),滿足條件就直接清理厂画,不滿足條件的話 就需要計(jì)算下一次清理時(shí)間凸丸,主要有兩種情況

  • 有空閑鏈接則下次清理時(shí)間等于keepAliveDurationNs-longestIdleDurationNs
  • 如果沒(méi)有空閑鏈接則需要下一個(gè)周期再清理keepAliveDurationNs

綜上所述,我們來(lái)梳理一下清理任務(wù)袱院,清理任務(wù)就是異步執(zhí)行的屎慢,遵循兩個(gè)指標(biāo),最大空閑連接數(shù)量和最大空閑時(shí)長(zhǎng)忽洛,滿足其一則清理空閑時(shí)長(zhǎng)最大的那個(gè)連接腻惠,然后循環(huán)執(zhí)行,要么等待一段時(shí)間欲虚,要么繼續(xù)清理下一個(gè)連接集灌,直到清理所有連接,清理任務(wù)才結(jié)束复哆,下一次put的時(shí)候欣喧,如果已經(jīng)停止的清理任務(wù)則會(huì)被再次觸發(fā)

接下來(lái)是connectionBecameIdle方法

/**
   * Notify this pool that {@code connection} has become idle. Returns true if the connection has
   * been removed from the pool and should be closed.
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewStreams || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }
  

該方法主要作用是當(dāng)有連接空閑時(shí),喚起cleanup線程清洗連接池梯找,connectionBecameIdle標(biāo)示一個(gè)連接處于空閑狀態(tài)唆阿,即沒(méi)有流任務(wù),那么就需要調(diào)用該方法初肉,由ConnectionPool來(lái)決定是否需要清理該連接酷鸦。

/** Close and remove all idle connections in the pool. */
  public void evictAll() {
    List<RealConnection> evictedConnections = new ArrayList<>();
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        if (connection.allocations.isEmpty()) {
          connection.noNewStreams = true;
          evictedConnections.add(connection);
          i.remove();
        }
      }
    }

    for (RealConnection connection : evictedConnections) {
      closeQuietly(connection.socket());
    }
  }

evictAll方法就是關(guān)閉所有鏈接饰躲,在這里就不贅述了牙咏。

接下來(lái)需要了解下pruneAndGetAllocationCount方法

/**
   * Prunes any leaked allocations and then returns the number of remaining live allocations on
   * {@code connection}. Allocations are leaked if the connection is tracking them but the
   * application code has abandoned them. Leak detection is imprecise and relies on garbage
   * collection.
   */
  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();
  }

該方法內(nèi)使用了另一個(gè)類叫做** StreamAllocation**,后邊會(huì)分析到,暫且先了解下該方法的大致功能是什么嘹裂,詳細(xì)說(shuō)明在后邊會(huì)說(shuō)到妄壶,到時(shí)回過(guò)頭來(lái)看這段方法的代碼會(huì)理解的更透徹,pruneAndGetAllocationCount主要是用來(lái)標(biāo)記泄露連接的寄狼。內(nèi)部通過(guò)遍歷傳入進(jìn)來(lái)的RealConnection的StreamAllocation列表丁寄,如果StreamAllocation被使用則接著遍歷下一個(gè)StreamAllocation。如果StreamAllocation未被使用則從列表中移除泊愧,如果列表中為空則說(shuō)明此連接連接沒(méi)有引用了伊磺,返回0,表示此連接是空閑連接删咱,否則就返回非0表示此連接是活躍連接屑埋。

??說(shuō)完了put方法,接下來(lái)分析get方法痰滋,代碼如下

/**
   * Returns a recycled connection to {@code address}, or null if no such connection exists. The
   * route is null if the address has not yet been routed.
   */
  @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    //斷言摘能,判斷線程是不是被自己鎖住了
    assert (Thread.holdsLock(this));
    // 遍歷已有連接集合
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        //復(fù)用這個(gè)連接
        streamAllocation.acquire(connection, true);
        //返回這個(gè)連接
        return connection;
      }
    }
    return null;
  }

代碼很簡(jiǎn)單续崖,遍歷connections 中的所有 RealConnection 尋找同時(shí)滿足條件的RealConnection。
這里同樣用到了一個(gè)類StreamAllocation团搞,那到底StreamAllocation做了什么事情严望,我們來(lái)深入分析下。

StreamAllocation

該類屬于核心類逻恐,也是相對(duì)底層的一個(gè)類像吻,在分析這個(gè)類之前,需要粗略的了解下兩個(gè)工具類复隆,如下:

HttpCodec

在客戶端和服務(wù)器之間的數(shù)據(jù)傳輸都是靠流進(jìn)行傳輸?shù)南舳梗贠kHttp中HttpCodec的作用就是劉操作,它會(huì)將請(qǐng)求的數(shù)據(jù)序列化之后發(fā)送到網(wǎng)絡(luò)昏名,并將接收的數(shù)據(jù)反序列化為應(yīng)用程序方便操作的格式涮雷,HttpCodec提供了以下方法:

  • writeRequestHeaders 為發(fā)送請(qǐng)求而提供的,寫(xiě)入請(qǐng)求頭部轻局。
  • createRequestBody 為發(fā)送請(qǐng)求而提供的洪鸭,創(chuàng)建請(qǐng)求體,以用于發(fā)送請(qǐng)求體數(shù)據(jù)
  • finishRequest 為發(fā)送請(qǐng)求而提供的仑扑,結(jié)束請(qǐng)求發(fā)送览爵。
  • readResponseHeaders 為獲得響應(yīng)而提供的,讀取響應(yīng)頭部镇饮。
  • openResponseBody 為獲得響應(yīng)而提供的蜓竹,打開(kāi)請(qǐng)求體,以用于后續(xù)獲取請(qǐng)求體數(shù)據(jù)
  • cancel 取消請(qǐng)求執(zhí)行

ConnectionSpec

在OkHttp中储藐,ConnectionSpec用于描述傳輸HTTP流量的socket連接的配置俱济。對(duì)于https請(qǐng)求,這些配置主要包括協(xié)商安全連接時(shí)要使用的TLS版本號(hào)和密碼套件钙勃,是否支持TLS擴(kuò)展等蛛碌。暫且只需要知道大致的作用豈可,因?yàn)榫唧w實(shí)現(xiàn)我也不是很明白辖源。

OK蔚携,有了上邊的鋪墊,我們來(lái)看一下StreamAllocation的幾個(gè)核心方法

newStream

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

這個(gè)方法沒(méi)什么可說(shuō)的克饶,里邊主要調(diào)用了兩個(gè)重要方法

  • findHealthyConnection獲取健康的鏈接
  • resultConnection.newCodec 創(chuàng)建編解碼工具HttpCodec

根據(jù) OkHttpClient中的設(shè)置酝蜒,連接超時(shí)、讀超時(shí)矾湃、寫(xiě)超時(shí)及連接失敗是否重試亡脑,調(diào)用 findHealthyConnection() 完成 連接,即RealConnection 的創(chuàng)建。然后根據(jù)HTTP協(xié)議的版本創(chuàng)建Http1Codec或Http2Codec远豺。

findHealthyConnection

/**
   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
   * until a healthy connection is found.
   */
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

首先通過(guò)findConnection獲取到一個(gè)鏈接奈偏,如果當(dāng)前鏈接成功次數(shù)為0代表是新的鏈接,直接返回躯护;如果不是新連接惊来,要判斷當(dāng)前鏈接是不是健康鏈接,如果是 返回棺滞,如果不是 繼續(xù)循環(huán)裁蚁。

findConnection

/**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection. We need to be careful here because our
      // already-allocated connection may have been restricted from creating new streams.
      /**嘗試獲取已經(jīng)存在的鏈接*/
      releasedConnection = this.connection;
      /**判斷這個(gè)鏈接是否滿足要求,滿足的話就直接使用已經(jīng)存在的鏈接*/
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        // We had an already-allocated connection and it's good.
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        // If the connection was never reported acquired, don't report it as released!
        releasedConnection = null;
      }

      /**如果沒(méi)有已經(jīng)存在的鏈接继准,就去連接池中獲取一個(gè)*/
      if (result == null) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        /**獲取到了就作為目標(biāo)鏈接枉证,后邊會(huì)以返回值的形式返回*/
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          /**獲取不到就更換路由,更換地址*/
          selectedRoute = route;
        }
      }
    }
    //關(guān)閉資源
    closeQuietly(toClose);
    /**以下是一些監(jiān)聽(tīng)回調(diào)*/
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }

    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    /**更換路由一直嘗試移必,直到從連接池中獲取到鏈接*/
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }

      /**如果從連接池中獲取不到可用鏈接室谚,那么就重新創(chuàng)建一個(gè)新的鏈接*/
      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        route = selectedRoute;
        refusedStreamCount = 0;
        result = new RealConnection(connectionPool, selectedRoute);
        /**將新的鏈接加入到弱引用集合中*/
        acquire(result, false);
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    /**鏈接握手并更新路由數(shù)據(jù)庫(kù)*/
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      /**將該簡(jiǎn)介放到連接池中*/
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      /**如果這個(gè)鏈接被多路復(fù)用,那么就同連接池排重一下*/
      if (result.isMultiplexed()) {
        /**該socket是被重復(fù)使用的socket的*/
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    /**如果是重復(fù)的socket則關(guān)閉socket崔泵,不是則socket為nul秒赤,什么也不做*/
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    /**返回鏈接*/
    return result;
  }

這段代碼比較長(zhǎng),大致總結(jié)如下:

  1. 首先判斷是否有已存在的鏈接憎瘸,如果有并且是可用狀態(tài)那么就是用已存在的鏈接
  2. 如果沒(méi)有就根據(jù)address去連接池中獲取一個(gè)鏈接入篮,如果有就使用,如果沒(méi)有就更換路由再次去連接池中獲取鏈接幌甘,如果有則直接使用
  3. 如果以上條件都不滿足的情況下潮售,直接創(chuàng)建(new)一個(gè)新的鏈接,并且將新的鏈接RealConnection通過(guò)acquire方法關(guān)聯(lián)到connection.allocations
  4. 最后做一個(gè)去重的判斷如果是重復(fù)的socket锅风,則關(guān)閉

以上大致是findConnection大致流程酥诽,涉及到了RealConnection的connect方法,有興趣的童鞋可以繼續(xù)深入了解下遏弱。

acquire和release

/**
   * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
   * {@link #release} on the same connection.
   */
  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
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();
  }

之前提到了RealConnection是通過(guò)acquire方法關(guān)聯(lián)到connection.allocations上的盆均,allocations是RealConnection定義的一個(gè)StreamAllocation的引用集合List<Reference<StreamAllocation>> allocations = new ArrayList<>();
StreamAllocationReference 是一個(gè)弱引用塞弊,所以在acquire方法調(diào)用的時(shí)候漱逸,實(shí)際上相當(dāng)于connection的引用計(jì)數(shù)器+1; 反之release 相當(dāng)于引用計(jì)數(shù)器-1;
以上是StreamAllocation類的一些重要方法分析,剩下一些方法注釋都很多游沿,不再贅述了饰抒。

intercept方法

??有上邊一大大大大堆的鋪墊之后終于到了ConnectInterceptor核心方法intercept分析了,直接代碼:

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);

  }
}

你沒(méi)看錯(cuò)诀黍,ConnectInterceptor的代碼就這么少袋坑,重要的幾個(gè)類上邊都有過(guò)分析,相信這幾行代碼都知道什么意思了眯勾,我也不再贅述了枣宫,接下來(lái)有了streamAllocation婆誓、httpCodecconnection之后也颤,下一層CallServerInterceptor改如何工作呢洋幻?

總結(jié)

?? ConnectInterceptor核心代碼比較多,需要消化消化翅娶,有許多關(guān)于HTTP的基礎(chǔ)知識(shí)還是需要鞏固的文留,要不然有些代碼看起來(lái)真的是很費(fèi)勁,本人就是這方面知識(shí)匱乏竭沫,后邊還需更多努力才行燥翅。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜕提,隨后出現(xiàn)的幾起案子森书,更是在濱河造成了極大的恐慌,老刑警劉巖谎势,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拄氯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡它浅,警方通過(guò)查閱死者的電腦和手機(jī)译柏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)姐霍,“玉大人鄙麦,你說(shuō)我怎么就攤上這事∧髡郏” “怎么了胯府?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)恨胚。 經(jīng)常有香客問(wèn)我骂因,道長(zhǎng),這世上最難降的妖魔是什么赃泡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任寒波,我火速辦了婚禮,結(jié)果婚禮上升熊,老公的妹妹穿的比我還像新娘俄烁。我一直安慰自己,他們只是感情好级野,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布页屠。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辰企。 梳的紋絲不亂的頭發(fā)上风纠,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音牢贸,去河邊找鬼议忽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛十减,可吹牛的內(nèi)容都是我干的栈幸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼帮辟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼速址!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起由驹,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芍锚,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蔓榄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體并炮,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年甥郑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逃魄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澜搅,死狀恐怖伍俘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勉躺,我是刑警寧澤癌瘾,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站饵溅,受9級(jí)特大地震影響妨退,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜕企,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一咬荷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糖赔,春花似錦萍丐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至奋构,卻和暖如春壳影,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弥臼。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工宴咧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人径缅。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓掺栅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親纳猪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氧卧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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