Contents

OKHTTP 简介

1. 简介

在本教程中,我们将探讨发送不同类型的 HTTP 请求以及接收和解释 HTTP 响应的基础知识。然后我们将学习如何使用 OkHttp 配置客户端。

最后,我们将讨论使用自定义标头、超时、响应缓存等配置客户端的更高级用例。

2. OkHttp 概述

OkHttp 是适用于 Android 和 Java 应用程序的高效 HTTP 和 HTTP/2 客户端。

它具有高级功能,例如连接池(如果 HTTP/2 不可用)、透明 GZIP 压缩和响应缓存,可以完全避免网络重复请求。

它还能够从常见的连接问题中恢复;在连接失败时,如果服务有多个 IP 地址,它可以重试对备用地址的请求。

在高层次上,客户端是为阻塞同步调用和非阻塞异步调用而设计的。

OkHttp 支持 Android 2.3 及更高版本。对于 Java,最低要求是 1.7。

现在我们已经给出了一个简短的概述,让我们看一些使用示例。

3. Maven依赖

首先,我们将该库作为依赖项添加到pom.xml中:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.1</version>
</dependency>

要查看此库的最新依赖项,请查看Maven Central 上的页面。

4. 使用 OkHttp 同步 GET

要发送同步 GET 请求,我们需要基于URL构建一个Request对象并进行Call。执行后,我们将返回一个Response实例:

@Test
public void whenGetRequest_thenCorrect() throws IOException {
    Request request = new Request.Builder()
      .url(BASE_URL + "/date")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}

5. 使用 OkHttp 的异步 GET

要进行异步 GET,我们需要将Call加入队列。Callback允许我们在响应可读时读取它。这发生在响应标头准备好之后。

读取响应正文可能仍会阻塞。OkHttp 目前不提供任何异步 API 来接收部分响应正文:

@Test
public void whenAsynchronousGetRequest_thenCorrect() {
    Request request = new Request.Builder()
      .url(BASE_URL + "/date")
      .build();
    Call call = client.newCall(request);
    call.enqueue(new Callback() {
        public void onResponse(Call call, Response response) 
          throws IOException {
            // ...
        }
        
        public void onFailure(Call call, IOException e) {
            fail();
        }
    });
}

6.带有查询参数的GET

最后,要将查询参数添加到我们的 GET 请求中,我们可以利用HttpUrl.Builder

在我们构建 URL 之后,我们可以将它传递给我们的Request对象:

@Test
public void whenGetRequestWithQueryParameter_thenCorrect() 
  throws IOException {
    
    HttpUrl.Builder urlBuilder 
      = HttpUrl.parse(BASE_URL + "/ex/bars").newBuilder();
    urlBuilder.addQueryParameter("id", "1");
    String url = urlBuilder.build().toString();
    Request request = new Request.Builder()
      .url(url)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}

7. POST 请求

现在让我们看一个简单的 POST 请求,其中我们构建了一个RequestBody 来发送参数 “username” 和 “password”

@Test
public void whenSendPostRequest_thenCorrect() 
  throws IOException {
    RequestBody formBody = new FormBody.Builder()
      .add("username", "test")
      .add("password", "test")
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/users")
      .post(formBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    
    assertThat(response.code(), equalTo(200));
}

我们的文章,使用 OkHttp 发布请求 的快速指南,有更多使用 OkHttp 的 POST 请求示例。

8. 文件上传

8.1. 上传一个文件

在此示例中,我们将演示如何上传File。我们将使用MultipartBody.Builder上传“ *test.ext”*文件:

@Test
public void whenUploadFile_thenCorrect() throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("file", "file.txt",
        RequestBody.create(MediaType.parse("application/octet-stream"), 
          new File("src/test/resources/test.txt")))
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/upload")
      .post(requestBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}

8.2. 获取文件上传进度

然后我们将学习如何获取File上传的进度。我们将扩展RequestBody以获得对上传过程的可见性。 上传方法如下:

@Test
public void whenGetUploadFileProgress_thenCorrect() 
  throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("file", "file.txt",
        RequestBody.create(MediaType.parse("application/octet-stream"), 
          new File("src/test/resources/test.txt")))
      .build();
      
    ProgressRequestWrapper.ProgressListener listener 
      = (bytesWritten, contentLength) -> {
        float percentage = 100f * bytesWritten / contentLength;
        assertFalse(Float.compare(percentage, 100) > 0);
    };
    ProgressRequestWrapper countingBody
      = new ProgressRequestWrapper(requestBody, listener);
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/upload")
      .post(countingBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}

现在这里是接口ProgressListener,它使我们能够观察上传进度:

public interface ProgressListener {
    void onRequestProgress(long bytesWritten, long contentLength);
}

接下来是ProgressRequestWrapper,它是RequestBody的扩展版本:

public class ProgressRequestWrapper extends RequestBody {
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink bufferedSink;
        countingSink = new CountingSink(sink);
        bufferedSink = Okio.buffer(countingSink);
        delegate.writeTo(bufferedSink);
        bufferedSink.flush();
    }
}

最后,这里是CountingSink,它是ForwardingSink的扩展版本:

protected class CountingSink extends ForwardingSink {
    private long bytesWritten = 0;
    public CountingSink(Sink delegate) {
        super(delegate);
    }
    @Override
    public void write(Buffer source, long byteCount)
      throws IOException {
        super.write(source, byteCount);
        
        bytesWritten += byteCount;
        listener.onRequestProgress(bytesWritten, contentLength());
    }
}

注意:

  • 当将ForwardingSink扩展为*“CountingSink”时,我们重写了write()*方法来计算写入(传输)的字节数
  • 当将RequestBody扩展为“ ProgressRequestWrapper ”时,重写了writeTo()方法以使用我们的“ForwardingSink”

9. 设置自定义标题

9.1. 在请求上设置标头

要在请求上设置任何自定义标头,我们可以使用简单的addHeader调用:

@Test
public void whenSetHeader_thenCorrect() throws IOException {
    Request request = new Request.Builder()
      .url(SAMPLE_URL)
      .addHeader("Content-Type", "application/json")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    response.close();
}

9.2. 设置默认标题

在此示例中,我们将看到如何在客户端本身上配置默认标头,而不是在每个请求上都设置它。

例如,如果我们想为每个请求设置一个内容类型*“application/json”*,我们需要为我们的客户端设置一个拦截器:

@Test
public void whenSetDefaultHeader_thenCorrect() 
  throws IOException {
    
    OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(
        new DefaultContentTypeInterceptor("application/json"))
      .build();
    Request request = new Request.Builder()
      .url(SAMPLE_URL)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    response.close();
}

这是DefaultContentTypeInterceptor,它是Interceptor的扩展版本:

public class DefaultContentTypeInterceptor implements Interceptor {
    
    public Response intercept(Interceptor.Chain chain) 
      throws IOException {
        Request originalRequest = chain.request();
        Request requestWithUserAgent = originalRequest
          .newBuilder()
          .header("Content-Type", contentType)
          .build();
        return chain.proceed(requestWithUserAgent);
    }
}

请注意,拦截器会将标头添加到原始请求中。

10. 不要跟随重定向

在此示例中,我们将了解如何配置OkHttpClient以停止跟踪重定向。

默认情况下,如果使用*HTTP 301 Moved Permanently 响应 GET 请求,*则会自动遵循重定向。在某些用例中,这非常好,但在其他用例中则不需要。

为了实现这种行为,当我们构建客户端时,我们需要将followRedirects设置为false

请注意,响应将返回HTTP 301状态代码:

@Test
public void whenSetFollowRedirects_thenNotRedirected() 
  throws IOException {
    OkHttpClient client = new OkHttpClient().newBuilder()
      .followRedirects(false)
      .build();
    
    Request request = new Request.Builder()
      .url("http://t.co/I5YYd9tddw")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(301));
}

如果我们使用true参数打开重定向(或完全删除它),客户端将遵循重定向并且测试将失败,因为返回代码将是 HTTP 200。

11. 超时

当对等方无法访问时,我们可以使用超时来使调用失败。网络故障可能是由于客户端连接问题、服务器可用性问题或介于两者之间的任何原因。OkHttp 支持连接、读取和写入超时。

在这个例子中,我们用 1 秒的readTimeout构建了我们的客户端,而 URL 的提供延迟了 2 秒:

@Test
public void whenSetRequestTimeout_thenFail() 
  throws IOException {
    OkHttpClient client = new OkHttpClient.Builder()
      .readTimeout(1, TimeUnit.SECONDS)
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/delay/2")
      .build();
 
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}

请注意,测试将失败,因为客户端超时低于资源响应时间。

12. 取消通话

我们可以使用Call.cancel()立即停止正在进行的通话。如果线程当前正在写入请求或读取响应,则会抛出IOException

当不再需要调用时,我们使用此方法来保护网络,例如当我们的用户离开应用程序时:

@Test(expected = IOException.class)
public void whenCancelRequest_thenCorrect() 
  throws IOException {
    ScheduledExecutorService executor
      = Executors.newScheduledThreadPool(1);
    Request request = new Request.Builder()
      .url(BASE_URL + "/delay/2")  
      .build();
    int seconds = 1;
    long startNanos = System.nanoTime();
    Call call = client.newCall(request);
    executor.schedule(() -> {
        logger.debug("Canceling call: "  
            + (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
            
        logger.debug("Canceled call: " 
            + (System.nanoTime() - startNanos) / 1e9f);
        
    }, seconds, TimeUnit.SECONDS);
    logger.debug("Executing call: " 
      + (System.nanoTime() - startNanos) / 1e9f);
    Response response = call.execute();
	
    logger.debug(Call was expected to fail, but completed: " 
      + (System.nanoTime() - startNanos) / 1e9f, response);
}

13.响应缓存

要创建Cache,我们需要一个可以读写的缓存目录,以及缓存大小的限制。

客户端将使用它来缓存响应:

@Test
public void  whenSetResponseCache_thenCorrect() 
  throws IOException {
    int cacheSize = 10 * 1024 * 1024;
    File cacheDirectory = new File("src/test/resources/cache");
    Cache cache = new Cache(cacheDirectory, cacheSize);
    OkHttpClient client = new OkHttpClient.Builder()
      .cache(cache)
      .build();
    Request request = new Request.Builder()
      .url("http://publicobject.com/helloworld.txt")
      .build();
    Response response1 = client.newCall(request).execute();
    logResponse(response1);
    Response response2 = client.newCall(request).execute();
    logResponse(response2);
}

启动测试后,第一次调用的响应不会被缓存。对方法cacheResponse的调用将返回null,而对方法 networkResponse 的调用返回来自网络的响应。

缓存文件夹也将被缓存文件填充。

第二次调用执行将产生相反的效果,因为响应已经被缓存。这意味着对networkResponse的调用将返回null,而对cacheResponse的调用将从缓存返回响应。

为了防止响应使用缓存,我们可以使用CacheControl.FORCE_NETWORK。为了防止它使用网络,我们可以使用CacheControl.FORCE_CACHE

需要注意的是,如果我们使用*FORCE_CACHE,*并且响应需要网络,OkHttp将返回 504 Unsatisfiable Request 响应。