您现在的位置是:首页 >技术教程 >OKHttp_官方文档[译文]网站首页技术教程

OKHttp_官方文档[译文]

FDoubleman 2024-09-05 00:01:04
简介OKHttp_官方文档[译文]
  • OKHttp功能类介绍

  • OKHttp网络请求流程分析

  • OKHttp连接池

  • OKHttp分发器

  • OKHttp拦截器

    • RetryAndFollowUpInterceptor
    • BridgeInterceptor
    • CacheInterceptor
    • ConnectInterceptor
    • CallServerInterceptor

总览

OkHttp

HTTP是现代应用程序网络的方式。这就是我们交换数据和媒体的方式。有效地执行HTTP可使您的内容加载更快并节省带宽。

OkHttp是默认情况下有效的HTTP客户端:

  • HTTP / 2支持允许对同一主机的所有请求共享一个套接字。
  • 连接池可减少请求延迟(如果HTTP / 2不可用)。
  • 透明的GZIP缩小了下载大小。
  • 响应缓存可以完全避免网络重复请求。

当网络出现问题时,OkHttp会坚持不懈:它将从常见的连接问题中静默恢复。如果您的服务具有多个IP地址,则在第一次连接失败时,OkHttp将尝试使用备用地址。这对于IPv4 + IPv6和冗余数据中心中托管的服务是必需的。 OkHttp支持现代TLS功能(TLS 1.3,ALPN,证书固定)。可以将其配置为回退以实现广泛的连接。

使用OkHttp很容易。它的请求/响应API具有流畅的构建器和不变性。它支持同步阻塞调用和带有回调的异步调用。

Get请求

该程序下载URL,并将其内容打印为字符串。

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}
Post请求

post提交数据到服务器的方式。

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}
要求

OkHttp可在Android 5.0+(API级别21+)和Java 8+上运行。
OkHttp依靠Okio获得高性能的I / O和Kotlin标准库。两者都是具有强大的向后兼容性的小型库。

我们强烈建议您保持OkHttp为最新。与自动更新Web浏览器一样,保持HTTPS客户端的最新状态是防范潜在安全问题的重要防御措施。我们跟踪动态TLS生态系统并调整OkHttp以改善连接性和安全性。

OkHttp使用您平台的内置TLS实现。在Java平台上,OkHttp还支持Conscrypt,它将BoringSSL与Java集成在一起。如果OkHttp是第一个安全提供程序,它将使用Conscrypt:

Security.insertProviderAt(Conscrypt.newProvider(), 1);

OkHttp 3.12.x分支支持Android 2.3+(API级别9+)和Java 7+。这些平台不支持TLS 1.2,因此不应使用。但是由于升级困难,我们将在2021年12月31日之前将重要补丁程序移植到3.12.x分支中。

发布

最新版本在Maven Central上可用。

implementation("com.squareup.okhttp3:okhttp:4.5.0")

Calls

HTTP客户端的工作是接受您的请求并产生响应。从理论上讲这很简单,但在实践中却很棘手

Requests

每个HTTP请求都包含一个URL,一个方法(如GET或POST)和标头列表。请求还可以包含body:特定内容类型的数据流。

Responses

响应使用代码(例如200表示成功或404表示找不到),标头和其自己的可选body来回答请求。

Rewriting Requests

当您向OkHttp提供HTTP请求时,就是在高层次上描述该请求:“使用这些标头向我获取此URL。”为了确保正确性和效率,OkHttp在传输请求之前会先对其进行重写。

OkHttp可以添加原始请求中不存在的标头,包括Content-Length,Transfer-Encoding,User-Agent,Host,Connection和Content-Type。它将添加用于透明响应压缩的Accept-Encoding头,除非该头已经存在。如果您有Cookie,OkHttp会在其中添加一个Cookie标头。

一些请求将具有缓存的响应。如果此缓存响应不新鲜,OkHttp可以执行条件GET来下载更新的响应,如果它比缓存的要新。这要求添​​加诸如If-Modified-Since和If-None-Match的标题。

Rewriting Responses

如果使用透明压缩,则OkHttp将删除相应的响应标头Content-Encoding和Content-Length,因为它们不适用于解压缩的响应正文。

如果有条件的GET成功,则按照规范的指示将来自网络和缓存的响应合并。

后续请求

当您请求的URL移动后,网络服务器将返回响应代码(例如302),以指示文档的新URL。 OkHttp将遵循重定向以检索最终响应。

如果响应发出授权挑战,OkHttp将要求Authenticator(如果已配置)满足挑战。如果身份验证器提供了凭据,则会使用该凭据重试请求。

重试请求

有时连接失败:池化连接已陈旧且已断开连接,或者无法访问网络服务器本身。如果可用,OkHttp将使用其他路由重试该请求。

Calls

通过重写,重定向,跟进和重试,您的简单请求可能会产生许多请求和响应。 OkHttp使用Call来建模通过许多中间请求和响应来满足您的请求的任务。通常数量不多!但是,很高兴知道,如果您的URL重定向或您故障转移到备用IP地址,您的代码将继续有效。

调用以两种方式之一执行:

  • 同步:您的线程阻塞,直到响应可读为止。
  • 异步:将请求放入任何线程中,并在响应可读时在另一个线程上被调用。

可以从任何线程取消呼叫。如果尚未完成,将导致呼叫失败!当取消调用时,编写请求正文或读取响应正文的代码将发生IOException。

调度

对于同步调用,您需要带上自己的线程并负责管理您发出的同时请求数。同时连接过多会浪费资源。太少会损害延迟。

对于异步调用,Dispatcher实施最大并发请求的策略。您可以设置每个Web服务器的最大值(默认为5)和整体(默认为64)。

Connections

尽管仅提供URL,但OkHttp使用以下三种类型计划其与Web服务器的连接:URL, Address, and Route.

URLs

URL(例如https://github.com/square/okhttp)是HTTP和Internet的基础。他们不仅是针对网络上所有内容的通用,分散式命名方案,还规定了如何访问网络资源。
网址是抽象的:

  • 他们指定Call可以是纯文本(http)或加密(https),但不应该使用哪种加密算法。他们也没有指定如何验证对等方的证书(HostnameVerifier)或可以信任的证书(SSLSocketFactory)。

  • 他们没有指定是否应使用特定的代理服务器或如何使用该代理服务器进行身份验证。

它们也很具体:每个网址都标识一个特定的路径( 如/square/okhttp )和查询(如?q=sharks&lang=en)。每个Web服务器托管许多URL。

Addresses

地址指定一个Web服务器(例如github.com)和连接到该服务器所需的所有静态配置:端口号,HTTPS设置和首选网络协议(例如HTTP / 2或SPDY)。

共享相同地址的URL也可能共享相同的基础TCP套接字连接。共享连接具有显着的性能优势:更低的延迟,更高的吞吐量(由于TCP启动缓慢)和节省的电池。 OkHttp使用一个ConnectionPool,它可以自动重用HTTP / 1.x连接并复用HTTP / 2和SPDY连接。

在OkHttp中,地址的某些字段来自URL(方案,主机名,端口),其余部分来自OkHttpClient。

Routes

路由提供实际连接到Web服务器所需的动态信息。这是要尝试的特定IP地址(由DNS查询发现),要使用的确切代理服务器(如果正在使用ProxySelector)以及要协商的TLS版本(对于HTTPS连接)。

一个地址可能有很多路由。例如,托管在多个数据中心中的Web服务器可能会在其DNS响应中产生多个IP地址。

Connections

当您使用OkHttp请求URL时,它的作用是:

  1. 它使用URL并配置了OkHttpClient来创建地址。此地址指定了我们如何连接到网络服务器。
  2. 它尝试从连接池中检索具有该地址的连接。
  3. 如果未在池中找到连接,则选择要尝试的路由。这通常意味着发出DNS请求以获取服务器的IP地址。然后根据需要选择TLS版本和代理服务器。
  4. 如果是新路由,则可以通过建立直接套接字连接,TLS隧道(用于HTTP代理上的HTTPS)或直接TLS连接来进行连接。它根据需要执行TLS握手。
  5. 它发送HTTP请求并读取响应。

如果连接出现问题,OkHttp将选择其他路由,然后重试。这样,当服务器地址的一部分无法访问时,OkHttp可以恢复。如果共用连接失效或尝试的TLS版本不受支持,此功能也很有用。 收到响应后,连接将返回到池中,以便可以将其重新用于以后的请求。闲置一段时间后,连接将从池中退出。

Events

通过事件,您可以捕获应用程序的HTTP调用中的指标。使用事件来监视:

  • 应用程序发出的HTTP调用的大小和频率。如果您Call请求次数过多,或者您Call的请求太大,那么您应该知道这一点!
  • 这些呼叫在基础网络上的性能。如果网络的性能不足,则需要改善网络或减少使用。
EventListener

子类EventListener和重写您感兴趣的事件的方法。在没有重定向或重试的成功HTTP调用中,此流程描述了事件的顺序。

image

这是一个示例事件侦听器,可显示带有时间戳的每个事件。

class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}

我们添加几个Call请求:

Request request = new Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build();

System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

侦听器将打印相应的事件:

REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd

请注意,第二个呼叫如何不触发连接事件。它重用了从第一个请求开始的连接,从而显着提高了性能。

EventListener.Factory

在前面的示例中,我们使用了一个名为callStartNanos的字段来跟踪每个事件的经过时间。这很方便,但是如果同时执行多个调用,它将不起作用。为了适应这种情况,请使用Factory为每个Call创建一个新的EventListener实例。这允许每个侦听器保持特定于呼叫的状态。

该sample factory 为每个呼叫创建唯一的ID,并使用该ID区分日志消息中的呼叫。

class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

  private void printEvent(String name) {
    long elapsedNanos = System.nanoTime() - callStartNanos;
    System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  ...
}

我们可以使用此侦听器来竞争一对并发的HTTP请求:

Request washingtonPostRequest = new Request.Builder()
    .url("https://www.washingtonpost.com/")
    .build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
  ...
});

Request newYorkTimesRequest = new Request.Builder()
    .url("https://www.nytimes.com/")
    .build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
  ...
});

在家用WiFi上进行的比赛显示,Times(0002)比Post(0001)稍早完成:

0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd

EventListener.Factory还可以将指标限制为调用的子集。这是随机抽取10%的指标:

class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };

  ...
}
Events with Failures

当操作失败时,将调用失败方法。这是connectFailed()用于在建立与服务器的连接时失败,而在HTTP调用永久失败时则用于callFailed()。发生故障时,开始事件可能没有相应的结束事件。

image

重试和后续活动

OkHttp具有弹性,可以自动从某些连接故障中恢复。在这种情况下,connectFailed()事件不是终端事件,也不是callFailed()之后的事件。尝试重试时,事件侦听器将收到多个相同类型的事件。

单个HTTP调用可能需要发出后续请求以处理身份验证质询,重定向和HTTP层超时。在这种情况下,可能会尝试多个连接,请求和响应。跟踪是单个调用可能触发相同类型的多个事件的另一个原因。

image

可用性

在OkHttp 3.11中,事件可以作为公共API使用。将来的版本可能会引入新的事件类型;您将需要覆盖相应的方法来处理它们。

HTTPS

OkHttp试图平衡两个相互冲突的问题:

  • 连接到尽可能多的主机。其中包括运行最新版本的boringssl的高级主机,以及运行较旧版本的OpenSSL的过时主机。
  • 连接的安全性。这包括使用证书验证远程Web服务器以及使用强密码交换的数据的私密性。

在协商与HTTPS服务器的连接时,OkHttp需要知道要提供哪些TLS版本和密码套件。想要最大程度地提高连接性的客户端将包括过时的TLS版本和弱设计密码套件。想要最大化安全性的严格客户端将仅限于最新的TLS版本和最强的密码套件。

特定的安全性与连接性决定由ConnectionSpec实施。 OkHttp包含四个内置的连接规范:

  • RESTRICTED_TLS是一种安全配置,旨在满足更严格的合规性要求。
  • MODERN_TLS是连接到现代HTTPS服务器的安全配置。
  • COMPATIBLE_TLS是一种安全配置,可连接到安全的HTTPS服务器,但不能连接到当前的HTTPS服务器。
  • CLEARTEXT是用于http:// URL的不安全配置。

这些宽松地遵循了Google云政策中设置的模型。我们跟踪对此政策的更改。 默认情况下,OkHttp将尝试建立MODERN_TLS连接。但是,如果现代配置失败,则可以通过配置客户端连接规范来允许回退到COMPATIBLE_TLS连接。

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

每个规范中的TLS版本和密码套件可随每个发行版而更改。例如,在OkHttp 2.2中,为了响应POODLE攻击,我们放弃了对SSL 3.0的支持。在OkHttp 2.3中,我们放弃了对RC4的支持。与桌面Web浏览器一样,保持OkHttp的最新状态是确保安全的最佳方法。

您可以使用一组自定义的TLS版本和密码套件来构建自己的连接规范。例如,此配置仅限于三个备受推崇的密码套件。它的缺点是它需要Android 5.0+和类似的当前Web服务器。

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();
证书固定

默认情况下,OkHttp信任主机平台的证书颁发机构。此策略可最大程度地提高连接性,但会受到诸如2011 DigiNotar攻击等证书颁发机构的攻击。它还假定您的HTTPS服务器的证书是由证书颁发机构签名的。

使用CertificatePinner限制受信任的证书和证书颁发机构。证书固定可以提高安全性,但会限制您的服务器团队更新其TLS证书的能力。没有服务器的TLS管理员的祝福,请不要使用证书固定!

  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));
      }
    }
  }
自定义可信证书

完整的代码示例显示了如何用您自己的证书集替换主机平台的证书颁发机构。如上所述,请勿使用未经服务器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.
  }

Interceptors

拦截器是一种强大的机制,可以监视,重写和重试Call。这是一个简单的拦截器,用于记录传出请求和传入响应。

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),则必须关闭以前的响应主体。

拦截器可以链接。假设您同时具有压缩拦截器和校验和拦截器:您需要确定数据是先压缩后再校验和,还是先校验后再压缩。 OkHttp使用列表来跟踪拦截器,并按顺序调用拦截器。

image

Application Interceptors

拦截器已注册为应用程序或网络拦截器。我们将使用上面定义的LoggingInterceptor来显示差异。
通过在OkHttpClient.Builder上调用addInterceptor()注册一个应用程序拦截器:

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();

URL http://www.publicobject.com/helloworld.txt重定向到https://publicobject.com/helloworld.txt,OkHttp自动遵循此重定向。我们的应用程序拦截器被调用一次,从chain.proceed()返回的响应具有重定向的响应:

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

我们可以看到我们被重定向了,因为response.request().url()与request.url()不同。这两个日志语句记录两个不同的URL。

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

网络请求还包含更多数据,例如OkHttp添加的Accept-Encoding:gzip标头,以宣传对响应压缩的支持。网络拦截器的链具有非空的连接,可用于询问用于连接到网络服务器的IP地址和TLS配置。

在应用程序拦截器和网络拦截器之间进行选择

Application interceptors

  • 无需关心中间响应,例如重定向和重试。
  • 即使从缓存提供HTTP响应,也总是被调用一次。
  • 遵守应用程序的原始意图。不用担心OkHttp注入的标头,例如If-None-Match。
  • 允许短路并且不调用Chain.proceed()。
  • 允许重试并多次调用Chain.proceed()。

Network Interceptors

  • 能够对重定向和重试之类的中间响应进行操作。
  • 不会为使网络短路的缓存响应调用。
  • 观察数据,就像通过网络传输数据一样。
  • 访问承载请求的连接。
重写请求

拦截器可以添加,删除或替换请求标头。他们还可以转换那些具有一个请求的主体。例如,如果您要连接到已知支持请求的网络服务器,则可以使用应用程序拦截器添加请求正文压缩。

/** 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();
      }
    };
  }
}
重写响应

相应地,拦截器可以重写响应头并转换响应主体。这通常比重写请求标头更危险,因为它可能违反网络服务器的期望! 如果您处在棘手的情况下并准备好处理后果,则重写响应标头是解决问题的有效方法。例如,您可以修复服务器的错误配置的Cache-Control响应标头,以实现更好的响应缓存:

/** 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();
  }
};

通常,当对Web服务器上的相应修复程序进行补充时,此方法最有效!

样例

我们编写了一些样例,展示了如何解决OkHttp的常见问题。通读它们以了解一切如何协同工作。随意剪切并粘贴这些示例;这就是他们的目的。

同步获取

下载文件,打印其标题,并将其响应主体打印为字符串。 响应主体上的string()方法对于小型文档而言既方便又高效。但是,如果响应主体很大(大于1 MiB),请避免使用string(),因为它将把整个文档加载到内存中。在这种情况下,最好将主体作为流处理。

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());
    }
  }
异步获取

在辅助线程上下载文件,并在响应可读时调用该文件。响应头准备好后进行回调。读取响应正文可能仍然会阻塞。 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());
        }
      }
    });
  }
Headers

通常,HTTP标头的工作方式类似于Map <String,String>:每个字段都有一个值或没有值。但是某些标头允许使用多个值,例如Guava的Multimap。例如,HTTP响应提供多个Vary标头是合法且普遍的。 OkHttp的API试图使两种情况都变得舒适。 编写请求标头时,请使用标头(名称,值)将名称的唯一出现设置为值。

如果存在现有值,将在添加新值之前将其删除。使用addHeader(name,value)添加标题而不删除已经存在的标题。 当读取响应头时,使用header(name)返回最后一次出现的命名值。通常这也是唯一的情况!如果不存在任何值,则header(name)将返回null。要将所有字段的值读取为列表,请使用headers(name)。
要访问所有Header,请使用Headers类,该类支持按索引访问。

  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"));
    }
  }
Post String

使用HTTP POST将请求正文发送到服务。本示例将markdown文档发布到将markdown呈现为HTML的Web服务。因为整个请求主体同时在内存中,所以请避免使用此API发布大型(大于1 MiB)文档。

  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
"
        + "--------
"
        + "
"
        + " * _1.0_ May 6, 2013
"
        + " * _1.1_ June 15, 2013
"
        + " * _1.2_ August 11, 2013
";

    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());
    }
  }
Post Streaming

在这里,我们将请求正文作为流发布。该请求正文的内容是在编写时生成的。此示例直接流入Okio缓冲接收器。您的程序可能更喜欢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
");
        sink.writeUtf8("-------
");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s
", 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());
    }
  }
提交一个文件

将文件用作请求正文很容易.

  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());
    }
  }
提交表单

使用FormBody.Builder可以构建类似于HTML <form>标记的请求正文。名称和值将使用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());
    }
  }
提交多部分请求

MultipartBody.Builder可以构建与HTML文件上传表单兼容的复杂请求主体。多部分请求正文的每个部分本身就是一个请求正文,并且可以定义自己的标头。如果存在,这些标题应描述零件主体,例如其内容处置。 Content-Length和Content-Type标头会自动添加。

  /**
   * 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());
    }
  }
使用Moshi解析JSON响应

Moshi是用于在JSON和Java对象之间进行转换的便捷API。在这里,我们使用它来解码来自GitHub API的JSON响应。

请注意,ResponseBody.charStream()使用Content-Type响应标头来选择在解码响应正文时要使用的字符集。如果未指定字符集,则默认为UTF-8。

  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;
  }
响应缓存

要缓存响应,您需要一个可读写的缓存目录,以及缓存大小的限制。缓存目录应该是私有的,不受信任的应用程序应该不能读取其内容!

多个缓存同时访问同一缓存目录是错误的。大多数应用程序应该只调用一次新的OkHttpClient(),并使用其缓存对其进行配置,并在各处使用同一实例。否则,这两个缓存实例将相互踩踏,破坏响应缓存,并可能导致程序崩溃。

响应缓存将HTTP标头用于所有配置。您可以添加请求标头,例如Cache-Control:max-stale = 3600,OkHttp的缓存将接受它们。您的网络服务器使用自己的响应标头配置响应的缓存时间,例如Cache-Control:max-age = 9600。有缓存头可用于强制缓存响应,网络响应或使用条件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));
  }

为了防止响应使用缓存,请使用CacheControl.FORCE_NETWORK。若要阻止它使用网络,请使用CacheControl.FORCE_CACHE。警告:如果使用FORCE_CACHE并且响应需要网络,则OkHttp将返回504 Unssatiable Request请求响应。

取消Call

使用Call.cancel()立即停止正在进行的请求。如果线程当前正在编写请求或正在读取响应,则它将收到IOException。当不再需要通话时,使用此功能可以节省网络。例如,当您的用户离开应用程序时。同步和异步调用都可以取消。

  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);
    }
  }
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);
    }
  }
单独配置每一个Call

所有HTTP客户端配置都保存在OkHttpClient中,包括代理设置,超时和缓存。当您需要更改单个调用的配置时,请调用OkHttpClient.newBuilder()。这将返回一个与原始客户端共享相同连接池,调度程序和配置的构建器。在下面的示例中,我们以500毫秒的超时时间发出另一个请求,并以3000毫秒的超时时间发出另一个请求。

  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);
    }
  }
处理身份验证

OkHttp可以自动重试未经身份验证的请求。当响应为“ 401未授权”时,将要求身份验证器提供凭据。实现应建立一个新请求,其中包括缺少的凭据。如果没有可用的凭据,则返回null以跳过重试。 使用Response.challenges()获取任何身份验证质询的方案和领域。完成基本挑战时,请使用Credentials.basic(用户名,密码)对请求标头进行编码。

  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());
    }
  }

为了避免在身份验证不起作用时进行多次重试,您可以返回null放弃。例如,当您已经尝试使用这些确切的凭据时,您可能希望跳过重试:

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

当达到应用程序定义的尝试限制时,您也可以跳过重试:

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

上面的代码依赖于此responseCount()方法:

  private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。