Calls
请求
每一个Http请求包含一个URL,请求方法(比如GET Or POST),以及一系列的header。请求也有可能包含一个请求体:某种特殊类型的数据流。响应
响应的内容包括一个响应码(比如200代表成功 或者 404代表未找到),响应头以及响应体。重写请求
当你使用OkHttp发起一个Http请求,Okhttp会重写你的请求在真正发送该请求之前。OkHttp会向你的请求添加如下的请求头,包括Content-Length,Transfer-Encoding, User-Agent, Host, Connection, and Content-Type. 为了实现响应压缩透明化,OKHttp会向请求中添加Accept-Encoding 请求头,除非已经存在了该请求头。如果你有Cookies,OkHttp会添加一个Cookie 请求头。
一些请求需要缓存,当缓存不是最新的时候,okHttp会发起一个GET请求更新缓存。这需要诸如If-Modified-Since 和If-None-Match添加进来。
重写响应
如果使用透明压缩,OKHttp会丢弃对应的Content-Encoding 和Content-Length响应头,因为这些响应头并不能用在解压响应体中。如果一个GET请求成功了,来自网络和缓存的相应会按照一定的规则合并。
后续请求
当你请求的URL已经被移除的时候,WebServer会返回一个302的状态码,以表名文档的新的URL,OkHttp会重定向以得到最终的响应。
如果响应要求授权验证,OkHttp会请求一个Authenticator
(如果已经配置)以满足授权要求.如果authenticator 提供了一个凭证,请求会携带该证书进行请求重试。
请求重试.
Calls
通过重写、重定向、后续和重试,你的一个简单的请求会产生很多请求和响应。OkHttp使用Call并通过许多必要的中间请求和响应来满足你请求的任务模型。通常情况下,这不是很多!如果你的URLs被重定向或者你切换到另外一个IP地址进行故障转移,你的代码仍然会正常工作。
Calls通过以下两种方式执行:
a)同步:你的线程会被阻塞,直到响应可读。
b)异步:在任意线程入队(队列)你的请求,当响应可读的时候,你可以在另一个线程得到Call Back回调。
Calls可以在任意线程被取消.这将使没有被执行完毕的Call执行失败!当一个call被取消的时候,正在写入请求体或者读取响应体的代码会得到一个IOException。
调度
对于同步请求,你需要管理并发请求,太多的并发连接浪费资源,太少又浪费延迟。
对于异步请求,Dispatcher实现了最大并发请求数策略.你可以设置每台WebServer(默认是5)和全部的最大并发数(默认64)。
Connections
虽然你只是提供了一个URL,Okhttp计划使用三种类型连接到你的WebServer:URL,Address和Route。
URLs
URLs(比如https://github.com/square/okhttp)是Http和Internet的基础。除了是网络上通用和分散的命名方案,他们还指定了如何访问网络资源。
URLs是抽象的:
a)指定请求是明文的(http)或者加密的(https),但是并没有指定使用哪一种加密算法,也没有指定如何验证对等端的证书(HostnameVerifier)或者哪一种证书应该被信任(SSLSocketFactory)
b)没有指定使用哪种代理服务器或者如何与该服务器进行身份认证
Urls也是具体的:每个Url声明了特定的路径(比如/square/okhttp)和查询(比如?q=shark&lang=en).每个WebServer hosts需要URLs.
Address
Address指定了一个WebServer(比如github.com)和连接到一个服务器的所有必要的静态配置:端口号,Https设置和网络协议(比如Http/2或者SPDY)
共享相同address的Urls也会共享相同的TCP socket连接。共享连接有如下益处:低延迟、高吞吐量(由于TCP慢启动)和保养电池。OkHttp使用连接池自动的复用http/1.x连接和Http/2、SPDY连接。
在OkHttp中,地址的一些字段来自于URL(scheme、hostname、port)、剩下的来自于OkHttpClient。
Routes
路由(Routes)提供了连接WebServer的动态信息。那就是一个指定的IP地址(被DNS查询到的),代理服务器(如果一个ProxySelector在使用)和TLS协议版本(为了Https连接)。
对于一个地址可能对应多个路由信息.比如在多个数据中心托管的Web服务器,它可能会在其DNS响应产生多个IP地址。
Connections
当你使用OkHttp请求一个URL,OkHttp会做如下工作:
1、OkHttp会用URL和OkHttpClient的配置信息去创建一个地址(Address),这个地址指定了我们如何去连接一个Web服务器.
2、OkHttp会根据上述的地址尝试着从连接池中取回一个可用的连接
3、如果在连接池中没有发现一个可用的连接,OkHttp会选择一个路由(Route)去尝试.这就意味着会做一个DNS请求以获取一个服务器的IP地址.必要情况下,OkHttp会选择一个TLS的版本和代理服务器
4、如果是一个新的路由,OkHttp会建立一个Socket连接,一个TLS通道(为了Https和Http代理)或者一个TLS连接.OkHttp的TLS 握手是必要的
5、发送Http请求、读取响应
如果连接出现了问题,OkHttp会选择另外一个路由然后再次尝试。当一个服务器的地址的一个子集不可达时,OkHttp能够自动恢复。当连接池是过时或者试图TLS版本不受支持时,这种方式是很有用的。
一旦接收到了响应,连接就会被返回给连接池留着之后复用.连接在连接池中闲置一段时间后,它会被清除。
Recipes
我们已经写了一些方法声明OkHttp是如何解决一些常见的问题.同步Get
下载一个文件,打印headers和以String字符串的方式打印响应体。
响应体的string()方法对于小的文件是一个方便高效的方法.但是如果响应体超过1MB,应该避免使用string()因为它会将整个文件加载到内存中,在这种场景下,可以使用流来进行处理
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.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());
}
}
异步Get
在工作线程下载一个文件,当响应可读的时候,会得到回调。这个回调在响应头准备好了之后产生.读取一个响应体可能会一直阻塞,目前OkHttp不提供异步API来接收响应体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(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());
}
}
});
}
获取headers
典型的Http Headers是Map<String,String>:每一个字段有一个值或者空值。但是一些headers允许多个值,像Guava的Multimap。比如,对于一个Http响应提供多个Vary 头。OkHttp的api试图使这两种情况都适用。当写请求头的时候,使用header(name,value)去设置name对应的唯一值。如果已经存在了,将会被新添加的值覆盖。使用addHeader(name,value)添加一个头不会覆盖已经存在的值。
当读取响应头的时候,使用head(name)返回最后设置的value值.如果没有获取到,则header(name)会返回null。如果想读取一个字段的对应的值的列表,可以使用headers(name)方法
为了访问所有的头,可以使用headers类,该类支持通过索引获取值
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"));
}
}
提交字符串
使用一个Http POST向服务发送一个请求体。这个例子是向Web服务发送了一个markdown文档,该Web服务可以将markdown文档渲染成一个HTML。因为整个请求体是被放置到内存中的,因此避免使用这个Api发送大文档(大于1MB)。
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
提交流
此处我们以流的形式提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
提交文件
使用文件作为请求体是非常简单的。
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());
}
}
提交表单参数
使用FormBody.Builder去构建一个请求体,作用和HTML中的<form>标签一样。键值对将使用一种HTML兼容形式的URL编码来进行编码。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());
}
}
提交分块请求
MultipartBody.Builder能够构建一个与HTML 文件上传表单相兼容的复杂请求体。分块请求的每一个部分本身就是一个请求体,可以单独定义其headers,这些请求头可以用来描述这块请求,例如他的Content-Disposition
。如果Content-Length
和Content-Type
可用的话,他们会被自动添加到请求头中。 /**
* The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
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());
}
}
使用MoShi解析一个JSON 响应
MoShi是一个很方便的Api,实现了Json与Java Objects之间的转换。下面这个例子,我们使用MoShi去解析一个来自GitHub Api的Json响应。
注意当解析一个响应体,ResponseBody.charStream() 会使用 Content-Type响应头选择使用哪一种字符集。默认情况下是UTF-8.
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;
}
响应缓存
为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。
一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp()
,在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。
响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600
,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600
。
有一些缓存heads会强制响应缓存,强制网络响应,或者通过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.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));
}
为了防止响应使用缓存,可以使用
CacheControl.FORCE_NETWORK,
为了防止使用网络,可以使用CacheControl.FORCE_CACHE
,注意如果你使用了FORCE_CACHE,但是响应需要使用网络,那么OKHttp会返回504Unsatisfiable Request响应。
取消一个Call
使用Call.cancel()可以立即终止一个请求。如果一个线程正在进行写请求或者读响应,则会收到一个IOException异常。当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。
你可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)
来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)
来取消所有带有这个tag的call。
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);
}
}
超时
当服务端不可达的时候,超时机制可以使请求结束。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。
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);
}
}
每个Call的配置
OKHTTPClient的所有的配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,调用OkHttpClient.newBuilder(),该方法返回共用一个连接池、分发器(dispatcher)和配置的原始Client。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。
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);
}
}
处理验证
OKHttp会自动重试未验证的请求。当一个响应是401未授权的时候,Authenticator 会被要求提供证书,Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。
可以使用 Response.challenges()
方法获取任何一个authentication challenges的schemes and realms of any . 当需要实现一
个Basic
challenge, 使用Credentials.basic(username, password)
来编码请求头。
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());
}
}
当授权无效的时候,为了避免多次重试,你可以返回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.
}
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
本篇翻译部分参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html,感谢。