OkHttp3的连接池及连接建立过程分析(下篇)

达芬奇密码2018-08-17 10:13

建立隧道连接

建立隧道连接的过程如下:

  1. 构造一个 建立隧道连接 请求。
  2. 与HTTP代理服务器建立TCP连接。
  3. 创建隧道。这主要是将 建立隧道连接 请求发送给HTTP代理服务器,并处理它的响应。
  4. 重复上面的第2和第3步,知道建立好了隧道连接。至于为什么要重复多次,及关于代理认证的内容,可以参考代理协议相关的内容。
  5. 建立协议。

关于建立隧道连接更详细的过程可参考 OkHttp3中的代理与路由 的相关部分。

建立普通连接

建立普通连接的过程比较直接:

  /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    connectSocket(connectTimeout, readTimeout);
    establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
  }
  1. 建立一个TCP连接。
  2. 建立协议。

更详细的过程可参考 OkHttp3中的代理与路由 的相关部分。

建立协议

不管是建立隧道连接,还是建立普通连接,都少不了 建立协议 这一步。这一步是在建立好了TCP连接之后,而在该TCP能被拿来收发数据之前执行的。它主要为数据的加密传输做一些初始化,比如TLS握手,HTTP/2的协议协商等。

  private void establishProtocol(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    if (route.address().sslSocketFactory() != null) {
      connectTls(readTimeout, writeTimeout, connectionSpecSelector);
    } else {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
    }

    if (protocol == Protocol.HTTP_2) {
      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.

      Http2Connection http2Connection = new Http2Connection.Builder(true)
          .socket(socket, route.address().url().host(), source, sink)
          .listener(this)
          .build();
      http2Connection.start();

      // Only assign the framed connection once the preface has been sent successfully.
      this.allocationLimit = http2Connection.maxConcurrentStreams();
      this.http2Connection = http2Connection;
    } else {
      this.allocationLimit = 1;
    }
  }
  1. 对于加密的数据传输,创建TLS连接。对于明文传输,则设置protocolsocketsocket指向直接与应用层,如HTTP或HTTP/2,交互的Socket: 对于明文传输没有设置HTTP代理的HTTP请求,它是与HTTP服务器之间的TCP socket; 对于明文传输设置了HTTP代理或SOCKS代理的HTTP请求,它是与代理服务器之间的TCP socket; 对于加密传输没有设置HTTP代理服务器的HTTP或HTTP2请求,它是与HTTP服务器之间的SSLScoket; 对于加密传输设置了HTTP代理服务器的HTTP或HTTP2请求,它是与HTTP服务器之间经过了代理服务器的SSLSocket,一个隧道连接; 对于加密传输设置了SOCKS代理的HTTP或HTTP2请求,它是一条经过了代理服务器的SSLSocket连接。

  2. 对于HTTP/2,会建立HTTP/2连接,并进一步协商连接参数,如连接上可同时执行的并发请求数等。而对于非HTTP/2,则将连接上可同时执行的并发请求数设置为1。

建立TLS连接

进一步来看建立协议过程中,为安全请求所做的建立TLS连接的过程:

  private void connectTls(int readTimeout, int writeTimeout,
      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.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake();
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
        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.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      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);
      }
    }
  }

TLS连接是对原始的TCP连接的一个封装,以提供TLS握手,及数据收发过程中的加密解密等功能。在Java中,用SSLSocket来描述。上面建立TLS连接的过程大体为:

  1. 用SSLSocketFactory基于原始的TCP Socket,创建一个SSLSocket。
  2. 配置SSLSocket。
  3. 在前面选择的ConnectionSpec支持TLS扩展参数时,配置TLS扩展参数。
  4. 启动TLS握手。
  5. TLS握手完成之后,获取握手信息。
  6. 对TLS握手过程中传回来的证书进行验证。
  7. 检查证书钉扎。
  8. 在前面选择的ConnectionSpec支持TLS扩展参数时,获取TLS握手过程中顺便完成的协议协商过程所选择的协议。这个过程主要用于HTTP/2的ALPN扩展。
  9. OkHttp主要使用Okio来做IO操作,这里会基于前面获取的SSLSocket创建用于执行IO的BufferedSource和BufferedSink等,并保存握手信息及所选择的协议。

具体来看ConnectionSpecSelector中配置SSLSocket的过程:

  /**
   * Configures the supplied {@link SSLSocket} to connect to the specified host using an appropriate
   * {@link ConnectionSpec}. Returns the chosen {@link ConnectionSpec}, never {@code null}.
   *
   * @throws IOException if the socket does not support any of the TLS modes available
   */
  public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
    ConnectionSpec tlsConfiguration = null;
    for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
      ConnectionSpec connectionSpec = connectionSpecs.get(i);
      if (connectionSpec.isCompatible(sslSocket)) {
        tlsConfiguration = connectionSpec;
        nextModeIndex = i + 1;
        break;
      }
    }

    if (tlsConfiguration == null) {
      // This may be the first time a connection has been attempted and the socket does not support
      // any the required protocols, or it may be a retry (but this socket supports fewer
      // protocols than was suggested by a prior socket).
      throw new UnknownServiceException(
          "Unable to find acceptable protocols. isFallback=" + isFallback
              + ", modes=" + connectionSpecs
              + ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
    }

    isFallbackPossible = isFallbackPossible(sslSocket);

    Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);

    return tlsConfiguration;
  }

这个过程分为如下的两个步骤:

  1. 从为OkHttp配置的ConnectionSpec集合中选择一个与SSLSocket兼容的一个。SSLSocket与ConnectionSpec兼容的标准如下:

    public boolean isCompatible(SSLSocket socket) {
    if (!tls) {
      return false;
    }
    
    if (tlsVersions != null
        && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
      return false;
    }
    
    if (cipherSuites != null
        && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
      return false;
    }
    
    return true;
    }
    
    /**
    * An N*M intersection that terminates if any intersection is found. The sizes of both arguments
    * are assumed to be so small, and the likelihood of an intersection so great, that it is not
    * worth the CPU cost of sorting or the memory cost of hashing.
    */
    private static boolean nonEmptyIntersection(String[] a, String[] b) {
    if (a == null || b == null || a.length == 0 || b.length == 0) {
      return false;
    }
    for (String toFind : a) {
      if (indexOf(b, toFind) != -1) {
        return true;
      }
    }
    return false;
    }
    

    即ConnectionSpec启用的TLS版本及密码套件,与SSLSocket启用的有交集。 2 将选择的ConnectionSpec应用在SSLSocket上。OkHttpClient中ConnectionSpec的应用:

      @Override
      public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) {
        tlsConfiguration.apply(sslSocket, isFallback);
      }
    

    而在ConnectionSpec中:

    /** Applies this spec to {@code sslSocket}. */
    void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
    
    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
    }
    
    /**
    * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
    * sslSocket}.
    */
    private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
    String[] cipherSuitesIntersection = cipherSuites != null
        ? intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
        : sslSocket.getEnabledCipherSuites();
    String[] tlsVersionsIntersection = tlsVersions != null
        ? intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
        : sslSocket.getEnabledProtocols();
    
    // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
    // the SCSV cipher is added to signal that a protocol fallback has taken place.
    if (isFallback && indexOf(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV") != -1) {
      cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
    }
    
    return new Builder(this)
        .cipherSuites(cipherSuitesIntersection)
        .tlsVersions(tlsVersionsIntersection)
        .build();
    }
    
  1. 主要是:

    • 求得ConnectionSpec启用的TLS版本及密码套件与SSLSocket启用的TLS版本及密码套件之间的交集,构造新的ConnectionSpec。
    • 重新为SSLSocket设置启用的TLS版本及密码套件为上一步求得的交集。

我们知道HTTP/2的协议协商主要是利用了TLS的ALPN扩展来完成的。这里再来详细的看一下配置TLS扩展的过程。对于Android平台而言,这部分逻辑在AndroidPlatform:

  @Override public void configureTlsExtensions(
      SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
    // Enable SNI and session tickets.
    if (hostname != null) {
      setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
      setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
    }

    // Enable ALPN.
    if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
      Object[] parameters = {concatLengthPrefixed(protocols)};
      setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
    }
  }

TLS扩展相关的方法不是SSLSocket接口的标准方法,不同的SSL/TLS实现库对这些接口的支持程度不一样,因而这里通过反射机制调用TLS扩展相关的方法。

这里主要配置了3个TLS扩展,分别是session tickets,SNI和ALPN。session tickets用于会话回复,SNI用于支持单个主机配置了多个域名的情况,ALPN则用于HTTP/2的协议协商。可以看到为SNI设置的hostname最终来源于Url,也就意味着使用HttpDns时,如果直接将IP地址替换原来Url中的域名来发起HTTPS请求的话,SNI将是IP地址,这有可能使服务器下发不恰当的证书。

TLS扩展相关方法的OptionalMethod创建过程也在AndroidPlatform中:

  public AndroidPlatform(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
      OptionalMethod<Socket> setHostname, OptionalMethod<Socket> getAlpnSelectedProtocol,
      OptionalMethod<Socket> setAlpnProtocols) {
    this.sslParametersClass = sslParametersClass;
    this.setUseSessionTickets = setUseSessionTickets;
    this.setHostname = setHostname;
    this.getAlpnSelectedProtocol = getAlpnSelectedProtocol;
    this.setAlpnProtocols = setAlpnProtocols;
  }

......

  public static Platform buildIfSupported() {
    // Attempt to find Android 2.3+ APIs.
    try {
      Class<?> sslParametersClass;
      try {
        sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
      } catch (ClassNotFoundException e) {
        // Older platform before being unbundled.
        sslParametersClass = Class.forName(
            "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
      }

      OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(
          null, "setUseSessionTickets", boolean.class);
      OptionalMethod<Socket> setHostname = new OptionalMethod<>(
          null, "setHostname", String.class);
      OptionalMethod<Socket> getAlpnSelectedProtocol = null;
      OptionalMethod<Socket> setAlpnProtocols = null;

      // Attempt to find Android 5.0+ APIs.
      try {
        Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
        getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
        setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
      } catch (ClassNotFoundException ignored) {
      }

      return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
          getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
      // This isn't an Android runtime.
    }

    return null;
  }

建立TLS连接的第7步,获取协议的过程与配置TLS的过程类似,同样利用反射调用SSLSocket的方法,在AndroidPlatform中:

  @Override public String getSelectedProtocol(SSLSocket socket) {
    if (getAlpnSelectedProtocol == null) return null;
    if (!getAlpnSelectedProtocol.isSupported(socket)) return null;

    byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
    return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
  }

至此我们分析了OkHttp3中,所有HTTP请求,包括设置了代理的明文HTTP请求,设置了代理的HTTPS请求,设置了代理的HTTP/2请求,无代理的明文HTTP请求,无代理的HTTPS请求,无代理的HTTP/2请求的连接建立过程,其中包括TLS的握手,HTTP/2的协议协商等。

总结一下,OkHttp中,IO相关的组件的其关系大体如下图所示:

Done。



相关阅读:

OkHttp3的连接池及连接建立过程分析(上篇)

OkHttp3的连接池及连接建立过程分析(中篇)

OkHttp3的连接池及连接建立过程分析(下篇)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者韩鹏飞授权发布。