Okhttp源碼學習四(連接攔截器的內部實現)

Okhttp的5個內置攔截器可以說是Okhttp的核心,因為整個請求的過程都被封裝在這5個攔截器里面。而5個攔截器里面的核心就是這篇要分析的ConnectInterceptor,因為ConnectInterceptor才是真正發起請求,建立連接地方

ConnectInterceptor

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

整個ConnectInterceptor類的代碼也就這么幾行,但從這幾行中可以看出,真正創建流和建立連接的邏輯其實都在StreamAllocation里面. StreamAllocation對象是從RealInterceptorChain獲取的. 通過之前的幾篇對Okhttp的源碼的學習,我們知道攔截器的攔截方法intercept(Chain chain)中的chain最開始是在RealCallgetResponseWithInterceptorChain()中初始化的:

 Response getResponseWithInterceptorChain() throws IOException {
........
//第二個參數就是StreamAllocation 類型,但是傳的是null
  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

   return chain.proceed(originalRequest);
 }

可以看到第一個攔截器鏈創建的時候,StreamAllocation 傳的是null,那么StreamAllocation 是什么時候賦值的呢,其實是在第一個攔截器RetryAndFollowUpInterceptor的攔截方法里面賦值的:

 @Override
 public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

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

    response = realChain.proceed(request, streamAllocation, null, null);
    ........
 } 

第一個攔截器給StreamAllocation賦值后,后面的攔截器中的攔截器鏈的StreamAllocation就都是這個值. 前面說StreamAllocation很重要,具體的建立連接過程,還有創建流都是在這個類里面,下面就看看StreamAllocation

StreamAllocation

先看一下StreamAllocation的成員和構造函數

public final class StreamAllocation {
  public final Address address;      //請求的url地址
  private RouteSelector.Selection routeSelection;    //選擇的路由
  private Route route;
  private final ConnectionPool connectionPool;    //連接池
  public final Call call;
  public final EventListener eventListener;
  private final Object callStackTrace;

  // State guarded by connectionPool.
  private final RouteSelector routeSelector;    //路由選擇器
  private int refusedStreamCount;    //拒絕的次數
  private RealConnection connection;  //連接
  private boolean reportedAcquired;
  private boolean released;
  private boolean canceled;
  private HttpCodec codec;    //負責寫入請求數據或讀出響應數據的IO流

  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
      EventListener eventListener, Object callStackTrace) {
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
    this.callStackTrace = callStackTrace;
  }
  ....
}

StreamAllocation的成員和構造函數可以看到,StreamAllocation里面主要包含了連接池,連接,還有流. 為了更好理解這個類,先屢一下基本概念:

  1. HTTP請求網絡的時候,首先需要通過一個Socket與服務端建立TCP連接,Socket中還需要有主機名host和端口號port
  2. 建立好連接后,就可以使用流在這個連接上向服務端寫入數據和讀取服務端返回的數據
  3. HTTP/1.1提出了Keep-Alive機制:當一個HTTP請求的數據傳輸結束后,TCP連接不立即釋放,如果此時有新的HTTP請求,且其請求的Host同上次請求相同,則可以直接復用未釋放的TCP連接,從而省去了TCP的釋放和再次創建的開銷,減少了網絡延時
  4. HTTP2.0的多路復用:允許同時通過單一的 HTTP/2 連接發起多重的請求-響應消息

OkHttp為了解耦,對請求中的各個概念進行了封裝, RealConnection就對應著請求中的連接,HttpCodec對應流,為了HTTP1.1的連接復用以及HTTP2.0的多路復用,就需要將請求連接保存下來,以便復用,所以就有了ConnectionPool. 而為了執行一次網絡請求,需要從連接池找到可用的的連接,然后創建流,所以就需要一個分配管理流的角色,這個角色就是StreamAllocation

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

調用了findHealthyConnection()resultConnection.newCodec(),先看findHealthyConnection()

//StreamAllocation.findHealthyConnection()
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) {
      //successCount為0,說明是新建立的連接,沒有用過,默認可用,直接返回
      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;
  }
}

又調用了findConnection去查找連接

//StreamAllocation.findConnection()
 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.
    releasedConnection = this.connection;
    toClose = releaseIfNoNewStreams();
    //如果當前StreamAllocation持有的連接不為空
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //將這個持有的連接賦值給result
      result = this.connection;
      releasedConnection = null;
    }
    if (!reportedAcquired) {
      // If the connection was never reported acquired, don't report it as released!
      releasedConnection = null;
    }
    //當前StreamAllocation持有的連接為空,reuslt在這里就會為空,說明還沒找到可用連接
    if (result == null) {
      // Attempt to get a connection from the pool.
      //從連接池中找一下,如果找到了會給持有的連接賦值
      Internal.instance.get(connectionPool, address, this, null);
      //如果從連接池中找到了可用的連接
      if (connection != null) {
        foundPooledConnection = true;
        //賦值給result
        result = connection;
      } else {
        //如果沒找到,將StreamAllocation持有的路由賦值給已找到的路由
        selectedRoute = route;
      }
    }
  }
  closeQuietly(toClose);

  if (releasedConnection != null) {
    eventListener.connectionReleased(call, releasedConnection);
  }
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
  }
  if (result != null) {  //result不為空意味著當前連接可以用,或者從連接池中找到了可以復用的連接
    // 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;
        }
      }
    }

    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);
      //關聯到流管理引用列表connection.allocations,并用this.connection記錄當前連接
      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.
  //與服務端建立TCP連接
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  routeDatabase().connected(result.route());

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

    // Pool the connection.
    //將新建的連接存進連接池
    Internal.instance.put(connectionPool, result);

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

  eventListener.connectionAcquired(call, result);
  return result;
 }

從源碼可以看到,OkHttp尋找可用連接的過程如下:

1. 如果是重定向請求,就使用StreamAllocation持有的連接
    releasedConnection = this.connection;
    //如果當前連接不能創建新的流,就釋放
    toClose = releaseIfNoNewStreams();
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //將StreamAllocation持有的連接賦值給result,表示就用這個持有的連接
      result = this.connection;
      //不釋放當前連接
      releasedConnection = null;
    }

StreamAllocation持有的連接this.connection一開始肯定是為null,但是當從連接池中找到了可用的連接后,或者從連接池沒找到,新建一個連接后,StreamAllocation持有的連接this.connection就不為null. 不為null的時候就把它賦值給 result,表示就用這個持有的連接。

但我們知道在RetryAndFollowUpInterceptor的攔截方法里面,StreamAllocation是新建的。每調用一次RetryAndFollowUpInterceptor的攔截方法就會新建一個StreamAllocation,也就是說每一次請求都會新建一個StreamAllocation. 那么也就意味著每一次請求使用的連接根本不可能用到StreamAllocation持有的連接this.connection,因為StreamAllocation 是新建的,this.connection一直是null. 那么什么時候this.connection才會不為null呢?其實只有在第一次請求,服務端返回一個比如狀態碼為307這樣的需要重定向的響應的時候,并且重定向的Request的host、port、scheme與之前一致時出現。在RetryAndFollowUpInterceptor中,如果響應為需要重定向,那么會再發起一次請求,第二次請求時,使用的StreamAllocation就是第一次創建的,這個時候就會用到這個StreamAllocation持有的連接(不太明白可以去看下Okhttp源碼學習三(重試和重定向,橋接,緩存攔截器的內部原理))

2. 如果不是重定向請求,就遍歷連接池中的所有連接,看是否有可復用的連接
  if (result == null) {    //result為null,意味著不是重定向請求
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    if (connection != null) {
      foundPooledConnection = true;
      result = connection;
    } else {
      selectedRoute = route;
    }
  }

調用了Internal.instance.get(connectionPool, address, this, null)從連接池中去找

public abstract class Internal {

  public static void initializeInstanceForTests() {
   // Needed in tests to ensure that the instance is actually pointing to something.
    new OkHttpClient();
  }

  public static Internal instance;

  public abstract void addLenient(Headers.Builder builder, String line);
  .......
}

Internal是一個抽象類,它的唯一實現是在 OkHttpClient中:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(Protocol.HTTP_2, Protocol.HTTP_1_1);

  static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);

  static {
    Internal.instance = new Internal() {
     ........
    @Override public RealConnection get(ConnectionPool pool, Address address,StreamAllocation streamAllocation, Route route) {
      //調用的是連接池ConnectionPool的get方法
      return pool.get(address, streamAllocation, route);
    }
    .........
  };
 ......
}

看一下ConnectionPool的get()方法:

 @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    //變量連接池中的所有連接,connections是Deque類型
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
   return null;
 }

調用了 connection.isEligible(address, route)來判斷是否可以復用

//RealConnection.isEligible()
public boolean isEligible(Address address, @Nullable Route route) {
  //如果當前連接上的并發流數量超過最大值1,或當前連接不能創建新的流,返回false
  if (allocations.size() >= allocationLimit || noNewStreams) return false;
  //如果兩個address除了host以外的所有域不相同,返回false
  if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
  //如果host也相同,那么當前連接可以復用,直接返回
  if (address.url().host().equals(this.route().address().url().host())) {
    return true; // This connection is a perfect match.
  }
  //http2連接為空,直接返回false
  if (http2Connection == null) return false;

  if (route == null) return false;
  //路由用到了代理,返回false
  if (route.proxy().type() != Proxy.Type.DIRECT) return false;
  if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
  //socket地址相同不同,返回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.
}

判斷是否可以復用的條件大致就是(后面對HTTP2的復用條件判斷暫時沒看明白):

當前連接的流的數量要少于1個,請求地址的host相同

如果連接池中有符合上面的條件的連接,就調用streamAllocation.acquire(connection, true);

//StreamAllocation.acquire()
public void acquire(RealConnection connection, boolean reportedAcquired) {
  assert (Thread.holdsLock(connectionPool));
  if (this.connection != null) throw new IllegalStateException();
  //給StreamAllocation持有的連接賦值
  this.connection = connection;
  this.reportedAcquired = reportedAcquired;
  //將連接connection與StreamAllocation綁定
  connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

connection.allocations是一個列表:

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

每一個連接對象RealConnection都有一個列表,列表的元素類型是StreamAllocation的弱引用,它用來記錄當前連接上建立的流。因為每一次請求都會創建一個新的StreamAllocation

回到StreamAllocation.findConnection()中:

 if (result == null) {
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    //如果連接池中有復用的連接,connection就不為null
    if (connection != null) {
      foundPooledConnection = true;
      //使用復用的連接
      result = connection;
    } else {
      selectedRoute = route;
    }
  }
}
closeQuietly(toClose);

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

如果從連接池找到了可以復用的連接,直接返回這個復用的連接

3. 如果在連接池沒有找到可復用的連接,就切換路由,再從連接池中找一次
 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;
      }
    }
  }

關于RouteSelector以及路由的選擇,切換,下篇再分析

4. 切換路由后,連接池中還是沒有找到可以復用的連接,就新建一個連接,并將新建的connection和當前的StreamAllocation綁定
 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);
    //綁定connection和StreamAllocation
    acquire(result, false);
  }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容