OKHttp全解析系列(六) --OKHttp的连接与请求

概述

本片文章主要讲解OKHttp的连接建立过程。我们先宏观的对OkHttp连接有个初步了解:

  • OKHttp是一个高效的http库,主要表现在:1 支持Http1,Http1.1,Http2协议,实现这一点主要因为Http连接不同于以往的网络框架,它是利用系统的Scoket实现的Http连接,可以满足Http1和Http2协议,通过前面的文章我们知道Http2协议解决了Http连接的复用问题,提高了Http的效率;2 另外针对Http协议中连接耗时的另外一个原因是TCP三次握手导致的,OKHttp 利用Http协议中keepalive connections机制(可以在传输数据后仍然保持连接),当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手,OKhttp内部建立了连接池,用来保存Http连接,Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。
  • OKHttp同时支持Http和Https协议

创建HTTP连接的流程

@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);
}
未命名文件 (1).png

OKHttp的连接部分可以分为两大类:1 从连接池中获取一个可用的连接,并将此链接和本次请求绑定,产生一个StreamAllcation;2:没有可用的连接,建立socket链路,完成TCP三次握手以及TLS握手,并与本次请求绑定,产生一个StreamAllcation。这里主要涉及到StreamAllcation,RealConnection,和ConnectionPool三个类。

  • StreamAllcation 完成获取一个健康的Http连接,从连接池中或者新建一个Http连接
  • RealConnectin 利用Socket建立一个真正的Http连接,并完成TCP、TLS握手
  • ConnectionPool 主要缓冲连接池,提高Http连接的复用率,提高Http的效率

RealConnection 建立HTTP连接

本文先从RealConnection建立HTTP开始分析。RealConnection建立HTTP的连接根据代理的不同,HTTP的连接分如下几种情况:

  • 无代理的HTTP请求,与服务器建立TCP连接;
  • 无代理的HTTPS请求,与服务器建立TCP连接,并完成TLS握手;
  • 无代理的HTTP2请求,与服务器建立好TCP连接,完成TLS握手及协议协商。
  • 设置了SOCKS代理的HTTP请求,通过代理服务器与HTTP服务器建立连接;
  • 设置了SOCKS代理的HTTPS请求,通过代理服务器与HTTP服务器建立连接,并完成TLS握手;
  • 设置了SOCKS代理的HTTP/2请求,通过代理服务器与HTTP服务器建立连接,并完成与服务器的TLS握手及协议协商;
  • 设置了HTTP代理的HTTP请求,与代理服务器建立TCP连接;HTTP代理服务器解析HTTP请求/响应的内容,并根据其中的信息来完成数据的转发。HTTP服务器如何知道服务器的地址呢?是通过Header中的HOST字段获取的。
  • 设置了HTTP代理的HTTPS、HTTP2请求,与HTTP服务器建立通过HTTP代理的隧道连接,并完成TLS握手;HTTP代理不再解析传输的数据,仅仅完成数据转发的功能。此时HTTP代理的功能如同SOCKS代理。
      public void connect(int connectTimeout, int readTimeout, int writeTimeout,
                      boolean connectionRetryEnabled, Call call, EventListener eventListener) {
    if (protocol != null) throw 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"));
      }
    }

    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, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ...
      }
    }

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

分析以上代码主要流程如下:
1 通过路由获取安全套件,并验证安全套件是否和协议一致:对于HTTP协议的请求,安全套件中必须包含CLEARTEXT,CLEATTEXT代表着明文传输;Android平台本身的安全策略是否允许向相应的主机发送明文请求。
2 进入循环创建连接直到创建成功,跳出循环。
3 首先根据路由判断是否需要建立隧道 ,建立隧道连接 或者建立普通的连接
4 建立协议,指的是建立TSL握手协议
5 对于HTTP2协议,设置连接的最大分配数,指一条HTTP连接上最多同时存在的请求数目。

建立普通Socket连接

建立普通连接的过程非常简单,主要创建Socket和建立Socket。本文不做详细的分析。

建立隧道Socket连接

是否需要建立隧道的依据如下:

  • 无代理的HTTP、HTTPS、HTTP2传输不需要隧道。
  • SOCKS代理的HTTP、HTTPs、HTTP2不需要建立隧道。
  • HTTP代理的HTTP协议不需要建立隧道。
  • HTTP代理的HTTPs、HTTP2协议需要建立隧道。
     private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
                             EventListener eventListener) throws IOException {
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
      connectSocket(connectTimeout, readTimeout, call, eventListener);
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

      if (tunnelRequest == null) break; // Tunnel successfully created.

      // The proxy decided to close the connection after an auth challenge. We need to create a new
      // connection, but this time with the auth credentials.
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
      eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
    }}

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

  • 创建 建立隧道连接 请求。主要设置HEADER中的Host和Proxy-Connection属性
  • 与HTTP代理服务器建立TCP连接。
  • 创建隧道。这主要是将 建立隧道连接 请求发送给HTTP代理服务器,并处理它的响应。
  • 如果建立失败,再次尝试建立隧道。

建立协议

      private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, Call call,
                                 EventListener eventListener) throws IOException {
    if (route.address().sslSocketFactory() == null) {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
      return;
    }

    eventListener.secureConnectStart(call);
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);

    if (protocol == Protocol.HTTP_2) {
      socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
      http2Connection = new Http2Connection.Builder(true)
              .socket(socket, route.address().url().host(), source, sink)
              .listener(this)
              .build();
      http2Connection.start();
    }}

如果是HTTP协议,不需要建立协议的过程,此时TCP握手已经完成,可以在这个连接上开始于服务器的通信;如果是HTTPS、HTTP2 协议则还需要建立协议 TLS协议,完成TLS的握手,验证服务器证书,以及协商机密算法、传输秘钥。

建立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();
      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) {
                ...
    }}

HTTPS或者HTTP2的连接,需要在原生已经完成TCP握手的连接基础上在包装一下,产生一个新的SSLSocket,并对这SSLSocket设置安全套件,之后开始进入TLS的协商阶段。
建立TLS流程如下:
1 根据以上步骤中建立的socket,创建一个新的SSlSocket;
2 设置SSlSocket的安全套件;
3 启动TLS握手,完成协议版本号、加密算法的协商,对服务器证书的认证,秘钥的交换;
4 对收到的证书验证是否支持特定的host;
5 检查证书pinner;
6 实例化输入输出流等属性。
到此,一个可用的HTTP或者HTTPs,或者HTTP2的连接已经完成了。接下来会将一个请求与这个连接绑定,对于HTTP1.0和HTTP1.1,一个连接只能同时绑定一个请求;而HTTP2连接可以同时可以绑定多个请求。

StreamAllocation 获取可用的HTTP连接

OkHttp中有三个概念需要了解一下,请求,连接和流。我们要明白HTTP通信执行网络请求需要在连接上建立一个新的。请求被封装成Call对象,连接被封装成Connection对象,流被封装成HttpCodec。StreamAllocation是流分配的逻辑,它负责为一个Call找到一个Connection,这个Connection可能是从连接池中拿到的,也可能是新建立的。以及在请求完成或者取消时释放资源。

      public final Address address;//请求的地址
      private RouteSelector.Selection routeSelection;
      private Route route;//路由
      private final ConnectionPool connectionPool;//连接池
      public final Call call;//请求
      public final EventListener eventListener;
      private final Object callStackTrace;//日志
      private final RouteSelector routeSelector;//路由选择器
      private int refusedStreamCount;//拒绝的次数
      private RealConnection connection;//连接
      private boolean canceled;//请求取消
      private HttpCodec codec;//流

下面分析它分配流的过程

   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);
   // 实例化流对象
     HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

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

这个方法完成两件事:1 获取一个健康的连接;2 实例化流对象
再来看获取健康连接的过程:findHealthyConnection方法内部通过调用findConnection获取到一个连接,然后对这个连接判断是否健康,如果不是健康的连接,再循环获取一个连接。我们直接分析findConnection的逻辑。

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    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.
      RealConnection allocatedConnection = this.connection;
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // Attempt to get a connection from the pool.
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
      } else {
        selectedRoute = route;
      }
    }

    // If we found a pooled connection, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      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++) {
          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) {
        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);
      }
    }

    // We have a connection. Either a connected one from the pool, or one we need to connect.
    eventListener.connectionAcquired(call, result);

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      return result;
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    result.connect(
        connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // Pool the 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.
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
     }

1、先找是否有已经存在的连接,如果有已经存在的连接,并且可以使用则直接返回。
2、根据已知的address在connectionPool里面找,如果有连接,则返回
3、更换路由,更换线路,在connectionPool里面再次查找,如果有则返回。
4、如果以上条件都不满足则直接new一个RealConnection出来
5、新建的RealConnection通过acquire关联到connection.allocations上
6、做去重判断,如果有重复的socket则关闭

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容