文章目录
-
- [1. 领域对象](#1. 领域对象)
-
- [1. Request](#1. Request)
- [2. Response](#2. Response)
-
- [2.1 请求重写](#2.1 请求重写)
- [2.2 重写返回](#2.2 重写返回)
- [2.3 跟踪请求](#2.3 跟踪请求)
- [2.4 自动重试](#2.4 自动重试)
- [3. Calls](#3. Calls)
- [2. 创建连接](#2. 创建连接)
- [3. 使用案例](#3. 使用案例)
-
- 1、同步GET请求
- 2、异步GET请求
- 3、发送和读取HTTP头
- [4、POST 字符传](#4、POST 字符传)
- [5、POST 流](#5、POST 流)
- [6、POST 文件内容](#6、POST 文件内容)
- [7、POST 表单](#7、POST 表单)
- 8、上传
- 9、使用Moshi在JSON和对象之间转换
- 10、缓存Response
- 11、取消Call调用
- 12、超时
- 13、设置
- 14、Authentication
- [15. 设置代理](#15. 设置代理)
1. 领域对象
HTTP客户端的作用就是接受你的请求,获取对应的返回。理论上简单的,然后实践过程会有一些陷阱。OkHttp是其中使用的比较广泛的一个。
okhttp主要包含的概念如下:
1. Request
包含请求的url,方法(GET/POST/PUT等待),HTTP头,以及请求体。
2. Response
返回的编码(200 成功, 404 页面不存在),HTTP头,以及返回体。
2.1 请求重写
我们会在高层次的描述请求哪个地址,包含什么头等等。为了正确性和高效,OkHttp会帮我们重写请求。
- 添加缺失的HTTP头,包括Content-Lenght、Transfer-Encoding、User-Agent、Host、Connection、Content-Type
- 动添加一个Accept-Encoding,支持自动解压gzip等压缩过的返回
- 如果你有Cookie,OkHttp会自动添加一个Cookie的HTTP头
- 可缓存的请求(Cache-Control、Expired、Etag等),自动添加If-Modified-Since、If-None-Match头
2.2 重写返回
- 如果返回是压缩过的,OkHttp会自动解压,并删除Content-Encoding和Content-Length头,因为这两个值解压后已经不准确
- 如果是条件获取(If-Modified-Since等),OkHttp会自动切换从本地缓存或者服务端获取内容
2.3 跟踪请求
- 自动完成302跳转,获取最终结果页面内容
- 如果配置了Authenticator,自动HTTP权限验证
2.4 自动重试
- 连接池内连接过期或者服务器不可触达,尝试重试(重试的机制还需要深挖)
3. Calls
由于请求重写、跟踪请求、自动重试等等,你描述的请求实际上会产生多个Request/Response, OkHttp将这个操作抽象为一个Call。
Call实际上有两种执行方式
- 同步执行,当前线程阻塞,指定请求完成
- 异步执行,当前请求如队列,完成请求会回调注册的接口
Call可以被其他线程取消。如果Call被取消后还有线程还有代码去写Request或者读Response会抛出IOException
在同步模式下,你使用自己线程,并负责管理simultaneous,太多的simultaneous connections浪费资源,过少则影响延时
在异步模式下,Dispatcher负责管理simultaneous,你可以设置每个webserver(默认5个)以及总(默认64个)的连接数。
2. 创建连接
尽管你只提供一个URL链接,OkHttp会使用三种类型去链接WebServer,它们分别是: URL、Address、Route
1、URLs
URL是HTTP和互联网的基础,处理提供Web上资源的统一命名服务,同样指定了怎么访问Web资源。
URL是抽象的
- 指定了请求内容是纯文本(http)还是加密的(https)
- 没有指定加解密算法,怎么去验证证书(HostnameVerifier),哪些证书是可靠的(SSLSocketFactory)
- 没有指定怎么使用代理服务器,以及代理服务器怎么鉴权
2、Addresses
Adress指定了一个WebServer,包括所有连接服务器所需要的信息(包括端口、HTTPS设置、倾向的网络协议,如HTTP/2、SPDY等)。
相同服务器地址的URL可能会共享底层的TCP连接,共享TCP连接会有明显的性能优势:
- 更低延迟
- 更高吞吐量(TCP慢启动)
- 省电
OkHttp通过ConnectionPool自动重用连接。
3、Routes
Routes提供了自动链接WebServer里的必要信息,包括
- IP地址,通过DNS获取
- 确定的代理服务器,如果配置了ProxySelector的话
- TLS(传输层安全协议)版本号
一个Adress可能会有多个Routes,比如一个跨数据中心部署的WebServer,DNS解析后肯能会有多个地址。
4、Connections
当你请求URL时,OkHttp为你做了以下操作:
- 使用URL和配置好的OkHttpClient创建一个Addresss,里边包含了我们怎么去连接Web服务器的信息。
- 尝试从ConnectionPool里获取一个对应Address的连接。
- 找不到连接的话,它会尝试选择一个Route,这意味着DNS解析、选择TLS版本以及代理服务器(需要的话)。
- 如果这是一个新的Route,它会创建一个Socket连接、TLS隧道(经过HTTPS的代理服务)、TLS连接。如果需要的话,完成TLS握手。
- 发生请求并读取结果。
如果连接有问题的话,OkHttp会重新选择另外一个Route并重试。这有助于OkHttp从服务端部分地址不可用时恢复。同样有助于当连接过期,或者TLS版本不支持的时候。
当响应读取完成后,连接会被退回到连接池中等待重用,一段时间没有使用后会被从连接池里清除。
3. 使用案例
我们提供了一下用OkHttp解决常见问题的样例。
1、同步GET请求
当响应结果比较小的时候(<1M),response.body().string()是方便且高效的,否则应该使用流读取。
Java
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder().url("https://publicobject.com").build();
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));
}
System.out.println(response.body().string());
}
}
2、异步GET请求
在OkHttp的工作线程里请求,当响应可读的时候调用回调函数。回调函数实在响应HTTP头读取后调用的,读取响应体还是需要同步完成。
Java
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder().url("http://publicobject.com").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());
}
}
});
}
3、发送和读取HTTP头
一般来说,HTTP头一个KEY会对应一个值,但是有的字段允许有多个值。OkHttp为这两种情况提供了支持:
- Request.Builder.header(name,value)方法,支持单值的HTTP头,把已存在的同名HTTP头删除。
- Request.Builder.addHeader(name,value)只是单纯的添加,不会有删除操作。
- Response.header(name),获取多值的最后一个,没有的话返回null。
- Response.headers(name),返回多值
Java
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();
try (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"));
}
}
4、POST 字符传
Java
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 = "* _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());
}
}
5、POST 流
使用流做为请求体,请求体内容会在发送请求的时候输出,这个例子里使用的是Okio的BufferedSink,你也可以通过BufferedSkin.outputStream获取输出流。
Java
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, 2*i));
}
}
};
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());
}
}
6、POST 文件内容
Java
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());
}
}
7、POST 表单
Java
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());
}
}
8、上传
Java
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 MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
9、使用Moshi在JSON和对象之间转换
Moshi是一个简便的JSON转换类库。
ResponseBody.charStream()使用Content-Type里的编码解释响应内容,找不到编码默认UTF-8。
Java
private final OkHttpClient client = new OkHttpClient();
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
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;
}
10、缓存Response
要缓存Response,你需要一个可以读写的目录,以及设置缓存的大小。 大多数应用只需要创建一次OkHttpClient对象,两个实例使用同一个缓存目录会导致冲突,甚至使程序崩溃。
Response缓存通过服务端HTTP头控制。
添加一下HTTP头可以控制缓存的行为:
- CacheControl.FORCE_NETWORK,强制从服务端加载。
- CacheControl.FORCE_CACHE,强制从缓存加载,当缓存中信息不可获取(不存在或者过期),返回504
Java
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.Builder().cache(cache).build();
}
public void run() throws Exception {
Request request = new Request.Builder().url("http://publicobject.com/helloworld.txt").build();
String response1Body;
try (Response response1 = client.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 = client.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));
}
11、取消Call调用
通Call.cancel()取消调用,如果另外一个线程正在写请求或者读取返回时,抛出IOException
Java
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);
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);
}
}
12、超时
当端不可触达的时候,客户端连接问题、服务端不可用、或者两者之间的问题,可以通过设置超时。OkHttp支持三种超时: 连接超时,写请求超时,读返回超时。
Java
private final OkHttpClient client;
public ConfigureTimeouts() throws Exception {
client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
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();
try (Response response = client.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
13、设置
所有的设置都是通过OkHttpClient完成的,包括代理、超时、缓存等等。当你需要为一个请求修改设置的时候,调用OkHttpClient.builder()方法,返回的builder和原始的OkHttpClient共享连接池、Dispatcher和配置信息,只需要修改特有的配置即可。
Java
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();
// Copy to customize OkHttp for this request.
OkHttpClient client1 = client.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 = client.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);
}
}
14、Authentication
OkHttp会自动重试未授权的请求,当服务端返回401的时候,OkHttp会尝试寻找Authenticator注册信息,如果有的话通过对应信息进行验证。代码关键点:
- response.challenges()
- Credentials.basic
- response.request().newBuilder()
Java
private final OkHttpClient client;
public Authenticate() {
client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override public Request authenticate(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();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Authenticator返回null的时候,OkHttp会停止重试。为了避免Authentication不起效时还反复重试, 可以再重试过一次之后,返回null退出执行
Java
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
你以可以通过Response.priorResponse()来计数调用了多少次,次数满是退出重试。
Java
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
15. 设置代理
Java
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1",12345));
OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).build();
Request request = new Request.Builder().url(url).header("Authorization", signature).post(RequestBody.create(MEDIA_JSON, postBody)).build();
Response response = client.newCall(request).execute();