本文結(jié)構(gòu)
HTTP客戶真?zhèn)€工作是接受你的request,并產(chǎn)生它的response。這個(gè)在理論上是簡(jiǎn)單的,但在實(shí)踐中確是很辣手。
每個(gè)HTTP要求中都包括1個(gè)URL,1個(gè)方法(如GET或POST),和1個(gè)要求頭列表(headers)。要求還可以含有1個(gè)要求體(body):1個(gè)特定內(nèi)容類型的數(shù)據(jù)流。
每個(gè)HTTP響應(yīng)中都包括1個(gè)狀態(tài)碼(如200代表成功,404代表未找??到),1個(gè)響應(yīng)頭列表(headers)和1個(gè)可選的響應(yīng)體(body)。
當(dāng)你的OkHttp發(fā)送1個(gè)HTTP要求,你在描寫1個(gè)高層次的要求:“給我獲得這個(gè)網(wǎng)址中的這些要求頭。”對(duì)正確性和效力,OkHttp發(fā)送前會(huì)重寫你的要求。
OkHttp可以從原來的要求中添加要求頭(headers),包括Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type。除非要求頭已存在緊縮響應(yīng),否則它還將添加1個(gè)Accept-Encoding要求頭。如果你有cookies,OkHttp還將添加1個(gè)Cookie要求頭。
1些要求會(huì)有1個(gè)緩存的響應(yīng)。當(dāng)這個(gè)緩存的響應(yīng)不是最新的,OkHttp會(huì)發(fā)送1個(gè)有條件的GET來下載更新的響應(yīng),如果它比緩存還新。它將會(huì)添加需要的要求頭,如IF-Modified-Since和If-None-Match。
如果使用的是透明緊縮,OkHttp會(huì)丟失相應(yīng)的響應(yīng)頭Content-Encoding和Content-Length,這是由于它們不能用于解壓響應(yīng)體(body)。
如果1個(gè)條件GET是成功的,在唆使的規(guī)范下響應(yīng)來自于網(wǎng)絡(luò)和緩存的合并。
當(dāng)你的要求的URL已移動(dòng),Web服務(wù)器將返回1個(gè)響應(yīng)碼像302,以表明本文檔的新的URL。OkHttp將依照重定向檢索的終究響應(yīng)。
如果響應(yīng)問題是1個(gè)的授權(quán)盤問,OkHttp將會(huì)要求身份驗(yàn)證(如果有1個(gè)已配置好),以滿足盤問。如果身份驗(yàn)證提供憑據(jù),要求將會(huì)帶著憑證進(jìn)行重試。
有時(shí)連接失敗:要末是連接池已過時(shí)和斷開,或是Web服務(wù)器本身沒法達(dá)成。如果有1個(gè)是可用的,OkHttp將會(huì)使用不同的路由進(jìn)行要求重試。
隨側(cè)重寫,重定向,后續(xù)和重試,你簡(jiǎn)單的要求可能會(huì)產(chǎn)生很多要求和響應(yīng)。OkHttp使用呼喚(Call)并通過許多必要的中間要求和響應(yīng)來滿足你要求的任務(wù)模型。通常情況,這是否是很多!如果您的網(wǎng)址被重定向,或如果您故障轉(zhuǎn)移到另外一個(gè)IP地址,但它會(huì)欣慰的知道你的代碼會(huì)繼續(xù)工作。
通過以下兩種方式進(jìn)行呼喚:
- 同步:直到響應(yīng),你的線程塊是可讀的。
- 異步:你在任何線程進(jìn)行排隊(duì)要求,并且當(dāng)響應(yīng)是可讀的時(shí)候,你會(huì)在另外一個(gè)線程得到回調(diào)。
呼喚(Calls)可以在任何線程中取消。如果它還沒有完成,它將作為失敗的呼喚(Calls)!當(dāng)呼喚(Call)被取消的時(shí)候,如果代碼試圖進(jìn)行寫要求體(request body)或讀取響應(yīng)體(response body)會(huì)遭受IOException異常。
對(duì)同步調(diào)用,你帶上你自己的線程,并負(fù)責(zé)管理并發(fā)要求。并發(fā)連接過量浪費(fèi)資源; 過少的危害等待時(shí)間。
對(duì)異步調(diào)用,調(diào)度實(shí)現(xiàn)了最大同時(shí)要求策略。您可以設(shè)置每一個(gè)Web服務(wù)器最大值(默許值為5),和整體(默許為64)。
雖然只提供了URL,但是OkHttp計(jì)劃使用3種類型連接你的web服務(wù)器:URL, Address, 和 Route。
URLs(如https://github.com/square/okhttp)是HTTP和因特網(wǎng)的基礎(chǔ)。除是網(wǎng)絡(luò)上通用的和分散的命名方案,他們還指定了如何訪問網(wǎng)絡(luò)資源。
他們還具體:每一個(gè)URL辨認(rèn)特定的路徑(如 /square/okhttp)和查詢(如 ?q=sharks&lang=en)。每一個(gè)Web服務(wù)器主機(jī)的網(wǎng)址。
Addresses指定網(wǎng)絡(luò)服務(wù)器(如github.com)和所有的靜態(tài)必要的配置,和連接到該服務(wù)器:端口號(hào),HTTPS設(shè)置和首選的網(wǎng)絡(luò)協(xié)議(如HTTP / 2或SPDY)。
同享相同地址的URL也能夠同享相同的基礎(chǔ)TCP套接字連接。同享1個(gè)連接有實(shí)實(shí)在在的性能優(yōu)點(diǎn):更低的延遲,更高的吞吐量(由于TCP慢啟動(dòng))和保養(yǎng)電池。OkHttp使用的ConnectionPool自動(dòng)重用HTTP / 1.x的連接和多樣的HTTP/ 2和SPDY連接。
在OkHttp地址的某些字段來自URL(scheme, hostname, port),其余來自O(shè)kHttpClient。
Routes提供連接到1個(gè)網(wǎng)絡(luò)服務(wù)器所必須的動(dòng)態(tài)信息。就是嘗試特定的IP地址(如由DNS查詢發(fā)現(xiàn)),使用確切的代理服務(wù)器(如果1個(gè)特定的IP地址的ProxySelector在使用中)和協(xié)商的TLS版本(HTTPS連接)。
可能有單個(gè)地址對(duì)應(yīng)多個(gè)路由。例如,在多個(gè)數(shù)據(jù)中心托管的Web服務(wù)器,它可能會(huì)在其DNS響應(yīng)產(chǎn)生多個(gè)IP地址。
當(dāng)你使用OkHttp進(jìn)行1個(gè)URL要求,下面是它的操作流程:
1旦響應(yīng)已被接收到,該連接將被返回到池中,以便它可以在將來的要求中被重用。連接在池中閑置1段時(shí)間后,它會(huì)被趕出。
我們已寫了1些方法,演示了如何解決OkHttp常見問題。通過瀏覽他們了解1切是如何正常工作的。可以自由剪切和粘貼這些例子。
下載文件,打印其頭部,并以字符串情勢(shì)打印其響應(yīng)體。
該string() 方法在響應(yīng)體中是方便快捷的小型文件。但是,如果響應(yīng)體是大的(大于1 MIB以上),它會(huì)在全部文件加載到內(nèi)存中,所以應(yīng)當(dāng)避免string() 。在這類情況下,更偏向于將響應(yīng)體作為流進(jìn)行處理。
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個(gè)工作線程的文件,當(dāng)響應(yīng)是可讀的時(shí)候,獲得回調(diào)(Callback)。當(dāng)響應(yīng)頭已準(zhǔn)備好后,將產(chǎn)生回調(diào)(Callback)。讀取響應(yīng)體可能1直阻塞。目前OkHttp不提供異步API來接收響應(yīng)體的部位。
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個(gè)Map<String, String> :每一個(gè)字段都有1個(gè)值或無值。但是,1些頭部(headers)允許多個(gè)值,比如Guava的Multimap。例如,它共同為1個(gè)HTTP響應(yīng)提供多個(gè)Vary頭。OkHttp的API,試圖使這兩種情況下都能舒適使用。
當(dāng)寫要求頭,用header(name, value)來為唯1出現(xiàn)的name設(shè)置value。如果存在現(xiàn)有值,在添加新的value之前,他們將被移除。使用addHeader(name, value)來添加頭部不需要移除當(dāng)前存在的headers。
當(dāng)讀取響應(yīng)頭,用header(name)返回最后設(shè)置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的要求體發(fā)送到服務(wù)。下面例子post了1個(gè)markdown文檔到1個(gè)的Web服務(wù)(將markdown作為HTML)。由于全部要求體是同時(shí)在內(nèi)存中,應(yīng)避免使用此API發(fā)送較大(大于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。正在生成要求體的內(nèi)容寫入到stream中。下面例子streams直接進(jìn)入 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個(gè)要求體,它就像1個(gè)HTML 的標(biāo)記。Names 和values將使用HTML兼容的表單URL編碼進(jìn)行編碼。
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可以構(gòu)建與HTML文件上傳表單兼容的復(fù)雜的要求主體。multipart要求體的每部份本身就是要求體,并且可以定義自己的頭部。如果存在,這些頭應(yīng)當(dāng)描寫的部份要求體,如它的Content-Disposition。如果Content-Length 和 Content-Type頭部可使用,則他們會(huì)自動(dòng)添加。
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個(gè)JSON和Java對(duì)象之間的便利轉(zhuǎn)換的API。這里,我們用它來解碼從GitHub的API 響應(yīng)的JSON。
需要注意的是ResponseBody.charStream()使用的Content-Type響應(yīng)頭進(jìn)行解碼時(shí),所使用的字符集,如果沒有指定字符集,它默許為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;
}
要緩存響應(yīng),你需要1個(gè)緩存目錄來進(jìn)行讀取和寫入,和1個(gè)緩存的大小限制。緩存目錄應(yīng)當(dāng)是私有的,不信任的利用程序不應(yīng)當(dāng)能夠?yàn)g覽其內(nèi)容!
多個(gè)緩存同時(shí)訪問相同的緩存目錄,這是毛病的。大多數(shù)利用程序應(yīng)當(dāng)調(diào)用1次new OkHttpClient(),用自己的緩存配置,在任何地方都使用相同的實(shí)例。否則,這兩個(gè)緩存實(shí)例將踩到對(duì)方,破壞響應(yīng)緩存,這可能使你的程序崩潰。
響應(yīng)緩存使用HTTP頭的所有配置。您可以添加要求頭Cache-Control: max-stale=3600和OkHttp的緩存會(huì)遵守他們。你的網(wǎng)絡(luò)服務(wù)器可以通過自己的響應(yīng)頭配置多長(zhǎng)時(shí)間緩存響應(yīng),如Cache-Control: max-age=9600。有緩存頭強(qiáng)迫緩存的響應(yīng),強(qiáng)迫網(wǎng)絡(luò)響應(yīng),或強(qiáng)迫使用條件GET驗(yàn)證的網(wǎng)絡(luò)響應(yīng)。
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));
}
為了避免使用緩存的響應(yīng),使用CacheControl.FORCE_NETWORK。為了避免它使用網(wǎng)絡(luò),使用CacheControl.FORCE_CACHE。正告:如果您使用FORCE_CACHE和響應(yīng)要求網(wǎng)絡(luò),OkHttp將會(huì)返回1個(gè)504不可滿足要求的響應(yīng)。
使用Call.cancel()立即停止正在進(jìn)行的Call。如果1個(gè)線程目前正在寫要求或讀響應(yīng),它還將收到1個(gè)IOException異常。當(dāng)1個(gè)Call不需要時(shí),使用此保護(hù)網(wǎng)絡(luò); 例如,當(dāng)用戶從利用程序?qū)Ш诫x開。同步和異步調(diào)用可以被取消。
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);
}
}
當(dāng)其查詢沒法訪問時(shí),使用超時(shí)失敗的調(diào)用。網(wǎng)絡(luò)劃分可以是由于客戶端連接問題,服務(wù)器可用性的問題,或之間的任何東西。OkHttp支持連接,讀取和寫入超時(shí)。
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中包括代理設(shè)置,超時(shí)和緩存。當(dāng)你需要改變單1Call的配置時(shí),調(diào)用OkHttpClient.newBuilder() 。這將返回同享相同的連接池,調(diào)度和配置與原來的客戶真?zhèn)€建造器。在下面的例子中,我們做了500毫秒超時(shí),另外1個(gè)3000毫秒超時(shí)要求。
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可以自動(dòng)重試未經(jīng)授權(quán)的要求。當(dāng)響應(yīng)是401 Not Authorized,1個(gè)Authenticator被要求提供憑據(jù)。實(shí)現(xiàn)應(yīng)當(dāng)建立1個(gè)包括缺少憑據(jù)的新要求。如果沒有憑證可用,則返回null跳太重試。
使用Response.challenges()取得任何認(rèn)證挑戰(zhàn)方案和領(lǐng)域。當(dāng)完成1個(gè)基本的挑戰(zhàn),用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());
}
為了不驗(yàn)證時(shí)不工作的重試,你可以返回null放棄。例如,當(dāng)這些確切的憑據(jù)已嘗試,您可以跳太重試:
if (credential.equals(response.request().header("Authorization"))) {
return null; //如果我們已使用這些憑據(jù)失敗,不重試
}
您也能夠跳太重試,當(dāng)你1個(gè)利用嘗試的次數(shù)超過了限制的次數(shù):
if (responseCount(response) >= 3) {
return null; //如果我們已失敗了3次,放棄。 .
}
這上面的代碼依賴于這個(gè)responseCount()方法:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
攔截器是1個(gè)強(qiáng)大的機(jī)制,它可以監(jiān)控,重寫和重試Calls。下面是記錄傳出要求和響應(yīng)傳入1個(gè)簡(jiǎn)單的攔截器。
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個(gè)呼喚chain.proceed(request)是每一個(gè)攔截器的實(shí)現(xiàn)的重要組成部份。這個(gè)看起來簡(jiǎn)單的方法是,所有的HTTP工作情況,產(chǎn)生滿足要求的響應(yīng)。
攔截器可以鏈接。假定你有1個(gè)既緊縮攔截器和攔截器校驗(yàn):你需要肯定數(shù)據(jù)是不是被緊縮,然后履行校驗(yàn),或是先校驗(yàn)然后再緊縮。OkHttp使用列表來跟蹤攔截器,為了攔截器被調(diào)用。
攔截器被注冊(cè)為任1利用程序或網(wǎng)絡(luò)攔截器。我們將使用LoggingInterceptor上面定義以示區(qū)分。
注冊(cè)1個(gè)利用程序攔截器通過在OkHttpClient.Builder上調(diào)用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遵守這類自動(dòng)重定向。我們的利用程序攔截器被調(diào)用1次,并且從返回的響應(yīng)chain.proceed()具有重定向的回應(yīng):
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() 。這兩個(gè)日志語句記錄兩個(gè)不同的URL。
注冊(cè)網(wǎng)絡(luò)攔截器相當(dāng)類似。調(diào)用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();
當(dāng)我們運(yùn)行這段代碼,攔截器運(yùn)行兩次。1個(gè)是初始要求http://www.publicobject.com/helloworld.txt,另外一個(gè)是用于重定向到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
網(wǎng)絡(luò)要求還含有更多的數(shù)據(jù),如OkHttp加入Accept-Encoding: gzip頭部通知支持緊縮響應(yīng)。網(wǎng)絡(luò)攔截器的鏈具有非空的連接,它可用于詢問IP地址和用于連接到網(wǎng)絡(luò)服務(wù)器的TLS配置。
每一個(gè)攔截器鏈(interceptor chain)具有相對(duì)優(yōu)勢(shì)。
攔截器可以添加,刪除或替換要求頭。他們還可以轉(zhuǎn)換要求體。例如,如果你連接到已知支持它的網(wǎng)絡(luò)服務(wù)器,你可使用利用程序攔截器添加要求體的緊縮。
/** 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();
}
};
}
}
相對(duì)應(yīng)的,攔截器也能夠重寫響應(yīng)頭和轉(zhuǎn)換響應(yīng)體。這通常不是重寫要求頭,由于它可能違背了Web服務(wù)器的期望致使更危險(xiǎn)!
如果你在1個(gè)辣手的情況下,并做好應(yīng)對(duì)的后果,重寫響應(yīng)頭是解決問題的有效方式。例如,您可以修復(fù)服務(wù)器的配置毛病的Cache-Control響應(yīng)頭以便更好地響應(yīng)緩存:
/** 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();
}
};
通常,此方法效果最好的時(shí)候,它補(bǔ)充了在Web服務(wù)器上相應(yīng)的修復(fù)!
OkHttp的攔截器需要OkHttp 2.2或更高。不幸的是,攔截器不能與OkUrlFactory工作,或說建立在其上的庫,包括 Retrofit ≤1.8和 Picasso≤2.4。
OkHttp試圖平衡兩個(gè)相互競(jìng)爭(zhēng)的耽憂:
當(dāng)觸及到HTTPS服務(wù)器的連接,OkHttp需要知道提供哪些TLS版本和密碼套件。如果客戶端想要最大限度地連接包括過時(shí)的TLS版本和弱由設(shè)計(jì)的密碼套件。客戶端想要最大限度地提高安全性,應(yīng)當(dāng)被要求使用最新版本的TLS和實(shí)力最強(qiáng)的加密套件。
具體的安全與連接的決定是由實(shí)行ConnectionSpec接口。OkHttp包括3個(gè)內(nèi)置的連接規(guī)格:
在每個(gè)規(guī)范的TLS版本和密碼套件可隨每一個(gè)發(fā)行版而更改。例如,在OkHttp 2.2,我們降落支持響應(yīng)POODLE攻擊的SSL 3.0。而在OkHttp 2.3我們降落的支持RC4。與桌面Web閱讀器,保持最新的OkHttp是保持安全的最好辦法。
你可以用1組自定義TLS版本和密碼套件建立自己的連接規(guī)格。例如,這類配置限制為3個(gè)備受推重的密碼套件。它的缺點(diǎn)是,它需要的Andr??oid 5.0+和1個(gè)類似的電流網(wǎng)絡(luò)服務(wù)器
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信任主機(jī)平臺(tái)的證書頒發(fā)機(jī)構(gòu)。這類策略最多的連接,但它受證書頒發(fā)機(jī)構(gòu)的攻擊,如2011 DigiNotar的攻擊。它還假定您的HTTPS服務(wù)器的證書是由證書頒發(fā)機(jī)構(gòu)簽署。
使用CertificatePinner來限制哪些證書和證書頒發(fā)機(jī)構(gòu)是可信任的。證書釘扎增強(qiáng)了安全性,但限制你的服務(wù)器團(tuán)隊(duì)的能力來更新自己的TLS證書。在沒有你的服務(wù)器的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套替換主機(jī)平臺(tái)的證書頒發(fā)機(jī)構(gòu)。如上所述,在沒有你的服務(wù)器的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.
}