1. 概述

本文我们将学习如何使用OkHttp常见用法,包括GET/POST请求,异步请求,文件上传,自定义请求头等等。

2. OkHttp 概述

OkHttp 是一款高效的 HTTP 客户端,适用于 Android 和 Java 应用开发。

OkHttp 具有一些很多高级特性

  • 支持HTTP/2,允许对同一服务器的所有请求共享一个socket
  • 支持连接池减少请求延迟 (如果HTTP2不可用)
  • GZIP压缩,减小数据传输
  • 响应缓存避免重复网络请求。

OkHttp 可在网络不稳定时自动恢复连接。如果访问的服务器有多个IP地址,第一个连接失败时,OkHttp将尝试使用备用地址。

OkHttp 使用简单方便,接口设计为fluent模式,支持同步阻塞调用和带有回调的异步调用。下面我们开始学习如何使用

3. Maven 依赖

首先添加 Maven 依赖

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>5.0.0-alpha.12</version>
</dependency>

最新版本访问 Maven 中央仓库.

4. OkHttp 同步请求

同步 GET 请求示例:

@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请求,我们需要将请求加入队列(enqueue)并提供一个回调函数。当响应头准备好时,回调函数被执行,但读取response body仍然可能阻塞。

@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. Query 请求参数

使用 HttpUrl.Builder 可用于向URL中添加请求参数:

@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 请求

说完了GET请求,我们来看发起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 POST请求示例请查看我们这篇文章,

8. 文件上传

8.1. 文件上传

使用 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. 获取文件上传进度

如何获取文件上传进度,我们需要重写 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());
    }
}

总结

  • CountingSink 重写 write() 方法用于计算传输了多少字节
  • ProgressRequestWrapper 继承 RequestBody 并重写 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. 设置默认请求头

我们可以设置默认请求头,就无需为每个请求重复设置。

例如,如果我们希望为每个请求设置 content-type 为 “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. 禁止重定向

默认情况下OkHttp会自动跟随重定向(HTTP 301),如果我们不想跳转,需要设置 followRedirectsfalse

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

    // 因为我们没有跟随重定向,所以应该返回301
    assertThat(response.code(), equalTo(301));
}

如果我们设置 followRedirectstrue,客户端会自动跟随重定向,返回状态码将为200。

11. 超时设置

网络故障可能是由于客户端连接问题、服务器宕机等其他问题。OkHttp 支持设置connect超时、read和write超时。

下面例子中,我们设置client端 readTimeout 为 1 秒,而请求响应被延迟了 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. 结果缓存

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

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

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

使用 CacheControl.FORCE_NETWORK 禁止使用缓存,使用 CacheControl.FORCE_CACHE 强制使用缓存。

14. 总结

在本文中,我们探讨了如何使用 OkHttp 作为 HTTP 和 HTTP/2 客户端的几个示例。

惯例,示例中的代码可以在 GitHub 项目 中找到。