OKHttp源码分析(六)连接管理 ConnectInterceptor 、StreamAllocation 和 RealConnection

1,394 阅读10分钟

ConnectInterceptor作为CacheInterceptor的后一个缓存,已经没有缓存这条退路了,必须要真刀真枪的请求网络了。到了现在才真正开始请求网络获取数据。从名字可以看出这个类是负责连接的。
看下具体拦截intercept方法的实现:

@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Request request = realChain.request();
  StreamAllocation streamAllocation = realChain.streamAllocation();

  // 创建HttpCodec和RealConnection
  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的拦截逻辑比较简单,主要是准备RealConnection和HttpCodec,这两个是请求的核心类。这两个类都是通过StreamAllocation创建出来的,StreamAllocation在第一个拦截器RetryAndFollowUpInterceptor就已经创建出来了。
ConnectInterceptor执行后,就达到了万事俱备的条件(已经连接成功),那么东风的到来就是到了最后一个拦截器CallServerInterceptor中,这个拦截器内部才是请求网络的主战场。这篇我们先分析ConnectInterceptorStreamAllocation
让我们先宏观看下这最后两个拦截器和他们之中组件的功能。

宏观分析

我们先宏观分析下OKHttp的网络请求部分,OKHttp的运行在网络的应用层之上,主要是实现了Http的逻辑,那么要使用网络请求,那么肯定需要往下层走,也就是TCP/UDP的网络层,在安卓的代码中,怎么使用这两种请求呢,其实安卓的操作系统已经给我们实现好了,我们只要简单的调用一下,传入我们需要传输的Http数据报,自然就完成了剩下的所有的工作。
OKHttp在这方面的工作也是一样的,单纯的使用系统函数进行网络请求,谁都会,但是还要考虑效率和其他的一些应用,这就比较难了。OKHttp在这方面的做法从上面介绍的最后两个拦截器中就可以看出,分为ConnectInterceptor负责连接,CallServerInterceptor负责传输数据。

连接过程

连接是什么,这是一个非常抽象的词。在网络中,就是指连接到服务器,建立一条通道,Http底层TCP是需要提前进行连接的,也就是三次握手,完成双发之间的约定,下面就可以正式的传递数据了。不建 立连接可否进行传输数据呢,UDP就是这个逻辑,但是不建立连接就不能保证可靠传输。连接需要进行三次握手,成功的建立是非常耗费资源的,所以能复用则复用,如果下次也是往这个服务器发送数据,当然是可以复用这个连接的。在OKHttp中,这个连接就是RealConnection,而负责复用连接的类就是ConnectInterceptor
底层的连接时通过Socket#connect()进行得。

传输过程

在上一步通过Socket#connect()建立连接以后,我们就可以往输入流中输入数据了,就是这么简单的完成了网络数据的传输,等着输出流的数据到来。至于底层的数据怎么在链路上进行传送,我们一概不用知道。CallServerInterceptor就是负责这个功能的。在OkHttp中负责传送数据的就是HttpCodec,他内部通过okio封装了Socket的输入/输出流,可以简单的理解为一个流,一个OkHttp的流。

StreamAllocation的作用是什么呢,因为连接过程的RealConnection需要和传输过程的HttpCodec进行通信,StreamAllocation相当于一个中介,使用到了中介者模式,这样连接和发送就能更专心的去完成自己的工作了。负责协调两者之间的工作,不但可以让它们各自的核心逻辑封装在各自的类内,不用关心和其他部分的通信部分。

经过上面对整体的OkHttp网络请求的具体分析,应该对大概有了了解。下面我们先分析连接部分。下一篇再分析传输部分。 本篇先跳过Http2.0和Https的处理代码,先主要分析整个流程。

ConnectInterceptor

这里我们先看下顶层ConnectInterceptor的代码,从外部使用角度分析下StreamAllocationRealConnection两个类。ConnectInterceptor拦截方法中,对StreamAllocation的外部调用很简单。

HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

先通过StreamAllocation的newSteam创建了HttpCodec,也就是数据传输的控件。后面直接调用了connection方法直接获取了连接类RealConnection。
简单的两部连接和传输的两大控件都已经创建好了。ConnectInterceptor就是这么的简单。下面我们先分析StreamAllocation。先通过newStream()和connection()为入口。

StreamAllocation

connection()的代码比较简单,直接获取了connection变量,可见在newStream()方法中就已经完成了连接和传输两个功能控件创建的工作。主要的逻辑都在newStream()中。

public synchronized RealConnection connection() {
  return connection;
}

newStream()具体代码如下。

public HttpCodec newStream(
    OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  。。。
  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找到可以使用的RealConnection,再通过RealConnection的newCodec新建一个传输的控件HttpCodec。findHealthyConnection直接调用了findConnection方法,核心的逻辑都在findConnection中。也就到了ConnectionPool的连接缓存部分了,为什么这部分也有缓存的,因为连接需要经过3次握手,还要经过TCP的慢启动等阶段,如果每次都进行重新创建的话,是非常耗时的。

ConnectionPool缓存连接

构造函数

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

构造函数可以传入两个配置,一个就是最大的连接数,一个是最长等待时间。

缓存获取

findConnection的方法比较长,总览就是获取缓存Connection,和RecyclerView获取缓存有点像。获取分为三步。

  1. StreamAllocation#connection是否可用
  2. 没有,从ConnectionPool获取,这部分获取分为两步,首先不带router获取,再获取router再次获取
  3. 没有,创建一个新的Connection 是不是和RecyclerView获取ViewHolder缓存类似呢,只不过这里是两级缓存,下面我们逐步分析下每个获取步骤。

一级缓存:本地StreamAllocation#connection是否可用

releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
  result = this.connection;
  releasedConnection = null;
}

closeQuietly(toClose);

if (releasedConnection != null) {
  eventListener.connectionReleased(call, releasedConnection);
}
if (result != null) {
  // If we found an already-allocated or pooled connection, we're done.
  route = connection.route();
  return result;
}

result变量就是最终的目标,如果result不为空,那么表示我们已经找到了可用的连接,直接return即可。那这个连接是否可以复用,是通过releaseIfNoNewStreams这个方法判断的,如果没有被回收。那么就是可以进行复用的。
下面看下StreamAllocation#connection的赋值来源。

StreamAllocation#connection的来源

StreamAllocation中的connection对象,主要通过两个方法进行赋值,releaseAndAcquire acquire,前者是被Http2.0使用的,这里主要分析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));
}

acquire的逻辑比较简单,直接对connection进行赋值。并对allocations进行填充,因为一个连接可以有多个流,在Http1.1版本中,一个连接只可以有一个流,而Http2.0中一个连接可以有很多个流。如果在Http1.1中,connection.allocations这个集合只会有一个元素。
acquire调用又有两个地方,一个是从ConnectionPool获取成功后,一个是创建新的Connection后。我们发现这两个部分都是在findConnection的后面才进行的。

一级缓存生效条件

所以这个变量的重新利用,肯定是在第一次请求后,执行过findConnection后面的逻辑,找到真正可以使用的Connection后,并重复进行请求的场景下,进行的。比如在RetryAndFollowUpInterceptor拦截器中的重试逻辑等。也就是肯定是在第二次重试中才会复用这个连接。

一级缓存可用性判断

主要逻辑在releaseIfNoNewStreams方法中:

private Socket releaseIfNoNewStreams() {
  assert (Thread.holdsLock(connectionPool));
  RealConnection allocatedConnection = this.connection;
  if (allocatedConnection != null && allocatedConnection.noNewStreams) {
    return deallocate(false, false, true);
  }
  return null;
}

主要逻辑就在noNewStreams这个变量中,如果这个变量为true,表示这个连接上不能创建新的流了,那么就会调用 deallocate进行回收,deallocate方法内部会置空connection变量,反之则不会进行回收。什么场景下这个值会为true呢?
比如服务器返回的数据中connection配置为close,那么这个连接就需要关闭了,noNewStreams就为true。当然还有其他很多场景。
总结是:这里指检查了noNewStreams,如果不为true,就可以进行复用。

二级缓存:ConnectionPool获取

如果上面的Connection不能使用,就会从ConnectionPool中获取,下面是获取的主要代码。

StreamAllocation.java
if (result == null) {
  //只通过address进行匹配
  Internal.instance.get(connectionPool, address, this, null);
  if (connection != null) {
    foundPooledConnection = true;
    result = connection;
  } else {
    selectedRoute = route;
  }
}

boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
  newRouteSelection = true;
  routeSelection = routeSelector.next();
}


if (result != null) {
  // If we found an already-allocated or pooled connection, we're done.
  route = connection.route();
  return result;
}


synchronized (connectionPool) {
  if (canceled) throw new IOException("Canceled");
  if (newRouteSelection) {
  List<Route> routes = routeSelection.getAll();
    for (int i = 0, size = routes.size(); i < size; i++) {
      Route route = routes.get(i);
      //只通过address+router进行匹配
      Internal.instance.get(connectionPool, address, this, route);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
        this.route = route;
        break;
      }
    }
  }

整体逻辑都是调用Internal.instance.get(connectionPool, address, this, route);从connectionPool获取可以复用的Connection。但是获取分为两部

  1. 只通过传入的address匹配
  2. 通过newRouteSelection判断是否根据router和adress进行匹配 整体逻辑就是这样,上面有两个点,需要分析下
什么是Router

Router翻译过来表示路由,通过RouteSelector进行获取,内部包含三个变量

final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
字段意义
proxy代理,表示连接的代理服务器
address表示连接到源服务器的规范,包括Url和协议和dns等
inetSocketAddressIP地址
while (hasNextProxy()) {
  Proxy proxy = nextProxy();
  for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
    Route route = new Route(address, proxy, inetSocketAddresses.get(i));
    if (routeDatabase.shouldPostpone(route)) {
      postponedRoutes.add(route);
    } else {
      routes.add(route);
    }
  }
}

router的创建主要通过上面的代码进行获取,inetSocketAddresses是从dns获取的IP地址列表,而nextProxy()获取下一个可用的代码。这里通过双层嵌套的循环,获取了代理proxy和可选IP地址的笛卡尔集。
可以得到:Router表示连接到代理服务器的途径,通过不同的IP地址或者不同的代理。当一条router不可用时,就可以使用下一条router。

ConnectionPool匹配规则

主要通过get方法进行获取

@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,之后通过connection.isEligible进行匹配,所有的匹配逻辑都在connection.isEligible中。如果匹配了,就通过上面谈过的acquire方法继续进行处理,

public boolean isEligible(Address address, @Nullable Route route) {
  // 如果这个连接不接受新的流,不能复用
  if (allocations.size() >= allocationLimit || noNewStreams) return false;
  // 如果地址的非主机字段不重叠,不能复用
  if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
  // 如果主机完全匹配,可以复用
  if (address.url().host().equals(this.route().address().url().host())) {
    return true; // This connection is a perfect match.
  }
  
  // 主机也不匹配,如果使用的是Http2.0协议 还有机会
  if (http2Connection == null) return false;
  // Http2.0协议下,如果复用需要满足的条件
  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;

  if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
  if (!supportsUrl(address.url())) return false;

  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.0,复用的协议就更加的灵活了。分两部分看下

协议共有判断
  1. 缓存的Connection,可以创建的流的数量不能超过allocationLimit限制,上面也说过这个变量,Http1.1只能创建一个流
  2. Internal.instance.equalsNonHost判读请求的address和缓存的address的变量,必须全部一致。包括以下变量。
    boolean equalsNonHost(Address that) {
      return this.dns.equals(that.dns)
      && this.proxyAuthenticator.equals(that.proxyAuthenticator)
      && this.protocols.equals(that.protocols)
      && this.connectionSpecs.equals(that.connectionSpecs)
      && this.proxySelector.equals(that.proxySelector)
      && equal(this.proxy, that.proxy)
      && equal(this.sslSocketFactory, that.sslSocketFactory)
      && equal(this.hostnameVerifier, that.hostnameVerifier)
      && equal(this.certificatePinner, that.certificatePinner)
      && this.url().port() == that.url().port();
    }
    
  3. 如果上面的两部通过了,判断url的host()部分,如果一样,说明这两个Connection是可以进行复用的。
  4. 如果两个host不一样呢,还有补救的措施,就是使用的是Http2.0协议,并且满足一些条件。
Http2.0独有判断
  1. 外部必须通过router进行匹配了,并且缓存的代理和新请求的代理类型不能是直连接,也就是没有代理。说明这种情况需要有代理的情况下使用。
  2. 后面包括了Https的相关处理,这里先略过了,包括certificatePinnerhostnameVerifier

上面就是从ConnectionPool获取缓存的全部逻辑,那么ConnectionPool什么时候插入缓存呢。

ConnectionPool插入时机

如果不是从ConnectionPool获取的连接就会加入ConnectionPool中。
插入的代码在通过Socket发送连接请求的下面,也就是说如果成功的进行了连接,才会插入到ConnectionPool。通过Internal.instance.put(connectionPool, result);进行插入。
如果连接失败了就不会插入了。

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

插入的时候,如果cleanupRunning为false,会提交一个线程到线程池,以做清理工作。
cleanupRunning是一种懒加载的模式。只有第一次才会进行。

ConnectionPool清除

清除的工作就是在加入的时候,在cleanupRunnable中完成的。

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,就表示没有可以回收的连接了。

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<RealConnection> 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.
      return keepAliveDurationNs;
    } else {
      // No connections, idle or in use.
      cleanupRunning = false;
      return -1;
    }
  }

  closeQuietly(longestIdleConnection.socket());

  // Cleanup again immediately.
  return 0;
}

内部的逻辑,整体分为两个部分,第一个就是遍历所有的连接获取当前的情况,得到三个数据

  1. 正在使用的连接
  2. 空闲的连接
  3. 最长的过期时间

得到上面的数据后,就会经过几个判断。

  1. 如果当前最大过期时间比传入的还大,那么就清除这个连接,并关闭socket
  2. 否则,如果有空闲的数量,就返回最近需要等待的时间
  3. 否则,如果有正在运行的连接,就返回最长的等待时间
  4. 否则,表示没有连接,返回-1,表示推出清除的线程池

获取缓存失败:新建Connection

上面两部都没有获取可用的连接,只能创建一个新的连接了。

if (selectedRoute == null) {
  selectedRoute = routeSelection.next();
}

route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);

这里选择了第一个router,并创建了一个新的RealConnection。下面调用了acquire进行赋值。

通过上面的三步获取,肯定拿到了合适的Connection了,下面就到了激动人心的时候了,正式开始连接。

连接过程

result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
    connectionRetryEnabled, call, eventListener);

直接调用了RealConnection的connect方法。

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
    EventListener eventListener) {
    。。。
    if (route.requiresTunnel()) {
      connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
      if (rawSocket == null) {
        break;
      }
    } else {
      connectSocket(connectTimeout, readTimeout, call, eventListener);
    }
    establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
    。。。
}

省略了部分代码,核心逻辑就是这么几行 这里判断了是否需要隧道,OkHttp支持是使用Http隧道传输Https的数据。隧道的支持后面会说。 connectSocket内部直接调用了

Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);

调用安卓系统提供的rawSocket,直接使用Tcp请求网络。 establishProtocol方法内部判断了是否使用Https和Http2.0,并提供了支持,这些部分专门后面会分析。

总结

纵观整个连接的过程,底层的逻辑比较简单,就是调用Socket的connect方法,但是这里涉及到了连接的复用和路由等处理。通过上面的分析,应该对OKHttp怎么建立连接,有了了解吧。 OkHttp连接和传输的3个主要重要的类分别是StreamAllocation 、RealConnection、HttpCodec。他们之间的关系如下

graph TD
RealConnection --> StreamAllocation1
RealConnection --> StreamAllocation2
RealConnection --> StreamAllocation3
StreamAllocation1 --> HttpCodec1
StreamAllocation2 --> HttpCodec2
StreamAllocation3 --> HttpCodec3

也就是一个连接上可以建立多个流,当然只在Http2.0的情况下可以。StreamAllocation相当于一个中介,连接两者的关系,功能包括获取一个连接,在连接上新建一个流。RealConnection就是一个连接,包括隧道、TSL、Http1.1、HTTP2.0的不同配置连接等。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿