OKHttp 官方文檔【二】


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

歡迎關注我的公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM