Android | 14 个例子带你完全入门 OkHttp

3,040 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情

OkHttp 是 Android 最流行的网络请求库,由 square 公司开发。除此之外,square 还开源了众多受欢迎的库,如 okio,picasso,retrofit,moshi 等。总之,square 是一个特别神奇的组织,成员都是行业权威,经验丰富的开发者。

本文演示了14个例子,覆盖了 OkHttp 所有可能使用到的用法,可以让大家快速入门。这些在日常开发中完全够用。

使用步骤

OkHttp 的使用有一定的步骤,大体如下。

  1. 初始化 OkHttpClient,官方建议整个应用只使用一个OkHttpClient对象。OkHttpClient可以设置缓存和超时时间等。
  2. 对于一个请求,实例化一个Request对象,构建请求的url请求体等信息。
  3. 使用OkHttpClient发送同步或异步请求,并获取响应结果。

可以看见使用 OkHttp 发送网络请求非常的简单,只需要简单的3个步骤。本文OkHttpClient的配置如下所示。设置连接超时为10s,写入超时为10s,读取超时为 30s,调用超时为 10s。

private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .writeTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .callTimeout(10, TimeUnit.SECONDS)
    .build();

同步 Get 请求

同步请求的意思是“执行请求”和“获取返回结果”是同步的,有先后顺序的。对于同步请求,OkHttp 内部不会开启线程去执行。请求和结果返回都在调用线程中执行。下面代码首先通过Builder模式创建了一个Requesturlhttps://publicobject.com/helloworld.txt,然后使用okHttpClient发送同步请求,调用线程会阻塞等待直到结果返回或者发生异常而退出。

public void synchronousGet() throws IOException {
    Request request = new Request.Builder()
            .url("https://publicobject.com/helloworld.txt")
            .build();

    // 使用 execute 发送同步请求,会阻塞直到结果返回
    try(Response response = okHttpClient.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));
        }

        // 如果 响应体 > 1MB,应避免使用 string() 方法,因为它会将整个文档加载到内存中。这种情况下,应该使用流的方式来处理 body
        System.out.println(response.body().string());
    }
}

上述代码打印出响应报文的所有头字段。

Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 03 Feb 2023 09:18:44 GMT
Content-Type: text/plain
Content-Length: 1759
Last-Modified: Tue, 27 May 2014 02:35:47 GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes

异步 Get 请求

异步请求和同步请求刚好相反。对于异步请求来说,OkHttp 内部会开启线程去执行,返回结果会在线程中回调。所以不要在回调函数onFailureonResponse**中去操作UI。**如有必要,可以配合使用HandlerRxJava切换到主线程操作。

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

    // 使用 enqueue 发送异步请求,调用线程不会阻塞
    okHttpClient.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 {
            System.out.println("Current thread: " + Thread.currentThread().getName());

            try(ResponseBody responseBody = response.body()) {
                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(responseBody.string());
            }

        }
    });

}

一个头字段设置多个值

OkHttp 支持使用addHeader方法对一个头字段设置多个值。

public void accessHeaders() throws Exception {
    Request request = new Request.Builder()
            .url("https://api.github.com/repos/square/okhttp/issues")
            .header("User-Agent", "OkHttp Headers.java")
            // OkHttp 支持一个头字段设置多个值
            .addHeader("Accept", "application/json; q=0.5")
            .addHeader("Accept", "application/vnd.github.v3+json")
            .build();

    try (Response response = okHttpClient.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"));
    }
}

程序打印结果。

Server: GitHub.com
Date: Mon, 06 Feb 2023 07:16:13 GMT
Vary: [Accept, Accept-Encoding, Accept, X-Requested-With]

Post 方式提交 String

OkHttp 中凡是 Post 请求必须指定请求体的媒体类型,下面程序使用 Post 方式向服务器提交一个字符串,媒体类型是text/x-markdown; charset=utf-8

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");

public void postString() 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 方式请求,并指定请求体的媒体类型
            .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
            .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        System.out.println(response.body().string());
    }
}

程序打印结果。

<h2><a id="user-content-releases" class="anchor" aria-hidden="true" href="#releases"><span aria-hidden="true" class="octicon octicon-link"></span></a>Releases</h2>
<ul>
<li>
<em>1.0</em> May 6, 2013</li>
<li>
<em>1.1</em> June 15, 2013</li>
<li>
<em>1.2</em> August 11, 2013</li>
</ul>

Post 方式提交流

OkHttp 允许我们将请求体的内容以流的方式写入,只要重写RequestBodycontentTypewriteTo方法。下面代码在writeTo方法中向输出流写入了2到997对应的素因子乘积。

public void postStreaming() throws Exception {
    // 重写 contentType 和 writeTo 方法
    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 = okHttpClient.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        System.out.println(response.body().string());
    }
}

打印出响应报文的响应体。

<h2><a id="user-content-numbers" class="anchor" aria-hidden="true" href="#numbers"><span aria-hidden="true" class="octicon octicon-link"></span></a>Numbers</h2>
<ul>
<li>2 = 2</li>
<li>3 = 3</li>
<li>4 = 2 × 2</li>
<li>5 = 5</li>
<li>6 = 3 × 2</li>
<li>7 = 7</li>
<li>8 = 2 × 2 × 2</li>
...
<li>992 = 31 × 2 × 2 × 2 × 2 × 2</li>
<li>993 = 331 × 3</li>
<li>994 = 71 × 7 × 2</li>
<li>995 = 199 × 5</li>
<li>996 = 83 × 3 × 2 × 2</li>
<li>997 = 997</li>
</ul>

Post 方式提交文件

OkHttp 提交文件很简单。和提交字符串一样,只需将文件作为请求体。

public void postFile() throws Exception {
    File file = new File("test.txt");

    Request request = new Request.Builder()
            .url("https://api.github.com/markdown/raw")
            // 直接传入 file 即可
            .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
            .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        System.out.println(response.body().string());
    }
}

程序打印结果。

<p>this is a test.</p>

Post 方式提交表单

FormBodyRequestBody的子类,使用FormBody可以轻松的将请求参数以表单编码方式提交。FormBody默认的媒体类型是application/x-www-form-urlencoded

public void postForm() throws Exception {
    RequestBody formBody = new FormBody.Builder()
            .add("search", "Jurassic Park")
            .build();
    Request request = new Request.Builder()
            // 该 url 被墙,暂时无法访问
            // java.net.SocketTimeoutException: connect timed out
            .url("https://en.wikipedia.org/w/index.php")
            .post(formBody)
            .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        System.out.println(response.body().string());
    }
}

Post 方式提交分块请求

当你要在一个请求中同时提交多种类型的数据时,可以使用MultipartBody来完成这个任务。下面程序提交了字符串和图片两种不同的数据。

public void postMultipartBody() 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("logo-square.png")))
            .build();

    Request request = new Request.Builder()
            .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
            // 该 url 被墙,暂时无法访问
            // java.net.SocketTimeoutException: connect timed out
            .url("https://api.imgur.com/3/image")
            .post(requestBody)
            .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            System.out.println(response.body().string());
    }
}

使用 Moshi 解析 Json 响应

Moshi 是一个序列/反序列化库。下面代码简单演示了它的用法,当然你还可以使用 Gson 等其他流行的框架。

// Moshi
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);

public void moshiParse() throws Exception {
    Request request = new Request.Builder()
            .url("https://api.github.com/gists/c2a7c39532239ff261be")
            .build();
            
    try (Response response = okHttpClient.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        // 使用 moshi 反序列化返回结果
        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;
}

缓存响应

OkHttp 可以以文件的方式缓存来自服务器的响应,节省网络流量。修改OkHttpClient的配置,添加缓存功能。设置缓存大小为 10M,缓存文件目录为okhttpcache

// Cache, 配置 OkHttp 本地缓存
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(new File("okhttpcache"), cacheSize);

// client
private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cache(cache)
    .connectTimeout(10, TimeUnit.SECONDS)
    .writeTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .callTimeout(10, TimeUnit.SECONDS)
    .build();

下面程序请求了同一个url两次。调用cacheResponse两次来观察打印结果,我们可以得出如下结论。

  1. 因为本地没有缓存,第一次调用的第一个请求会请求网络,并将响应缓存到本地。
  2. 第一次调用的第二个请求直接从本地缓存拿到响应结果,不会走网络请求。
  3. 第二次调用的所有请求均会从本地缓存拿到响应结果。
public void cacheResponse() throws Exception {
    Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();

    // 第一次请求
    String response1Body;
    try (Response response1 = okHttpClient.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 = okHttpClient.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));
}

程序打印结果。

// 第一次调用打印
Response 1 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response:    null
Response 1 network response:  Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}

Response 2 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response:  null

Response 2 equals Response 1? true

// 第二次调用打印
Response 1 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 network response:  null

Response 2 response:          Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response:    Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response:  null

Response 2 equals Response 1? true

取消正在执行的请求

使用 call.cancel() 可以立即停止正在进行的请求,同步和异步请求都可以取消。这种功能可以节省网络,例如当用户开应用程序时。下面请求的url会有2s的延迟,在一个线程中我们会在1s后取消这个请求,随后程序抛出了异常。

public void cancelCall() 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 = okHttpClient.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);
    }
}

程序打印结果。

0.00 Executing call.
1.02 Canceling call.
1.02 Canceled call.
1.02 Call failed as expected: java.io.IOException: Canceled

设置超时

OkHttp 支持设置连接、写入、读取和完全调用超时。下面程序请求的url有10s的延迟。而我们OkHttpClient设置的调用超时为10s,所以下面代码将会抛出异常。

public void timeout() throws Exception {
    Request request = new Request.Builder()
            .url("http://httpbin.org/delay/10") // This URL is served with a 10 second delay.
            .build();

    try (Response response = okHttpClient.newCall(request).execute()) {
        System.out.println("Response completed: " + response);
    }
}

程序打印结果。

java.io.InterruptedIOException: timeout
at okhttp3.internal.connection.RealCall.timeoutExit(RealCall.kt:398)
at okhttp3.internal.connection.RealCall.callDone(RealCall.kt:360)

改变单个 Call 的配置

OkHttpClient的配置对所有的请求都生效,有时候我们想对某个请求单独配置,比如改变超时时间等。这个时候就可以使用newBuilder方法,newBuilder会拷贝全局OkHttpClient成员变量的值重新生成一个OkHttpClient实例返回。

public void perCallConfiguration() 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 = okHttpClient.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 = okHttpClient.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);
    }
}

程序打印结果。

Response 1 failed: java.net.SocketTimeoutException: Read timed out
Response 2 succeeded: Response{protocol=http/1.1, code=200, message=OK, url=http://httpbin.org/delay/1}

身份验证

在请求需要身份验证的资源时,若没有权限服务器通常会返回401来告知客户端身份验证的方案和受保护的资源范。OkHttp 可以自动重试未经身份验证的请求,当响应为401未授权时,我们构建一个包含凭据的新请求。下面程序请求的url需要身份验证,在authenticator方法中第一次返回了401,于是我们添加了头字段Authorization,并按照服务器要求的身份验证方案生成了凭据再次发送请求。

public void authenticate() throws Exception {
    OkHttpClient okHttpClient1 = okHttpClient.newBuilder()
        .authenticator(new Authenticator() {
            @Nullable
            @Override
            public Request authenticate(@Nullable 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();

    Request request = new Request.Builder()
            .url("http://publicobject.com/secrets/hellosecret.txt")
            .build();

    try (Response response = okHttpClient1.newCall(request).execute()) {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

        System.out.println(response.body().string());
    }
}

程序打印结果。

Authenticating for response: Response{protocol=http/1.1, code=401, message=Unauthorized, url=https://publicobject.com/secrets/hellosecret.txt}
Challenges: [Basic authParams={realm=OkHttp Secrets}]

写在最后

如果你对我感兴趣,请移步到 blogss.cn ,或关注公众号:程序员小北,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注,这是我持续创作的动力 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于掘金,未经许可禁止转载 ©️