- Android
- Square
- OkHttp
OkHttp 使用指南(四)--Recipes
手打翻译原文,若转载本文请注明出处
写这篇文章之前是因为项目想使用 OkHttp 网络组件替换之前老是出问题的网络组件,在集成之前最重要的是调研,参考的文献主要也是官方提供的最新文档,这篇文章也是我从官方文档根据自己的理解翻译过来,Recipes 这篇主要介绍的也是 OkHttp 的使用方法例子。
我们使用 OkHttp 写了很多方法例子来说明解决使用过程中遇到的常见的问题。通过阅读这些例子这些组件是怎么在一起工作的。粘贴复制这些例子是自由免费的,因为这就是这些例子的用途。
Synchronous Get(同步 GET)
下载一个文件,以字符串的形式打印出他的头部信息,打印出响应数据体信息。
String() 方法作为一些小文件的响应数据体是非常方便和高效的。但是如果针对一些大文件的下载(大于 1MB 文件),尽量避免使用 String() 方法因为他会将整个文本加载到内存中。针对这种例子优先选择的解决方案是将数据体作为一个数据流来处理。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
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));
}
System.out.println(response.body().string());
}
Asynchronous Get(异步 GET)
在工作线程中进行下载任务,并且在响应到达的时候采用回调的方式通知。这个回调会等待响应信息头准备好之后发送,读取这个响应头信息仍然会阻塞。目前的 OKHttp 不支持异步的 APIS 来接收处理部分的响应体。
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(Request request, IOException throwable) {
throwable.printStackTrace();
}
@Override public void onResponse(Response response) throws IOException {
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(response.body().string());
}
});
}
Accessing Headers(访问头部)
典型的 HTTP 头部信息类似于 Map<String, String> :每一个属性都有一个值或者没有,但是某些信息头允许多个值信息,类似于 Guava Multimap。例如,对于 Http 响应信息头部包含多个变量值是合法常见的。OkHttp APIs 计划所有这些情况兼容。
使用 header(name, value) 方法来写唯一的请求头部信息。如果已经存在了一个值,在新的值添加前会移除掉当前值。使用 addHeader(name, value) 方法增加头部信息而不用移除之前已经录入的信息。
通过 header(name) 读取响应头部信息,返回的是最新的值,通常情况下都会返回值,但是如果本来就没有的话会返回 null ,通过使用 headers(name) 来访问头部信息。
通过 headers 类可以根据 index 来访问头部信息。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
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();
Response response = client.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"));
}
Post a String(发送一个字符串数据)
使用 Http 以 Post 方式来发送一个请求到服务端,这个例子是发送一个 markdown 文件到服务端来渲染成一个 HTML,因为整个请求是同时在内存中的,在使用这个 Api 时尽量避免使用数据量较大。
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 {
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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Post Streaming(Post 方式发送流式)
这里我们要说明的例子是使用流式的方式发送请求。请求数据体的内容已经产生并被写入。这个例子会将流直接写入到 Okio 缓冲槽中。你的项目可能会喜欢使用 OutputStream,这个你可以从 BufferedSink.outputStream()获取到。
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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Posting a File(发送一个文件)
简单使用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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Posting form parameters (发送表单参数)
使用 FormEncodingBuilder 建立一个请求链接类似于 HTML <form> 标签。
键值对会被使用 HTML 响应表单编码规范来编码。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody formBody = new FormEncodingBuilder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Posting a multipart request(发送多块请求)
MultipartBuilder 用来组装复杂的 HTML 文件上传表单请求数据体。其中多块请求数据体中的每一部分又是独立的请求数据体,并且可以自己定义数据头,如果这样的话数据头需要被定义成数据体的一部分,比如说作为 Content-Disposition 如果是有效的数据体的话 Content-Length 和 Content-Length 会自动加上的。
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 {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, "Square Logo"))
.addPart(
Headers.of("Content-Disposition", "form-data; name=\"image\""),
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
Parse a JSON Response With Gson(通过 Gson 来解析 JSON 格式的响应信息)
Gson 是一种将 JSON 转变为 JAVA 对象非常方便的 API,这里我通过 Gson GitHub API 来解析对应 JSON 响应数据。
需要注意的是 ResponseBody.charStream() 是使用相应信息的头部数据 Content-Type 来选择哪一种编码来解析响应的数据体。如果没有指定编码格式化默认使用的是 UTF-8 格式。
private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
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;
}
Response Caching(响应缓存)
对于响应缓存,你需要的是指定一个用来读取和写入的缓存存储路径,并且你需要指定一个缓存文件的大小限制。这个缓存的路径应该是私有的,并且对于不收信任的应用不能读取缓存的内容。
如果多个缓存同时访问同一个缓存地址的话会出现错误。大多数应用调用 new OkHttpClient() 一次,配置参数和缓存,在应用的其他使用它的实例对象进行操作。否则两个缓存对象会相互影响,混乱的响应缓可能会导致你的应用崩溃。
响应缓存使用 HTTP headers 作为所有的配置信息。你可以在请求头部增加 Cache-Control: max-stale=3600 这样 OkHttpClient 就会执行这个设置。你的服务端会响应头部使用自己的缓存时间配置信息,例如 Cache-Control: max-age=9600 这些缓存头部会强行拉取 缓存响应,网络响应,网络响应会被刷新通过有条件的 GET 请求。
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
client = new OkHttpClient();
client.setCache(cache);
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
String 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());
Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
String 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));
}
响应不用缓存,可以使用 CacheControl.FORCE_NETWORK。响应不用网络,使用 CacheControl.FORCE_CACHE。但是要注意的是如果你选择使用缓存的话,响应需要使用网络的话。OkHttp 会返回 504 Unsatisfiable Request 响应。
Canceling a Call(取消请求)
使用 Call.cancel() 立即停止正在执行的请求。如果一个线程正在执行请求或者接收一个响应的话,他会收到一个 IOException。可以使用这个方法来取消那些不在需要的网络请求一边节约网络。比如用户离开你的应用,无论你的同步还是异步的请求就可以被取消掉。
你也可以通过使用 tag 来同时取消掉多个请求。在构建一个请求的时候通过使用 RequestBuilder.tag(tag) 给请求打一个标签。然后通过 OkHttpClient.cancel(tag) 取消所有使用这个 tag 的请求。
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private final OkHttpClient client = new OkHttpClient();
public void run() 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 = client.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);
try {
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
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);
}
}
Timeouts(超时)
使用超时机制来取消一个另一个端点不可达的状态。网络模块可以划分为客户端连接问题、服务端可用问题或者两者都有,OkHttp 支持连接 read 和 write 三个时间的响应。
private final OkHttpClient client;
public ConfigureTimeouts() throws Exception {
client = new OkHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
client.setWriteTimeout(10, TimeUnit.SECONDS);
client.setReadTimeout(30, TimeUnit.SECONDS);
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
Response response = client.newCall(request).execute();
System.out.println("Response completed: " + response);
}
Per-call Configuration(单次配置)
所有的 Http 客户端配置都是在 OkHttpClient 设置包括代理设置、超时和缓存。当你需要单次配置客户端的时候话可以 clone OkHttpClient 这个将会返回一个浅 copy 对象这样你就可以定制单独的客户端设置了。下面的例子你可以看到一个设置 500ms 的超时和一个 30000ms 超时时间。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
.build();
try {
OkHttpClient cloned = client.clone(); // Clone to make a customized OkHttp for this request.
cloned.setReadTimeout(500, TimeUnit.MILLISECONDS);
Response response = cloned.newCall(request).execute();
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
try {
OkHttpClient cloned = client.clone(); // Clone to make a customized OkHttp for this request.
cloned.setReadTimeout(3000, TimeUnit.MILLISECONDS);
Response response = cloned.newCall(request).execute();
System.out.println("Response 2 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 2 failed: " + e);
}
}
Handling authentication(处理证书)
OkHttp 可以自动重试非证书的请求,当一个请求的响应是 401 Not Authorized ,Authenticator 被要求提供证书验证,需要重新执行一个包含证书的请求。如果没有证书可以提供会返回空来跳过重试。使用 Response.challenges() 获得一个模式和领域来针对所有的证书验证,当面对的是一个 Basic 验证。使用 Credentials.basic(username, password) 来编码请求信息。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
client.setAuthenticator(new Authenticator() {
@Override public Request authenticate(Proxy proxy, Response response) {
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();
}
@Override public Request authenticateProxy(Proxy proxy, Response response) {
return null; // Null indicates no attempt to authenticate.
}
});
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
为了避免验证失败出现的多次重试,你可以通过返回 null 来跳过。例如你可以跳过重试当你遇到这些证书被要求验证。
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
你也可以通过设置重试次数限制来跳过重试请求。
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
上述代码依赖 responseCount() 方法
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}