okhttp3实例源码浅析(5)-连接池管理

okhttp封装了一套连接池管理,通过复用连接,减少了频繁网络请求连接时的握手开销。

ConnectInterceptor这一层拦截器中获取缓存/新建socket连接:

@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对象,然后通过它获取HttpCodecRealConnection对象,最后一起传给下一层拦截器处理。

  • StreamAllocation即在RetryAndFollowUpInterceptor中创建后传下来的,负责管理Connections/Streams的资源分配和释放。
  • HttpCodec,它有两个子类Http1Codec和Http2Codec,根据不同的http协议创建。在最底层的拦截器CallServerInterceptor中通过它encodes HTTP请求和decodes HTTP响应。
  • RealConnection对象表示一个连接,内部有socket、输入输出流等成员变量。

接下来看StreamAllocation的newStream方法:

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      // 获取可用连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      // 根据http协议创建Http1Codec/Http2Codec
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
}

该方法中调用findHealthyConnection方法获取RealConnection,之后调用newCodec方法创建HttpCodec对象。
newCodec方法根据RealConnection中的协议类型,如果是http2则创建Http2Codec,其余创建Http1Codec。

继续看findHealthyConnection方法:

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {
      // 获取一个连接
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        // 判断是新建的连接还是复用的连接,新建的successCount值为0
        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.
      // 检查连接中的socket是否可用,不可用则标记connection.noNewStreams为true、从连接池中移除connection、关闭connection中的socket
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
}

该方法中通过while循环,不断获取RealConnection,直到获取到可用的连接。

接着看看findConnection方法中做了哪些操作:

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      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此处的作用是传递给eventListener回调方法
      releasedConnection = this.connection;
      // 判断connection是否可用,不可用则返回connection中的socket,并释放connection
      toClose = releaseIfNoNewStreams(); //-----①
      if (this.connection != null) {
        // We had an already-allocated connection and it's good.
        // 经过上面的方法,若connection仍存在,说明是可用的
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        // If the connection was never reported acquired, don't report it as released!
        // reportedAcquired默认为false
        releasedConnection = null;
      }

      if (result == null) {
        // 当前StreamAllocation中没有保存可用的connection,从连接池中查找。若连接池中存在可用connection,则会赋值给当前StreamAllocation的connection成员变量
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          // 找到可以connection
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    // 关闭前面调用releaseIfNoNewStreams方法返回的socket
    closeQuietly(toClose);

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
    }
    // 如果从连接池中获取的connection可用,直接返回连接
    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++) {
          // 遍历路由,查找匹配的connection
          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) {
        // 未找到路由匹配的connection
        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);
        // 保存记录该连接,会赋值给this.connection变量
        acquire(result, false); //-----③
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      // 找到路由匹配的connection,返回该connection
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    // 新创建的connection开始握手连接
    result.connect(
        connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      reportedAcquired = true;

      // Pool the connection.
      // 将新建的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.
      // 判断该connection是否支持多路复用,即是否是HTTP/2协议
      if (result.isMultiplexed()) {
        // 若连接池中存在相同host的连接,则释放重复的连接(保留连接池中的连接)
        socket = Internal.instance.deduplicate(connectionPool, address, this); //-----⑤
        // 替换为连接池中的连接
        result = connection;
      }
    }
    // 关闭需要释放的连接中的socket
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
}

findConnection方法大致流程即:
1)先判断StreamAllocation自身是否保存了connection且可用,若可用则返回该connection;
2)从连接池中查找可用connection;
3)重新创建connection,放入连接池并返回。

接着再具体分析下其中调用到的一些方法:
① releaseIfNoNewStreams()

判断是否释放StreamAllocation持有的connection

private Socket releaseIfNoNewStreams() {
    assert (Thread.holdsLock(connectionPool));
    RealConnection allocatedConnection = this.connection;
    if (allocatedConnection != null && allocatedConnection.noNewStreams) {
      // 根据是否存在了connection和connection的noNewStreams成员变量决定是否释放资源。noNewStreams为true表示该connection不能再创建流。
      return deallocate(false, false, true);
    }
    return null;
}

假设此时存在connection且noNewStreams为true

private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
    assert (Thread.holdsLock(connectionPool));

    if (streamFinished) {
      // 此时传入的streamFinished为true
      this.codec = null;
    }
    if (released) {
      this.released = true;
    }
    Socket socket = null;
    if (connection != null) {
      // 此时connection不为空
      if (noNewStreams) {
        connection.noNewStreams = true;
      }
      if (this.codec == null && (this.released || connection.noNewStreams)) {
        // this.codec经过上面赋值为null,connection.noNewStreams为true
        // 移除自己在connection中的allocations保存的引用
        release(connection);
        if (connection.allocations.isEmpty()) {
          // 若经过移除后,connection引用列表为空了,执行下面方法
          // 记录系统计时器当前值
          connection.idleAtNanos = System.nanoTime();
          // 调用连接池的方法,判断是将该连接移除(返回true)还是清理连接池(返回false)
          if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
            // 若是将连接移除,则获取该连接的socket,后续关闭socket
            socket = connection.socket();
          }
        }
        // 将自身的connection成员变量置为null
        connection = null;
      }
    }
    return socket;
}
② Internal.instance.get(connectionPool, address, this, route)

该方法会从连接池中查找可复用连接

Internal.instance在OkHttpClient的静态代码块中实例化,它的get方法中又调用了connectionPool的get方法

@Override public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
        return pool.get(address, streamAllocation, route);
}
③ acquire(result, false)

记录connection

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

    // 在此StreamAllocation中缓存connection
    this.connection = connection;
    // 从连接池获取的connection,reportedAcquired为true,否则为false
    this.reportedAcquired = reportedAcquired;
    // 增加connection被使用的计数
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

RealConnection中使用引用计数的方式记录该连接承载的流的数量

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

通过弱引用方式持有StreamAllocation引用,保存在RealConnection的allocations中

④ Internal.instance.put(connectionPool, result)

将connection保存进连接池

⑤ Internal.instance.deduplicate(connectionPool, address, this)

判断去重,使用连接池中的连接代替该StreamAllocation的connection

连接池ConnectionPool

上文中很多地方使用了Internal.instance.xx方法,实际就是调用了ConnectionPool的对应方法

static {
    Internal.instance = new Internal() {
      ···
      @Override public boolean connectionBecameIdle(
          ConnectionPool pool, RealConnection connection) {
        return pool.connectionBecameIdle(connection);
      }

      @Override public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
        return pool.get(address, streamAllocation, route);
      }

      @Override public Socket deduplicate(
          ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
        return pool.deduplicate(address, streamAllocation);
      }

      @Override public void put(ConnectionPool pool, RealConnection connection) {
        pool.put(connection);
      }
      ···
    };
}
一.连接池的初始化

在OkHttpClient的Builder中创建ConnectionPool对象

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

连接池默认最多容纳5个闲置连接,最长闲置5分钟

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

在RetryAndFollowUpInterceptor拦截器中创建StreamAllocation时传入connectionPool

streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()),
        call, eventListener, callStackTrace);
二.get 从连接池查找连接

ConnectionPool中使用Deque双端队列保存RealConnection

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

get方法中遍历connections,判断connection是否可用,若可用则调用StreamAllocation的acquire方法记录connection并返回,否则返回null

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

看下connection.isEligible方法,该方法判断connection是否可用

public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    // 判断此连接上已创建流的数量是否超过允许承载的并发流的最大数量,和noNewStreams标识是否置为true
    if (allocations.size() >= allocationLimit || noNewStreams) return false;

    // If the non-host fields of the address don't overlap, we're done.
    // 判断路由中的Address和当前接口请求的Address中的dns、proxyAuthenticator、protocols、connectionSpecs、proxySelector、proxy、sslSocketFactory、hostnameVerifier、certificatePinner、port是否一致
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // If the host exactly matches, we're done: this connection can carry the address.
    // 判断host是否一致,若判断到这里都一致,则完美匹配,返回可用
    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.
}
三.put 加入连接池
void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    // 判断当前是否在执行清除任务
    if (!cleanupRunning) {
      cleanupRunning = true;
      // 执行清除任务
      executor.execute(cleanupRunnable);
    }
    // 将connection加入缓存队列
    connections.add(connection);
}

将连接放入连接池会触发清除任务,清除超时闲置连接

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        // 清除连接,并返回执行下一次cleanup的间隔时间,若返回-1,则表示不需要再清理
        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) {
    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) {
          // 在使用计数加1
          inUseConnectionCount++;
          continue;
        }

        // 闲置计数加1
        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        // idleAtNanos的值为now-keepAliveDurationNs,所以idleDurationNs等于keepAliveDurationNs,这样做避免误差
        long idleDurationNs = now - connection.idleAtNanos;
        // 比较闲置最久的连接,用longestIdleConnection临时保存
        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).
        // 若闲置时间超时(默认5min)或闲置连接数超出允许最大闲置连接数(默认5个),则移除该连接
        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,返回-1,退出清理任务
        cleanupRunning = false;
        return -1;
      }
    }

    // 关闭需要清理的连接的socket,释放资源
    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    // 立即清理闲置时间最长的连接
    return 0;
}

看看pruneAndGetAllocationCount如何判断connection是否闲置

private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    // 获取connection中的引用列表
    List<Reference<StreamAllocation>> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      // 遍历allocations,取出引用
      Reference<StreamAllocation> reference = references.get(i);

      // 判断持有的引用是否存在,存在则continue
      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);

      // 移除引用;将connection.noNewStreams标识置为true
      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      // 若该connection的引用列表为空,即为闲置连接,返回0
      if (references.isEmpty()) {
        // idleAtNanos记录当前时间-闲置超时时间(默认5min)
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    // 返回引用个数,表示该连接上仍有多少个流在使用
    return references.size();
}
四.connectionBecameIdle 从连接池移除

该方法通知连接池此连接已闲置。返回true表示连接已从池中移除,需要关闭连接中的socket

boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewStreams || maxIdleConnections == 0) {
      // 若noNewStreams为true或连接池最大容量为0,则移除该连接
      connections.remove(connection);
      return true;
    } else {
      // 唤醒清理线程,执行清理任务
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
}
五.deduplicate 替换重复连接
@Nullable Socket deduplicate(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    // 遍历连接池中的connection
    for (RealConnection connection : connections) {
      // 判断连接是否可用、是否HTTP/2、是否重复
      if (connection.isEligible(address, null)
          && connection.isMultiplexed()
          && connection != streamAllocation.connection()) {
          // 替换streamAllocation中的连接,并返回旧连接中的socket,后续关闭该socket
        return streamAllocation.releaseAndAcquire(connection);
      }
    }
    return null;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值