1. 程式人生 > >Okhttp解析(四)網路連線的建立

Okhttp解析(四)網路連線的建立

Okhttp作為一款底層網路訪問框架,它和Volley等上層網路框架不一樣的地方在於,Okhttp自己實現了與服務端的TCP連線,並在此連線上根據HTTP協議的規範與服務端進行HTTP協議及內容的請求和響應。Okhttp將請求內容通過修正,填充等方式封裝成符合HTTP規範的HTTP請求內容,通過TCP連線,將內容以流的方式輸出給服務端,並從服務端返回的響應流中讀取出響應內容,根據HTTP協議解析並作出相應的響應。

Okhttp連線建立的情況區分


Okhttp支援HTTP 1.0/1.1, HTTP2,SPDY 三種HTTP協議,同時支援加密傳輸HTTPS,以及SOCKS代理和HTTP代理。那麼在連線建立時就需要處理它們組合時的處理操作了。這裡的連線根據代理型別做主要區分。

無代理

  1. 無代理的HTTP請求, 與伺服器建立TCP連線。
  2. 無代理的HTTPS加密請求, 與伺服器建立TCP連線,然後建立TLS加密連線。
  3. 無代理的HTTP2/SPDY請求, 與伺服器建立TCP連線,然後建立TLS加密連線,然後建立幀連線。

SOCKS代理

SOCKS代理伺服器會將請求資訊轉發給目標HTTP伺服器,目標HTTP伺服器返回資訊之後,SOCKS代理伺服器再將響應資訊返回給客戶端。SOCKS代理伺服器只會對資訊進行轉發,而不會解析修改。
1. SOCKS代理下的HTTP請求, 與SOCKS代理伺服器建立TCP連線。
2. SOCKS代理下的HTTPS加密請求,

通過SOCKS代理伺服器與HTTP伺服器建立連線,然後建立TLS加密連線。
3. SOCKS代理下的HTTP2/SPDY請求, 通過SOCKS代理伺服器與HTTP伺服器建立連線,然後建立TLS加密連線,然後建立幀連線。

HTTP代理

  1. HTTP代理下的HTTP請求, 與HTTP代理伺服器建立TCP連線。HTTP代理伺服器會解析請求的內容資訊,並根據這些資訊去請求目標HTTP伺服器,目標HTTP2伺服器返回資訊之後,再解析響應內容,然後再將這些響應資訊返回給客戶端。HTTP伺服器會解析和修改請求和響應的內容。
  2. HTTP代理下的HTTPS加密請求, 與目標伺服器建立通過HTTP代理的隧道連線,然後建立TLS加密連線。因為是加密連線,HTTP代理伺服器不再解析請求和響應內容,而只是轉發資料。
  3. HTTP代理下的HTTP2/SPDY請求, 與目標伺服器建立通過HTTP代理的隧道連線,然後建立TLS加密連線,然後建立幀連線。因為是加密連線,HTTP代理伺服器不再解析請求和響應內容,而只是轉發資料。

雖然情況分為很多種,但是重要的部分只是以下幾個階段。

  1. 根據是否需要代理,建立Socket連線。
  2. 進入Socket連線階段,判斷是否需要TLS連線,進入TLS連線處理。
  3. 進入TLS連線處理階段,判斷是否需要建立隧道連線。
  4. 完成TLS握手,判斷是否需要幀連線。

關於SOCKS代理和HTTP代理區別

Socks代理

是全能代理,就像有很多跳線的轉接板,它只是簡單地將一端的系統連線到另外一端。支援多種協議,包括http、ftp請求及其它型別的請求。它分socks 4 和socks 5兩種型別,socks 4只支援TCP協議而socks 5支援TCP/UDP協議,還支援各種身份驗證機制等協議。其標準埠為1080。socks代理相應的採用socks協議的代理伺服器就是SOCKS伺服器,是一種通用的代理伺服器。Socks是個電路級的底層閘道器,是DavidKoblas在1990年開發的,此後就一直作為Internet RFC標準的開放標準。Socks不要求應用程式遵循特定的作業系統平臺,Socks 代理與應用層代理、 HTTP 層代理不同,Socks代理只是簡單地傳遞資料包,而不必關心是何種應用協議(比如FTP、HTTP和NNTP請求)。所以,Socks代理比其他應用層代理要快得多。它通常繫結在代理伺服器的1080埠上。如果您在企業網或校園網上,需要透過防火牆或通過代理伺服器訪問Internet就可能需要使用SOCKS。一般情況下,對於撥號上網使用者都不需要使用它。注意,瀏覽網頁時常用的代理伺服器通常是專門的http代理,它和SOCKS是不同的。因此,您能瀏覽網頁不等於您一定可以通過SOCKS訪問Internet。 常用的防火牆,或代理軟體都支援SOCKS,但需要其管理員開啟這一功能。如果您不確信您是否需要SOCKS或是否有SOCKS可用,請與您的網路管理員聯絡。


HTTP代理

www對於每一個上網的人都再熟悉不過了,www連線請求就是採用的http協議,所以我們在瀏覽網頁,下載資料(也可採用ftp協議)是就是用http代理。它通常繫結在代理伺服器的80、3128、8080等埠上。

詳見

http://blog.csdn.net/clh604/article/details/9235597

http://blog.csdn.net/extraordinarylife/article/details/52512860

Okhttp連線建立的流程

有了以上對HTTP連線建立的大概瞭解後,我們接下來分析程式碼就會更容易理解了。我們從RealConnection的connect方法開始

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    //根據連線配置資訊建立連線配置選擇器,用於後面自動重試路由地址建立可用TCP連線
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    Proxy proxy = route.proxy();
    Address address = route.address();
    //對於普通HTTP連線,連線配置資訊中必須包含CLEARTEXT,也就是明文傳輸
    if (route.address().sslSocketFactory() == null
        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication not supported: " + connectionSpecs));
    }
    //進入while迴圈,建立連線,直到連線成功
    while (protocol == null) {
      try {
        //建立Socket,如果是無代理或HTTP代理,交給SocketFactory建立Socket,如果是SOCKS代理,則手動建立一個代理Socket
        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
        //連線Socket
        connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;

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

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

可以看到上面很簡單,首先建立一個底層Socket,然後進入Socket連線階段。我們繼續看connectSocket

/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    //設定讀取超時時間
    rawSocket.setSoTimeout(readTimeout);
    分android和java兩種平臺進行Socket的連線,其實也就是呼叫socket的connect
    try {
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      throw new ConnectException("Failed to connect to " + route.socketAddress());
    }
    //根據socket取得封裝後的輸入和輸出流,類似InputStream和OutputStream
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));

    if (route.address().sslSocketFactory() != null) {
      //建立TLS連線,看看哪些情況需要建立TLS連線
      connectTls(readTimeout, writeTimeout, connectionSpecSelector);
    } else {
      //不需要建立TLS連線的,統一當做HTTP 1.1協議
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
    }

    if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
      //如果是SPDY或者HTTP2協議連線,則需要建立幀連線
      FramedConnection framedConnection = new FramedConnection.Builder(true)
          .socket(socket, route.address().url().host(), source, sink)
          .protocol(protocol)
          .listener(this)
          .build();
      //傳送幀連線相關的資訊
      framedConnection.sendConnectionPreface();

      //獲取一個幀連線支援的最大併發流的數量
      // Only assign the framed connection once the preface has been sent successfully.
      this.allocationLimit = framedConnection.maxConcurrentStreams();
      this.framedConnection = framedConnection;
    } else {
      //不支援幀連線的,最大併發流為1,也是不支援流併發
      this.allocationLimit = 1;
    }
  }

可以看到這裡分為3個步驟。

  1. 建立了底層Socket的連線,並獲得輸入和輸出流。
  2. 判斷是否進行TLS握手連線。HTTPS,HTTP2,SPDY三種情況下需要建立TLS加密連線。
  3. 判斷是否需要建立幀連線,並進行幀連線握手。HTTP2,SPDY情況下需要建立幀連線。

接著我們進入建立TLS連線的過程,

private void connectTls(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    //在HTTP代理下,HTTPS,HTTP2,SPDY情況下,要建立隧道連線
    if (route.requiresTunnel()) {
      createTunnel(readTimeout, writeTimeout);
    }

    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      //基於之前的TCP連線,建立加密的SSLSocket連線
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      //配置加密連線需要的祕鑰,TLS版本等資訊
      // 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());
      }
      //開始TLS握手
      // Force handshake. This can throw!
      sslSocket.startHandshake();
      //獲取TLS握手後的結果
      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());

      //建立了TLS連線,獲取協商之後的HTTP協議
      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      //指定預設的socket連線,輸入和輸出流
      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 {
      //進行TLS握手結束階段的資源清理
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      //TLS連線不成功的話,關閉TLS連線
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

可以看到TLS連線這個階段主要做了以下操作。

  1. 判斷是否需要建立隧道連線。在HTTP代理下,HTTPS,HTTP2,SPDY情況下,要建立隧道連線
  2. 建立SSLSocket這個TLS連線,並開始握手。
  3. 獲取握手後的返回資訊,例如連線協議的商定,版本資訊,加密演算法,證書的驗證等。
  4. TLS成功建立後,更新socket,HTTP協議,輸入輸出流等資訊。
  5. 釋放資源,TLS連線成功後的資源清理和失敗後的關閉操作。

接下來我們看看是如何建立隧道連線的。

/**
   * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
   * the proxy connection. This may need to be retried if the proxy requires authorization.
   */
  private void createTunnel(int readTimeout, int writeTimeout) throws IOException {
    //建立隧道連線的請求
    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    //隧道連線的請求行
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    //while迴圈中,不斷重試建立隧道連線,直到建立成功
    while (true) {
      //建立隧道連線
      Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
      source.timeout().timeout(readTimeout, MILLISECONDS);
      sink.timeout().timeout(writeTimeout, MILLISECONDS);
      //輸出隧道連線的請求頭和請求行資料,請求建立隧道連線
      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
      tunnelConnection.finishRequest();
      //讀取響應資訊
      Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
      // The response body from a CONNECT should be empty, but if it is not then we should consume
      // it before proceeding.
      long contentLength = OkHeaders.contentLength(response);
      if (contentLength == -1L) {
        contentLength = 0L;
      }
      //忽略隧道連線請求過程中響應的資料
      Source body = tunnelConnection.newFixedLengthSource(contentLength);
      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
      body.close();

      switch (response.code()) {
        case HTTP_OK:
          //響應碼返回HTTP_OK,說明隧道連線建立成功
          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
          // that it will almost certainly fail because the proxy has sent unexpected data.
          if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
            throw new IOException("TLS tunnel buffered too many bytes!");
          }
          return;

        case HTTP_PROXY_AUTH:
          //響應碼返回HTTP_PROXY_AUTH,說明還需要進行證書驗證,while迴圈再次請求
          tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
          if (tunnelRequest != null) continue;
          throw new IOException("Failed to authenticate with proxy");

        default:
          throw new IOException(
              "Unexpected response code for CONNECT: " + response.code());
      }
    }
  }

  /**
   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if no tunnel is
   * necessary. Everything in the tunnel request is sent unencrypted to the proxy server, so tunnels
   * include only the minimum set of headers. This avoids sending potentially sensitive data like
   * HTTP cookies to the proxy unencrypted.
   */
  private Request createTunnelRequest() throws IOException {
    //建立隧道連線的請求,就是指定請求頭的一些欄位資訊
    return new Request.Builder()
        .url(route.address().url())
        .header("Host", Util.hostHeader(route.address().url(), true))
        .header("Proxy-Connection", "Keep-Alive")
        .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
        .build();
  }

可見隧道連線的建立過程,是通過封裝的Http1xStream流物件,輸出建立隧道連線所需要的請求頭和請求行資訊,去請求建立隧道連線,這個過程中,可能需要證書的驗證,因此,在while迴圈中,根據返回的響應資訊重新建立請求,並提交請求,知道返回隧道建立成功或丟擲異常。

到這裡的話,Okhttp網路連線的建立過程就講解完成了。