本文结构
HTTP客户端的工作是接受你的request,并产生它的response。这个在理论上是简单的,但在实践中确是很棘手。
每一个HTTP请求中都包含一个URL,一个方法(如GET或POST),和一个请求头列表(headers)。请求还可以含有一个请求体(body):一个特定内容类型的数据流。
每一个HTTP响应中都包含一个状态码(如200代表成功,404代表未找到),一个响应头列表(headers)和一个可选的响应体(body)。
当你的OkHttp发送一个HTTP请求,你在描述一个高层次的要求:“给我获取这个网址中的这些请求头。”对于正确性和效率,OkHttp发送前会重写你的请求。
OkHttp可以在原先的请求中添加请求头(headers),包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type。除非请求头已经存在压缩响应,否则它还将添加一个Accept-Encoding请求头。如果你有cookies,OkHttp还将添加一个Cookie请求头。
一些请求会有一个缓存的响应。当这个缓存的响应不是最新的时候,OkHttp会发送一个有条件的GET来下载更新的响应,如果它比缓存还新。它将会添加需要的请求头,如IF-Modified-Since和If-None-Match。
如果使用的是透明压缩,OkHttp会丢失相应的响应头Content-Encoding和Content-Length,这是因为它们不能用于解压响应体(body)。
如果一个条件GET是成功的,在指定的规范下,响应来自于网络和缓存的合并。
当你的请求的URL已经移动,Web服务器将返回一个响应码像302,以表明本文档的新的URL。OkHttp将按照重定向检索最终响应。
如果响应问题是一个的授权盘问,OkHttp将会要求身份验证(如果有一个已经配置好),以满足盘问。如果身份验证提供凭据,请求将会带着凭证进行重试。
有时连接失败:要么是连接池已经过时和断开,或是Web服务器本身无法达成。如果有一个是可用的,OkHttp将会使用不同的路由进行请求重试。
随着重写,重定向,后续和重试,你简单的要求可能会产生很多请求和响应。OkHttp使用呼叫(Call)并通过许多必要的中间请求和响应来满足你请求的任务模型。通常情况,这是不是很多!如果您的网址被重定向,或者如果您故障转移到另一个IP地址,但它会欣慰的知道你的代码会继续工作。
通过以下两种方式进行呼叫:
- 同步:直到响应,你的线程块是可读的。
- 异步:你在任何线程进行排队请求,并且当响应是可读的时候,你会在另一个线程得到回调。
呼叫(Calls)可以在任何线程中取消。如果它尚未完成,它将作为失败的呼叫(Calls)!当呼叫(Call)被取消的时候,如果代码试图进行写请求体(request body)或读取响应体(response body)会遭受IOException异常。
对于同步调用,你带上你自己的线程,并负责管理并发请求。并发连接过多浪费资源; 过少的危害等待时间。
对于异步调用,调度实现了最大同时请求策略。您可以设置每个Web服务器最大值(默认值为5),和整体值(默认为64)。
虽然只提供了URL,但是OkHttp计划使用三种类型连接到你的web服务器:URL, Address, 和 Route。
URLs(如https://github.com/square/okhttp)是HTTP和因特网的基础。除了是网络上通用和分散的命名方案,他们还指定了如何访问网络资源。
他们还具体:每个URL识别特定的路径(如 /square/okhttp)和查询(如 ?q=sharks&lang=en)。每个Web服务器主机的网址。
Addresses指定网络服务器(如github.com)和所有的静态必要的配置,以及连接到该服务器:端口号,HTTPS设置和首选的网络协议(如HTTP / 2或SPDY)。
共享相同地址的URL也可以共享相同的基础TCP套接字连接。共享一个连接有实实在在的性能优点:更低的延迟,更高的吞吐量(由于TCP慢启动)和保养电池。OkHttp使用的ConnectionPool自动重用HTTP / 1.x的连接和多样的HTTP/ 2和SPDY连接。
在OkHttp地址的某些字段来自URL(scheme, hostname, port),其余来自OkHttpClient。
Routes提供连接到一个网络服务器所必需的动态信息。就是尝试特定的IP地址(如由DNS查询发现),使用确切的代理服务器(如果一个特定的IP地址的ProxySelector在使用中)和协商的TLS版本(HTTPS连接)。
可能有单个地址对应多个路由。例如,在多个数据中心托管的Web服务器,它可能会在其DNS响应产生多个IP地址。
当你使用OkHttp进行一个URL请求时,下面是它的操作流程:
一旦响应已经被接收到,该连接将被返回到池中,以便它可以在将来的请求中被重用。连接在池中闲置一段时间后,它会被赶出。
我们已经写了一些方法,演示了如何解决OkHttp常见问题。通过阅读他们了解一切是如何正常工作的。可以自由剪切和粘贴这些例子。
下载文件,打印其头部,并以字符串形式打印其响应体。
该string() 方法在响应体中是方便快捷的小型文件。但是,如果响应体较大(大于1 MIB以上),它会将整个较大文件加载到内存中,所以应该避免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());
}
下载一个工作线程的文件,当响应是可读的时候,获取回调(Callback)。当响应头已经准备好后,将产生回调(Callback)。读取响应体可能一直阻塞。目前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 {
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(response.body().string());
}
});
}
典型的HTTP头工作就像一个Map<String, String> :每个字段都有一个值或无值。但是,一些头部(headers)允许多个值,比如Guava的Multimap。例如,它共同为一个HTTP响应提供多个Vary头。OkHttp的API,试图使这两种情况下都能舒适使用。
当写请求头,用header(name, value)来为唯一出现的name设置value。如果它本身存在值,在添加新的value之前,他们会被移除。使用addHeader(name, value)来添加头部不需要移除当前存在的headers。
当读取响应头,用header(name)返回最后设置name的value。如果没有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();
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的请求体发送到服务。下面例子post了一个markdown文档到一个的Web服务(将markdown作为HTML)。由于整个请求体是同时在内存中,应避免使用此API发送较大(大于1 MIB)的文件。
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请求体作为stream。将正在生成请求体的内容写入到stream中。下面例子streams直接进入 Okio缓冲水槽。你的程序可能更喜欢使用OutputStream,你可以通过BufferedSink.outputStream()获得 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());
}
将文件作为请求体是很容易的。
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());
}
使用FormBody.Builder建立一个请求体,它就像一个HTML 的标记。Names 和values将使用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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
MultipartBody.Builder可以构建与HTML文件上传表单兼容的复杂请求主体。multipart请求体的每一部分本身就是请求体,并且可以定义自己的头部。如果存在,这些头应该描述的部分请求体,如它的Content-Disposition。如果Content-Length 和 Content-Type头部可以使用,则他们会自动添加。
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();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
GSON是实现JSON和Java对象之间便利转换的API。这里,我们用它来解码从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 entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}
static class Gist {
Map files;
}
static class GistFile {
String content;
}
要缓存响应,你需要有一个缓存目录来进行读取和写入,并限制缓存的大小。缓存目录应该是私有的,不被信任的应用程序不能够阅读其内容!
多个缓存同时访问相同的缓存目录,这是错误的。大多数应用程序应该调用一次new OkHttpClient(),在任何地方都使用相同的实例和自己的缓存配置。否则,这两个缓存实例将踩到对方,破坏响应缓存,这可能使你的程序崩溃。
响应缓存使用HTTP头进行配置。您可以添加请求头Cache-Control: max-stale=3600,这样OkHttp的缓存就会遵循他们。你的网络服务器可以通过自己的响应头配置缓存多长时间的响应,如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.Builder()
.cache(cache)
.build();
}
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可以禁止使用网络。警告:如果您使用FORCE_CACHE和响应来自网络,OkHttp将会返回一个504不可满足请求的响应。
通过Call.cancel()来立即停止正在进行的Call。如果一个线程目前正在写请求或读响应,它还将收到一个IOException异常。当一个Call不需要时,使用取消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);
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);
}
}
当无法访问查询时,将调用超时失败。超时在网络划分中可以是由于客户端连接问题,服务器可用性的问题,或两者之间的任何东西。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();
Response response = client.newCall(request).execute();
System.out.println("Response completed: " + response);
}
所有的HTTP客户端都在OkHttpClient中配置,这包括代理设置,超时和缓存。当你需要改变单一Call的配置时,调用OkHttpClient.newBuilder() 。这将返回共享相同的连接池,调度和配置的原客户端的建造器。在下面的例子中,我们做了500毫秒超时,另外一个3000毫秒超时请求。
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 {
// Copy to customize OkHttp for this request.
OkHttpClient copy = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
Response response = copy.newCall(request).execute();
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
try {
// Copy to customize OkHttp for this request.
OkHttpClient copy = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
Response response = copy.newCall(request).execute();
System.out.println("Response 2 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 2 failed: " + e);
}
}
OkHttp能够自动重试未经授权的请求。当响应是401 Not Authorized,一个Authenticator被要求提供凭据。应该建立一个包含缺少凭据的新要求。如果没有凭证可用,则返回null跳过重试。
使用Response.challenges()获得任何认证挑战方案和领域。当完成一个基本的挑战,用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 {
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();
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 (responseCount(response) >= 3) {
return null; //如果我们已经失败了3次,放弃。 .
}
这上面的代码依赖于下面的responseCount()方法:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
拦截器是一个强大的机制,它可以监控,重写和重试Calls。下面是一个简单记录传出请求和响应传入的拦截器。
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
呼叫chain.proceed(request)是实现每个拦截器的的重要组成部分。这个看起来简单的方法是,所有的HTTP工作情况,产生满足请求的响应。
拦截器可以链接。假设你有一个可以压缩和校验的拦截器:你需要确定数据是否可以压缩,然后再执行校验,或者是先校验然后再压缩。为了拦截器被调用,OkHttp使用列表来跟踪拦截器,。
拦截器可以注册为应用拦截器或网络拦截器。我们将使用LoggingInterceptor来区别。
通过在OkHttpClient.Builder上调用addInterceptor()来注册应用程序拦截器:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
该URL http://www.publicobject.com/helloworld.txt重定向到https://publicobject.com/helloworld.txt,并OkHttp遵循这种自动重定向。我们的应用拦截器被调用一次,并且从返回的响应chain.proceed()具有重定向:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
我们可以看到,我们被重定向是因为response.request().url()不同于request.url() 。这两个日志语句记录两个不同的URL。
注册网络拦截器很类似。调用addNetworkInterceptor()代替addInterceptor() :
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
当我们运行这段代码,拦截器运行两次。一个是初始请求http://www.publicobject.com/helloworld.txt,另一个是用于重定向到https://publicobject.com/helloworld.txt。
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
网络请求还包含很多数据,如OkHttp加入Accept-Encoding: gzip头部通知支持压缩响应。网络拦截器的链具有非空的连接,它可用于询问IP地址和连接到网络服务器的TLS配置。
每个拦截器链(interceptor chain)都具有相对优势。
拦截器可以添加,删除或替换请求头。他们还可以转换请求体。例如,如果你连接到已知支持它的网络服务器,你可以使用应用程序拦截器添加请求体的压缩。
/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}
Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}
private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}
@Override public long contentLength() {
return -1; // We don't know the compressed length in advance!
}
@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
}
相对应的,拦截器也可以重写响应头和转换响应体。通常不要重写请求头,因为它可能违反了Web服务器的期望导致更危险!
在一个棘手的情况下,如果已经做好应对的后果,重写响应头是解决问题的有效方式。例如,您可以修复服务器的配置错误的Cache-Control响应头以便更好地响应缓存:
/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
}
};
通常此方法效果最好,它补充了在Web服务器上相应的修复!
OkHttp的拦截器需要OkHttp 2.2或更高。不幸的是,拦截器不能与OkUrlFactory工作,或者建立在这之上的库,包括 Retrofit ≤1.8和 Picasso≤2.4。
OkHttp试图平衡两个相互竞争的担忧:
当涉及到HTTPS服务器的连接,OkHttp需要知道提供哪些TLS版本和密码套件。如果客户端想要最大限度地连接包括过时的TLS版本和弱由设计的密码套件。通过使用最新版本的TLS和实力最强的加密套件来最大限度地提高客户端的安全性。
具体的安全与连接是由ConnectionSpec接口决定。OkHttp包括三个内置的连接规格:
在每一个规范的TLS版本和密码套件都可随每个发行版而更改。例如,在OkHttp 2.2,我们下降支持响应POODLE攻击的SSL 3.0。而在OkHttp 2.3我们下降的支持RC4。对于桌面Web浏览器,保持最新的OkHttp是保持安全的最好办法。
你可以用一组自定义TLS版本和密码套件建立自己的连接规格。例如,限制配置三个备受推崇的密码套件。它的缺点是,它需要的Android 5.0+和一个类似的电流网络服务器
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Collections.singletonList(spec))
.build();
默认情况下,OkHttp信任主机平台的证书颁发机构。这种策略最多的连接,但它受证书颁发机构的袭击,如2011 DigiNotar的攻击。它还假定您的HTTPS服务器的证书是由证书颁发机构签署。
使用CertificatePinner来限制哪些证书和证书颁发机构是可信任的。证书钉扎增强了安全性,但这会限制你的服务器团队更新自己的TLS证书。在没有你的服务器的TLS管理员的同意下,不要使用证书钉扎!
public CertificatePinning() {
client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.build())
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/robots.txt")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
for (Certificate certificate : response.handshake().peerCertificates()) {
System.out.println(CertificatePinner.pin(certificate));
}
}
下面完整的代码示例演示了如何用自定义证书替换主机平台的证书。如上所述,在没有你的服务器的TLS管理员的同意下,不要使用自定义证书!
private final OkHttpClient client;
public CustomTrust() {
SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream());
client = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory())
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
}
private InputStream trustedCertificatesInputStream() {
... // Full source omitted. See sample.
}
public SSLContext sslContextForTrustedCertificates(InputStream in) {
... // Full source omitted. See sample.
}