1. 程式人生 > >OKHttp原始碼分析3

OKHttp原始碼分析3

1 概述

上篇文章,我們詳細分析了OKHttp中Request的建立和傳送過程。其中sendRequest(), readResponse(), followUpRequest()三個關鍵方法在底層HttpEngine中實現。革命尚未成功,我們接下來在這篇文章中分析HttpEngine中的這三個方法。由於底層HttpEngine涉及到很多Http協議方面東西,對Http協議不熟悉的同學可以先閱讀我的這篇文章 Http協議簡介

2 sendRequest()原始碼分析

sendRequest()方法是client向server傳送request的主要方法。它先對request的header添加了一些預設欄位,如keep-alive。然後對cache進行處理,判斷是否可以直接使用cache。如果不行,才真正傳送網路request。
Markdown

  public void sendRequest() throws RequestException, RouteException, IOException {
    if (cacheStrategy != null) return; // Already sent.
    if (transport != null) throw new IllegalStateException();

    // 對header的處理,利用app中使用者構造的原始request
    // 主要是對header進行新增。如新增"Connection: Keep-Alive"首部。後面單獨分析
    Request request = networkRequest(userRequest);

    // 對cache的處理
InternalCache responseCache = Internal.instance.internalCache(client); // 利用request為key,從cache中取出response Response cacheCandidate = responseCache != null ? responseCache.get(request) : null; // 判斷cache是否可用,利用Expires,Last-Modified,Date,Age等header欄位,後面詳細分析 long now = System.currentTimeMillis(); cacheStrategy = new
CacheStrategy.Factory(now, request, cacheCandidate).get(); // cache可用或網路被禁止使用則networkRequest為null networkRequest = cacheStrategy.networkRequest; // cache不可用,則cacheResponse為null。對應情況有,不允許使用cache,沒有對應cache,cache過期需要重新驗證 cacheResponse = cacheStrategy.cacheResponse; if (responseCache != null) { responseCache.trackResponse(cacheStrategy); } if (cacheCandidate != null && cacheResponse == null) { // cache已過期,不可用,關閉它 closeQuietly(cacheCandidate.body()); } if (networkRequest != null) { // networkRequest不為空,代表cache不可用,且網路可用 // 從這兒可以看出,cache可用時會直接使用cache,不可用才走網路資料。這也是符合Http常規做法的。 if (connection == null) { // 連線到server,直接連線或通過代理均可 connect(); } // 構造HttpTransport,與傳送request到網路中去有關 transport = Internal.instance.newTransport(connection, this); // 將start line,headers,body寫入到buffer中,以等待發送出去 if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) { // 從request的header中獲取content-length long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { // bufferRequestBody表示body在記憶體中了,這可能是多次傳送重試等情況 // content-length太大 if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // content-length已知,是個準確值 // 可以將start line和header寫入HttpConnection中,此處涉及到Http報文結構和傳送,後面重點講解 transport.writeRequestHeaders(networkRequest); // 構造request body的buffer,長度為content-length requestBodyOut = new RetryableSink((int) contentLength); } else { // content-length還不確定,此時不能設定content-length首部,因為它還不確定。 // 要等到整個body準備好後,才能計算出content-length requestBodyOut = new RetryableSink(); } } else { transport.writeRequestHeaders(networkRequest); requestBodyOut = transport.createRequestBody(networkRequest, contentLength); } } } else { // networkRequest為null,要麼cache可用,要麼網路被禁止使用 if (connection != null) { // 回收網路connection,並關閉它 Internal.instance.recycle(client.getConnectionPool(), connection); connection = null; } if (cacheResponse != null) { // cache可用。可用代表有此request的cache response,且沒有過期 this.userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); } else { // 網路被禁止使用,自己構造一個504的response,gateway timeout 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(); } // 將利用cache或自己生成的504response,進行gzip壓縮 // 前面提到過,request的headers中聲明瞭支援gzip壓縮,故response中最好加入gzip壓縮。 userResponse = unzip(userResponse); } }

我們接下來分析sendRequest()中使用到的一些比較重要的方法。networkRequest()方法作用為,在原有的request基礎上新增一些header。從這些header中我們可以看出,OKHttp預設是使用Keep-Alive,response body支援gzip壓縮,支援Cookie的使用。看到了吧,分析底層程式碼有助於我們對Http協議的理解和對OKHttp特性的掌握。

  private Request networkRequest(Request request) throws IOException {
    Request.Builder result = request.newBuilder();

    // 利用url解析出host,然後新增host header。它指明瞭server地址
    if (request.header("Host") == null) {
      result.header("Host", Util.hostHeader(request.httpUrl()));
    }

    // 新增Connection首部,Keep-Alive表示持久連線,一次request和response完成後,HTTP並不立刻關閉。
    if (request.header("Connection") == null) {
      result.header("Connection", "Keep-Alive");
    }

    // 新增Accept-Encoding首部,gzip表示可接收gzip格式的壓縮編碼
    if (request.header("Accept-Encoding") == null) {
      transparentGzip = true;
      result.header("Accept-Encoding", "gzip");
    }

    // 處理cookie header
    CookieHandler cookieHandler = client.getCookieHandler();
    if (cookieHandler != null) {
      // 將使用者構建的原始request中的header弄成Map結構
      Map<String, List<String>> headers = OkHeaders.toMultimap(result.build().headers(), null);
      // 從URI中解析出cookie,並新增到Map中,其key為"Cookie"
      Map<String, List<String>> cookies = cookieHandler.get(request.uri(), headers);

      // 新增Cookie和Cookie2 header
      OkHeaders.addCookies(result, cookies);
    }

    // 新增User-Agent header,它表示client端是啥東西,比如瀏覽器
    // 對於OKHttp來說,就是okhttp和它的版本號
    if (request.header("User-Agent") == null) {
      result.header("User-Agent", Version.userAgent());
    }

    return result.build();
  }

下面分析下cache是否可用的判斷邏輯,也就是下面這行程式碼的執行邏輯。
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();

public final class CacheStrategy {
    // 構造方法,nowMillis為傳入的系統此刻時間
    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        // 取出response中的headers
        Headers headers = cacheResponse.headers();
        // 遍歷所有headers,解析出與cache過期有關的headers,並給相應成員變數賦值
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            // Date header處理
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            // Expires header處理
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            // Last-Modified header處理
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            // ETag header處理
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            // Age header處理
            ageSeconds = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

    public CacheStrategy get() {
      // CacheStrategy生成的主要方法
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // 網路被使用者禁止使用,並且cache不可用,此時networkRequest和cacheResponse都為null
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

    private CacheStrategy getCandidate() {
      // 沒有此request的cache response
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 對於HTTPS,必須有handshake欄位,否則認為此cache不可用
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // 此response不能使用cache,比如金融類資料,一般追求實時性,不適合使用cache
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      // 使用cache前需要先驗證一下儲存的response,或者request中有條件GET的headers
      // noCache()方法命名不好,有歧義。它不是表示不能使用cache或者沒有cache,而是表示使用前要先驗證。
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      // 計算cache是否過期
      long ageMillis = cacheResponseAge(); // 目前response生成時的絕對時間
      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());
      }

      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\"");
        }
        // cache可用時,將networkRequest賦值為null,可以看出OKHttp是優先使用cache的
        return new CacheStrategy(null, builder.build());
      }

      // 條件GET的處理。
      // 條件GET一般是cache過期了,需要傳送驗證request給server,以判斷cache response是否修改了。如果沒有修改,還是可以接著使用cache的。
      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);
    }
}

writeRequestHeaders()向HttpConnection的buffer中以UTF-8的編碼格式寫入start line和headers,合適的時機會發送到socket中傳輸出去

public final class HttpTransport implements Transport {
  // 這個名字起的不好,這個方法不僅寫入了headers,還寫入了start line
  public void writeRequestHeaders(Request request) throws IOException {
    // 傳送request之前必須立刻呼叫,它記錄了傳送request的系統時間
    httpEngine.writingRequestHeaders();
    // 生成start line,後面有詳細分析
    String requestLine = RequestLine.get(
        request, httpEngine.getConnection().getRoute().getProxy().type());
    // 將start line和headers寫入到buffer中,UTF-8格式,合適的時機再將buffer中資料通過socket傳輸出去
    httpConnection.writeRequest(request.headers(), requestLine);
  }
}

public final class RequestLine {
  // 生成request的start line,Http協議中它的格式為 method url version
  static String get(Request request, Proxy.Type proxyType) {
    StringBuilder result = new StringBuilder();
    // 寫入method
    result.append(request.method());
    result.append(' ');

    // 寫入url
    if (includeAuthorityInRequestLine(request, proxyType)) {
      result.append(request.httpUrl());
    } else {
      result.append(requestPath(request.httpUrl()));
    }

    // 寫入version,可以看到OKHttp支援的是HTTP/1.1版本
    result.append(" HTTP/1.1");
    return result.toString();
  }
}

3 readResponse()原始碼分析

public void readResponse() throws IOException {
    if (userResponse != null) {
      // response已經有了,這可能是利用cache生成的response或其他情況,
      // 此時我們就不用去接收server端的response了,其實一般此時也沒有server端的response讓我們去接收,哈哈~
      return; 
    }
    if (networkRequest == null && cacheResponse == null) {
      throw new IllegalStateException("call sendRequest() first!");
    }
    if (networkRequest == null) {
      return; // No network response to read.
    }

    Response networkResponse;

    if (forWebSocket) {
      // 先將start line和header寫入socket中
      transport.writeRequestHeaders(networkRequest);
      // 傳送request,並讀取response,後面詳細分析
      networkResponse = readNetworkResponse();

    } else if (!callerWritesRequestBody) {
      // 先執行攔截器,再寫入request到HttpConnection的buffer中,最後傳送buffer,並讀取response
      // 和上面情況比較像,這裡就不展開分析了
      networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);

    } else {
      // 將request body的buffer發出去,這樣requestBodyOut中就有了body
      if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) {
        bufferedRequestBody.emit();
      }

      // 處理request headers,並將start line和header寫入socket中
      if (sentRequestMillis == -1) {
        if (OkHeaders.contentLength(networkRequest) == -1
            && requestBodyOut instanceof RetryableSink) {
          // 如果之前content-length值不清楚,此時在body已經ready的情況下,可以計算出content-length,並將它新增到header中
          long contentLength = ((RetryableSink) requestBodyOut).contentLength();
          networkRequest = networkRequest.newBuilder()
              .header("Content-Length", Long.toString(contentLength))
              .build();
        }
        // 將start line和header寫入socket中
        transport.writeRequestHeaders(networkRequest);
      }

      // 將body寫入socket中
      if (requestBodyOut != null) {
        if (bufferedRequestBody != null) {
          // This also closes the wrapped requestBodyOut.
          bufferedRequestBody.close();
        } else {
          requestBodyOut.close();
        }
        if (requestBodyOut instanceof RetryableSink) {
          // body 寫入socket中
          transport.writeRequestBody((RetryableSink) requestBodyOut);
        }
      }

      // 傳送request,並讀取response,後面會詳細分析
      networkResponse = readNetworkResponse();
    }

    // 開始處理獲取到的response
    // 讀取並處理response的headers
    receiveHeaders(networkResponse.headers());

    // cache response存在的情況下,應該是cache過期了,做了一次條件GET來驗證cache的內容是否有變更。
    // 根據Http協議,如果cache未變,回覆304,not modified。且response中不會包含body,
    // 如果cache改變,回覆200, OK。response中包含body
    if (cacheResponse != null) {
      if (validate(cacheResponse, networkResponse)) {
        // 再驗證通過,cache內容未變,使用cache構造response
        userResponse = cacheResponse.newBuilder()
            .request(userRequest)
            .priorResponse(stripBody(priorResponse))
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
        releaseConnection();

        // 更新cache
        InternalCache responseCache = Internal.instance.internalCache(client);
        responseCache.trackConditionalCacheHit();
        responseCache.update(cacheResponse, stripBody(userResponse));
        userResponse = unzip(userResponse);
        return;
      } else {
        // cache未命中,response中會包含我們想要的body的。關閉cache body流
        closeQuietly(cacheResponse.body());
      }
    }

    // cache未命中,利用server返回的response string構造client使用的Response物件
    // 此時會將response快取起來,以便下次使用
    userResponse = networkResponse.newBuilder()
        .request(userRequest)
        .priorResponse(stripBody(priorResponse))
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (hasBody(userResponse)) {
      maybeCache();
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }

下面詳細分析readNetworkResponse(),它會通過socket流讀取response string的start line,headers和body。

  private Response readNetworkResponse() throws IOException {
    // 將HttpTransport中的buffer flush出去
    transport.finishRequest();

    // 讀取server的response string,並構造出Response物件
    Response networkResponse = transport.readResponseHeaders()
        .request(networkRequest)
        .handshake(connection.getHandshake())
        .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
        .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
        .build();

    if (!forWebSocket) {
      networkResponse = networkResponse.newBuilder()
          .body(transport.openResponseBody(networkResponse))
          .build();
    }

    return networkResponse;
  }

public final class HttpTransport implements Transport {

  @Override public Response.Builder readResponseHeaders() throws IOException {
    return httpConnection.readResponse();
  }
}

public final class HttpConnection {

  public Response.Builder readResponse() throws IOException {
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }

    try {
      while (true) {
        // 解析start line,response的start line格式為 protocol,code, message
        StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
        // 將解析出的protocol, code, message分別放入構造的Response物件中
        Response.Builder responseBuilder = new Response.Builder()
            .protocol(statusLine.protocol)
            .code(statusLine.code)
            .message(statusLine.message);

        // 解析response string的headers
        Headers.Builder headersBuilder = new Headers.Builder();
        // 一行行讀取headers, 直到遇到空行結束
        readHeaders(headersBuilder);
        headersBuilder.add(OkHeaders.SELECTED_PROTOCOL, statusLine.protocol.toString());
        // 將headers新增到Response物件中
        responseBuilder.headers(headersBuilder.build());

        // 如果返回code不是100, continue,則可以直接將Response物件返回
        // 對於100,continue,server還會繼續返回response string,我們需要在while迴圈中繼續接收並解析
        if (statusLine.code != HTTP_CONTINUE) {
          state = STATE_OPEN_RESPONSE_BODY;
          return responseBuilder;
        }
      }
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + connection
          + " (recycle count=" + Internal.instance.recycleCount(connection) + ")");
      exception.initCause(e);
      throw exception;
    }
  }
}

4 followUpRequest()原始碼分析

client傳送一個request之後,server可能回覆一個重定向的response,並在這個response中告知client需要重新訪問的server的IP。此時,client需要重新向新的server傳送request,並等待新server的回覆。所以我們需要單獨判斷重定向response,併發送多次request。有了OKHttp,這一切你都不用管,它會自動幫你完成所有這一切。OKHttp中followUpRequest()方法就是完成這個功能的。是不是瞬間感覺OKHttp強大到不要不要的啊~。這個方法流程比較簡單,就不給出流程圖了。親們如果對Http協議不熟悉,可以先看我的這篇文章Http協議簡介

  public Request followUpRequest() throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Proxy selectedProxy = getRoute() != null
        ? getRoute().getProxy()
        : client.getProxy();
    int responseCode = userResponse.code();

    // 利用responseCode來分析是否需要自動傳送後續request
    switch (responseCode) {
      // 未認證使用者,不能訪問server或代理,故需要傳送認證的request
      case HTTP_PROXY_AUTH:
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }

      case HTTP_UNAUTHORIZED:
        return OkHeaders.processAuthHeader(client.getAuthenticator(), userResponse, selectedProxy);

      // 永久重定向,暫時重定向,永久移動了等和重定向相關的response,response code中以3打頭的都是
      // 它們需要重新發送request給新的server,新sever的ip在response中會給出
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        if (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) {
            return null;
        }
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.getFollowRedirects()) return null;

        // 新的server的IP地址在response的Location header中給出
        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userRequest.httpUrl().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userRequest.httpUrl().scheme());
        if (!sameScheme && !client.getFollowSslRedirects()) return null;

        // Redirects don't include a request body.
        Request.Builder requestBuilder = userRequest.newBuilder();
        if (HttpMethod.permitsRequestBody(userRequest.method())) {
          requestBuilder.method("GET", null);
          requestBuilder.removeHeader("Transfer-Encoding");
          requestBuilder.removeHeader("Content-Length");
          requestBuilder.removeHeader("Content-Type");
        }

        // 刪掉使用者認證資訊
        if (!sameConnection(url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      default:
        return null;
    }
  }

5 總結

OKHttp底層原始碼還是相當複雜的,畢竟它的功能如此之強大嘛。OKHttp預設採用了Keep-Alive持久連線技術,可支援gzip編碼的response。在cache的處理上,如果cache可用,則直接使用cache,否則使用網路資料。OKHttp會做cache過期的判斷和過期後的再驗證。有了OKHttp,這一切你都不用管,它幫你cover掉了!

當需要做使用者驗證和重定向時,我們一般需要傳送認證request,或向新server傳送request,也就是要重新再生成新request併發送出去。有了OKHttp,這一切你都不用管,它又幫你cover掉了!

OKHttp功能實在是強大到爆表,掌握好了它的實現原理和底層流程之後,你就可以在專案中游刃有餘的放心使用它了!另外,小編可是花了整個週末才完成了這幾篇文章,走過路過的朋友幫忙寫寫評論吧,謝謝!