OkHttp 源码分析(五)——ConnectInterceptor

0、前言

前面的文章中,我们分析了http的缓存策略和Okhttp缓存拦截器的缓存机制,我们知道,在没有缓存命中的情况下,需要对网络资源进行请求,这时候拦截链就来到ConnectInterceptor。

ConnectInterceptor的主要作用是和服务器建立连接,在连接建立后通过okio获取通向服务端的输入流Source和输出流Sink。

1、源码分析

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

ConnectInterceptor拦截器的intercept方法很简单,首先拿到StreamAllocation对象streamAllocation,这个对象在RetryAndFollowUpInterceptor拦截器中已经初始化好了,这在RetryAndFollowUpInterceptor拦截器分析中提到过。在拿到streamAllocation后,后续的连接建立工作都交给streamAllocation来完成。

ConnectionPool

我们知道http连接的需要三次握手、四次挥手操作,如果每次请求进行握手连接和挥手释放资源,则会消耗很多不必要的时间。针对这种情况,Http协议提供了keep-alive的请求头字段,来保持长连接,以便下次进行请求时不必进行重新握手操作。OkHttp为了方便管理所有连接,采用连接池ConnectionPool。

ConnectionPool的主要功能就是为了降低由于频繁建立连接导致的网络延迟。它实现了复用连接的策略。ConnectionPool双端队列Deque<RealConnection>来保存它所管理的所有RealConnection

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

ConnectionPool会对连接池中的最大空闲连接以及连接的保活时间进行控制,maxIdleConnections和keepAliveDurationNs成员分别体现对最大空闲连接数及连接保活时间的控制。ConnectionPool的初始化由OkhttpClient的Builder类完成,默认最大空闲连接数为5、保活时间5分钟。此外,我们也可以初始化OkHttpClient自定义ConnectionPool。ConnectionPool有提供put、get、evictAll等操作,但对连接池的操作,是通过Internal.instance进行的。

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

StreamAllocation

StreamAllocation是用来建立执行HTTP请求所需网络设施的组件,如其名字所显示的那样,分配Stream。StreamAllocation的构造函数如下:

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.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去查找是否有可服用的连接,有则复用,无则返回一个新建的连接:

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;
        }
      }
      //如果不是一个新的连接,判断是否一个健康的连接。
      //不健康的RealConnection条件为如下几种情况: 
      //RealConnection对象 socket没有关闭 
      //socket的输入流没有关闭 
      //socket的输出流没有关闭 
      //http2时连接没有关闭 
      // 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方法:

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

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

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

findConnection这个方法代码比较长,大致流程如下:

  1. 判断streamAllocation对象内部是否有可复用的connection对象;

  2. 如果streamAllocation对象无可用的connection对象,则通过Internal.instance从连接池中查找可用的connection对象;

  3. 如果连接池中仍未找到,则遍历所有路由路径,尝试再次从ConnectionPool中寻找可复用的连接;

  4. 前面三步都未找到,则新建一个连接,进行TCP + TLS握手

  5. 将新建的连接放入连接池中,并返回结果

 上面关键的步骤在于通过Internal.instance的get方法在连接池中查找可复用的连接,上面提到了连接池的操作是通过Internal.instance进行的,Internal.instance的get方法最终调用的是ConnectionPool的get方法:

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

ConnectionPool的get方法遍历connections队列,通过isEligible检测连接是否可复用,可复用则通过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;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

isEligible方法判断连接是否可用:

 public boolean isEligible(Address address, @Nullable Route route) {
    // 如果这个连接不接受新的流,返回false.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    //判断host是否匹配
    // 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/
    //上面条件不满足,判断是否http2
    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. 路由必须共享一个IP地址。这要求我们为两个主机都有DNS地址,这只发生在路由规划之后。我们            
    //不能合并使用代理的连接,因为代理没有告诉我们源服务器的IP地址。
    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. 此连接的服务器证书必须覆盖新主机
    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.
  }

新建流(HttpCodec)

回到StreamAllocation的newStream方法,在获取到Connection对象后,接下来就是新建流,即HttpCodec对象。HttpCodec对象是封装了底层IO的可以直接用来收发数据的组件(依赖okio库),它会将请求的数据序列化之后发送到网络,并将接收的数据反序列化为应用程序方便操作的格式。

public interface HttpCodec {
  /**
   * The timeout to use while discarding a stream of input data. Since this is used for connection
   * reuse, this timeout should be significantly less than the time it takes to establish a new
   * connection.
   */
  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;

  /** Returns an output stream where the request body can be streamed. */
  Sink createRequestBody(Request request, long contentLength);

  /** This should update the HTTP engine's sentRequestMillis field. */
  void writeRequestHeaders(Request request) throws IOException;

  /** Flush the request to the underlying socket. */
  void flushRequest() throws IOException;

  /** Flush the request to the underlying socket and signal no more bytes will be transmitted. */
  void finishRequest() throws IOException;

  /**
   * Parses bytes of a response header from an HTTP transport.
   *
   * @param expectContinue true to return null if this is an intermediate response with a "100"
   *     response code. Otherwise this method never returns null.
   */
  Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;

  /** Returns a stream that reads the response body. */
  ResponseBody openResponseBody(Response response) throws IOException;

  /**
   * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
   * That may happen later by the connection pool thread.
   */
  void cancel();
}

HttpCodec作用:

  • 创建请求体,以用于发送请求体数据。

  • 写入请求头

  • 结束请求发送

  • 读取响应头部。

  • 打开请求体,以用于后续获取请求体数据。

  • 取消请求执行

 拿到HttpCodec对象后,回到拦截器处,将

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值