OKhttp源碼學習(七)—— 攔截器_ConnectInterceptor

ConnectInterceptor連接攔截器分析

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

經過前幾個攔截器的預熱,終於來到了攔截器的重頭戲了,連接相關的攔截器。這個也耗費了較多時間去準備。(代碼較多,擼代碼請慎重)

在分析第一個攔截器中RetryAndFollowUpInterceptor,我們知道,當時初始化了一個StreamAllocation的連接對象,也提供了一些對連接對象操作的方法,如取消連接等,但是卻沒有立刻的做連接,只是一直把這個對象往下傳遞。而在各種初始化之後(Gzip, Header, 以及cookie的處理攔截器緩存攔截器),再進行連接操作。

intercept(攔截)

  @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, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

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

咋眼一看,這個攔截器的代碼很簡單啊,只有短短的幾行代碼。但是裏面蘊含的東西,有點多,需要細細分析。

//獲取在第一個攔截器就創建的StreamAllocation 類,而這個類,創建時候傳入了一個ConnectionPool,以及地址相關信息
StreamAllocation streamAllocation = realChain.streamAllocation();

//通過streamAllocation ,newStream,這個裏面會創建連接等一系列的操作。
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);

//這裏是是獲取前一步的connection.
RealConnection connection = streamAllocation.connection();

//這裏是把前面創建的連接,傳遞到下一個攔截器
return realChain.proceed(request, streamAllocation, httpCodec, connection);

步驟詳細分析

PS: 源代碼較多,大部分分析會在代碼以註釋的形式存在。

基本步驟就上面展示了,我們理清楚以下幾個類的調用關係,來分析一下連接是如何一步步建立的:

  1. StreamAllocation
  2. ConnectionPool
  3. RealConnection
1. StreamAllocation實體

首先,StreamAllocation的初始化在第一個攔截器裏面,

streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

傳入了三個參數,一個連接池,一個地址類,一個調用堆棧跟蹤相關的。

在StreamAllocation構造函數中,主要是把這個三個參數保存爲內部變量,供後面使用,還有一個就是同時創建了一個線路選擇器:

this.routeSelector = new RouteSelector(address, routeDatabase());

用於後面選擇線路使用。

2. newStream()方法

StreamAllocation 的 newStream()是一個建立連接的重要方法,接下來就是一步步對裏面的代碼擼一擼:

  public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    //1. 獲取設置的連接超時時間,讀寫超時的時間,以及是否進行重連。 
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
     // 2. 獲取健康可用的連接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
     
     //3. 通過resultConnection初始化,對請求以及結果 編解碼的類(分http 1.1 和http 2.0)。
     // 這裏主要是初始化,在後面一個攔截器纔用到這相關的東西。
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

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

在上面的代碼中最重要的,是註釋 第二點,獲取健康可用的連接,那我們繼續深入:

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {

   // 1. 加了個死循環,一直找可用的連接
    while (true) {
     
      // 2. 這裏繼續去挖掘,尋找連接
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

     // 3. 連接池同步獲取,上面找到的連接是否是一個新的連接,如果是的話,就直接返回了,就是我們需要找
    // 的連接了
      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      //4.  如果不是一個新的連接,那麼通過判斷,是否一個可用的連接。
      // 裏面是通過Socket的一些方法進行判斷的,有興趣的,可以繼續研究一下
      // 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;
    }
  }

上面的代碼,重要的也是註釋的第二點:繼續去挖掘,尋找連接, 我們一直在找連接,但是到現在爲止,都是還沒到真正的連接部分 ~~#!

我們繼續擼啊擼:(其實你看到下面一大段代碼,你就知道,其實應該就是我們要找的地方了)

  /**
   * 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,
      boolean connectionRetryEnabled) throws IOException {

    Route selectedRoute;
   
   // 1. 同步線程池,來獲取裏面的連接
    synchronized (connectionPool) {
        
         // 2. 做些判斷,是否已經釋放,是否編解碼類爲空,是否用戶已經取消
         if (released) throw new IllegalStateException("released");
         if (codec != null) throw new IllegalStateException("codec != null");
         if (canceled) throw new IOException("Canceled");
         
         // 3. 嘗試用一下現在的連接,判斷一下,是否有可用的連接
         // Attempt to use an already-allocated connection.
          RealConnection allocatedConnection = this.connection;
         if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
          }
          
         // 4. 嘗試在連接池中獲取一個連接,get方法中會直接調用,注意最後一個參數爲空
         // 裏面是一個for循環,在連接池裏面,尋找合格的連接
         // 而合格的連接會通過,StreamAllocation中的acquire方法,更新connection的值。
      
         // Attempt to get a connection from the pool.
         Internal.instance.get(connectionPool, address, this, null);
         if (connection != null) {
             return connection;
          }

          selectedRoute = route;
    }
    //5. 判斷上面得到的線路,是否空,如果爲空的,尋找一個可用的線路
    // 對於線路的選,可以深究一下這個RouteSeletor
    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();
    }

    RealConnection result;

    //6. 繼續線程池同步下去獲取連接
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");
   
      // 7. 由於上面我們獲取了一個線路,無論是新建的,或者已有的。
      // 我們通過這個線路,繼續在連接池中尋找是否有可用的連接。
      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // This could match due to connection coalescing.
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) return connection;

      // 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;
      
      // 8. 如果前面這麼尋找,都沒在連接池中找打可用的連接,那麼就新建一個
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }
    
    // 9. 這裏就是就是連接的操作了,終於找到連接的正主了,這裏會調用RealConnection的連接方法,進行連接操作。
    // 如果是普通的http請求,會使用Socket進行連接
    // 如果是https,會進行相應的握手,建立通道的操作。
    // 這裏就不對裏面的操作進行詳細分析了,有興趣可以在進去看看
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    routeDatabase().connected(result.route());

    Socket socket = null;
    
   // 10. 最後就是同步加到 連接池裏面了
    synchronized (connectionPool) {
      // Pool the connection.
      Internal.instance.put(connectionPool, result);

      // 最後加了一個多路複用的判斷,這個是http2纔有的
      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

總結一下,上面的代碼分爲以下幾步:

  1. 前置做些判斷,是否已經釋放,是否編解碼類爲空,是否用戶已經取消;
  2. 嘗試用一下現在的連接,判斷一下,是否有可用的連接,有就返回;
  3. 嘗試在連接池中獲取一個連接(線路爲空);
  4. 獲取線路;
  5. 通過獲取到的線路,再去連接池取,是否有可用連接,有就返回;
  6. 前面都找不到可用連接,新建一個;
  7. 對新建的連接,進行連接操作(Socket);
  8. 把剛新建的連接,丟到連接池裏面。

到這裏爲止,我們就已經獲取到了一個連接了,這個連接攔截器的主要功能其實已經達到了。

迴歸到攔截器,下一個方法:streamAllocation.connection()。其實這個非常簡單,就是獲取前面創建的 realConnection而已。

最後的最後,就是把我們的StreamAlloaction, RealConnection, 以及新建的HttpCodec(請求,結果編解碼類),傳遞到下一個攔截器去。

總結:

這是很重要的一個攔截器,這裏面把連接建立起來了。同時新建了一個編解碼的類,爲後面的數據交換讀取做了鋪墊。

其實分析還是比較粗糙的,有很多地方,還需要深入去解剖,也留下了一下學習的空間:

  1. RouteSelector,線路的選擇,是通過什麼來選擇線路的?
  2. 從連接池裏獲取已有的連接,是如何判斷它是否可用的?
  3. 新建連接,進行連接操作時候,http 和 https是有什麼差異的?
  4. http2的多路複用,是如何實現的?

系列(簡書地址):
OKhttp源碼學習(一)—— 基本請求流程
OKhttp源碼學習(二)—— OkHttpClient
OKhttp源碼學習(三)—— Request, RealCall
OKhttp源碼學習(四)—— RetryAndFollowUpInterceptor攔截器
OKhttp源碼學習(五)—— BridgeInterceptor攔截器
OKhttp源碼學習(六)—— CacheInterceptor攔截器
OKhttp源碼學習(八)——CallServerInterceptor攔截器
OKhttp源碼學習(九)—— 任務管理(Dispatcher)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章