OkHttp源码阅读(七) —— 拦截器之ConnectInterceptor

  Duang!~ Duang!~ Duang!~重磅来袭,OkHttp里个人觉得最重要也是最有特色的角色出现了(因为看的最懵逼),这个拦截器里边的子角色很多,也同样都很重要,最最最最重要的是代码量太TM多了,读这块代码的时候有一句特别经典的话常常出现在我脑海里,那就是"卧槽!这TM什么玩应儿!!!!!",不过硬骨头也得啃啊,一口啃不动那就一点一点的啃!!

前言

  在分析ConnectInterceptor之前,先声明几点注意事项,首先这个拦截器里边涉及到知识点和代码太多了,没必要每一行代码都要知道是什么意思,有些类和方法只需要知道它的作用是什么就可以了,可以忽略细节,主要还是把重点放到核心逻辑上,所以下面先预热一些类的定义和作用。

  1. RealConnetion
  2. ConnectionPool
  3. StreamAllocation

RealConnection

  从字面上来看RealConnection的意思是真正的链接,没错!RealConnection所做的事情也是与服务器建立通信链接,Realconnection是Connection接口的实现类,在Connction接口定义中,有几个方法

public interface Connection {
  /** Returns the route used by this connection. */
  Route route();

  /**
   * Returns the socket that this connection is using. Returns an {@linkplain
   * javax.net.ssl.SSLSocket SSL socket} if this connection is HTTPS. If this is an HTTP/2
   * connection the socket may be shared by multiple concurrent calls.
   */
  Socket socket();

  /**
   * Returns the TLS handshake used to establish this connection, or null if the connection is not
   * HTTPS.
   */
  @Nullable Handshake handshake();

  /**
   * Returns the protocol negotiated by this connection, or {@link Protocol#HTTP_1_1} if no protocol
   * has been negotiated. This method returns {@link Protocol#HTTP_1_1} even if the remote peer is
   * using {@link Protocol#HTTP_1_0}.
   */
  Protocol protocol();
}

上边代码可以看出RealConnection实现该接口后, 就意味着客户端和服务器有了一条通信链路,RealConnetion做了好多事情,包括三次握手建立普通链接建立隧道链接TLS链接代理等等,而对于Android开发人员来说,这些具体实现细节没有必要研究的很细,如果感兴趣的话可以进一步深入的了解,本人能力有限,暂时放弃深入了解具体实现。不过RealConnection中有些属性和方法的含义是非常有必要了解的。
首先先看一下RealConnection这个类的重要属性定义

/**连接池*/
  private final ConnectionPool connectionPool;
  private final Route route;

  // The fields below are initialized by connect() and never reassigned.
  //下面这些字段,通过connect()方法开始初始化,并且绝对不会再次赋值
  /** The low-level TCP socket. */
  /**底层socket*/
  private Socket rawSocket;

  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  /**应用层socket*/
  private Socket socket;
  /**握手*/
  private Handshake handshake;
  /**协议*/
  private Protocol protocol;
  /**http2链接*/
  private Http2Connection http2Connection;
  /**通过source和sink,与服务器进行输入输出流交互*/
  private BufferedSource source;
  private BufferedSink sink;

  // The fields below track connection state and are guarded by connectionPool.

  /** If true, no new streams can be created on this connection. Once true this is always true. */

  /**下面这个字段是 属于表示链接状态的字段,并且有connectPool统一管理
   * 如果noNewStreams被设为true,则noNewStreams一直为true,不会被改变,并且表示这个链接不会再创新的stream流*/

  public boolean noNewStreams;

  public int successCount;

  /**
   * The maximum number of concurrent streams that can be carried by this connection. If {@code
   * allocations.size() < allocationLimit} then new streams can be created on this connection.
   */
  /**并发流上限,此链接可以承载最大并发流的限制,如果不超过限制,可以随意增加*/
  public int allocationLimit = 1;

  /** Current streams carried by this connection. */
  /**统计当前链接上创建了哪些流,相当于计数器*/
  public final List> allocations = new ArrayList<>();

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;

通过上面的代码基本上可以得出一个结论,

  1. 除了route字段,部分字段都是在connect()方法里边赋值的,并且不会再次赋值
  2. 链接时通过source和sink以流的方式和服务器交互
  3. noNewStream可以简单理解为它表示该连接不可用。这个值一旦被设为true,则这个conncetion则不会再创建stream.
  4. allocationLimit是分配流的数量上限,一个connection最大只能支持一个并发
  5. allocations是关联StreamAllocation,它用来统计在一个连接上建立了哪些流,通过StreamAllocation的acquire方法和release方法可以将一个allcation对象添加到链表或者移除链表,

对RealConnection的属性有了简单的了解之后,我们来分析分析它的方法,像什么建立三次握手,建立普通链接,建立隧道链接方法什么的,我就不分析,因为没必要研究这么底层,何况我也不怎么明白。主要分析对整体逻辑比较重要的对后边的分析有影响的方法。

isHealthy方法

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

如代码所示,这个方法是用来判断一个链接是否是健康链接,只有健康的链接才能被复用,那么怎么判断一个链接是健康的链接呢?如代码所示要同时满足一下几个要求,该链接才是健康的链接

  • socket必须是关闭的状态(socket.isClosed())
  • socket输入流是关闭状态(socket.isInputShutdown())
  • socket输出流是关闭状态(socket.isOutputShutdown())
  • 如果是http2链接,http2也要处于关闭状态(http2Connection.isShutdown())

剩下的是链接超时时socket的状态判断

isEligible方法

 /**
   * Returns true if this connection can carry a stream allocation to {@code address}. If non-null
   * {@code route} is the resolved route for a connection.
   */
  public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

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

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

    // 2. The routes must share an IP address. This requires us to have a DNS address for both
    // hosts, which only happens after route planning. We can't coalesce connections that use a
    // proxy, since proxies don't tell us the origin server's IP address.
    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. 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.
  }

如上面代码所示,isEligible方法的作用是通过address和route判断当前realConnection是否可以被复用,代码逻辑比较简单,都是一些if-else的判断,大致逻辑判断可以总结以下几点:

  • 如果链接达到了并发上限,则该链接不能被复用
  • 通过host域还有route的address逻辑关系,判断该链接是否可以被复用(有些地方不是很清楚)

最后简单的了解下newCodec方法

public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
      StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, chain, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

主要是通过当前是不是HTTP/2来创建一个HttpCodec。HttpCodec的作用其实就是读写流,有时间的童鞋可以仔细研究下。

ConnectionPool

  连接池在okhttp中连接池的作用可谓是重中之重,其复用机制很大程度上提高了网络性能,降低了延迟和提升了响应速度,连接池的精髓当然也是在于链接的复用,究竟ConnectionPool怎么复用的链接,在这之前先铺垫一些内容。
  我们常常在浏览器开发者工具中或者是客户端网络代码编写的过程经常会看到这样的代码"keep-alive",keep-alive的作用就是让客户端可服务器保持长连接,并且这个链接时可复用的,可为什么链接的复用会提高性能呢?这就要从一个网络请求响应的过程说起了.
  首先通常情况下,我们发起一个HTTP请求的时候,开始阶段要先完成TCP的三次握手,然后再传输数据,最后再释放链接,我们知道,在高并发下,如果请求数量很多,或者是同一个客户端发起多次频繁的请求,那么每次的请求都要进行三次握手,最后释放链接这是一个非常消耗性能的操作,所有就引申出了长连接,如图所示

1.jpeg

如图所示 在timeout空闲时间内,链接时不会关闭的,相同重复的request将复用原先的connection,减少握手的次数,从而提升性能,那么OkHttp是如何做到链接复用的呢?主角登场ConnectionPool

首先简单的看一下ConnectionPool类的注释

/**
 * Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
 * share the same {@link Address} may share a {@link Connection}. This class implements the policy
 * of which connections to keep open for future use.
 */

大致的意思就是管理HTTP和HTTP/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

/**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  /**这是一个后台线程用于清除过期的链接,一个线程池里最多只能运行一个线程,这个线程池允许被回收*/
  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. */
  /**每个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;

从上边代码可以了解到,ConnectionPool的核心还是RealConnection的容器,控制RealConnection的读取,该容器是顺序容器,不是关联容器,ConnectionPool中使用双端队列来存放RealConnection,而且ConnectionPool对连接池中的最大空闲连接数和保活时间做了控制,maxIdleConnectionskeepAliveDurationNs成员分别体现对最大空闲连接数及连接保活时间的控制。这种控制通过匿名的Runnable cleanupRunnable在线程池executor中执行,cleanupRunning是清理的标记位,并在向连接池中添加新的RealConnection触发。

  ConnectionPool的对象创建是可以OkHttp初始化时用户自己创建,可以指定一些需要的自定义参数,这里就分析下默认情况下ConnectionPool的参数配置,首先构造方法:

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

方法注释大概的意思是创建一个适用于单个应用程序的新连接池。该连接池的参数将在未来的okhttp中发生改变,目前最多可容乃5个空闲的连接,存活期是5分钟,maxIdleConnectionskeepAliveDurationNs是清理淘汰连接的的指标,这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。

接下来就是分析ConnectionPool如何通过双端队列进行RealConnection的管理
首先看一下put方法:

void put(RealConnection connection) {
//断言表达式,put的基本条件是,当前线程是否持有该对象的锁
    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清理,代码如下

long cleanup(long now) {
    //正在使用connection数量
    int inUseConnectionCount = 0;
    //空闲的connection数量
    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;
  }

以上代码流程大致这样,通过for循环查找最长空闲时间的连接以及对应空闲时长,然后判断是否超出最大空闲连接数(maxIdleConnections)或者或者超过最大空闲时间(keepAliveDurationNs),满足条件就直接清理,不满足条件的话 就需要计算下一次清理时间,主要有两种情况

  • 有空闲链接则下次清理时间等于keepAliveDurationNs-longestIdleDurationNs
  • 如果没有空闲链接则需要下一个周期再清理keepAliveDurationNs

综上所述,我们来梳理一下清理任务,清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,直到清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发

接下来是connectionBecameIdle方法

/**
   * 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 {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }
  

该方法主要作用是当有连接空闲时,唤起cleanup线程清洗连接池,connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么就需要调用该方法,由ConnectionPool来决定是否需要清理该连接。

/** Close and remove all idle connections in the pool. */
  public void evictAll() {
    List evictedConnections = new ArrayList<>();
    synchronized (this) {
      for (Iterator i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        if (connection.allocations.isEmpty()) {
          connection.noNewStreams = true;
          evictedConnections.add(connection);
          i.remove();
        }
      }
    }

    for (RealConnection connection : evictedConnections) {
      closeQuietly(connection.socket());
    }
  }

evictAll方法就是关闭所有链接,在这里就不赘述了。

接下来需要了解下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();
  }

该方法内使用了另一个类叫做** StreamAllocation**,后边会分析到,暂且先了解下该方法的大致功能是什么,详细说明在后边会说到,到时回过头来看这段方法的代码会理解的更透彻,pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。

  说完了put方法,接下来分析get方法,代码如下

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

代码很简单,遍历connections 中的所有 RealConnection 寻找同时满足条件的RealConnection。
这里同样用到了一个类StreamAllocation,那到底StreamAllocation做了什么事情,我们来深入分析下。

StreamAllocation

该类属于核心类,也是相对底层的一个类,在分析这个类之前,需要粗略的了解下两个工具类,如下:

HttpCodec

在客户端和服务器之间的数据传输都是靠流进行传输的,在OkHttp中HttpCodec的作用就是刘操作,它会将请求的数据序列化之后发送到网络,并将接收的数据反序列化为应用程序方便操作的格式,HttpCodec提供了以下方法:

  • writeRequestHeaders 为发送请求而提供的,写入请求头部。
  • createRequestBody 为发送请求而提供的,创建请求体,以用于发送请求体数据
  • finishRequest 为发送请求而提供的,结束请求发送。
  • readResponseHeaders 为获得响应而提供的,读取响应头部。
  • openResponseBody 为获得响应而提供的,打开请求体,以用于后续获取请求体数据
  • cancel 取消请求执行

ConnectionSpec

在OkHttp中,ConnectionSpec用于描述传输HTTP流量的socket连接的配置。对于https请求,这些配置主要包括协商安全连接时要使用的TLS版本号和密码套件,是否支持TLS扩展等。暂且只需要知道大致的作用岂可,因为具体实现我也不是很明白。

OK,有了上边的铺垫,我们来看一下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 创建编解码工具HttpCodec

根据 OkHttpClient中的设置,连接超时、读超时、写超时及连接失败是否重试,调用 findHealthyConnection() 完成 连接,即RealConnection 的创建。然后根据HTTP协议的版本创建Http1Codec或Http2Codec。

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

首先通过findConnection获取到一个链接,如果当前链接成功次数为0代表是新的链接,直接返回;如果不是新连接,要判断当前链接是不是健康链接,如果是 返回,如果不是 继续循环。

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,
      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 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是被重复使用的socket的*/
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    /**如果是重复的socket则关闭socket,不是则socket为nul,什么也不做*/
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    /**返回链接*/
    return result;
  }

这段代码比较长,大致总结如下:

  1. 首先判断是否有已存在的链接,如果有并且是可用状态那么就是用已存在的链接
  2. 如果没有就根据address去连接池中获取一个链接,如果有就使用,如果没有就更换路由再次去连接池中获取链接,如果有则直接使用
  3. 如果以上条件都不满足的情况下,直接创建(new)一个新的链接,并且将新的链接RealConnection通过acquire方法关联到connection.allocations
  4. 最后做一个去重的判断如果是重复的socket,则关闭

以上大致是findConnection大致流程,涉及到了RealConnection的connect方法,有兴趣的童鞋可以继续深入了解下。

acquire和release

/**
   * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
   * {@link #release} on the same connection.
   */
  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));
  }

  /** Remove this allocation from the connection's list of allocations. */
  private void release(RealConnection connection) {
    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
      Reference reference = connection.allocations.get(i);
      if (reference.get() == this) {
        connection.allocations.remove(i);
        return;
      }
    }
    throw new IllegalStateException();
  }

之前提到了RealConnection是通过acquire方法关联到connection.allocations上的,allocations是RealConnection定义的一个StreamAllocation的引用集合List> allocations = new ArrayList<>();
StreamAllocationReference 是一个弱引用,所以在acquire方法调用的时候,实际上相当于connection的引用计数器+1; 反之release 相当于引用计数器-1;
以上是StreamAllocation类的一些重要方法分析,剩下一些方法注释都很多,不再赘述了。

intercept方法

  有上边一大大大大堆的铺垫之后终于到了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);

  }
}

你没看错,ConnectInterceptor的代码就这么少,重要的几个类上边都有过分析,相信这几行代码都知道什么意思了,我也不再赘述了,接下来有了streamAllocationhttpCodecconnection之后,下一层CallServerInterceptor改如何工作呢?

总结

   ConnectInterceptor核心代码比较多,需要消化消化,有许多关于HTTP的基础知识还是需要巩固的,要不然有些代码看起来真的是很费劲,本人就是这方面知识匮乏,后边还需更多努力才行。

你可能感兴趣的:(OkHttp源码阅读(七) —— 拦截器之ConnectInterceptor)