转载

14 | Square 现代高效的 HTTP 客户端 okhttp(一)基本用法

作者简介:ASCE1885, 《Android 高级进阶》 作者。

本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!

本文分析的源码版本已经 fork 到我的 Github 。

14 | Square 现代高效的 HTTP 客户端 okhttp(一)基本用法

Photo by Anthony Tran(https://www.ssyer.com/author/16547)

通过前面一个系列对 okio 的分析,我们终于迎来 okhttp。相信每一位 Android 开发者对这个网络框架都不陌生,但要说很熟悉也不尽然,因为大多数情况下我们不会直接对 okhttp 的 API 去做操作,而是通过另外一个库 retrofit 来完成。因此作为 okhttp 的开篇,我们先来介绍它的基本用法和核心特性,后面再根据这些特性逐一展开源码剖析。

从官网的介绍可以知道,okhttp 是一个现代的高效的 HTTP 和 HTTP/2 客户端,支持 Android 2.3 及以上版本,对 Java 版本的最低要求是 1.7,它广泛应用在 Android 开发和 Java 后端开发中,具有如下特性:

  • 支持 HTTP/2,支持对同一台主机的所有请求共享同一个 socket
  • 当 HTTP/2 不可用时,使用连接池减少请求的延迟
  • 透明的 GZIP 压缩,可以减少下载的数据大小
  • 支持对响应做缓存,能够完全避免重复的网络请求

okhttp 在网络性能很差的情况下能够很好的工作,它能够从常见的网络连接问题中静默恢复。如果你的 HTTP 服务有多个 IP 地址,okhttp 在第一次连接失败时,会尝试其他可选的地址。这对于 IPv4+IPv6 以及托管在冗余数据中心的服务来说是必要的。okhttp 使用现代的 TLS 特性(SNI, ALPN)初始化 HTTP 连接,当握手失败时,会降级使用 TSL1.0 尝试初始化连接。

okhttp 的使用很简单,它的请求和响应 API 基于流式 builder 模式设计,同时是 不可变类 。支持同步阻塞的调用和基于回调的异步调用。接下来我们通过官方提供的例子来一览 okhttp 的常见用法。

同步的 GET 请求

本例子展示的是使用 GET 请求下载一个 txt 文件,并打印 HTTP 响应头(response header)信息和响应体(response body)中的信息,需要注意的是,响应体的 string 方法的使用只能针对小文件,如果文件比较大,例如大于 1M,那么就要避免使用 string 方法,因为该方法会把整个文件内容都加载进内存,对内存消耗较大,可以转而将响应体中的内容当作 stream 来处理。代码如下所示,请重点关注 同步请求 的方法 execute

// 构造一个 OkHttpClient 对象,它是 okhttp 总的入口
private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    // 构造一个 HTTP 请求
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    // try with resource 语法,推荐使用
    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));
      }
      // 获取响应中的body信息
      System.out.println(response.body().string());
    }
}

异步的 GET 请求

本例子同样是下载文件,但它使用一个独立的工作线程进行文件的下载,当响应到来时再回调回当前线程。需要注意的是,在回调方法中对响应体内容的读取依然是同步的,当前 okhttp 并没有提供异步的 API 来接收 response body 的内容,代码如下所示,请重点关注异步请求的方法 enqueue

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

HTTP headers

通常情况下,HTTP headers 是一个 Map<String, String> 的结构,也就是其中每个元素要么有值有么为空,但某些特殊的 headers 允许存在多个值,类似 Guava 库中的 Multimap 结构。例如在 HTTP 响应头中经常会存在多个 Vary 类型的 headers,okhttp API 同时支持这两种情况。

我们以请求头的构造为例,使用 header(name, value) 方法可以给 name 这个类型设置唯一的值 value ,如果已经存在值,那么原来的值会被删掉,并重新赋值为新值;使用 addHeader(name, value) 则不会删掉原有的值,这样类型 name 会对应多个 value

当从响应中读取 header 数据时,使用 header(name) 方法可以返回最新的 name 值,通常也会是唯一的值,如果 name 类型对应的 header 不存在,则 header(name) 方法返回 null。如果想要一次把某个类型的所有 header 值读取出来,可以使用 headers(name) ,它会返回一个列表。

代码如下所示:

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    // 请求中设置 headers
    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);

      // 读取响应中的 headers
      System.out.println("Server: " + response.header("Server"));
      System.out.println("Date: " + response.header("Date"));
      System.out.println("Vary: " + response.headers("Vary"));
      }
}

POST 发送字符串信息

下面的例子我们使用 HTTP POST 来发送一个字符串到某个服务接口,具体是发送一个 markdown 格式的字符串到 Web Service,然后被渲染为 HTML。可以看到,由于这个字符串很小,因此整个请求体都是放在内存中的,如果是大文件(大于 1M)那么不建议使用这种方式:

// 表明数据是 markdown 格式
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 {
    // 要发送的 markdown 格式字符串
    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());
    }
}

POST 发送流数据

当要发送的数据量比较大时,我们就不能直接把整个文件或者数据加载进内存,这时需要使用流方式来边读边写,输出流我们选用 okio 的 BufferedSink,代码如下所示,和上一个例子相比,唯一的区别就是 RequestBody 的构造过程:

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

POST 发送文件

使用 POST 发送文件比较简单,因为 RequestBody 本身支持 File 这种类型的参数,代码如下所示:

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

POST 发送 form 表单

通过使用 FormBody.Builder ,我们可以模拟 form 表单的提交,传入的 name 和 value 会自动使用 HTML 兼容的 form 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());
    }
}

POST 发送 multipart 请求

MultipartBody.Builder 可以用来构建兼容 HTML 文件上传表单的复杂请求体。Multipart 请求体中每个部分又是一个请求体,这些子请求体可以定义属于自己的请求头,这些请求头用来描述这部分请求,例如 Content-Disposition 类型,可以定义如下:

Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"

如果 Content-LengthContent-Type 这两种类型的 header 存在的话,会自动添加,示例代码如下所示:

```java

// 此处以 imgur 这个第三方服务提供的 API 接口为例来展示文件上传的功能,如果你要将其集成到自己的服务中,那么需要申请一个自己的 client id,详情可以参见官方说明: https://apidocs.imgur.com/

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 {

// 此处调用的是 imgur 的图片上传接口,接口文档可以参见: https://apidocs.imgur.com/#c85c9dfc-7487-4de2-9ecd-66f727cf3139

RequestBody requestBody = new MultipartBody.Builder()

.setType(MultipartBody.FORM)

.addFormDataPart("title", "Square Logo")

.addFormDataPart("image", "logo-square.png",

原文  https://xiaozhuanlan.com/topic/4109768523
正文到此结束
Loading...