本文结构
HTTP客户真个工作是接受你的request,并产生它的response。这个在理论上是简单的,但在实践中确是很辣手。
每个HTTP要求中都包括1个URL,1个方法(如GET或POST),和1个要求头列表(headers)。要求还可以含有1个要求体(body):1个特定内容类型的数据流。
每个HTTP响应中都包括1个状态码(如200代表成功,404代表未找到),1个响应头列表(headers)和1个可选的响应体(body)。
当你的OkHttp发送1个HTTP要求,你在描写1个高层次的要求:“给我获得这个网址中的这些要求头。”对正确性和效力,OkHttp发送前会重写你的要求。
OkHttp可以从原来的要求中添加要求头(headers),包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type。除非要求头已存在紧缩响应,否则它还将添加1个Accept-Encoding要求头。如果你有cookies,OkHttp还将添加1个Cookie要求头。
1些要求会有1个缓存的响应。当这个缓存的响应不是最新的,OkHttp会发送1个有条件的GET来下载更新的响应,如果它比缓存还新。它将会添加需要的要求头,如IF-Modified-Since和If-None-Match。
如果使用的是透明紧缩,OkHttp会丢失相应的响应头Content-Encoding和Content-Length,这是由于它们不能用于解压响应体(body)。
如果1个条件GET是成功的,在唆使的规范下响应来自于网络和缓存的合并。
当你的要求的URL已移动,Web服务器将返回1个响应码像302,以表明本文档的新的URL。OkHttp将依照重定向检索的终究响应。
如果响应问题是1个的授权盘问,OkHttp将会要求身份验证(如果有1个已配置好),以满足盘问。如果身份验证提供凭据,要求将会带着凭证进行重试。
有时连接失败:要末是连接池已过时和断开,或是Web服务器本身没法达成。如果有1个是可用的,OkHttp将会使用不同的路由进行要求重试。
随侧重写,重定向,后续和重试,你简单的要求可能会产生很多要求和响应。OkHttp使用呼唤(Call)并通过许多必要的中间要求和响应来满足你要求的任务模型。通常情况,这是否是很多!如果您的网址被重定向,或如果您故障转移到另外一个IP地址,但它会欣慰的知道你的代码会继续工作。
通过以下两种方式进行呼唤:
- 同步:直到响应,你的线程块是可读的。
- 异步:你在任何线程进行排队要求,并且当响应是可读的时候,你会在另外一个线程得到回调。
呼唤(Calls)可以在任何线程中取消。如果它还没有完成,它将作为失败的呼唤(Calls)!当呼唤(Call)被取消的时候,如果代码试图进行写要求体(request body)或读取响应体(response body)会遭受IOException异常。
对同步调用,你带上你自己的线程,并负责管理并发要求。并发连接过量浪费资源; 过少的危害等待时间。
对异步调用,调度实现了最大同时要求策略。您可以设置每一个Web服务器最大值(默许值为5),和整体(默许为64)。
虽然只提供了URL,但是OkHttp计划使用3种类型连接你的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套接字连接。同享1个连接有实实在在的性能优点:更低的延迟,更高的吞吐量(由于TCP慢启动)和保养电池。OkHttp使用的ConnectionPool自动重用HTTP / 1.x的连接和多样的HTTP/ 2和SPDY连接。
在OkHttp地址的某些字段来自URL(scheme, hostname, port),其余来自OkHttpClient。
Routes提供连接到1个网络服务器所必须的动态信息。就是尝试特定的IP地址(如由DNS查询发现),使用确切的代理服务器(如果1个特定的IP地址的ProxySelector在使用中)和协商的TLS版本(HTTPS连接)。
可能有单个地址对应多个路由。例如,在多个数据中心托管的Web服务器,它可能会在其DNS响应产生多个IP地址。
当你使用OkHttp进行1个URL要求,下面是它的操作流程:
1旦响应已被接收到,该连接将被返回到池中,以便它可以在将来的要求中被重用。连接在池中闲置1段时间后,它会被赶出。
我们已写了1些方法,演示了如何解决OkHttp常见问题。通过浏览他们了解1切是如何正常工作的。可以自由剪切和粘贴这些例子。
下载文件,打印其头部,并以字符串情势打印其响应体。
该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());
}
下载1个工作线程的文件,当响应是可读的时候,获得回调(Callback)。当响应头已准备好后,将产生回调(Callback)。读取响应体可能1直阻塞。目前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头工作就像1个Map<String, String> :每一个字段都有1个值或无值。但是,1些头部(headers)允许多个值,比如Guava的Multimap。例如,它共同为1个HTTP响应提供多个Vary头。OkHttp的API,试图使这两种情况下都能舒适使用。
当写要求头,用header(name, value)来为唯1出现的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了1个markdown文档到1个的Web服务(将markdown作为HTML)。由于全部要求体是同时在内存中,应避免使用此API发送较大(大于1 MIB)的文件。
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf⑻");
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⑻");
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⑻");
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建立1个要求体,它就像1个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是1个JSON和Java对象之间的便利转换的API。这里,我们用它来解码从GitHub的API 响应的JSON。
需要注意的是ResponseBody.charStream()使用的Content-Type响应头进行解码时,所使用的字符集,如果没有指定字符集,它默许为UTF⑻ 。
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;
}
要缓存响应,你需要1个缓存目录来进行读取和写入,和1个缓存的大小限制。缓存目录应当是私有的,不信任的利用程序不应当能够浏览其内容!
多个缓存同时访问相同的缓存目录,这是毛病的。大多数利用程序应当调用1次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将会返回1个504不可满足要求的响应。
使用Call.cancel()立即停止正在进行的Call。如果1个线程目前正在写要求或读响应,它还将收到1个IOException异常。当1个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中包括代理设置,超时和缓存。当你需要改变单1Call的配置时,调用OkHttpClient.newBuilder() 。这将返回同享相同的连接池,调度和配置与原来的客户真个建造器。在下面的例子中,我们做了500毫秒超时,另外1个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,1个Authenticator被要求提供凭据。实现应当建立1个包括缺少凭据的新要求。如果没有凭证可用,则返回null跳太重试。
使用Response.challenges()取得任何认证挑战方案和领域。当完成1个基本的挑战,用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; //如果我们已使用这些凭据失败,不重试
}
您也能够跳太重试,当你1个利用尝试的次数超过了限制的次数:
if (responseCount(response) >= 3) {
return null; //如果我们已失败了3次,放弃。 .
}
这上面的代码依赖于这个responseCount()方法:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
拦截器是1个强大的机制,它可以监控,重写和重试Calls。下面是记录传出要求和响应传入1个简单的拦截器。
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;
}
}
1个呼唤chain.proceed(request)是每一个拦截器的实现的重要组成部份。这个看起来简单的方法是,所有的HTTP工作情况,产生满足要求的响应。
拦截器可以链接。假定你有1个既紧缩拦截器和拦截器校验:你需要肯定数据是不是被紧缩,然后履行校验,或是先校验然后再紧缩。OkHttp使用列表来跟踪拦截器,为了拦截器被调用。
拦截器被注册为任1利用程序或网络拦截器。我们将使用LoggingInterceptor上面定义以示区分。
注册1个利用程序拦截器通过在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遵守这类自动重定向。我们的利用程序拦截器被调用1次,并且从返回的响应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();
当我们运行这段代码,拦截器运行两次。1个是初始要求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服务器的期望致使更危险!
如果你在1个辣手的情况下,并做好应对的后果,重写响应头是解决问题的有效方式。例如,您可以修复服务器的配置毛病的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包括3个内置的连接规格:
在每个规范的TLS版本和密码套件可随每一个发行版而更改。例如,在OkHttp 2.2,我们降落支持响应POODLE攻击的SSL 3.0。而在OkHttp 2.3我们降落的支持RC4。与桌面Web阅读器,保持最新的OkHttp是保持安全的最好办法。
你可以用1组自定义TLS版本和密码套件建立自己的连接规格。例如,这类配置限制为3个备受推重的密码套件。它的缺点是,它需要的Android 5.0+和1个类似的电流网络服务器
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));
}
}
完全的代码示例显示了如何用自己的1套替换主机平台的证书颁发机构。如上所述,在没有你的服务器的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.
}
上一篇 Java泛型中通配符的使用
下一篇 计算机网络面试题