敞开成长之旅!这是我参与「日新计划 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. 第2次调用的一切恳求均会从本地缓存拿到呼应成果。
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());
    }
    // 第2次恳求
    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
// 第2次调用打印
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 ,或关注公众号:程序员小北,进一步了解。

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