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

推荐阅读更多精彩内容