OKHttp全解析系列(六) --OKHttp的連接與請求

概述

本片文章主要講解OKHttp的連接建立過程。我們先宏觀的對OkHttp連接有個初步了解:

  • OKHttp是一個高效的http庫,主要表現(xiàn)在:1 支持Http1,Http1.1,Http2協(xié)議,實現(xiàn)這一點主要因為Http連接不同于以往的網(wǎng)絡(luò)框架,它是利用系統(tǒng)的Scoket實現(xiàn)的Http連接,可以滿足Http1和Http2協(xié)議,通過前面的文章我們知道Http2協(xié)議解決了Http連接的復(fù)用問題,提高了Http的效率;2 另外針對Http協(xié)議中連接耗時的另外一個原因是TCP三次握手導(dǎo)致的,OKHttp 利用Http協(xié)議中keepalive connections機制(可以在傳輸數(shù)據(jù)后仍然保持連接),當(dāng)客戶端需要再次獲取數(shù)據(jù)時,直接使用剛剛空閑下來的連接而不需要再次握手,OKhttp內(nèi)部建立了連接池,用來保存Http連接,Okhttp支持5個并發(fā)KeepAlive,默認鏈路生命為5分鐘(鏈路空閑后,保持存活的時間)。
  • OKHttp同時支持Http和Https協(xié)議

創(chuàng)建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 從連接池中獲取一個可用的連接,并將此鏈接和本次請求綁定,產(chǎn)生一個StreamAllcation;2:沒有可用的連接,建立socket鏈路,完成TCP三次握手以及TLS握手,并與本次請求綁定,產(chǎn)生一個StreamAllcation。這里主要涉及到StreamAllcation,RealConnection,和ConnectionPool三個類。

  • StreamAllcation 完成獲取一個健康的Http連接,從連接池中或者新建一個Http連接
  • RealConnectin 利用Socket建立一個真正的Http連接,并完成TCP、TLS握手
  • ConnectionPool 主要緩沖連接池,提高Http連接的復(fù)用率,提高Http的效率

RealConnection 建立HTTP連接

本文先從RealConnection建立HTTP開始分析。RealConnection建立HTTP的連接根據(jù)代理的不同,HTTP的連接分如下幾種情況:

  • 無代理的HTTP請求,與服務(wù)器建立TCP連接;
  • 無代理的HTTPS請求,與服務(wù)器建立TCP連接,并完成TLS握手;
  • 無代理的HTTP2請求,與服務(wù)器建立好TCP連接,完成TLS握手及協(xié)議協(xié)商。
  • 設(shè)置了SOCKS代理的HTTP請求,通過代理服務(wù)器與HTTP服務(wù)器建立連接;
  • 設(shè)置了SOCKS代理的HTTPS請求,通過代理服務(wù)器與HTTP服務(wù)器建立連接,并完成TLS握手;
  • 設(shè)置了SOCKS代理的HTTP/2請求,通過代理服務(wù)器與HTTP服務(wù)器建立連接,并完成與服務(wù)器的TLS握手及協(xié)議協(xié)商;
  • 設(shè)置了HTTP代理的HTTP請求,與代理服務(wù)器建立TCP連接;HTTP代理服務(wù)器解析HTTP請求/響應(yīng)的內(nèi)容,并根據(jù)其中的信息來完成數(shù)據(jù)的轉(zhuǎn)發(fā)。HTTP服務(wù)器如何知道服務(wù)器的地址呢?是通過Header中的HOST字段獲取的。
  • 設(shè)置了HTTP代理的HTTPS、HTTP2請求,與HTTP服務(wù)器建立通過HTTP代理的隧道連接,并完成TLS握手;HTTP代理不再解析傳輸?shù)臄?shù)據(jù),僅僅完成數(shù)據(jù)轉(zhuǎn)發(fā)的功能。此時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 通過路由獲取安全套件,并驗證安全套件是否和協(xié)議一致:對于HTTP協(xié)議的請求,安全套件中必須包含CLEARTEXT,CLEATTEXT代表著明文傳輸;Android平臺本身的安全策略是否允許向相應(yīng)的主機發(fā)送明文請求。
2 進入循環(huán)創(chuàng)建連接直到創(chuàng)建成功,跳出循環(huán)。
3 首先根據(jù)路由判斷是否需要建立隧道 ,建立隧道連接 或者建立普通的連接
4 建立協(xié)議,指的是建立TSL握手協(xié)議
5 對于HTTP2協(xié)議,設(shè)置連接的最大分配數(shù),指一條HTTP連接上最多同時存在的請求數(shù)目。

建立普通Socket連接

建立普通連接的過程非常簡單,主要創(chuàng)建Socket和建立Socket。本文不做詳細的分析。

建立隧道Socket連接

是否需要建立隧道的依據(jù)如下:

  • 無代理的HTTP、HTTPS、HTTP2傳輸不需要隧道。
  • SOCKS代理的HTTP、HTTPs、HTTP2不需要建立隧道。
  • HTTP代理的HTTP協(xié)議不需要建立隧道。
  • HTTP代理的HTTPs、HTTP2協(xié)議需要建立隧道。
     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);
    }}

建立隧道連接的過程如下:

  • 創(chuàng)建 建立隧道連接 請求。主要設(shè)置HEADER中的Host和Proxy-Connection屬性
  • 與HTTP代理服務(wù)器建立TCP連接。
  • 創(chuàng)建隧道。這主要是將 建立隧道連接 請求發(fā)送給HTTP代理服務(wù)器,并處理它的響應(yīng)。
  • 如果建立失敗,再次嘗試建立隧道。

建立協(xié)議

      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協(xié)議,不需要建立協(xié)議的過程,此時TCP握手已經(jīng)完成,可以在這個連接上開始于服務(wù)器的通信;如果是HTTPS、HTTP2 協(xié)議則還需要建立協(xié)議 TLS協(xié)議,完成TLS的握手,驗證服務(wù)器證書,以及協(xié)商機密算法、傳輸秘鑰。

建立TLS協(xié)議

     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的連接,需要在原生已經(jīng)完成TCP握手的連接基礎(chǔ)上在包裝一下,產(chǎn)生一個新的SSLSocket,并對這SSLSocket設(shè)置安全套件,之后開始進入TLS的協(xié)商階段。
建立TLS流程如下:
1 根據(jù)以上步驟中建立的socket,創(chuàng)建一個新的SSlSocket;
2 設(shè)置SSlSocket的安全套件;
3 啟動TLS握手,完成協(xié)議版本號、加密算法的協(xié)商,對服務(wù)器證書的認證,秘鑰的交換;
4 對收到的證書驗證是否支持特定的host;
5 檢查證書pinner;
6 實例化輸入輸出流等屬性。
到此,一個可用的HTTP或者HTTPs,或者HTTP2的連接已經(jīng)完成了。接下來會將一個請求與這個連接綁定,對于HTTP1.0和HTTP1.1,一個連接只能同時綁定一個請求;而HTTP2連接可以同時可以綁定多個請求。

StreamAllocation 獲取可用的HTTP連接

OkHttp中有三個概念需要了解一下,請求,連接和流。我們要明白HTTP通信執(zhí)行網(wǎng)絡(luò)請求需要在連接上建立一個新的。請求被封裝成Call對象,連接被封裝成Connection對象,流被封裝成HttpCodec。StreamAllocation是流分配的邏輯,它負責(zé)為一個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;//拒絕的次數(shù)
      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方法內(nèi)部通過調(diào)用findConnection獲取到一個連接,然后對這個連接判斷是否健康,如果不是健康的連接,再循環(huán)獲取一個連接。我們直接分析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、先找是否有已經(jīng)存在的連接,如果有已經(jīng)存在的連接,并且可以使用則直接返回。
2、根據(jù)已知的address在connectionPool里面找,如果有連接,則返回
3、更換路由,更換線路,在connectionPool里面再次查找,如果有則返回。
4、如果以上條件都不滿足則直接new一個RealConnection出來
5、新建的RealConnection通過acquire關(guān)聯(lián)到connection.allocations上
6、做去重判斷,如果有重復(fù)的socket則關(guān)閉

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容