OKHttp源码解析-连接池

  • Android-Retrofit源码解析(一)调用流程(上)
  • Android-Retrofit源码解析(一)调用流程(下)

前面分析Retrofit的源码,Retrofit底层使用了OkHttp来做网络请求操作。在介绍ConnectInterceptor时有设计到OkHttp的连接池ConnectionPool。本篇文章将详细介绍其实现。

文章目录

    • 核心类
    • 初始化
    • put
    • get
    • clean

核心类

OkHttp的连接池主要涉及几个核心类,ConnectionPool.javaRealConnection.javaStreamAllocation.javaInternal.java

  • ConnectionPool.java
    这个类主要负责管理HTTP HTTP/2连接的重用以减少网络延时。它实现了连接重用、清理的的策略。

  • Internal.java
    外部对连接池不直接操作,而是通过这个类来完成。这个类是一个抽象类,只有一个唯一的实现,就在OKHttpClient中。

  • RealConnection.java
    这是真正处理物理连接的类。普通Socket的连接、SSL Socket的连接、IO的读写都是在这个类完成的。

  • StreamAllocation.java
    这个类主要用于处理物理Sokect连接、Streams(逻辑上的Http Request流和Response流)以及Calls(请求数据,包括原始请求以及重定向的请求或者需要证书认证的请求)之间的关系。每个连接有它们自己的分配上限,这决定了每个连接能够承载多少路并发流。HTTP 1.x一次最多有一路流,HTTP/2可以有多路。
    这个类提供了每一个连接以及每个连接里的流的释放的API。
    这个类还可以提供取消异步请求的API。

初始化

连接池的初始化在OKHttpClient的Builder函数里面。

  public static final class Builder {
	  ...
    ConnectionPool connectionPool;
	  ...

    public Builder() {
	  ...
      connectionPool = new ConnectionPool();
	  ...
    }
}

我们知道Java类的初始化顺序

再看下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);
    }
  }

初始化连接池的时候主要传入了2个参数。一个是maxIdleConnections,表示连接池中最大的空闲连接数,最大是5。keepAliveDuration表示空闲连接与服务器保持连接的时间,最大是5分钟。

put

put方法是在StreamAllocation类的findConnection方法里面调用的。findConnection已经在Android-Retrofit源码解析(一)调用流程(下)里分析过了。我们只看下相关的代码。

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
      //1. 获取到IP地址的集合,然后根据IP地址去连接池里查找是否有可复用的连接。
        // 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;
          }
        }
      }

//2-1.如果连接池中没有找到可复用的连接,就创建一个连接
      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);
      }
    }
    
//2-2.如果连接池中找到了可复用的连接,就直接返回
    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

	//3. 创建连接对象后,调用其connect方法,与服务器建立物理上的连接
    // 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;

	//4. 将新创建的连接放入连接池。
      // Pool the connection.
      Internal.instance.put(connectionPool, result);

	//4. 如果是HTTP2的连接,且有多路,就释放当前连接,使用其它路的连接。
      // 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);
...
}

接下来看下put进连接池里面做了什么操作。

  void put(RealConnection connection) {
  // 1. 确保当前线程持有ConnectionPool对象的锁。
    assert (Thread.holdsLock(this));
    // 2. 默认cleanupRunning=false,所以进来会执行一遍clean
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    // 3. 把当前connection加入到了connections中。
    connections.add(connection);
  }

clean的内容我们后面讲。先来看下这个connections。connections是一个ArrayDeque。ArrayQueue的内部是一个动态扩展的循环数组,通过head和tail维护数组的开始和结尾,其插入删除的效率非常高,都是O(N)。
OkHttpClient的Dispatcher类里面有三个Call队列,readyAsyncCalls、runningAsyncCalls、runningSyncCalls,他们也是用ArrayQueue实现的。

get

get方法和put方法一样,也是只在StreamAllocation类的findConnection方法里面调用。一共有2处调用,都在findConnection方法中,来看下代码。

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
      ...
	Socket toClose;
	// 1. 获取到connectionPool对象锁
    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;
      // 2. 如果这个连接不允许创建新的流,就释放这个连接。释放后this.connection被置为null。
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
      // 3. 如果这个连接已经分配了并且是可用的,把这个连接赋值给result,后续直接返回这个result。
        // 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) {
      // 4. 如果现有的连接是不可用的。尝试从连接池中获取一个可用连接。
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
        // 5-1. 在连接池中找到了可用连接
          foundPooledConnection = true;
          result = connection;
        } else {
        // 5-2. 连接池中没有找到可用连接。
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
    // 6. 如果 现有连接可用,或者现有连接不可用但是在连接池中找到了可用连接。直接返回。
      // If we found an already-allocated or pooled connection, we're done.
      return result;
    }

	//7. 如果没有可复用连接,并且如果可选路由和路由集合为空,就创建一个新的路由列表。
    // 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) {
      // 8. 获取到了一组路由列表之后,可以尝试从连接池中获取每个路由上是否有可用连接。
      //这是有可能找到的,因为存在连接合并的情况。
        // 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);
          // 9. 根据路由,查找可复用连接。
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            result = connection;
            this.route = route;
            break;
          }
        }
      }

      if (!foundPooledConnection) {
      // 10. 如果还是没找到,就新建一个连接。每个连接都会至少有一个路由。
        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);
      }
    }
    ...
}

两处调用分别在上述代码的第4个和第9个注释的。这两处调用唯一的区别就是一个没有传route,一个传了route。只有HTTP/2的请求才有这个route值。来看下get方法的实现。

  @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
   // 1. 检查是否持有锁。
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
      // 2. Eligible的中文意思是资格,如果这个连接可复用,就取出返回。
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    // 3.如果遍历连接池仍没有找到可复用连接,返回null。
    return null;
  }

这里有两处关键点,一个是connection.isEligible方法,一个是streamAllocation.acquire方法。先看下isEligible的实现。

  public boolean isEligible(Address address, @Nullable Route route) {
    //如果这个连接已经分配的流达到了最大可分配的流或者被限制了不能创建新的流,直接返回。
    //HTTP/1 最多支持一路流,HTTP/2支持多路,一般建议不小于100.  具体可看https://stackoverflow.com/questions/39759054/how-many-concurrent-requests-should-we-multiplex-in-http-2
    // 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;

    // 如果连接池中的Address和目标Address完全一致,由于上面已经判断了这个还可以分配流。所以找到直接返回。
    // 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. 这个连接必须是HTTP/2的连接
    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false;

	//2.这些路由必须有同一个IP地址。IP地址和域名的映射通过DNS服务来实现。
    // 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.这个连接的服务器证书必须要包含新的主机
    // 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. 证书页的域名必须匹配。
    // 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.
  }

如果连接池中有可用连接,调用streamAllocation.acquire(connection, true);

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

	// 将连接池的连接复制给当前连接
    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    //为该连接添加一个StreamAllocationReference引用。
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

get方法的实现就分析完了,它先判断了连接池中是否有和该请求地址一样的可用连接,如果有就将其取出返回,并向该连接的引用计数List> allocations里添加一个引用。

clean

在put方法里,执行了一个cleanupRunnable任务。

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

这个任务就是用来完成清理工作的。

  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 {
            //调用wait方法,表示等待
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };

wait方法会释放调当前持有的锁,并把任务加入到条件等待队列中,当前线程的状态会变为WATING或者TIME_WATING。如果等待时间到了,或者别的线程调用了notify/nofityAll方法,这个时候任务会从等待队列中移除,重新竞争锁。如果竞争到了,会从wait中返回,由于cleanupRunnable里面是一个死循环,因此会执行下一次清理工作。如果没有竞争到,任务会加入到对象锁等待队列中,线程的状态会变为BLOCKED,只有获得了锁后才会从wait调用中返回。

下面看一下cleanup的原理。

  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

	// 1. 获取锁对象
    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
    // 2. 遍历连接池,查找可以清理的连接
      for (Iterator i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

		// 3. 如果当前连接正在使用,则查找下一个。
		//判断当前是否在使用的依据就是连接池的List> allocations列表的大小。
        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
		// 4. 如果当前连接没有正在使用,那么空闲数加1
        idleConnectionCount++;

		// 5. 接下来查找空闲时长最长的那个连接
        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

     // 6 -1. 遍历完成后,如果最长的空闲时间超过了设置的keepAlive时间,一般是5分钟,
     //或者空闲的连接超过了最大空闲数,一般是5个,就把这个连接从连接池中移除并关闭。
      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) {
        //  6-2. 如果存在空闲连接数,那么返回的等待时间就是距离保活时间的剩余时间
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
      //如果所有的连接都在使用,那么等待5分钟再清理
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
      //如果连接池中没有对象,不需要清理。恢复cleanupRunning标志位。
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

清理的原理总结一下:

  • 遍历连接池,计算正在使用的连接数、空闲连接数和查找空闲时间最长的连接。

  • 遍历完成后,如果最长的空闲时间超过了设置的keepAlive时间,一般是5分钟,
    或者空闲的连接超过了最大空闲数,一般是5个,就把这个连接从连接池中移除并关闭。

  • 如果还不满足清理条件,且存在空闲连接数,等待空闲连接已经空闲的时间到距离保活时间后,发起下一次清理。

  • 如果所有的连接都在使用,那么等待5分钟再清理。

  • 如果连接池中没有对象,不需要清理。恢复cleanupRunning标志位。

你可能感兴趣的:(Android学习总结)