奇怪的知识: okhttp 是如何支持 Http2 的?

Posted 鸿洋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了奇怪的知识: okhttp 是如何支持 Http2 的?相关的知识,希望对你有一定的参考价值。


本文作者


链接:

https://juejin.im/post/5ef474426fb9a07e9950874f

本文由作者授权发布。


1
简介


Http2.0 这个吧肯定是真香的,其中特别是二进制分帧和多路复用。


但是我一直有些疑惑,Http2.0为什么后端支持了前端就能直接访问2.0版本了,Okhttp如何开启的Http2.0呢?


简单说下Http2.0


二进制分帧层 (Binary Framing Layer)


帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧.


多路复用 (MultiPlexing)


在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题.http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接。


服务端推送 (Server Push)


浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。


Header 压缩 (HPACK)


使用 HPACK 算法来压缩首部内容

https://httpwg.org/specs/rfc7541.html


2
Http2.0 你必须知道的小秘密


引:IIS currently supports HTTP/2 only over TLS. When making an HTTPS connection to a web server running IIS on Windows 10, HTTP/2 is used if the client and server both support it. In IIS, we've implemented HTTP/2 as transparently as possible - you shouldn't need to change anything in your application for HTTP/2 to work. Certain HTTP/1.1 optimizations (domain sharding, inlining, etc.) are no longer recommended in HTTP/2, though, so you should plan to remove these in the future.


Http2.0必须建立在TLS的基础上,也就是必须是Https的请求。


TLS


Http2.0的前置条件是实现了https。


而Https则是在Http的基础上增加了一层Tls。这个东西在大厂的面试中其实是一个高频考点了,简单的说Tls就是一个前后端约定好后续加密方式的过程。这篇文章写的很好,详细可以参考这个传送门,而整体流程如下图。


https://www.jianshu.com/p/07a1e362e1ba



  1. client 发起第一次client hello过程,请求 Https 连接,发送可用的 TLS 版本和可用的密码套件。

  2. server 发起第一次server hello过程,返回证书,密码套件和 TLS 版本等信息。

  3. 生成随机对称密钥,使用证书中的服务端公钥加密,发送给服务端

  4. 服务端使用私钥解密获取对称密钥


不知道各位有没有思考过一个问题,为什么只要后端将接口升级到Http2.0的支持之后,客户端就能自动的把所有的请求切换到Http2.0上呢?还有2.0和Tls到底有什么关系呢?


ALPN((Application Layer Protocol Negotiation)协议

ALPN (Application Layer Protocol Negotiation)是TLS的扩展,允许在安全连接的基础上进行应用层协议的协商。ALPN支持任意应用层协议的协商,目前应用最多是HTTP2的协商。当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2,因为浏览器是基于ALPN协议来判断服务器是否支持HTTP2协议。


ALPN是TLS的扩展协议,而ALPN的作用就是告诉客户端,当前服务端支持的接口协议版本有哪些,当然这里会有很多种。


所有上看的问题的答案基本呼之欲出,贴一张朋友吊打我的图。2.0必须使用TLS的原因就是因为这个ALPN的拓展协议。


奇怪的知识: okhttp 是如何支持 Http2 的?


3
OkHttp Connection  分析


Okhttp是如何实现的这整个流程呢,我画了个大概的流程图。




ConnectInterceptor


/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

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

其实这里还有个小知识点:


一个 TCP 连接可以对应几个 HTTP 请求?


从拦截器实现可以发现,Okhttp实现了一个连接池,当ConnectionInterceptor被调用的时候,先是判断连接池内有没有空闲并且健康的可用连接,然后再使用连接去调度下一个拦截器,那么也就是一个tcp连接的存活时间是大于Http请求的,所以一个Tcp可以对应多个Http请求。


这个拦截器的作用就是在发起实际请求之前构建好连接,然后使用这个连接发起访问,这里的核心就是调用了streamAllocation.newStream(client, chain, doExtensiveHealthChecks);,获取一个连接对象。


RealConnection


我们主要说些connet方法,它是整个Http2.0的开启流程的关键。


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

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    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 {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;

        eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);

        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }

        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }

    if (route.requiresTunnel() && rawSocket == null) {
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }

    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

其中while true 循环内会去构建一个socket连接,当socket连接构建成功之后,会调用establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);方法,这个就是整篇文章的主角了。


  private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener)
 throws IOException 
{
    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);
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

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

看到最后几行代码,其实已经能知道了。


只要当前协议包含了HTTP_2,OKhttp就会开启Http2.0模式,否则则降级成1.1的代码。而如何去获取协议就是connectTls这个方法了,而且Tls完整流程都在方法内。


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.
      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();
      // block for session establishment
      SSLSession sslSocketSession = sslSocket.getSession();
      // 获取HandShake 信息
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
        if (!peerCertificates.isEmpty()) {
          X509Certificate cert = (X509Certificate) peerCertificates.get(0);
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified:"
                  + "     certificate: " + CertificatePinner.pin(cert)
                  + "     DN: " + cert.getSubjectDN().getName()
                  + "     subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
        } else {
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified (no certificates)");
        }
      }

      // 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.
      // 成功之后,保存HandShake以及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);
      }
    }
  }

这里要先引申出一个概念,Okhttp设计之初就是一个java平台通用的网络库,对于不同的java版本,还有安卓的底层适配逻辑是不同的。


简单的说Okhttp就是抽象了下所有Tls,SSLSocket相关的代码,然后通过一个Platform,根据当前使用环境的不同,去反射调用不同的实现类,然后这个抽象的类去调用Platform的实现类代码,做到多平台的兼容。


其中Tls当生成好SSLSocket之后,就会开始进行client say hello 和server say hello的操作了,这部分完全和https定义的一模一样。


Handshake则会把服务端支持的Tls版本,加密方式等都带回来,然后会把这个没有验证过的HandShake用X509Certificate去验证证书的有效性。然后会通过Platform去从SSLSocket去获取ALPN的协议支持信息,当后端支持的协议内包含Http2.0时,则就会把请求升级到Http2.0阶段。


4
 总结


学习过程中,最好是带着疑问去思考,然后再去做一部分源码追溯,这样事半功倍,同时也能把之前的一部分困惑消灭,同时加深记忆力。


之前Https的一系列问题,我都是靠博客之类的去学习的,基本上不超过两三天就会遗忘啊,同时对于2.0的开启也是一个不求甚解的过程,基本上我之前的后端同事说我们已经是2.0了,我就只能哦一句。


还有一点就是本文只介绍了前置操作,而关于Http2.0的分帧等操作你们可以看下这篇文章啊传送门之HTTP 2.0与OkHttp。

https://juejin.im/post/5c789dbce51d451ecc2028e7


这几年https和Http2.0基本都是高频出现的面试题了,希望文章能对大家的认知有一定的帮助。最后能不能给我的gayhub的辣鸡项目点个赞。


说起来你们不信,今天我休假,可能我还在睡觉,留言放的可能不及时,大家见谅。


推荐阅读







如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

以上是关于奇怪的知识: okhttp 是如何支持 Http2 的?的主要内容,如果未能解决你的问题,请参考以下文章

OkHttp学习 - POM 文件

http2 似乎不适用于 OkHttp3 和 retrofit2

OkHttp中如何实现接收服务器推送?

okhttp-utils的封装之okhttp的使用

OkHttp3 简述

HTTP2 默认为 HTTP/1.1