Android網絡編程(八) 之 HttpURLConnection原理分析

1 使用回顧

我們在前面博文《Android網絡編程(四) 之 HttpClient與HttpURLConnection》中已經對HttpURLConnection的使用進行過介紹。今天我們接着往下來閱讀HttpURLConnection的關鍵源碼從而它進行更加深入的理解。開始前,先來回顧一下簡單的使用,通過使用步驟來深入分析每行代碼背後的原理,代碼如:

InputStream inStream = null;
HttpURLConnection conn = null;
try {
    URL url = new URL("https://blog.csdn.net/lyz_zyx");
    conn = (HttpURLConnection)url.openConnection();
    if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
        inStream = conn.getInputStream();
        String result = inputStreamToString(inStream); // inputStreamToString方法用於解析轉換,實現請見前面文章
        // ……
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        if (inStream != null) {
            inStream.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    if (conn != null) {
        conn.disconnect();
    }
}

這是一個最簡單的使用例子,可見通過創建URL對象、創建HttpURLConnection對象、設置請求方法、獲得響應狀態代碼並判斷是否爲HTTP_OK(200),最後解析結果。

2 原理分析

2.1 創建URL對象

來看看URL類,源碼如下:

URL.java

public URL(String spec) throws MalformedURLException {
    this(null, spec);
}
public URL(URL context, String spec) throws MalformedURLException {
    this(context, spec, null);
}
public URL(URL context, String spec, URLStreamHandler handler) throws MalformedURLException {
    String original = spec;
    int i, limit, c;
    int start = 0;
    String newProtocol = null;
    boolean aRef=false;
    boolean isRelative = false;

    // Check for permission to specify a handler
    if (handler != null) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkSpecifyHandler(sm);
        }
    }

    try {
        limit = spec.length();
        while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {
            limit--;        //eliminate trailing whitespace
        }
        while ((start < limit) && (spec.charAt(start) <= ' ')) {
            start++;        // eliminate leading whitespace
        }

        if (spec.regionMatches(true, start, "url:", 0, 4)) {
            start += 4;
        }
        if (start < spec.length() && spec.charAt(start) == '#') {
            /* we're assuming this is a ref relative to the context URL.
             * This means protocols cannot start w/ '#', but we must parse
             * ref URL's like: "hello:there" w/ a ':' in them.
             */
            aRef=true;
        }
        for (i = start ; !aRef && (i < limit) &&
                 ((c = spec.charAt(i)) != '/') ; i++) {
            if (c == ':') {

                String s = spec.substring(start, i).toLowerCase();
                if (isValidProtocol(s)) {
                    newProtocol = s;
                    start = i + 1;
                }
                break;
            }
        }
        // 關鍵代碼1,解析URL的協議,賦予類成員變量protocol
        // Only use our context if the protocols match.
        protocol = newProtocol;
        if ((context != null) && ((newProtocol == null) || newProtocol.equalsIgnoreCase(context.protocol))) {
            // ……
        }

        if (protocol == null) {
            throw new MalformedURLException("no protocol: "+original);
        }

       // 關鍵代碼2,獲取URLStreamHandler對象賦予類成員變量handler
        if (handler == null && (handler = getURLStreamHandler(protocol)) == null) {
            throw new MalformedURLException("unknown protocol: "+protocol);
        }

        this.handler = handler;

        // ……

        // 關鍵代碼3,調用URLStreamHandler對象的parseURL方法解析URL信息
        handler.parseURL(this, spec, start, limit);

    } catch(MalformedURLException e) {
        throw e;
    } catch(Exception e) {
        MalformedURLException exception = new MalformedURLException(e.getMessage());
        exception.initCause(e);
        throw exception;
    }
}

請看三個參數的構造函數,函數內部主要做了三個事情:

  1. 解析URL的協議,賦予類成員變量protocol,該邏輯在方法內完成,如上述代碼;
  2. 通過方法getURLStreamHandler並傳入protocol來獲取URLStreamHandler對象賦予類成員變量handler;
  3. 調用變量handler(URLStreamHandler類對象)的parseURL方法解析URL信息。

2.1.1 getURLStreamHandler方法

來看看關鍵代碼2getURLStreamHandler方法內部是如何獲得URLStreamHandler對象的,代碼見如下:

static URLStreamHandler getURLStreamHandler(String protocol) {

    URLStreamHandler handler = handlers.get(protocol);
    if (handler == null) {
        // ……       

        // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https.
        // Fallback to built-in stream handler.
        if (handler == null) {
            try {
                // 關鍵代碼
                handler = createBuiltinHandler(protocol);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
        // END Android-added: Custom built-in URLStreamHandlers for http, https.

        // ……
    }
    return handler;
}
private static URLStreamHandler createBuiltinHandler(String protocol)
        throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    URLStreamHandler handler = null;
    if (protocol.equals("file")) {
        handler = new sun.net.www.protocol.file.Handler();
    } else if (protocol.equals("ftp")) {
        handler = new sun.net.www.protocol.ftp.Handler();
    } else if (protocol.equals("jar")) {
        handler = new sun.net.www.protocol.jar.Handler();
    } else if (protocol.equals("http")) {
        handler = (URLStreamHandler)Class.forName("com.android.okhttp.HttpHandler").newInstance();
    } else if (protocol.equals("https")) {
        handler = (URLStreamHandler)Class.forName("com.android.okhttp.HttpsHandler").newInstance();
    }
    return handler;
}

URLStreamHandler方法接收第1步獲得的協議變量protocol,然後內部又訪問了一個關鍵的方法createBuiltinHandler,createBuiltinHandler方法內部通過判斷協議類型來返回相對應的URLStreamHandler,這裏我們只來討論http和https協議情況,它們分別返回的URLStreamHandler對象是:com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler。

com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler的源碼需要單獨往https://android.googlesource.com/platform/external/okhttp/ 進行下載。HttpsHandler是繼承自HttpHandler,區別在於添加了對TLS的支持,而且從源碼可見,它們分別指定了默認端口是80和433。

HttpHandler.java

public class HttpHandler extends URLStreamHandler {
    // ……
}

HttpsHandler.java

public final class HttpsHandler extends HttpHandler {
    // ……
}

2.1.2 URLStreamHandler.parseURL方法

我們再回頭看回URL類的構造函數關鍵代碼3,它內部調用變量handler(URLStreamHandler類對象)的parseURL方法解析URL信息,代碼見如下:

URLStreamHandler.java

protected void parseURL(URL u, String spec, int start, int limit) {
        // These fields may receive context content if this was relative URL
        String protocol = u.getProtocol();
        String authority = u.getAuthority();
        String userInfo = u.getUserInfo();
        String host = u.getHost();
        int port = u.getPort();
        String path = u.getPath();
        String query = u.getQuery();
        String ref = u.getRef();

        // ……
        setURL(u, protocol, host, port, authority, userInfo, path, query, ref);
    }

   protected void setURL(URL u, String protocol, String host, int port,
                         String authority, String userInfo, String path,
                         String query, String ref) {
    if (this != u.handler) {
        throw new SecurityException("handler for url different from " + "this handler");
    }
    // ensure that no one can reset the protocol on a given URL.
    u.set(u.getProtocol(), host, port, authority, userInfo, path, query, ref);
}

parseURL方法中主要是解析出URL中的字符串信息,最後將這些字符串結果再回設置給URL類中去。以https://blog.csdn.net/lyz_zyx 爲例,完成了URL對象的創建後,調用以下代碼可見上述7個參數的值是:

URL url = new URL("https://blog.csdn.net/lyz_zyx");
String protocol = url.getProtocol();        // 代表協議,值是:https
String authority = url.getAuthority();      // 代表host和port兩部分,值是:blog.csdn.net
String userInfo = url.getUserInfo();        // 代表用戶信息,值是:null
String host = url.getHost();                 // 代表主機,值:blog.csdn.net
int port = url.getPort();                    // 代表端口,這裏沒有指定,所以值是:-1,如果使用的是getDefaultPort方法獲取的話,值是:443
String path = url.getPath();                // 代表路徑,值:/lyz_zyx
String query = url.getQuery();              // 代表參數,這裏後面沒跟有參數,值是:null
String ref = url.getRef();                  // 代表錨點,值是:null

2.2 創建HttpURLConnection對象

HttpURLConnection對象通過URL的openConnection方法返回,來看看代碼:

URL.java

public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}

從上面創建URL對象得知,handler對象是HttpsHandler,而HttpsHandler又繼承於HttpHandler,所以這裏的實現代碼在okhttp框架中的HttpHandler中,代碼如下:

okhttp->HttpHandler.java

@Override 
protected URLConnection openConnection(URL url) throws IOException {
// 關鍵代碼
    return newOkUrlFactory(null /* proxy */).open(url);
}
@Override 
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
    if (url == null || proxy == null) {
        throw new IllegalArgumentException("url == null || proxy == null");
    }
    // 關鍵代碼
    return newOkUrlFactory(proxy).open(url);
}

兩個重載的方法都是指向於newOkUrlFactory方法,該方法雖然只有一行關鍵代碼,但其實是做了兩件事情:

  1. 通過newOkUrlFactory方法創建一個OkUrlFactory對象;
  2. 調用OkUrlFactory對象的open方法返回URLConnection對象。

2.2.1 newOkUrlFactory方法

來看看newOkUrlFactory方法的源代碼做了什麼:

okhttp->HttpHandler.java

protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
// 關鍵代碼1,創建OkUrlFactory對象
    OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
    // For HttpURLConnections created through java.net.URL Android uses a connection pool that
    // is aware when the default network changes so that pooled connections are not re-used when
    // the default network changes.
// 關鍵代碼2,設置連接池
    okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
    return okUrlFactory;
}

該方法最終是創建了一個newOkUrlFactory對象並返回,其中裏面也做了兩件事情:

1.    通過createHttpOkUrlFactory方法創建OkUrlFactory對象;
2.    給okUrlFactory對象的client方法返回的對象設置連接池。

2.2.1.1 createHttpOkUrlFactory方法

okhttp->HttpHandler.java

public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
    OkHttpClient client = new OkHttpClient();

    // Explicitly set the timeouts to infinity.
    client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
    client.setReadTimeout(0, TimeUnit.MILLISECONDS);
    client.setWriteTimeout(0, TimeUnit.MILLISECONDS);

    // Set the default (same protocol) redirect behavior. The default can be overridden for
    // each instance using HttpURLConnection.setInstanceFollowRedirects().
    client.setFollowRedirects(HttpURLConnection.getFollowRedirects());

    // Do not permit http -> https and https -> http redirects.
    client.setFollowSslRedirects(false);

    // Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).
    client.setConnectionSpecs(CLEARTEXT_ONLY);

    // When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using
    // ProxySelector.getDefault().
    if (proxy != null) {
        client.setProxy(proxy);
    }

    // OkHttp requires that we explicitly set the response cache.
    OkUrlFactory okUrlFactory = new OkUrlFactory(client);

    // Use the installed NetworkSecurityPolicy to determine which requests are permitted over
    // http.
    OkUrlFactories.setUrlFilter(okUrlFactory, CLEARTEXT_FILTER);

    ResponseCache responseCache = ResponseCache.getDefault();
    if (responseCache != null) {
        AndroidInternal.setResponseCache(okUrlFactory, responseCache);
    }
    return okUrlFactory;
}

該方法中,首先new了一個OkHttpClient對象,然後給該對象設置了:連接超時時間、讀超時時間、寫超時時間、重定向相關、通信方式、代理等。

setConnectTimeout、setReadTimeout 和 setWriteTimeout,其值爲0表示無超時。

setConnectionSpecs方法設置通信方式,CLEARTEXT_ONLY實際上是Collections.singletonList(ConnectionSpec.CLEARTEXT)的列表,表示不需要TLS,即明文傳輸。

接着通過OkHttpClient對象來創建一個OkUrlFactory對象,並對其設置指定host的服務器進行明文通信(setUrlFilter方法)和請求緩存信息(setResponseCache),最後就是返回對象。

HttpsHandler繼承於HttpHandler,最大的區別在於OkUrlFactory對象的創建,我們也來看看它的源碼:

okhttp->HttpsHandler.java

public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
    // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
    OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);

    // All HTTPS requests are allowed.
    OkUrlFactories.setUrlFilter(okUrlFactory, null);

    OkHttpClient okHttpClient = okUrlFactory.client();

    // Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.
    okHttpClient.setProtocols(HTTP_1_1_ONLY);

    okHttpClient.setConnectionSpecs(Collections.singletonList(TLS_CONNECTION_SPEC));

    // Android support certificate pinning via NetworkSecurityConfig so there is no need to
    // also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections
    // in Android should therefore always use the default certificate pinner, whose set of
    // {@code hostNamesToPin} is empty.
    okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);

    // OkHttp does not automatically honor the system-wide HostnameVerifier set with
    // HttpsURLConnection.setDefaultHostnameVerifier().
    okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
    // OkHttp does not automatically honor the system-wide SSLSocketFactory set with
    // HttpsURLConnection.setDefaultSSLSocketFactory().
    // See https://github.com/square/okhttp/issues/184 for details.
    okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());

    return okUrlFactory;
}

setProtocols:方法傳入了HTTP_1_1_ONLY是一個列表,它對應着TLS握手ClientHello階段中,支持最高的協議版本version。關於TLS握手過程,請參考《Android網絡編程(三) 之 網絡請求握手過程》

setConnectionSpecs:方法傳入的是Collections.singletonList(TLS_CONNECTION_SPEC),表示支持TLS,即密文傳輸。

setCertificatePinner:方法設置證書鎖定,在Android中始終使用默認的證書CertificatePinner.DEFAULT。

setHostnameVerifier:方法指定驗證證書的HostnameVerifier。

sslSocketFactory:方法設置SSLSocket的工廠。

2.2.1.2 setConnectionPool方法

okhttp->OkHttpClient.java

public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
  this.connectionPool = connectionPool;
  return this;
}

通過setConnectionPool方法給OkUrlFactory內的OkHttpClient對象設置連接池configAwareConnectionPool.get(),來看看參數ConnectionPool是何方神聖代碼:

okhttp->HttpHandler.java

private final ConfigAwareConnectionPool configAwareConnectionPool = ConfigAwareConnectionPool.getInstance();

configAwareConnectionPool事實上是一個ConfigAwareConnectionPool單例,再來看看ConfigAwareConnectionPool的代碼:

okhttp->ConfigAwareConnectionPool.java

public synchronized ConnectionPool get() {
  if (connectionPool == null) {
    // Only register the listener once the first time a ConnectionPool is created.
    if (!networkEventListenerRegistered) {
      networkEventDispatcher.addListener(new NetworkEventListener() {
        @Override
        public void onNetworkConfigurationChanged() {
          synchronized (ConfigAwareConnectionPool.this) {
            // If the network config has changed then existing pooled connections should not be
            // re-used. By setting connectionPool to null it ensures that the next time
            // getConnectionPool() is called a new pool will be created.
            connectionPool = null;
          }
        }
      });
      networkEventListenerRegistered = true;
    }
    connectionPool = new ConnectionPool(
        CONNECTION_POOL_MAX_IDLE_CONNECTIONS, CONNECTION_POOL_KEEP_ALIVE_DURATION_MS);
  }
  return connectionPool;
}

代碼中可見,get方法內做了監聽網絡變化事件,一旦網絡發生變化,將會把connectionPool對象置爲null,方法下面會執行創建出新的ConnectionPool對象,其構造函數參數:

CONNECTION_POOL_MAX_IDLE_CONNECTIONS:表示空閒TCP連接的最大數量,默認爲5個。

CONNECTION_POOL_KEEP_ALIVE_DURATION_MS:表示TCP連接最長的空閒時長,默認爲5分鐘。

ConnectionPool類用於TCP連接池的封裝,內部維護着一個雙向的先進先出的ArrayDeque隊列

2.2.2 open方法

看回上面,創建HttpURLConnection對象中的第2件事,調用OkUrlFactory對象的open方法,請看源代碼:

okhttp->OkUrlFactory.java

public HttpURLConnection open(URL url) {
  return open(url, client.getProxy());
}
HttpURLConnection open(URL url, Proxy proxy) {
  String protocol = url.getProtocol();
  OkHttpClient copy = client.copyWithDefaults();
  copy.setProxy(proxy);

  if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy, urlFilter);
  if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy, urlFilter);
  throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}

可見,方法內根據協議創建出HttpURLConnectionImplHttpsURLConnectionImpl對象並返回。其中:

HttpURLConnectionImpl繼承於HttpURLConnection;

HttpsURLConnectionImpl同樣經過繼承DelegatingHttpsURLConnection和HttpsURLConnection最終也是繼承於HttpURLConnection。

2.3 執行網絡請求

當創建了URL和HttpURLConnection對象後,接下來就是執行網絡的請求。我們在使用上知道,無論是調用:conn.getResponseCode()、conn.connect() 還是conn.getInputStream() 都可以完成網絡的請求操作。那麼我們先來看看這三個方法到底內部做了些什麼事情。請看代碼:

HttpURLConnection.java

public int getResponseCode() throws IOException {
    if (responseCode != -1) {
        return responseCode;
    }
    Exception exc = null;
    try {
        getInputStream();
    } catch (Exception e) {
        exc = e;
    }

    // ……
}

getResponseCode方法很好理解,首先判斷responseCode是否不爲-1,代表如果已經執行過請求,就直接返回上次的請求碼就可以。否則就執行getInputStream方法進行網絡請求。可以理解成getResponseCode方法如果從未執行過網絡請求的話,會調用到getInputStream方法中去完成網絡請求。

我們再來看看connect方法代碼:

okhttp->HttpURLConnectionImpl.java

@Override 
public final void connect() throws IOException {
  initHttpEngine();
  boolean success;
  do {
    success = execute(false);
  } while (!success);
}

connect方法很簡單的幾行代碼,核心就是兩件事情:initHttpEngine方法和execute方法。

我們再來看看getInputStream方法的代碼:

okhttp->HttpURLConnectionImpl.java

@Override 
public final InputStream getInputStream() throws IOException {
  if (!doInput) {
    throw new ProtocolException("This protocol does not support input");
  }

  HttpEngine response = getResponse();

  // if the requested file does not exist, throw an exception formerly the
  // Error page from the server was returned if the requested file was
  // text/html this has changed to return FileNotFoundException for all
  // file types
  if (getResponseCode() >= HTTP_BAD_REQUEST) {
    throw new FileNotFoundException(url.toString());
  }

  return response.getResponse().body().byteStream();
}

getInputStream方法內裏也是兩件核心的事情:請求getResponse方法獲得HttpEngine對象,然後返回HttpEngine對象的內容字節流。所以關鍵點就在於getResponse方法中,再繼續看代碼:

okhttp->HttpURLConnectionImpl.java

private HttpEngine getResponse() throws IOException {
  initHttpEngine();

  if (httpEngine.hasResponse()) {
    return httpEngine;
  }

  while (true) {
    if (!execute(true)) {
      continue;
    }

    Response response = httpEngine.getResponse();
    Request followUp = httpEngine.followUpRequest();

    if (followUp == null) {
      httpEngine.releaseStreamAllocation();
      return httpEngine;
    }

    if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) {
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    // The first request was insufficient. Prepare for another...
    url = followUp.url();
    requestHeaders = followUp.headers().newBuilder();

    // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect
    // should keep the same method, Chrome, Firefox and the RI all issue GETs
    // when following any redirect.
    Sink requestBody = httpEngine.getRequestBody();
    if (!followUp.method().equals(method)) {
      requestBody = null;
    }

    if (requestBody != null && !(requestBody instanceof RetryableSink)) {
      throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
    }

    StreamAllocation streamAllocation = httpEngine.close();
    if (!httpEngine.sameConnection(followUp.httpUrl())) {
      streamAllocation.release();
      streamAllocation = null;
    }

    httpEngine = newHttpEngine(followUp.method(), streamAllocation, (RetryableSink) requestBody,
        response);
  }
}

從代碼可見,getResponse方法內部同樣也存在connect方法中要執行的initHttpEngine方法和execute方法。所以上述三個方法:getResponseCode、connect 和getInputStream,它們的核心原理就可以通過分析getResponse方法來得到答案了。先來了解一下getResponse方法內部有要關鍵就是調用了兩個方法:

  1. initHttpEngine是初始化HttpEngine;
  2. execute是最終執行的發送網絡請求。

2.3.1 initHttpEngine方法

來看看initHttpEngine方法源碼。

okhttp->HttpURLConnectionImpl.java

private void initHttpEngine() throws IOException {
  if (httpEngineFailure != null) {
    throw httpEngineFailure;
  } else if (httpEngine != null) {
    return;
  }

  connected = true;
  try {
    if (doOutput) {
      if (method.equals("GET")) {
        // they are requesting a stream to write to. This implies a POST method
        method = "POST";
      } else if (!HttpMethod.permitsRequestBody(method)) {
        throw new ProtocolException(method + " does not support writing");
      }
    }
    // If the user set content length to zero, we know there will not be a request body.
    httpEngine = newHttpEngine(method, null, null, null);
  } catch (IOException e) {
    httpEngineFailure = e;
    throw e;
  }
}

上述方法中,會判斷doOutput是否爲true,如果是且請求方法是GET的話,會將其改爲POST。先來聊聊doOutput和doInput是什麼:

doOutput:表示是否通過請求體發送數據給服務端,默認爲false。

doInput:表示是否讀取服務端返回的響應體中的數據,默認爲true。

因爲GET請求方法是沒有請求體的,所以如果doOutput爲true的話,就要將其修改爲POST的方式來請求。

接着,通過newHttpEngine方法來創建HttpEngine對象。往下看代碼:

okhttp->HttpURLConnectionImpl.java

private HttpEngine newHttpEngine(String method, StreamAllocation streamAllocation, RetryableSink requestBody, Response priorResponse)
    throws MalformedURLException, UnknownHostException {
  // OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
  RequestBody placeholderBody = HttpMethod.requiresRequestBody(method) ? EMPTY_REQUEST_BODY : null;
  URL url = getURL();
  HttpUrl httpUrl = Internal.instance.getHttpUrlChecked(url.toString());
 // 關鍵代碼1,創建Request對象
  Request.Builder builder = new Request.Builder()
      .url(httpUrl)
      .method(method, placeholderBody);
  Headers headers = requestHeaders.build();
  for (int i = 0, size = headers.size(); i < size; i++) {
    builder.addHeader(headers.name(i), headers.value(i));
  }

  boolean bufferRequestBody = false;
  if (HttpMethod.permitsRequestBody(method)) {
    // Specify how the request body is terminated.
    if (fixedContentLength != -1) {
      builder.header("Content-Length", Long.toString(fixedContentLength));
    } else if (chunkLength > 0) {
      builder.header("Transfer-Encoding", "chunked");
    } else {
      bufferRequestBody = true;
    }

    // Add a content type for the request body, if one isn't already present.
    if (headers.get("Content-Type") == null) {
      builder.header("Content-Type", "application/x-www-form-urlencoded");
    }
  }

  if (headers.get("User-Agent") == null) {
    builder.header("User-Agent", defaultUserAgent());
  }

  Request request = builder.build();

  // If we're currently not using caches, make sure the engine's client doesn't have one.
  OkHttpClient engineClient = client;
  if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) {
    engineClient = client.clone().setCache(null);
  }
  // 關鍵代碼2,創建 HttpEngine對象
  return new HttpEngine(engineClient, request, bufferRequestBody, true, false, streamAllocation, requestBody, priorResponse);
}

上述方法開始前,先通過HttpUrl和RequestBody來new了一個Request對象,然後給該對象添加header和設置:Content-Length、Transfer-Encoding、Content-Type、User-Agent等這些header值,更多header的介紹可見HTTP響應頭和請求頭信息對照表。最後就是創建HttpEngine對象,代碼如下。

okhttp->HttpEngine.java

public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
    boolean callerWritesRequestBody, boolean forWebSocket, StreamAllocation streamAllocation,
    RetryableSink requestBodyOut, Response priorResponse) {
  this.client = client;
  this.userRequest = request;
  this.bufferRequestBody = bufferRequestBody;
  this.callerWritesRequestBody = callerWritesRequestBody;
  this.forWebSocket = forWebSocket;
  this.streamAllocation = streamAllocation != null ? streamAllocation : new StreamAllocation(client.getConnectionPool(), createAddress(client, request));
  this.requestBodyOut = requestBodyOut;
  this.priorResponse = priorResponse;
}

2.3.2 execute方法

完成了HttpEngine的初始化後,接着回到HttpURLConnectionImpl類中調用的execute方法做了什麼事情。

okhttp->HttpURLConnectionImpl.java

private boolean execute(boolean readResponse) throws IOException {
  boolean releaseConnection = true;
  if (urlFilter != null) {
// 關鍵代碼1,判斷是否可以與指定host的服務器進行明文通信 
    urlFilter.checkURLPermitted(httpEngine.getRequest().url());
  }
  try {
//關鍵代碼2,發起網絡請求
    httpEngine.sendRequest();
    Connection connection = httpEngine.getConnection();
    if (connection != null) {
      route = connection.getRoute();
      handshake = connection.getHandshake();
    } else {
      route = null;
      handshake = null;
    }
//關鍵代碼3,讀取響應
    if (readResponse) {
      httpEngine.readResponse();
    }
    releaseConnection = false;

    return true;
  } catch (RequestException e) {
    // ……
  } catch (RouteException e) {
    // ……
  } catch (IOException e) {
    // ……
  } finally {
    // ……
  }
}

execute方法裏做了3件事情:

  1. 判斷指定host服務器進行明文通信
  2. 發起網絡請求
  3. 讀取響應

2.3.2.1判斷指定host服務器進行明文通信

先看關鍵代碼1處,方法先判斷若urlFilter不爲空,則調用checkURLPermitted來判斷是否可以與指定host的服務器進行明文通信。urlFilter對象經構造方法傳入,從OkUrlFactory.java中new HttpURLConnectionImpl或new HttpsURLConnectionImpl得知,urlFilter是在HttpHandler.java中通過setUrlFilter方法傳入CLEARTEXT_FILTER,而在HttpsHandler.java中通過setUrlFilter方法傳入null,所以具體實現代碼在HttpHandler.java中。

okhttp->HttpHandler.java

private static final class CleartextURLFilter implements URLFilter {
    @Override
    public void checkURLPermitted(URL url) throws IOException {
        String host = url.getHost();
        if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host)) {
            throw new IOException("Cleartext HTTP traffic to " + host + " not permitted");
        }
    }
}

2.3.2.2 發起網絡請求

看回execute中關鍵代碼2,這裏就是發起網絡請求,我們看回HttpEngine類中的實現:

okhttp->HttpEngine.java

public void sendRequest() throws RequestException, RouteException, IOException {
  if (cacheStrategy != null) return; // Already sent.
  if (httpStream != null) throw new IllegalStateException();
 
// 關鍵代碼1,通過newworkRequest方法處理Request對象,添加Host、Connection、Accept-Encoding、User-Agent請求頭
  Request request = networkRequest(userRequest);

//關鍵代碼2,獲取緩存,默認情況下是開啓緩存的,除非在HttpURLConnection中setUseCaches傳入false
  InternalCache responseCache = Internal.instance.internalCache(client);
  Response cacheCandidate = responseCache != null ? responseCache.get(request) : null;

//關鍵代碼2,通過CacheStrategy.Factory裏緩存相關策略計算,獲得networkRequest和cacheResponse兩個對象,分別表示網絡請求對象和緩存請求對象
  long now = System.currentTimeMillis();
  cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
  networkRequest = cacheStrategy.networkRequest;
  cacheResponse = cacheStrategy.cacheResponse;

  if (responseCache != null) {
    responseCache.trackResponse(cacheStrategy);
  }

  if (cacheCandidate != null && cacheResponse == null) {
    closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
  }

// 關鍵代碼3,判斷networkRequest不爲null,表示無緩存或緩存過期,需求請求網絡
  if (networkRequest != null) {
// 建立Socket連接
    httpStream = connect();
    httpStream.setHttpEngine(this);

    // If the caller's control flow writes the request body, we need to create that stream
    // immediately. And that means we need to immediately write the request headers, so we can
    // start streaming the request body. (We may already have a request body if we're retrying a failed POST.)
    if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) {
      long contentLength = OkHeaders.contentLength(request);
      if (bufferRequestBody) {
        if (contentLength > Integer.MAX_VALUE) {
          throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
              + "setChunkedStreamingMode() for requests larger than 2 GiB.");
        }

        if (contentLength != -1) {
          // 已知長度情況下,即設置過Content-Length情況下,往httpStream寫入請求頭信息和創建RetryableSink對象
          httpStream.writeRequestHeaders(networkRequest);
          requestBodyOut = new RetryableSink((int) contentLength);
        } else {
          // 未知長度情況下,只創建RetryableSink對象,待後面body準備完畢後再設置
          requestBodyOut = new RetryableSink();
        }
      } else {
        httpStream.writeRequestHeaders(networkRequest);
        requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
      }
    }
   } 
// 關鍵代碼4,該請求的緩存有效,直接使用緩存請求
else {
    if (cacheResponse != null) {
      // We have a valid cached response. Promote it to the user response immediately.
      this.userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
    } else {
      // We're forbidden from using the network, and the cache is insufficient.
      this.userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
    }

    userResponse = unzip(userResponse);
  }
}

這方法很重要,我們將其分爲做了4件事,如上述註釋中的關鍵代碼,分別有:

  1. 通過newworkRequest方法處理Request對象,添加Host、Connection、Accept-Encoding、User-Agent請求頭;
  2. 通過計算緩存是否可用,然後獲得網絡請求對象networkRequest和緩存對象cacheResponse;
  3. 判斷網絡請求對象不爲空,則執行網絡相關請求操作;
  4. 否則執行緩存相關請求操作。

上述4件事中,我們重點來關注第2和第3件事情。

緩存策略

來來看下關鍵代碼2中所做的第2件事,獲得網絡請求和緩存請求對象,關鍵看cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();這行代碼所調用的邏輯。

okhttp->CacheStrategy.java

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }
  return candidate;
}

get方法調用了getCandidate方法獲得返回的結果對象,判斷如果禁止使用網絡且緩存不足則返回一個新創建的networkRequest和cacheResponse都爲空的CacheStrategy對象。我們重點來關注getCandidate方法的邏輯。

okhttp->CacheStrategy.java

private CacheStrategy getCandidate() {
  // 如果請求對應的緩存爲空,則返回新創建的cacheResponse爲空的CacheStrategy對象
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // 如果缺少所需的握手,則返回新創建的cacheResponse爲空的CacheStrategy對象
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // 如果不允許緩存,則返回新創建的cacheResponse爲空的CacheStrategy對象
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  long ageMillis = cacheResponseAge();
  long freshMillis = computeFreshnessLifetime();

  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  long maxStaleMillis = 0;
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

// 判斷緩存是否過期,如果新鮮,則返回新創建的networkRequest爲空的CacheStrategy對象
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
    if (ageMillis + minFreshMillis >= freshMillis) {
      builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
      builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
  }

// 如果緩存已過期,需要進一步與服務端進行驗證是否還可以使用
  Request.Builder conditionalRequestBuilder = request.newBuilder();

  if (etag != null) {
    conditionalRequestBuilder.header("If-None-Match", etag);
  } else if (lastModified != null) {
    conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
  } else if (servedDate != null) {
    conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
  }

  Request conditionalRequest = conditionalRequestBuilder.build();
  return hasConditions(conditionalRequest)
      ? new CacheStrategy(conditionalRequest, cacheResponse)
      : new CacheStrategy(conditionalRequest, null);
}

上述方法就是整個HttpURLConnection的緩存策略所關鍵的地方。緩存計算完畢後,接着就是來處理網絡請求或者緩存請求了。

網絡請求

再來來看下關鍵代碼3中所做的第3件事,網絡請求相關的邏輯,就要跳到connect方法去,請繼續看代碼。

okhttp->HttpEngine.java

private HttpStream connect() throws RouteException, RequestException, IOException {
  boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");
  return streamAllocation.newStream(client.getConnectTimeout(),
      client.getReadTimeout(), client.getWriteTimeout(),
      client.getRetryOnConnectionFailure(), doExtensiveHealthChecks);
}

方法內主要是調用了streamAllocation對象的newStream方法,接着往下看

okhttp->StreamAllocation.java

public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,
    boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
    throws RouteException, IOException {
  try {
// 關鍵代碼,尋找一個健康的連接
    RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
        writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);

    HttpStream resultStream;
    if (resultConnection.framedConnection != null) {
//使用HTTP/2協議
      resultStream = new Http2xStream(this, resultConnection.framedConnection);
    } else {
// 使用HTTP/1.x協議
      resultConnection.getSocket().setSoTimeout(readTimeout);
      resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
      resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
      resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink);
    }
    // ……
}
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
    int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
    throws IOException, RouteException {
  while (true) {
// 關鍵代碼
    RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
        connectionRetryEnabled);

    // ……
  }
}
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    boolean connectionRetryEnabled) throws IOException, RouteException {

  // ……
  Route route = routeSelector.next();
  RealConnection newConnection = new RealConnection(route);
  acquire(newConnection);

  synchronized (connectionPool) {
    Internal.instance.put(connectionPool, newConnection);
    this.connection = newConnection;
    if (canceled) throw new IOException("Canceled");
  }

// 關鍵代碼,創建與服務端建立Socket連接
  newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),
      connectionRetryEnabled);
  routeDatabase().connected(newConnection.getRoute());

  return newConnection;
}

okhttp->RealConnection.java

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
    List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {

// ……

  while (protocol == null) {
    try {
      rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
          ? address.getSocketFactory().createSocket()
          : new Socket(proxy);
// 關鍵代碼,發起與服務端的TCP連接
      connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
    } catch (IOException e) {
      // ……
    }
  }
}
private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
    ConnectionSpecSelector connectionSpecSelector) throws IOException {
  rawSocket.setSoTimeout(readTimeout);
  try {
// 關鍵代碼,服務端建立TCP連接
    Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
  } catch (ConnectException e) {
    throw new ConnectException("Failed to connect to " + route.getSocketAddress());
  }
  source = Okio.buffer(Okio.source(rawSocket));
  sink = Okio.buffer(Okio.sink(rawSocket));

  if (route.getAddress().getSslSocketFactory() != null) {
// 關鍵代碼,TLS握手
    connectTls(readTimeout, writeTimeout, connectionSpecSelector);
  } else {
    protocol = Protocol.HTTP_1_1;
    socket = rawSocket;
  }
  // ……
}

由於底層代碼過多和複雜的原因,這裏省略了較多的過程,只保留一部分關鍵代碼,我們對其進行一個大概的瞭解即可。能看見得是,這些方法的邏輯中最後就是跟Socket進行相應的處理。

2.3.2.3 讀取響應

完成了請求後,最後就是讀取響應了,繼續看回execute中關鍵代碼。

okhttp->HttpEngine.java

public void readResponse() throws IOException {
  if (userResponse != null) {
    return; // Already ready.
  }
  if (networkRequest == null && cacheResponse == null) {
    throw new IllegalStateException("call sendRequest() first!");
  }
  if (networkRequest == null) {
    return; // No network response to read.
  }

  Response networkResponse;

  if (forWebSocket) {
    httpStream.writeRequestHeaders(networkRequest);
    networkResponse = readNetworkResponse();

  } else if (!callerWritesRequestBody) {
    networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);

  } else {
    // ……
// 關鍵代碼1,獲得網絡請求結果
    networkResponse = readNetworkResponse();
  }

  receiveHeaders(networkResponse.headers());

  // If we have a cache response too, then we're doing a conditional get.
  if (cacheResponse != null) {
    if (validate(cacheResponse, networkResponse)) {
//關鍵代碼2,讀取緩存請求結果
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .headers(combine(cacheResponse.headers(), networkResponse.headers()))
          .cacheResponse(stripBody(cacheResponse))
          .networkResponse(stripBody(networkResponse))
          .build();
      networkResponse.body().close();
      releaseStreamAllocation();

      // Update the cache after combining headers but before stripping the
      // Content-Encoding header (as performed by initContentStream()).
      InternalCache responseCache = Internal.instance.internalCache(client);
      responseCache.trackConditionalCacheHit();
      responseCache.update(cacheResponse, stripBody(userResponse));
      userResponse = unzip(userResponse);
      return;
    } else {
      closeQuietly(cacheResponse.body());
    }
  }

//關鍵代碼3,讀取網絡請求結果
  userResponse = networkResponse.newBuilder()
      .request(userRequest)
      .priorResponse(stripBody(priorResponse))
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();

  if (hasBody(userResponse)) {
//關鍵代碼4,保存緩存
    maybeCache();
    userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
  }
}
private void maybeCache() throws IOException {
  InternalCache responseCache = Internal.instance.internalCache(client);
  if (responseCache == null) return;

  // Should we cache this response for this request?
  if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
// 關鍵代碼,移除無效的緩存
        responseCache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
    return;
  }

  // 關鍵代碼,將本次請求結果緩存
  storeRequest = responseCache.put(stripBody(userResponse));
}

從上述註釋可見,方法開頭先判斷readResponse是否爲真,因爲我們從前面得知,通過conn.connect()進行請求的話,傳入的是false,而通過conn.getInputStream()進行請求會傳入true。

過程中關鍵代碼1處是獲得網絡請求的結果,然後去驗證網絡請求的結果和緩存請求的結果哪個是有效的,而進行相應的結果讀取,最後就是更新緩存。

3 原理總結

到這裏,通過源碼分析原理就全部完成了,整個過程代碼量還是非常多。我們平時在學習優秀框架時,並不一定全部代碼完全讀懂,其實能明白大概原理以及從中吸收框架設計的思想就已經足夠了。那我們現在用簡短的話來總結一下HttpURLConnection的原理:

第1  首先new了一個URL類的對象,內部就是解析URL的協議、主機名、端口、路徑、參數等信息。

    1.1 其中在解析協議時創建對應的URLStreamHandler對象,若協議是http或https的話,其URLStreamHandler對象是google的okhttp框架中的com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler。

   1.2 所以說,使用httpURLConnection框架進行網絡請求,實際上就是使用okhttp進行的。

第2  通過URL類的openConnection方法創建HttpURLConnection類對象,現實過程也是在okhttp中進行,

   2.1 內部先創建一個指定的host服務端和請求緩存信息的OkUrlFactory對象,該對象內包含着一個OkHttpClient對象,過程中給OkHttpClient對象設置連接時間、讀寫時間、通信方式(是否需要TLS)、連接池(雙向的先進先出的ArrayDeque隊列,默認空閒TCP連接的最大數量爲5個,默認TCP連接最長的空閒時長爲5分鐘)。

   2.2 創建OkHttpClient對象後便調用其open方法返回HttpURLConnection的實現類對象:HttpURLConnectionImpl或HttpsURLConnectionImpl。

第3  創建完了HttpURLConnection對象後,就是讓其執行網絡請求了。

   3.1 通過簡單分析得知,無論是conn.getResponseCode()、conn.connect() 還是conn.getInputStream() 進行網絡請求,最終會調用到initHttpEngineexecute方法

   3.2 initHttpEngine方法從名字便得知是初始化一個HttpEngine對象,過程中通過創建一個Request對象,並使Request對象添加header和設置:Content-Length、Transfer-Encoding、Content-Type、User-Agent等這些header值。

   3.3 execute方法就是核心網絡請求的邏輯,其內部主要是先通過一個緩存策略計算緩存是否不爲空、是否握手正常、是否允許緩存、是否新鮮等邏輯來返回一個緩存請求對象cacheResponse和網絡請求對象。如果緩存方案有效,則網絡請求對象是null,相反若緩存方案無效,該網絡請求對象就是上面所創建的Request對象。網絡請求過程內在就是通過Socket來進行與服務端的TCP握手和TLS握手連接的相關網絡處理。

   3.4 最後同樣判斷緩存請求或網絡請求對象是否有效再進行相應的結果讀取,並更新緩存。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章