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

欢迎关注我的公众号

posted @ 2020-08-01 13:38  bjxiaxueliang  阅读(1771)  评论(0编辑  收藏  举报