OKHttp 官方文件【二】

xiaxveliang 發表於 2020-08-01

OkHttp 是這幾年比較流行的 Http 客戶端實現方案,其支援HTTP/2、支援同一Host 連線池複用、支援Http快取、支援自動重定向 等等,有太多的優點。
一直想找時間瞭解一下 OkHttp 的實現原理 和 具體原始碼實現,不過還是推薦在使用 和 瞭解其原理之前,先通讀一遍 OkHttp 的官方文件,由於官方文件為英文,我在通讀的時候,順便翻譯了一下,如翻譯有誤,請幫忙指正

OKHttp 官方文件【一】

OKHttp 官方文件【二】

OkHttp官方API地址:
https://square.github.io/okhttp/

六、HTTPS

OkHttp 試圖平衡以下兩個矛盾的問題:

  • 連線到儘可能多的主機:這包括執行最新版本的boringssl的高階主機,以及執行舊版本OpenSSL的較過時的主機;
  • 連線的安全性:這包括使用證照對遠端web伺服器進行驗證,以及使用強密碼交換隱私資料;

當與HTTPS伺服器進行協商握手時,OkHttp 需要知道使用的哪一個 TLS 版本 和 加密套件。一個客戶端想要最大程度的連線,需要相容比較早的TLS版本 和 對應的較弱的密碼套件;一個客戶端想要最大程度的提高安全性,需要使用最新的TLS版本,並且只用安全級別最高的密碼套件;

特定的安全性與連線性策略由ConnectionSpec實現。OkHttp 包括四個內建的連線策略:

  • RESTRICTED_TLS 是一種安全的配置,旨在滿足更嚴格的安全性要求;
  • MODERN_TLS 是一個連線到當代流行HTTPS伺服器的安全配置;
  • COMPATIBLE_TLS 是一種安全配置,可連線到安全的HTTPS伺服器,但不相容當前流行的HTTPS伺服器版本。
  • CLEARTEXT 是一種明文網路請求,不安全的網路配置,用於Http;

以上策略鬆散地遵循 Google Cloud Policies,OkHttp遵循以下策略:

預設情況下,OkHttp 嘗試建立一個MODERN_TLS 策略的連線, 但是,如果 MODERN_TLS 策略失敗,則可以通過 connectionSpecs 配置回退到 COMPATIBLE_TLS 連線。

 OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
    .build();

支援的TLS版本加密套件會隨著 OkHttp 每一個release版本的釋出而有所改變。例如,OkHttp 2.2版本中為應對 POODLE 攻擊,我們停止了SSL 3.0的支援;OkHttp 2.3 版本中,我們停止了對 RC4 的支援。與你PC上安裝的瀏覽器軟體一樣,始終保持使用OkHttp的最新版本,是保證安全的最佳途徑。

你可以自定義 TLS版本加密套件來構建自己的連線策略。 例如,此配置僅限於三個備受推崇的密碼套件。 缺點是執行版本需要為 Android 5.0+ 以及類似策略的 webserver。

ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_2)
    .cipherSuites(
          CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
          CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
    .build();

OkHttpClient client = new OkHttpClient.Builder()
    .connectionSpecs(Collections.singletonList(spec))
    .build();

6.1、Debugging TLS Handshake Failures

TLS握手 要求客戶端和伺服器共享一個通用的TLS版本和密碼套件,這取決於JVM版本、 Android版本、OkHttp版本以及webserver的配置。 如果沒有通用的密碼套件和TLS版本,您的呼叫將失敗,錯誤如下所示:

Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7f2719a89e80:
    Failure in SSL library, usually a protocol error
        error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake 
        failure (external/openssl/ssl/s23_clnt.c:770 0x7f2728a53ea0:0x00000000)
    at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)

您可以使用Qualys SSL Labs檢查Web伺服器的配置,OkHttp的TLS配置歷史記錄在 tls_configuration_history.md

應用程式預期安裝在較早的Android裝置上,需要考慮到相容 Google Play Services’ ProviderInstaller。 這將提高使用者的安全性並增強與webservers的連線性。

6.2、Certificate Pinning

預設情況下,OkHttp信任您的手機內建的所有TSL證照。此策略可最大程度地提高連線性,但會受到諸如 2011 DigiNotar 攻擊等證照頒發機構的攻擊。 這種策略假定您的HTTPS伺服器證照預設是由證照頒發機構簽名的。

  private final OkHttpClient client = new OkHttpClient.Builder()
      .certificatePinner(
          new CertificatePinner.Builder()
              .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
              .build())
      .build();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/robots.txt")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      for (Certificate certificate : response.handshake().peerCertificates()) {
        System.out.println(CertificatePinner.pin(certificate));
      }
    }
  }

6.3、Customizing Trusted Certificates

以下完整的示例程式碼展示瞭如何用您自己的證照集替換主機平臺的證照頒發機構。 如上所述,如果沒有伺服器的TLS管理員的許可,請不要使用自定義證照!

  private final OkHttpClient client;

  public CustomTrust() {
    X509TrustManager trustManager;
    SSLSocketFactory sslSocketFactory;
    try {
      trustManager = trustManagerForCertificates(trustedCertificatesInputStream());
      SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(null, new TrustManager[] { trustManager }, null);
      sslSocketFactory = sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
      throw new RuntimeException(e);
    }

    client = new OkHttpClient.Builder()
        .sslSocketFactory(sslSocketFactory, trustManager)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    System.out.println(response.body().string());
  }

  private InputStream trustedCertificatesInputStream() {
    ... // Full source omitted. See sample.
  }

  public SSLContext sslContextForTrustedCertificates(InputStream in) {
    ... // Full source omitted. See sample.
  }

7、Interceptors

攔截器可以監聽、重寫、重試 網路請求,攔截器的作用非常強大。以下是一個簡單的攔截器,日誌列印網路請求request資料response資料

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));

    return response;
  }
}

chain.proceed(request) 呼叫是每個攔截器的關鍵部分,這個看起來很簡單的方法是所有HTTP工作發生的地方,它生成一個響應來滿足請求。如果 chain.proceed(request) 被多次呼叫,之前的 response body 必須關閉。

攔截器可以組成執行鏈,假設你同時擁有一個壓縮攔截器一個校驗攔截器,你需要決定資料是被壓縮然後校驗,還是校驗然後壓縮。OkHttp 將攔截器組成一個列表按順序執行。

Interceptors

7.1、Application Interceptors

攔截器分為應用程式網路攔截器,我們使用 LoggingInterceptor 來展示差異。
利用 OkHttpClient.BuilderaddInterceptor()來註冊應用攔截器:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

請求地址由http://www.publicobject.com/helloworld.txt重定向到https://publicobject.com/helloworld.txt,OkHttp 自動執行該重定向。應用攔截器被執行一次,response 資料由 chain.proceed()返回,返回的response為重定向後的 response 資料。

INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example

INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

我們可以看到URL被重定向為不同URL的表現為,API response.request().url()不同於request.url(),兩條不同的日誌,對應兩條不同的url。

7.2、Network Interceptors

註冊一個網路攔截器與註冊應用攔截器非常相似,呼叫addNetworkInterceptor()而不是addInterceptor():

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

Request request = new Request.Builder()
    .url("http://www.publicobject.com/helloworld.txt")
    .header("User-Agent", "OkHttp Example")
    .build();

Response response = client.newCall(request).execute();
response.body().close();

當我們執行以上程式碼,攔截器會被執行兩次,一次是初始請求 http://www.publicobject.com/helloworld.txt,一次重定向到 https://publicobject.com/helloworld.txt

INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt

INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip

INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive

網路請求中還包含其他引數,例如 Accept-Encoding:gzipheader資料的新增,以支援 response 請求資料的壓縮。網路攔截器 擁有一個非空的連線,可以用於查詢IP地址 與 查詢伺服器的TLS配置 (The network interceptor’s Chain has a non-null Connection that can be used to interrogate the IP address and TLS configuration that were used to connect to the webserver.)。

7.3、Choosing between application and network interceptors

每個攔截器都有各自的優點:

Application interceptors

  • 無需關注類似重定向重試之類的中間響應;
  • 只是呼叫一次,如果HTTP響應是從快取中獲取的(Are always invoked once, even if the HTTP response is served from the cache.)
  • 關注應用程式的最初意圖,不要關心一些注入Header,類似If-None-Match
  • Permitted to short-circuit and not call Chain.proceed() (不知道該怎麼翻譯,理解的小夥伴請留言).
  • 允許重試,並多次呼叫Chain.proceed().
  • 可以呼叫withConnectTimeout、withReadTimeout、withWriteTimeout表明請求超時;

Network Interceptors

  • 能夠操作中間響應,如重定向和重試;
  • Not invoked for cached responses that short-circuit the network(不知道該怎麼翻譯,理解的小夥伴請留言).
  • 檢測網路傳輸的資料;
  • 對於攜帶網路請求的連線,是可通過的;

7.4、Rewriting Requests

攔截器可以新增、移除、替換request headers,攔截器還可以轉換 request body資料。例如:如果webserver伺服器支援 request body資料壓縮,攔截器可以新增壓縮相關欄位。

/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

7.5、Rewriting Responses

對比以上的Rewriting Requests,攔截器可重寫response headers和轉換response body。通常來說,Rewriting Responses相比Rewriting Requests來說是比較危險的,因為這可能違反webserver的預期。

如果你遇到某種棘手的情況下並準備好解決這個問題,則重寫response headers是解決問題的有效辦法。 例如,您可以修復伺服器的錯誤配置Cache-Control響應Header,以更好實現的響應資料快取:

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

通常這種方法是有效地,用來修復webserver的錯誤!

八、Recipes

我們編寫了一些示例程式碼,展示如何解決OkHttp的常見問題。 通讀它們以瞭解一切如何協同工作。 隨意剪下並貼上這些示例,這就是他們存在的目的。

8.1、Synchronous Get

Download a file, print its headers, and print its response body as a string.

下載一個檔案,列印它的header資料,並將 response body資料列印為一個字串。

對於小型檔案,用string() 方法展示response body資料簡單又方便,但是如果response body資料大於1M,避免使用string() 方法,這種方法會將響應資料讀進記憶體。大檔案的情況最好將response body作為資料流進行處理。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      Headers responseHeaders = response.headers();
      for (int i = 0; i < responseHeaders.size(); i++) {
        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
      }

      System.out.println(response.body().string());
    }
  }

8.2、Asynchronous Get

在工作執行緒中下載一個檔案,並在響應可讀時被回撥。回撥是在response headers準備好之後進行的,此時讀取response body可能仍會引起阻塞。OkHttp目前沒有提供非同步API來部分接收響應體。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        try (ResponseBody responseBody = response.body()) {
          if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

          Headers responseHeaders = response.headers();
          for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
          }

          System.out.println(responseBody.string());
        }
      }
    });
  }

8.3、Accessing Headers

通常HTTP headers 的工作方式類似於Map<String, String>,每一個Key對應一個Value。 但是headers允許存在多個Value,比如Multimap。 例如,HTTP response header中包含多個Vary是合法的, OkHttp的API試圖使相容這兩種情況。

當我們重寫request headers時,使用API header(name, value)去新增一個Header,如果對應的Header資料已經存在,則將移除原有的Header,新增新的Header;使用API addHeader(name, value)新增Header,則不用移除之前存在的Header。

當讀取response header時,使用header(name)返回從後向前第一個遇到的 name相同的Header的Value資料,通常只會遇到一個。如果 對應的value不存在, header(name)將返回空。讀取所有的Header資料,使用API headers(name)

使用API Headers讀取全部headers資料時,支援通過index進行讀取。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println("Server: " + response.header("Server"));
      System.out.println("Date: " + response.header("Date"));
      System.out.println("Vary: " + response.headers("Vary"));
    }
  }

8.4、Posting a String

使用HTTP POST向service傳送request body資料。舉例中向一個WebServer傳送了一個markdown檔案,WebServer收到markdown檔案後,會渲染成一個HTML。由於所有的request body都儲存到記憶體中,應避免使用以下API,Post超過1M的檔案。

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

8.5、Post Streaming

這裡,我們以資料流的形式Post資料,請求的正文是正在被編寫生成的內容。這個示例中資料流直接進入Okio緩衝buffer中。在你的實際使用中,可能更喜歡用 OutputStream,可以從BufferedSink.outputStream()獲取資料。

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8("Numbers\n");
        sink.writeUtf8("-------\n");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + " × " + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

8.6、Posting a File

使用一個檔案,作為request body是非常簡單的。

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File("README.md");

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

8.7、Posting form parameters

使用 FormBody.Builder去構建一個request body,其類似於 HTML <form> 標籤,KeyValue會被編碼為HTML相容的URL編碼。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
    Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

8.8、Posting a multipart request

MultipartBody.Builder 可以建立與 HTML forms相容的複雜request bodies。每一部分multipart request body它自己本身就是一個request body,可以定義自己的專屬headers。如果出現這種情況,這些headers資料會被描述為body的一部分,例如Content-DispositionContent-Length、Content-Type將會被自動新增到請求Header中。

  /**
   * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
   * these examples, please request your own client ID! https://api.imgur.com/oauth2
   */
  private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

    Request request = new Request.Builder()
        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
        .url("https://api.imgur.com/3/image")
        .post(requestBody)
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

8.9、Parse a JSON Response With Moshi

Moshi 是一個非常好用的API,幫助完成JSON字串資料和Java objects完成互相轉化。這裡我們使用Moshi去解析response返回的JSON資料。

  private final OkHttpClient client = new OkHttpClient();
  private final Moshi moshi = new Moshi.Builder().build();
  private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/gists/c2a7c39532239ff261be")
        .build();
    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      Gist gist = gistJsonAdapter.fromJson(response.body().source());

      for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
        System.out.println(entry.getKey());
        System.out.println(entry.getValue().content);
      }
    }
  }

  static class Gist {
    Map<String, GistFile> files;
  }

  static class GistFile {
    String content;
  }

8.10、Response Caching

為了快取responses資料,你需要建立一個儲存目錄,這個目錄你可以讀寫,並限制快取的大小。快取目錄應該是一個私有目錄,不可信的應用程式不能訪問該目錄。

多個快取同時訪問同一快取目錄是一個錯誤。 大多數應用程式應該只呼叫一次new OkHttpClient(),並使用其快取對其進行配置,並在各處使用同一例項。 否則,這兩個快取例項將相互讀寫,汙染快取快取,並可能導致程式崩潰。

響應快取使用HTTP headers進行快取配置。您可以新增請求Header,如Cache-Control: max-stale=3600,OkHttp的快取遵循該Header規則。 webserver利用Header,如Cache-Control: max-age=9600配置響應資料的過期時間。 有快取頭可用於強制快取響應,網路響應或使用條件GET驗證網路響應。OkHttp同樣包含一些API,可強制從快取獲取資料、強制從網路獲取資料或強制一個網路獲取的資料需要另一個Get請求的確認。

  private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    String response1Body;
    try (Response response1 = client.newCall(request).execute()) {
      if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

      response1Body = response1.body().string();
      System.out.println("Response 1 response:          " + response1);
      System.out.println("Response 1 cache response:    " + response1.cacheResponse());
      System.out.println("Response 1 network response:  " + response1.networkResponse());
    }

    String response2Body;
    try (Response response2 = client.newCall(request).execute()) {
      if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

      response2Body = response2.body().string();
      System.out.println("Response 2 response:          " + response2);
      System.out.println("Response 2 cache response:    " + response2.cacheResponse());
      System.out.println("Response 2 network response:  " + response2.networkResponse());
    }

    System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  }

為了防止使用快取資料,可使用APICacheControl.FORCE_NETWORK。若要阻止OkHttp使用網路資料,可使用CacheControl.FORCE_CACHE。 注意:如果你使用FORCE_CACHEAPI,但請求卻必須需要response資料,那麼OkHttp將返回 504 Unsatisfiable Request 錯誤資訊。

8.11、Canceling a Call

使用API Call.cancel()去停止馬上要執行的 call請求,此時如果某一個執行緒正在讀寫response資料,此時將會收到一個IOException異常。使用這個API去儲存你的請求,當該請求不在需要被執行時,如當你關閉一個APP時,無論是同步還是非同步的請求都需要被取消。

  private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
    try (Response response = call.execute()) {
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

8.12、Timeouts

當一個連線不可達到時,使用timeouts回撥錯誤資訊。網路不可達可能是連線問題、伺服器不可能、或者以上兩個都有問題,OkHttp在連線、讀、寫階段均支援超時回撥。

  private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    try (Response response = client.newCall(request).execute()) {
      System.out.println("Response completed: " + response);
    }
  }

8.13、Per-call Configuration

All the HTTP client configuration lives in OkHttpClient including proxy settings, timeouts, and caches. When you need to change the configuration of a single call, call OkHttpClient.newBuilder(). This returns a builder that shares the same connection pool, dispatcher, and configuration with the original client. In the example below, we make one request with a 500 ms timeout and another with a 3000 ms timeout.

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
        .build();

    // Copy to customize OkHttp for this request.
    OkHttpClient client1 = client.newBuilder()
        .readTimeout(500, TimeUnit.MILLISECONDS)
        .build();
    try (Response response = client1.newCall(request).execute()) {
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

    // Copy to customize OkHttp for this request.
    OkHttpClient client2 = client.newBuilder()
        .readTimeout(3000, TimeUnit.MILLISECONDS)
        .build();
    try (Response response = client2.newCall(request).execute()) {
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }

8.14、Handling authentication

OkHttp can automatically retry unauthenticated requests. When a response is 401 Not Authorized, an Authenticator is asked to supply credentials. Implementations should build a new request that includes the missing credentials. If no credentials are available, return null to skip the retry.

Use Response.challenges() to get the schemes and realms of any authentication challenges. When fulfilling a Basic challenge, use Credentials.basic(username, password) to encode the request header.

  private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            if (response.request().header("Authorization") != null) {
              return null; // Give up, we've already attempted to authenticate.
            }

            System.out.println("Authenticating for response: " + response);
            System.out.println("Challenges: " + response.challenges());
            String credential = Credentials.basic("jesse", "password1");
            return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
          }
        })
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

      System.out.println(response.body().string());
    }
  }

To avoid making many retries when authentication isn’t working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted:

  if (credential.equals(response.request().header("Authorization"))) {
    return null; // If we already failed with these credentials, don't retry.
   }

You may also skip the retry when you’ve hit an application-defined attempt limit:

  if (responseCount(response) >= 3) {
    return null; // If we've failed 3 times, give up.
  }

This above code relies on this responseCount() method:

  private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }

9、Supported Versions

Supported Versions

歡迎關注我的公眾號