OkHttpClient原始碼分析(五)—— ConnectInterceptor和CallServerInterceptor

chaychan發表於2019-01-04

上一篇我們介紹了快取攔截器CacheInterceptor,本篇將介紹剩下的兩個攔截器: ConnectInterceptorCallServerInterceptor

ConnectInterceptor

該攔截器主要是負責建立可用的連結,主要作用是開啟了與伺服器的連結,正式開啟了網路請求。 檢視其intercept()方法:

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //從攔截器鏈中獲取StreamAllocation物件
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    
    //建立HttpCodec物件
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    
    //獲取realConnetion
    RealConnection connection = streamAllocation.connection();

    //執行下一個攔截器,返回response
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
複製程式碼

可以看到intercept中的處理很簡單,主要有以下幾步操作:

  1. 從攔截器鏈中獲取StreamAllocation物件,在講解第一個攔截器RetryAndFollowUpInterceptor的時候,我們已經初步瞭解了StreamAllocation物件,在RetryAndFollowUpInterceptor中僅僅只是建立了StreamAllocation物件,並沒有進行使用,到了ConnectInterceptor中,StreamAllocation才被真正使用到,該攔截器的主要功能都交給了StreamAllocation處理;

  2. 執行StreamAllocation物件的 newStream() 方法建立HttpCodec,用於處理編碼Request和解碼Response;

  3. 接著通過呼叫StreamAllocation物件的 connection() 方法獲取到RealConnection物件,這個RealConnection物件是用來進行實際的網路IO傳輸的。

  4. 呼叫攔截器鏈的**proceed()**方法,執行下一個攔截器返回response物件。

上面我們已經瞭解了ConnectInterceptor攔截器的intercept()方法的整體流程,主要的邏輯是在StreamAllocation物件中,我們先看下它的 newStream() 方法:

 public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    ...
    try {
      //建立RealConnection物件
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      //建立HttpCodec物件
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
      
      synchronized (connectionPool) {
        codec = resultCodec;
        //返回HttpCodec物件
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }
複製程式碼

newStream()方法中,主要是建立了RealConnection物件(用於進行實際的網路IO傳輸)和HttpCodec物件(用於處理編碼Request和解碼Response),並將HttpCodec物件返回。

findHealthyConnection()方法用於建立RealConnection物件:

 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {//while迴圈
      //獲取RealConnection物件
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);
    
      //同步程式碼塊判斷RealConnection物件的successCount是否為0
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          //如果為0則返回
          return candidate;
        }
      }

      //對連結池中不健康的連結做銷燬處理
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }
複製程式碼

以上程式碼主要做的事情有:

  1. 開啟一個while迴圈,通過呼叫findConnection()方法獲取RealConnection物件賦值給candidate;
  2. 如果candidate 的successCount 為0,直接返回candidate,while迴圈結束;
  3. 呼叫candidate的isHealthy()方法,進行“健康檢查”,如果candidate是一個不“健康”的物件,其中不“健康”指的是Socket沒有關閉、或者它的輸入輸出流沒有關閉,則對呼叫noNewStreams()方法進行銷燬處理,接著繼續迴圈。

我們看下findConnection()方法做了哪些操作:

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    ...
    RealConnection result = null;
    ...
    synchronized (connectionPool) {
      ...
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        //如果不為 null,則複用,賦值給 result
        result = this.connection;
        releasedConnection = null;
      }
      ...
      //如果result為 null,說明上面找不到可以複用的
      if (result == null) {
        //從連線池中獲取,呼叫其get()方法
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          //找到對應的 RealConnection物件
          //更改標誌位,賦值給 result
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    
    ...
    if (result != null) {
      //已經找到 RealConnection物件,直接返回
      return result;
    }
    
    ...
     //連線池中找不到,new一個
     result = new RealConnection(connectionPool, selectedRoute);
    ...
    
    ...
    //發起請求
    result.connect(
        connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
    ...
    //存進連線池中,呼叫其put()方法
    Internal.instance.put(connectionPool, result);
    ...
    return result;
  }
複製程式碼

以上程式碼主要做的事情有:

  1. StreamAllocation的connection如果可以複用則複用;
  2. 如果connection不能複用,則從連線池中獲取RealConnection物件,獲取成功則返回;
  3. 如果連線池裡沒有,則new一個RealConnection物件;
  4. 呼叫RealConnection的connect()方法發起請求;
  5. 將RealConnection物件存進連線池中,以便下次複用;
  6. 返回RealConnection物件。

ConnectionPool 連線池介紹

剛才我們說到從連線池中取出RealConnection物件時呼叫了Internal的get()方法,存進去的時候呼叫了其put()方法。其中Internal是一個抽象類,裡面定義了一個靜態變數instance:

public abstract class Internal {
    ...
    public static Internal instance;
    ...
}
複製程式碼

instance的例項化是在OkHttpClient的靜態程式碼塊中:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  ...
  static {
      Internal.instance = new Internal() {
         ...
          @Override public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
            return pool.get(address, streamAllocation, route);
         }
         ...
         @Override public void put(ConnectionPool pool, RealConnection connection) {
           pool.put(connection);
         }
      };
  }
  ...
}
複製程式碼

這裡我們可以看到實際上 Internal 的 get()方法和put()方法是呼叫了 ConnectionPool 的get()方法和put()方法,這裡我們簡單看下ConnectionPool的這兩個方法:

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

@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;
  }
複製程式碼

在get()方法中,通過遍歷connections(用於存放RealConnection的ArrayDeque佇列),呼叫RealConnection的isEligible()方法判斷其是否可用,如果可用就會呼叫streamAllocation的acquire()方法,並返回connection。

我們看下呼叫StreamAllocation的acquire()方法到底做了什麼操作:

public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    //賦值給全域性變數
    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    //建立StreamAllocationReference物件並新增到allocations集合中
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }
複製程式碼
  1. 先是從連線池中獲取的RealConnection物件賦值給StreamAllocation的成員變數connection;

  2. 建立StreamAllocationReference物件(StreamAllocation物件的弱引用), 並新增到RealConnection的allocations集合中,到時可以通過allocations集合的大小來判斷網路連線次數是否超過OkHttp指定的連線次數。

接著我們檢視ConnectionPool 的put()方法:

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

put()方法在將連線新增到連線池之前,會先執行清理任務,通過判斷cleanupRunning是否在執行,如果當前清理任務沒有執行,則更改cleanupRunning標識,並執行清理任務cleanupRunnable。

我們看下清理任務cleanupRunnable中到底做了哪些操作:

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) {
            }
          }
        }
      }
    }
  };
複製程式碼

可以看到run()方法裡面是一個while死迴圈,其中呼叫了cleanup()方法進行清理操作,同時會返回進行下次清理的間隔時間,如果返回的時間間隔為-1,則會結束迴圈,如果不是-1,則會呼叫wait()方法進行等待,等待完成後又會繼續迴圈執行,具體的清理操作在cleanup()方法中:

long cleanup(long now) {
    //正在使用的連線數
    int inUseConnectionCount = 0;
    //空閒的連線數
    int idleConnectionCount = 0;
    //空閒時間最長的連線
    RealConnection longestIdleConnection = null;
    //最大的空閒時間,初始化為 Long 的最小值,用於記錄所有空閒連線中空閒最久的時間
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
      //for迴圈遍歷connections佇列
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //如果遍歷到的連線正在使用,則跳過,continue繼續遍歷下一個
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        //當前連線處於空閒,空閒連線數++
        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) {
        //如果存在空閒連線
        //計算出執行緒清理的時間即(保活時間-最大空閒時間),並返回
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
         //沒有空閒連線,返回keepAliveDurationNs
        return keepAliveDurationNs;
      } else {
        //連線池中沒有連線存在,返回-1
        cleanupRunning = false;
        return -1;
      }
    }

    //關閉空閒時間最長的連線
    closeQuietly(longestIdleConnection.socket());

    return 0;
  }
複製程式碼

cleanup()方法通過for迴圈遍歷connections佇列,記錄最大空閒時間和空閒時間最長的連線;如果存在超過空閒保活時間或空閒連線數超過最大空閒連線數限制的連線,則從connections中移除,最後執行關閉該連線的操作。

主要是通過pruneAndGetAllocationCount()方法判斷連線是否處於空閒狀態:

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

      ...
      
      references.remove(i);
      connection.noNewStreams = true;
      
      ...
      
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }
複製程式碼

該方法通過for迴圈遍歷RealConnection的allocations集合,如果當前遍歷到的StreamAllocation被使用就遍歷下一個,否則就將其移除,如果移除後列表為空,則返回0,所以如果方法的返回值為0則說明當前連線處於空閒狀態,如果返回值大於0則說明連線正在使用。

CallServerInterceptor

接下來講解最後一個攔截器CallServerInterceptor了,檢視intercept()方法:

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //相關物件的獲取 
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();

    ...
    
    //寫入請求頭
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //判斷是否有請求體
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        //詢問伺服器是否願意接收請求體
        httpCodec.flushRequest();//重新整理請求
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        //伺服器願意接收請求體
        //寫入請求體
        ...
      } else if (!connection.isMultiplexed()) {
        streamAllocation.noNewStreams();
      }
    }

    //結束請求
    httpCodec.finishRequest();

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      //根據伺服器返回的資料構建 responseBuilder物件
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    //構建 response物件
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    ...
    
    //設定 response的 body
    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
   
   //如果請求頭中 Connection對應的值為 close,則關閉連線
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }
    
    ...
    
    return response;
  }
複製程式碼

以上程式碼具體的流程:

  1. 從攔截器鏈中獲取到儲存的相關物件;
  2. 呼叫HttpCodec的writeRequestHeaders()方法寫入請求頭;
  3. 判斷是否需要寫入請求體,先是判斷請求方法,如果滿足,請求頭通過攜帶特殊欄位Expect: 100-continue來詢問伺服器是否願意接收請求體;
  4. 結束請求;
  5. 根據伺服器返回的資料構建response物件;
  6. 關閉連線;
  7. 返回response;

  好了,到這裡OkHttpClient原始碼分析就結束了,相信看完本套原始碼解析會加深你對OkHttpClient的認識,同時也學到了其巧妙的程式碼設計思路,在閱讀原始碼的過程中,我們的編碼能力也逐步提升,如果想要寫更加優質的程式碼,閱讀原始碼是一件很有幫助的事。

相關文章