OkHttp. RecoConnection StreamAllocation

OkHttp. RealConnection StreamAllocation

测试下连接Charles代理的情况,okHttp中Proxy的变化

RealConnection

RealConnection是Connection的实现类,代表着Socket链接的建立,内部持有Socket连接。
下面直接看下RealConnection的属性,因为OkHttp支持Http 1.1 和Http 2(支持多路复用,创建多个流),这里只分析Http 1.1协议的部分

  private final ConnectionPool connectionPool;//复用Connection的池
  private final Route route;//路由

  // The fields below are initialized by connect() and never reassigned.

  /** The low-level TCP socket. */
  private Socket rawSocket;//TCPSocket链路

  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  private Socket socket;//应用层的Socket链路,可能是SSLSocket Tls链路,也可能是TCP Sockate链路
  private Handshake handshake;//握手
  private Protocol protocol;//Http 协议
  private Http2Connection http2Connection;//Http2的连接
  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. */
  public boolean noNewStreams;//表示是否可以新建stream

  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;//允许承载的并发流的限制,Http 1.1 是1

  /** Current streams carried by this connection. */
  public final List> allocations = new ArrayList<>();//建立在这个连接上Stream的列表,通过StreamAllocation的acquire方法和release方法可以将一个StreamAllocation对象添加到链表或者移除列表

  /** Nanotime timestamp when {@code allocations.size()} reached zero. */
  public long idleAtNanos = Long.MAX_VALUE;//空闲的到达时间

继续分析RealConnection里面比较重要的方法,这里只分析主干,细枝末节的不做分析,如果有需要的话在深入分析,那么就看下连接的建立的方法

  public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List connectionSpecs = route.address().connectionSpecs();
    //根据ConnectionSpec构造出ConnectionSpecSelector
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    while (true) {
      try {
            //如果需要建立隧道
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
        // 建立TCP连接
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        //协议的确认,如果是HTTPs的话,还需要建立TLS连接
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ...
            //这里的connectionRetryEnabled默认值为true,是从OKHttpClient中传过来的,默认值为true
            //这里会执行ConnectionSpecSelector的connectionFailed()方法,如果是SSLHandshakeException,SSLProtocolException,SSLException那么就会执行后备策略,继续尝试TLS连接
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }

    ...
    //Http2的连接,这里不分析
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

隧道的建立过程这里不做分析,感兴趣的自己去查一下相关知识

  1. connectSocket() 建立TCP连接
  2. 协议的确认establishProtocol()

追踪connectSocket

  private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();
    
    //根据代理的类型,来创建不同的Socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
    //由具体的平台去做具体的socket连接,在Android平台上就是socket.connect(address, connectTimeout);
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }

    // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
    try {
    //根据socket得到跟服务端的输入输出流
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
  }
  1. 根据代理的类型来创建socket
  2. 再由具体的平台去建立socket连接
  3. 根据socket连接得到具体的输入输出流

再看下协议的确认establishProtocol()

  private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
      //如果是Http协议,这里如果SSLSocketFactory如果为null,等于就是Http请求,否则就是Https请求
    if (route.address().sslSocketFactory() == null) {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        startHttp2(pingIntervalMillis);
        return;
      }

      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    //建立Tls连接
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

不考虑Http 2的情况

  1. 如果是Http请求的话,那么只需要建立TCP连接就行,赋值然后返回
  2. 如果是Https请求的话,那么还需要建立Tls连接

再看下connectTls(),传入了上面创建的ConnetionSpecSelector

  private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      //基于rawSocket构造出SSLSocket
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      //确认sslSocket的密码套件,Tls版本和是否支持扩展
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      //Tls握手,密码套件协商等...
      sslSocket.startHandshake();
      // block for session establishment
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      // Verify that the socket's certificates are acceptable for the target host.
      //验证socket的证书是否是客户端要访问的域名
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      //检查当前的证书,是否符合证书固定,证书固定(CertificatePinner)
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      //Tls连接成功,保留握手和ALPN协议。对输入输出流重新赋值等
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }
  1. 基于rawSocket构造出SSLSocket
  2. 确认sslSocket的密码套件,Tls版本和是否支持扩展
  3. 开始Tls握手
  4. 域名验证
  5. 如果有证书固定的话,检查是否符合证书固定的要求

插一个题外话,这里其实没追踪到底在哪里做了证书的验证和密码套件,Tls版本的确认

sslSocket.startHandshake();在SSLSocket上开始握手,这个方法是同步的,此时SSLSocket上已经装载了支持的密码套件,Tls版本等信息。

在这个连接上启动ssl握手,常见的原因包括,需要使用新的加密密钥,更改密码套件或发起一个新的会话,为了强制进行完全重新认证,开始此握手之前,当前会话可能会失效,如果已经通过连接发送了数据,他将在握手过程中流动。对于在连接上的握手,这个方法是同步的


SSLSession sslSocketSession = sslSocket.getSession();
返回此sslSocket连接正在使用的sslSession。这个sslSession可能存活的时间比较长,sslSession会指定特定的密码套件,并且认证会话中的客户端和服务端的身份,

连接的建立流程分析完了,在看下RealConnection中的关键的方法

  /** Returns true if this connection is ready to host new streams. */
  //判断这个连接是否支持创建新的Stream
  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;
  }
/**
   * 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.
   */
   //判断对于给定的Addres和Route,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.
  }

判断对于给定的Addres和Route,Connection可以继续使用,重用的判断。这里主要会被ConnectionPool调用

Connection是关键类,把ConnectionPool和Stream串联起来

ConnectionPool

ConnectionPool是管理Http 1.1 和Http 2 的Connection的,以便于减少网络延迟,共用同一个connection,减少三次握手,密码协商等过程。相同的Address将共享同一个Connection
下面分析一下ConnectionPool的具体实现

/**
   * 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.
   */
   //用于清理空闲Connection的的线程池,只会创建一个后台线程,因为只有一个清理任务在执行
  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;//对于每个Address最大的空闲连接数
  private final long keepAliveDurationNs;//存活的周期
  //清理空闲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 {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };

  private final Deque connections = new ArrayDeque<>();//Connection的队列
  boolean cleanupRunning;//是否正在执行清理Connection 的任务

  /**
   * 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.
   */
   //默认构造函数,对于相同的Address最大5个空闲连接,最多存活5分钟
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

接着看一下清理任务的执行,其中最重要的方法是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. */ //如果空闲连接超过了空闲时间,或者超过了最大空闲连接数,那么就清除空闲时间最长的Connection 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. return keepAliveDurationNs; } else { // No connections, idle or in use. cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; } /** * 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. */ //移除一些可能泄漏的Stream(流),引起泄漏的原因可能是Connection还在引用和跟踪,但是应用层已经放弃使用它们了。最终返回真正还在用的Stream的数量 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()移除一些泄漏的Stream(流),引起泄漏的原因可能是Connection还在引用和跟踪,但是应用层已经放弃使用它们了。最终返回真正还在用的Stream的数量,即分配在这个Connection上引用还被应用层使用的Stream数量

cleanUp()方法总结,返回的是距离下一次执行清理任务的阻塞时间

  1. 遍历在ConnectionPool的连接队列connections

    1.1 执行pruneAndGetAllocationCount()移除应用层不再追踪的Stream

    1.2 找到空闲connections中空闲时间最长的Connection
  2. 如果空闲Connection的空闲时间大于存活时间,或者空闲Connection的数量大于最大空闲连接数,那么就移除这个Connection,并且返回0,继续执行下一次的清理
  3. 如果不符合2的条件,并且空闲连接数大于0,那么就会返回keepAliveDurationNs - longestIdleDurationNs;让线程阻塞这段时间,等到下一次唤醒线程后,接着判断是否有空闲连接需要清楚,
  4. 如果不符合2和3的条件,那么就会阻塞最大存活时间
  5. 如果ConnectionPool里面,没有Connection(不管是空闲还是在用的),那么就让清理任务结束,cleanupRunning=false
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

清理任务的启动是在往ConnectionPool里面put的时候,如果任务没在执行,就先执行任务,然后把Connection加入connections中,如果清理任务已经在执行,那么就直接把Connection加入connections中

上面主要是分析ConnectionPool中关于清理任务的部分,下面看一下关于获取Connection以重用的Connection

  /**
   * 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添加进connection中的allocations列表中
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

遍历connections,返回一个符合connection.isEligible(address, route)方法的Connection,用于重用,否者返回null

上面其实只是分别介绍了Connection和ConnectionPool,在分析的过程中会发现其实还有一个比较重要的类StreamAllocation,之后会再分析StreamAllocation。并且这篇文章并没有把Connection和ConnectionPool串联起来,后期把这些StreamAllocation和HttpCodec都分析完之后再串联起来吧

你可能感兴趣的:(OkHttp. RecoConnection StreamAllocation)