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),如果我们不想跳转,需要设置 followRedirects 为 false。
@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));
}
如果我们设置 followRedirects 为 true,客户端会自动跟随重定向,返回状态码将为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 项目 中找到。