okhttp6

okhttp分享六:ConnectInterceptor(2)

前一章我们已经介绍了路由、连接、请求流等,这一章我们来看下okhttp的连接池,以及okhttp如何维护连接、请求流之间的关系。

一、okhttp中连接、连接池、流的关系

之前我们说过,客户端与服务端网络通信的过程,首先建立连接(tcp/udp),连接建立后基于当前连接及请求协议(http)构建信息传输流,与服务器通信。那连接与请求流的对应关系又是怎样的呢。

1.1、连接复用

http1.x规定一个tcp连接只能同时并发一个请求流,而http2.x则需要客户端与服务器协商后规定一个tcp连接上可以并发几个请求流,没有具体限制请求流的数量。
我们知道http是跑在tcp连接上的协议,而tcp连接建立及释放需要经历三次握手与四次挥手,十分耗时,所以在http1.1上提出了keep-alive且默认开启,实现了连接的复用。Keep-Alive: timeout=5, max=100,timeout=5表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。这就是http1.x在协议上对连接复用的支持。

1.2、ConnectionPool

上面讲的是协议层的复用,在此基础上okhttp在客户端也对连接进行了复用,这个复用就是基于它的连接池实现的,这也是okhttp的优点及特点之一。下面我们分析下这个ConnectionPool,先看下它的属性

private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;
  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) {
            }
          }
        }
      }
    }
  };

  private final Deque connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();
  boolean cleanupRunning;

按顺序解释下

  • executor: 一个用于清除过期连接的线程池,根据我们之前介绍的线程池知识,这个线程池阀值是Integer.MAX_VALUE,它不保留任何最小线程,随时创建更多的线程数,而且如果线程空闲后,只能多活60秒。所以也就说如果收到20个并发请求,线程池会创建20个线程,当完成后的60秒后会自动关闭所有20个线程。
  • maxIdleConnections:每个address的最大空闲连接数
  • keepAliveDurationNs:空闲连接可存在最大时间
  • cleanupRunnable:清理任务
  • connections:连接的双向队列
  • routeDatabase:路由信息数据库
  • cleanupRunning:清理任务正在执行的标志

在看下其构造方法

/**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;//最大空闲连接数
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);//空闲连接可存在最大时间

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

从构造方式可以看出,这个连接池最大空闲连接数为5,每个连接可存活时间为5分钟。这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。ConnectionPool实例化是在okhttpClient实例化的时候做的。
既然是一个连接池,那必然有存取,我们来看一下存取对应的get与put方法

 /**
   * Returns a recycled connection to {@code address}, or null if no such connection exists. The
   * route is null if the address has not yet been routed.
   */
  @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 寻找同时满足条件的RealConnection。

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

put方法更为简单,就是异步触发清理任务,然后将连接添加到队列中。那么下面开始重点分析他的清理任务。

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

逻辑也很清晰,调用cleanup方法进行清理,该方法会返回一个等待时间,若等待时间为-1,说明没有当前需要清理的连接,退出清理任务,等下次put连接进线程池时重新触发清理任务;若时间大于0,则等待相应时间后再次触发清理任务。我们再来看下cleanup函数:

/**
   * Performs maintenance on this pool, evicting the connection that has been idle the longest if
   * either it has exceeded the keep alive limit or the idle connections limit.
   *
   * 

Returns the duration in nanos to sleep until the next scheduled call to this method. Returns * -1 if no further cleanups are required. */ long cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { for (Iterator i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // If the connection is in use, keep searching. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } //统计空闲连接数量 idleConnectionCount++; // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { //找出空闲时间最长的连接以及对应的空闲时间 longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside // of the synchronized block). //在符合清理条件下,清理空闲时间最长的连接 connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // A connection will be ready to evict soon. //不符合清理条件,则返回下次需要执行清理的等待时间,也就是此连接即将到期的时间 return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // All connections are in use. It'll be at least the keep alive duration 'til we run again. //没有空闲的连接,则隔keepAliveDuration(分钟)之后再次执行 return keepAliveDurationNs; } else { // No connections, idle or in use. //清理结束 cleanupRunning = false; return -1; } } //关闭socket资源 closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. //这里是在清理一个空闲时间最长的连接以后会执行到这里,需要立即再次执行清理 return 0; }

  • 1、遍历连接池中所有链接,调用pruneAndGetAllocationCount方法判断连接是否活跃,若处于活跃状态,活跃连接数+1,结束此次判断,进入对下一连接的判断
  • 2、若处于空闲状态,空闲连接数+1,找出空闲时间最长的连接及其对应空闲时间
  • 3、若空闲时间最长的连接对应的空闲时间大于空闲连接可存活时间或空闲连接数大于最大空闲连接数,则将空闲时间最长的连接从连接池(列表)中移除,并立刻执行下一轮清理任务
  • 4、若3对应条件不满足,但连接池中存在空闲连接,则返回下次执行清理任务所需等待时间,为空闲连接可存活时间与当前空闲时间最长的连接对应的空闲时间。
  • 5、若3、4都不满足,且活跃连接数大于0,说明当前连接池中所有连接都处于工作状态,下次执行连接清理任务所需的等待时间就等于空闲连接可存活时间
  • 6、若3、4、5都不满足,说明当前连接池中没有连接,清理任务结束,返回-1

接着看pruneAndGetAllocationCount方法

/**
   * Prunes any leaked allocations and then returns the number of remaining live allocations on
   * {@code connection}. Allocations are leaked if the connection is tracking them but the
   * application code has abandoned them. Leak detection is imprecise and relies on garbage
   * collection.
   */
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference reference = references.get(i);

      if (reference.get() != null) {
        i++;
        continue;
      }

      // We've discovered a leaked allocation. This is an application bug.
      StreamAllocation.StreamAllocationReference streamAllocRef =
          (StreamAllocation.StreamAllocationReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }

pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。
看下ConnectionPool中的另外几个方法

/**
   * Notify this pool that {@code connection} has become idle. Returns true if the connection has
   * been removed from the pool and should be closed.
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
     //该连接已经不可用
    if (connection.noNewStreams || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      //欢迎clean 线程
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }
···
 /**
   * Replaces the connection held by {@code streamAllocation} with a shared connection if possible.
   * This recovers when multiple multiplexed connections are created concurrently.
   */
  Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, null)
          && connection.isMultiplexed()
          && connection != streamAllocation.connection()) {
        return streamAllocation.releaseAndAcquire(connection);
      }
    }
    return null;
  }

connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么就会调用该方法,由ConnectionPool来决定是否需要清理该连接。deduplicate方法主要是针对HTTP/2场景下多个多路复用连接清除的场景。如果是当前连接是HTTP/2,那么所有指向该站点的请求都应该基于同一个TCP连接。

二、StreamAllocation

之前我们简单介绍过http2与http1.x相比,最明显的优化就是实现了多路复用技术,即多个请求流可以共用同一个tcp连接。在okhttp中来说,就是针对http1.x协议,一个RealConnection上只能承载一个httpcodec;针对http2.0协议,多个httpcodec可以公用一个RealConnection。而StreamAllocation这个类就是用来处理连接(RealConnection)和请求流(httpcodec)之间的关系的。
这个类之前是在RetryAndFollowUpInterceptor中实例化的,会传入当前请求相关信息、网络请求所用连接池等。具体调用时,先会根据请求获取连接,再会根据获取连接生成流。
先看一下属性即构造函数

public final Address address;
  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;//流
  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;
  }

重要属性都加了备注,接着看下其核心方法。

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

newStream 方法中

  • 1、通过findHealthyConnection获取一个连接
  • 2、resultConnection.newCodec获取流
    看下这个findHealthyConnection
/**
   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
   * until a healthy connection is found.
   */
  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;
        }
      }

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

从注释可以看出,这个方法主要通过while循环找到一个健康的链接,逻辑很简单,首先找到连接再判断该连接是否可用,若可用则返回。关键方法就是findConnection()。

 /**
   * 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;
    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.
      RealConnection allocatedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
          // 如果已经存在的连接满足要求,则使用已存在的连接
        return allocatedConnection;
      }
      //从缓存中去取
      // Attempt to get a connection from the pool.
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }

      selectedRoute = route;
    }
       // 线路的选择,多ip的支持
    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      //里面是个递归
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // 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;
     // 以上都不符合,创建一个连接
      result = new RealConnection(connectionPool, selectedRoute);
      acquire(result);
    }
     //连接并握手
    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    //更新本地数据库
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // 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()) {
        //调用connectionPool的deduplicate方法去重。
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    //如果是重复的socket则关闭socket,不是则socket为nul,什么也不做
    closeQuietly(socket);
    //返回整个连接
    return result;
  }
  • 1、若已存在可用连接,则直接返回可用连接
  • 2、根据请求地址(address)去连接池中匹配连接,若命中则取出返回
  • 3、若2中未命中,则替换路由线路,继续去线程池中寻找。
  • 4、若尝试完可用路由,仍然无法找到对应连接,则重新new一个RealConnection
  • 5、调用acquire方法将streamAllocation,RealConnection两者关联起来
  • 6、调用新建connection的connect方法,构建socket连接,并更新路由数据库。
  • 7、将6中的连接放入连接池
  • 8、去重操作(针对http2)
  • 9、返回result(RealConnection)

findConnection返回Connection后还需要判断返回连接是否可用(健康),看下具体代码

/** Returns true if this connection is ready to host new streams. */
  public boolean isHealthy(boolean doExtensiveChecks) {
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
      return false;
    }

    if (http2Connection != null) {
      return !http2Connection.isShutdown();
    }

    if (doExtensiveChecks) {
      try {
        int readTimeout = socket.getSoTimeout();
        try {
          socket.setSoTimeout(1);
          if (source.exhausted()) {
            return false; // Stream is exhausted; socket is closed.
          }
          return true;
        } finally {
          socket.setSoTimeout(readTimeout);
        }
      } catch (SocketTimeoutException ignored) {
        // Read timed out; socket is good.
      } catch (IOException e) {
        return false; // Couldn't read; socket is closed.
      }
    }

    return true;
  }

从代码可以看出,健康的连接需要

  • 1、socket连接未关闭
  • 2、socket连接对应的输入输出可用
  • 3、若是http2,则连接不能shudown

这里有个参数doExtensiveChecks,这个参数标志是否需要额外确认。看下其他几个方法

public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
    eventListener.responseBodyEnd(call, bytesRead);

    Socket socket;
    Connection releasedConnection;
    boolean callEnd;
    synchronized (connectionPool) {
      if (codec == null || codec != this.codec) {
        throw new IllegalStateException("expected " + this.codec + " but was " + codec);
      }
      if (!noNewStreams) {
        connection.successCount++;
      }
      releasedConnection = connection;
      socket = deallocate(noNewStreams, false, true);
      if (connection != null) releasedConnection = null;
      callEnd = this.released;
    }
    closeQuietly(socket);
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }

    if (e != null) {
      eventListener.callFailed(call, e);
    } else if (callEnd) {
      eventListener.callEnd(call);
    }
  }

该方法会在请求流结束后调用,用于关闭流。主要看下deallocate方法

/**
   * Releases resources held by this allocation. If sufficient resources are allocated, the
   * connection will be detached or closed. Callers must be synchronized on the connection pool.
   *
   * 

Returns a closeable that the caller should pass to {@link Util#closeQuietly} upon completion * of the synchronized block. (We don't do I/O while synchronized on the connection pool.) */ private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) { assert (Thread.holdsLock(connectionPool)); if (streamFinished) { this.codec = null; } if (released) { this.released = true; } Socket socket = null; if (connection != null) { if (noNewStreams) { connection.noNewStreams = true; } if (this.codec == null && (this.released || connection.noNewStreams)) { release(connection); if (connection.allocations.isEmpty()) { connection.idleAtNanos = System.nanoTime(); if (Internal.instance.connectionBecameIdle(connectionPool, connection)) { socket = connection.socket(); } } connection = null; } } return socket; }

deallocate(boolean, boolean, boolean)方法也简单,根据传入的三个布尔类型的值进行操作,noNewStreams连接是否可以新建流,若为true则该连接上不可以再新建流,即不可用;released表示当前连接是否被释放,streamFinished表示是否需要关闭流。具体逻辑如下

  • 1、若streamFinished为true,则将对应httpcodec对象置null
  • 2、若released为true,则表示当前连接需要被释放
  • 3、若当前连接上的流为null,且当前连接被释放或不可用,则执行以下逻辑
    • 3.1、RealConnection对应的allocations清空release(connection)
    • 3.2、将当前连接变为空闲连接并启动cleanup任务
    • 3.3、获取当前连接的socket在realconnection置null后返回
  • 4、关闭3中返回的socket

最后看下streamFailed方法,如果在连接中过程出现异常,会调用streamFailed(IOException)方法

public void streamFailed(IOException e) {
    Socket socket;
    Connection releasedConnection;
    boolean noNewStreams = false;

    synchronized (connectionPool) {
      if (e instanceof StreamResetException) {
        ErrorCode errorCode = ((StreamResetException) e).errorCode;
        if (errorCode == ErrorCode.REFUSED_STREAM) {
          // Retry REFUSED_STREAM errors once on the same connection.
          refusedStreamCount++;
          if (refusedStreamCount > 1) {
            noNewStreams = true;
            route = null;
          }
        } else if (errorCode != ErrorCode.CANCEL) {
          // Keep the connection for CANCEL errors. Everything else wants a fresh connection.
          noNewStreams = true;
          route = null;
        }
      } else if (connection != null
          && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
        noNewStreams = true;

        // If this route hasn't completed a call, avoid it for new connections.
        if (connection.successCount == 0) {
          if (route != null && e != null) {
            routeSelector.connectFailed(route, e);
          }
          route = null;
        }
      }
      releasedConnection = connection;
      socket = deallocate(noNewStreams, false, true);
      if (connection != null || !reportedAcquired) releasedConnection = null;
    }

    closeQuietly(socket);
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
  }

该方法主要是对一些属性的设置,更新路由信息以及关闭连接、socket,逻辑比较简单,不做赘述。

三、连接流程梳理

最后我们再回到ConnectInterceptor,看下intercept方法,整理下连接构建的简单流程

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

首先了通过streamAllocation的newStream方法获取一个流(HttpCodec 是个接口,根据协议的不同,由具体的子类的去实现),第二步就是获取对应的RealConnection,
StreamAllocation的newStream()内部其实是通过findHealthyConnection()方法获取一个RealConnection,而在findHealthyConnection()里面通过一个while(true)死循环不断去调用findConnection()方法去找RealConnection。在findConnection()里面主要是通过3重判断:1、如果有已知连接且可用,则直接返回,2、如果在连接池有对应address的连接,则返回,3、切换路由再在连接池里面找下,如果有则返回,如果上述三个条件都没有满足,则直接new一个RealConnection。然后开始构建socket连接、握手,握手结束后,把连接加入连接池,如果在连接池有重复连接,和合并连接。findHealthyConnection()中会对findConnection()返回的连接进行可用性判断,调用isHealthy方法。

四、CallServerInterceptor

到ConnectInterceptor结束,我们已经成功与服务器构建好连接、流,下面就要开始收发数据了,CallServerInterceptor就是负责数据的收发工作。
在OkHttp里面读取数据主要是通过以下四个步骤来实现的

  • 写入请求头
  • 写入请求体
  • 读取响应头
  • 读取响应体
@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();

    long sentRequestMillis = System.currentTimeMillis();

    realChain.eventListener().requestHeadersStart(realChain.call());
    //写入请求头
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);
    
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }
    //写入请求体
      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
        realChain.eventListener()
            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
      } else if (!connection.isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
        // from being reused. Otherwise we're still obligated to transmit the request body to
        // leave the connection in a consistent state.
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();
//读取响应头
    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
//读取响应体
    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual response
      responseBuilder = httpCodec.readResponseHeaders(false);

      response = responseBuilder
              .request(request)
              .handshake(streamAllocation.connection().handshake())
              .sentRequestAtMillis(sentRequestMillis)
              .receivedResponseAtMillis(System.currentTimeMillis())
              .build();

      code = response.code();
    }

    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), response);

    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }

流程较为简单,就不详细说明了。有几个地方涉及http协议,简单说下

  • Expect: 100-continue

在客户端发送 Request Message 之前,HTTP/1.1 协议允许客户端先判定服务器是否愿意接受客户端发来的消息主体(基于 Request Headers)。即Client 在发送大数据之前,需要与Sever完成一次握手,收到Sever端返回的 100-continue 应答以后,Client在下个请求中才开始发送(较大)数据。因此code为100的响应是没有响应体的。
这么做的原因是,如果客户端直接发送请求数据,但是服务器又将该请求拒绝的话,这种行为将带来很大的资源开销。

  • 101 Switching Protocols

服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。
只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP 版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。

  • 204 No Content

服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。
如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。
由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。

  • 205 Reset Content

服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。
与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。

  • Connection:close

表示当前连接需要关闭,不可再在此连接上发送新的请求

对照代码看下ok的实现,至此okhttp流程已全部讲完,最后看一下整体的流程图。


okhttp整体流程.png

你可能感兴趣的:(okhttp6)