本文結構
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個備受推重的密碼套件。它的缺點是,它需要的Andr??oid 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泛型中通配符的使用
下一篇 計算機網絡面試題