前言
需要下載 OkHttp3 請看這里 OkHttp3 jar 包下載
需要下載 Okio 請看這里 Okio jar 包下載
Okio 可以幫助 OkHttp 用于快速 I/O 和調整緩沖區(qū)大小。
想要了解 OkHttp 源碼請看這里 OkHttp GitHub 主頁
配置
- MAVEN
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>(insert latest version)</version>
</dependency>
- GRADLE
compile 'com.squareup.okhttp3:okhttp:(insert latest version)'
implementation("com.squareup.okhttp3:okhttp:3.12.0")
樣例
- GET
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
- POST
public static final MediaType JSON
= MediaType.parse("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
更多詳細的使用方案請看第三大點 Recipes
。
一只洒、Calls
HTTP 客戶端的工作是接受你的請求并生成其響應。這在理論上很簡單锋恬,但在實現(xiàn)時卻很復雜也物。
1.1 Requests — 請求
每個 HTTP 請求都包含一個 URL,一個方法(如 GET
或 POST
)和一個標頭列表黑低。除此之外锤灿,它還可以包含請求主體:特定內(nèi)容類型的數(shù)據(jù)流挽拔。
1.2 Responses — 響應
響應使用返回碼(例如 200 表示成功或 404 表示未找到),標頭以及自己的可選主體來響應請求但校。
1.3 Rewriting Requests — 重寫請求
當你向 OkHttp 提供 HTTP 請求時螃诅,你是在高語境中描述請求:“使用這些標頭獲取此 URL∽创眩”為了正確性和效率术裸,OkHttp 在傳輸之前會重寫你的請求。
OkHttp 可能會添加原始請求中不存在的標頭亭枷,包括 Content-Length
袭艺,Transfer-Encoding
,User-Agent
叨粘,Host
猾编,Connection
和 Content-Type
。例如升敲,它將為透明的響應壓縮添加 Accept-Encoding
標頭答倡,除非標頭已存在。如果你有 cookie驴党,OkHttp 將添加一個 Cookie
標頭苇羡。
某些請求將具有緩存響應。當這個緩存的響應不是最新時鼻弧,OkHttp 可以執(zhí)行條件 GET
來下載更新后的響應。這需要添加 If-Modified-Since
和 If-None-Match
等標頭锦茁。
1.4 Rewriting Responses — 重寫響應
如果使用透明壓縮攘轩,OkHttp 將刪除相應的響應頭部 Content-Encoding
和 Content-Length
,因為它們不適用于解壓縮的響應主體码俩。
如果條件 GET
成功度帮,則根據(jù)規(guī)范合并來自網(wǎng)絡和緩存的響應。
1.5 Follow-up Requests — 后續(xù)跟蹤請求
當你請求的 URL 已經(jīng)發(fā)生更改,Web 服務器將返回一個響應代碼笨篷,如 302瞳秽,指示文檔的新 URL。OkHttp 將遵循重定向來獲取最終響應率翅。
如果響應發(fā)出認證要求练俐,OkHttp 將要求 Authenticator
(如果配置了一個)來滿足認證。如果驗證者提供了憑證冕臭,則會使用包含憑證的請求重試連接腺晾。
1.6 Retrying Requests — 重試請求
有時連接失敗:例如某個池連接失效并斷開連接辜贵,或者無法訪問 Web 服務器本身悯蝉。如果有其他的可用路由,OkHttp 將重試該請求托慨。
1.7 Calls — 任務
通過重寫鼻由,重定向,后續(xù)跟蹤和重試厚棵,你的簡單請求可能會產(chǎn)生出許多請求和響應蕉世。OkHttp 使用 Call
來模擬滿足你的請求的任務,但是需要許多中間請求和響應窟感。通常這不會很多讨彼!但是,令人欣慰的是柿祈,如果你的 URL 被重定向或者故障轉移到備用 IP 地址哈误,你的程序將繼續(xù)有效運行。
Call
以兩種方式之一被執(zhí)行:
同步執(zhí)行:你的線程將被阻塞躏嚎,直到響應可讀蜜自。
異步執(zhí)行:你將請求排入任一線程,并在響應可讀時在另一個線程上進行回調卢佣。
可以從任何線程取消任務重荠。這將使任務失敗,如果它尚未完成虚茶。正在編寫請求正文或讀取響應正文的代碼在其調用被取消時將遇到 IOException
異常戈鲁。
1.8 Dispatch — 調度
對于同步調用,你需要自己處理線程嘹叫,并負責管理同時發(fā)起的請求數(shù)婆殿。同時連接太多會浪費資源;太少會損害延遲罩扇。
對于異步調用婆芦,Dispatcher(調度器)
實現(xiàn)最大同時請求的策略怕磨。你可以設置每個網(wǎng)絡服務器的最大值(默認值為 5)和總體數(shù)(默認值為 64)。
二消约、Connections
雖然你只提供 URL肠鲫,但 OkHttp 使用三種類型設計其與 Web 服務器的連接:URL,地址和路由或粮。
2.1 URLs
URLs(如 https://github.com/square/okhttp
)是 HTTP 和 Internet 的基礎导饲。它們指定了如何訪問 Web 資源。
URLs 是抽象的:
它們指定網(wǎng)絡呼叫可以是明文(
http
)或加密(https
)被啼,但不應使用某種加密算法帜消。它們也沒有指定如何驗證對等方的證書(HostnameVerifier)或可以信任哪些證書(SSLSocketFactory)。它們不指定是否應該使用特定代理服務器或如何使用該代理服務器進行身份驗證浓体。
它們也具體定義了:每個 URL 標識一個特定的路徑(如 /square/okhttp
)和查詢(如 ?q=sharks&lang=en
)泡挺。每個 Web 服務器都托管許多 URL。
2.2 Addresses — 地址
地址特指 Web 服務器(如 github.com
)以及連接到該服務器所需的所有靜態(tài)配置:端口號命浴,HTTPS 設置和首選網(wǎng)絡協(xié)議(如 HTTP / 2 或 SPDY)娄猫。
共享相同地址的 URL 也可以共享相同的底層 TCP 套接字連接。共享連接具有顯著的性能優(yōu)勢:更低的延遲生闲,更高的吞吐量(由于 TCP 的慢啟動)和節(jié)省電池媳溺。OkHttp 使用 ConnectionPool
,它自動重用 HTTP / 1.x 連接并多路復用 HTTP / 2 和 SPDY 連接碍讯。
在 OkHttp 中悬蔽,地址的某些字段來自 URL(模式,主機名捉兴,端口)蝎困,其余字段來自 OkHttpClient。
2.3 Routes — 路由
路由提供實際連接到 Web 服務器所需的動態(tài)信息倍啥。包括要嘗試的特定 IP 地址(由 DNS 查詢發(fā)現(xiàn))禾乘,要使用的確切代理服務器(如果正在使用 ProxySelector
),以及要協(xié)商的 TLS 版本(對于 HTTPS 連接)虽缕。
對于單個地址可能有很多路由始藕。例如,托管在多個數(shù)據(jù)中心中的 Web 服務器可能會在其 DNS 響應中生成多個 IP 地址氮趋。
2.4 Connections — 連接
當你使用 OkHttp 請求 URL 時伍派,它將起到如下作用:
使用 URL 和已配置的 OkHttpClient 來創(chuàng)建地址。此地址指定我們將如何連接到 Web 服務器剩胁。
它嘗試從連接池中檢索與該地址的連接拙已。
如果它在池中找不到連接,則會選擇要嘗試的路由摧冀。這通常意味著發(fā)出 DNS 請求以獲取服務器的 IP 地址。然后,如有必要索昂,它會選擇 TLS 版本和代理服務器建车。
如果它是一條新路由,則通過構建套接字連接椒惨,TLS 隧道(通過 HTTP 代理的 HTTPS)或直接用 TLS 連接來連接缤至。它根據(jù)需要進行 TLS 握手。
它發(fā)送 HTTP 請求并讀取響應康谆。
如果連接出現(xiàn)問題领斥,OkHttp 將選擇另一條路由并重試。這允許 OkHttp 在服務器地址無法訪問時進行恢復沃暗。對于池連接過時或者不支持當前的 TLS 版本的問題也很有幫助月洛。
收到響應后,連接將返回到連接池中孽锥,以便重新用于之后的請求嚼黔。若一段時間閑置后租幕,該連接將從池中剔除嫩与。
三、Recipes — 使用方案
以下是一些使用方案宫补,演示了如何使用 OkHttp 解決常見問題盛撑。
3.1 Synchronous Get — 同步 Get 操作
下載一個文件碎节,將響應頭部和響應體作為字符串打印出來。
對于小文檔來說抵卫,使用 response.body.string()
方法將響應體轉換為字符串非常方便有效狮荔。但是如果響應體很大(大于 1 MiB),則避免使用 string()
陌僵,因為它會將整個文檔加載到內(nèi)存中轴合。在這種情況下,應該將響應體轉換為流來處理碗短。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
}
3.2 Asynchronous Get — 異步 Get 操作
在子線程中下載文件受葛,并在響應可讀時進行回調。
回調是在響應標頭準備好之后進行的偎谁。讀取響應體時仍可能會阻塞总滩。OkHttp 目前不提供異步 API 來接收部分響應主體。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(responseBody.string());
}
}
});
}
3.3 Accessing Headers — 獲取請求頭部 / 響應頭部
一般來說巡雨,HTTP 標頭的形式類似于 Map <String闰渔,String>
,每個字段都有一個值或沒有铐望。但是有一些標頭允許有多個值冈涧,比如 Guava 的 Multimap茂附。例如,HTTP 響應中提供多個 Vary
標頭值是合法且常見的督弓。
在編寫請求頭部時营曼,可以使用 header(name,valuse)
來設置唯一的 (name,value)
對。如果該命名已經(jīng)存在值愚隧,則在添加新值之前會將舊值刪除蒂阱。使用 addHeader(name,value)
添加頭部將不會刪除已存在的頭部狂塘。
讀取響應頭部時录煤,使用 response.header(name)
返回最后一次出現(xiàn)的該命名值,通常這也是唯一出現(xiàn)的荞胡。如果不存在該值妈踊,則 header(name)
將返回 null。如果要將所有該字段的值作為列表讀取硝训,可以使用 headers(name)
响委。
要訪問所有頭部,請使用支持索引訪問的 Headers
類窖梁。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}
}
3.4 Posting a String — 提交字符串
使用 HTTP POST 將請求體發(fā)送給服務器赘风。以下示例是將一個 markdown 文檔提交到 Web 服務器中,將 markdown 呈現(xiàn)為 HTML纵刘。因為整個請求體會被加載到內(nèi)存中邀窃,因此避免使用此 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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.5 Post Streaming — 提交流
在這里我們將請求體作為流提交假哎,請求體的內(nèi)容是動態(tài)生成的瞬捕。此示例的流直接進入 Okio 緩沖接收器。你的程序可能更喜歡 OutputStream
舵抹,你可以從 BufferedSink.outputStream()
獲取它肪虎。
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}
private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.6 Posting a File — 提交文件
將文件作為請求主體很簡單。
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
File file = new File("README.md");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.7 Posting form parameters — 提交表單參數(shù)
使用 FormBody.Builder
構建一個像 HTML <form>
標記的請求體惧蛹。名稱和值將使用與 HTML 兼容的表單 URL 編碼格式進行編碼扇救。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.8 Posting a multipart request — 提交多部分請求
MultipartBody.Builder
可以構建與 HTML 文件上傳表單兼容的復雜請求主體。多部分請求主體的每個部分本身都是一個請求主體香嗓,并且可以定義自己的頭部迅腔,這些頭部應該描述自身主體,例如其 Content-Disposition
靠娱。如果 Content-Length
和 Content-Type
頭部可用沧烈,則會自動添加它們。
/**
* The imgur client ID for OkHttp recipes.
* If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
3.9 Parse a JSON Response With Moshi — 使用 Moshi 解析 JSON 響應
Moshi 是一個方便的 API像云,用來在 JSON 和 Java 對象之間進行轉換锌雀。在這里蚂夕,我們使用它來解碼一個來自 GitHub API 的 JSON 響應。
請注意腋逆,ResponseBody.charStream()
使用 Content-Type
響應頭來選擇在解碼響應主體時使用哪個 charset双抽。如果沒有指定 charset,則默認為 UTF-8
闲礼。
private final OkHttpClient client = new OkHttpClient();
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gistJsonAdapter.fromJson(response.body().source());
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}
}
static class Gist {
Map<String, GistFile> files;
}
static class GistFile {
String content;
}
3.10 Response Caching — 緩存響應
要緩存響應,你需要一個可以讀取和寫入的緩存目錄铐维,以及限制緩存大小柬泽。緩存目錄應該是私有的,不受信任的應用程序不應該能夠讀取其內(nèi)容嫁蛇!
讓多個緩存同時訪問同一緩存目錄是錯誤的锨并。大多數(shù)應用程序應該只調用一次 new OkHttpClient()
,使用自身的緩存配置它睬棚,并在全局使用相同的 OkHttpClient
實例第煮。否則,兩個緩存實例將相互影響抑党,破壞響應緩存包警,并可能導致程序崩潰。
響應緩存使用 HTTP 標頭進行所有配置底靠。你可以添加請求頭部害晦,如 Cache-Control:max-stale = 3600
,OkHttp 的緩存將遵循它們暑中。你的 Web 服務器使用自己的響應標頭配置緩存響應的時間壹瘟,例如 Cache-Control:max-age = 9600
。緩存標頭可強制緩存響應鳄逾,強制網(wǎng)絡響應稻轨,或強制使用條件 GET 驗證網(wǎ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();
String response1Body;
try (Response response1 = client.newCall(request).execute()) {
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
}
String response2Body;
try (Response response2 = client.newCall(request).execute()) {
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
}
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
要阻止響應使用緩存雕凹,請使 CacheControl.FORCE_NETWORK
殴俱。要阻止它使用網(wǎng)絡,請使用 CacheControl.FORCE_CACHE
请琳。警告:如果你使用 FORCE_CACHE
并且響應需要網(wǎng)絡粱挡,OkHttp 將返回 504 Unsatisfiable Request
響應。
3.11 Canceling a Call — 取消任務
使用 Call.cancel()
立即停止正在進行的任務俄精。如果線程當前正在寫入請求或讀取響應询筏,則它將收到一個 IOException
。當不再需要執(zhí)行網(wǎng)絡任務時竖慧,使用它來保護網(wǎng)絡嫌套;例如逆屡,當用戶不再需要使用你的應用程序。同步和異步調用都可以取消踱讨。
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
final long startNanos = System.nanoTime();
final Call call = client.newCall(request);
// Schedule a job to cancel the call in 1 second.
executor.schedule(new Runnable() {
@Override public void run() {
System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
call.cancel();
System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
}
}, 1, TimeUnit.SECONDS);
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
try (Response response = call.execute()) {
System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
(System.nanoTime() - startNanos) / 1e9f, response);
} catch (IOException e) {
System.out.printf("%.2f Call failed as expected: %s%n",
(System.nanoTime() - startNanos) / 1e9f, e);
}
}
3.12 Timeouts — 超時
當服務端無法訪問時魏蔗,使用超時來使任務失效。網(wǎng)絡割裂可能是由于客戶端連接問題痹筛,服務器可用性問題或其他問題莺治。OkHttp 支持連接,讀取和寫入超時帚稠。
private final OkHttpClient client;
public ConfigureTimeouts() throws Exception {
client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
3.13 Per-call Configuration — 單任務配置
所有 HTTP 客戶端配置都存在于 OkHttpClient
中谣旁,包括代理設置,超時和緩存滋早。當你需要更改單個任務的配置時榄审,請調用 OkHttpClient.newBuilder()
。這將返回與原始客戶端共享相同連接池杆麸,調度程序和配置的構建器搁进。在下面的示例中,我們發(fā)出一個設置為 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();
// Copy to customize OkHttp for this request.
OkHttpClient client1 = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
try (Response response = client1.newCall(request).execute()) {
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
// Copy to customize OkHttp for this request.
OkHttpClient client2 = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
try (Response response = client2.newCall(request).execute()) {
System.out.println("Response 2 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 2 failed: " + e);
}
}
3.14 Handling authentication — 處理身份驗證
OkHttp 可以自動重試未經(jīng)身份驗證的請求。如果響應為 401 Not Authorized
减细,則要求 Authenticator
提供證書匆瓜。實現(xiàn)時應該構建一個包含缺失證書的新請求。如果沒有可用的證書未蝌,則返回 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 {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
System.out.println("Authenticating for response: " + response);
System.out.println("Challenges: " + response.challenges());
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
為了避免在身份驗證不起作用時進行多次重試,可以返回 null 以停止重試纸型。例如拇砰,你可能希望在嘗試過這些確切憑證時跳過重試:
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
當達到應用程序定義的嘗試次數(shù)限制時也可以跳過重試:
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
上面的代碼依賴于這個 responseCount()
方法:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
四、Interceptors — 攔截器
攔截器是一種強大的機制狰腌,可以監(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;
}
}
調用 chain.proceed(request)
是每個攔截器實現(xiàn)的關鍵部分。這個看起來很簡單的方法是所有 HTTP 工作發(fā)生的地方,用來產(chǎn)生滿足請求的響應光坝。
攔截器可以被鏈接尸诽。假設你同時擁有壓縮攔截器以及校驗和攔截器:你需要決定是先進行數(shù)據(jù)壓縮, 再進行校驗和盯另;還是先進行校驗和然后再壓縮性含。OkHttp 使用列表來跟蹤攔截器,并按順序調用攔截器鸳惯。
4.1 Application Interceptors — 應用攔截器
攔截器有的被注冊為應用程序攔截器商蕴,有的則為網(wǎng)絡攔截器。我們將使用上面定義的 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
我們可以看到發(fā)生了重定向,因為 response.request().url()
與 request.url()
不同抄腔。這兩個日志語句記錄了兩個不同的 URL瓢湃。
4.2 Network Interceptors — 網(wǎng)絡攔截器
注冊網(wǎng)絡攔截器非常相似,不過是調用 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
網(wǎng)絡請求中還包含更多數(shù)據(jù)落蝙,例如 OkHttp 添加的 Accept-Encoding:gzip
頭部,用于聲告對響應壓縮的支持暂幼。網(wǎng)絡攔截器的 Chain(鏈)
具有非空 Connection(連接)
筏勒,可用于詢問用于連接到 Web 服務器的 IP 地址和 TLS 配置。
4.3 Choosing between application and network interceptors — 選擇應用程序攔截器還是網(wǎng)絡攔截器
每種攔截鏈都有其相對優(yōu)點旺嬉。
4.3.1 Application interceptors — 應用程序攔截器
不需要擔心重定向或重試等中間響應管行。
始終調用一次,即使 HTTP 響應是從緩存提供的邪媳。
便于觀察應用程序的初始意圖捐顷,不關注 OkHttp 注入的頭部,如
If-None-Match
雨效。允許短路而不是調用
Chain.proceed()
迅涮。允許重試并多次調用
Chain.proceed()
。
4.3.2 Network Interceptors — 網(wǎng)絡攔截器
能夠對重定向或重試等中間響應進行操作徽龟。
在使網(wǎng)絡短路的緩存響應情況下不進行調用叮姑。
觀察所有通過網(wǎng)絡傳輸?shù)臄?shù)據(jù)。
可以訪問攜帶請求的
Connection(連接)
顿肺。
4.4 Rewriting Requests — 重寫請求
攔截器可以添加戏溺,刪除或替換請求頭部渣蜗。它們還可以轉變那些擁有請求主體的請求。例如旷祸,你可以使用應用程序攔截器對請求主體進行壓縮耕拷,只要你已知你要連接的 Web 服務器支持這一操作即可。
/** 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();
}
};
}
}
4.5 Rewriting Responses — 重寫響應
對應的托享,攔截器可以重寫響應頭以及轉變響應體骚烧。但是,這通常比重寫請求頭部更危險闰围,因為它可能違背了 Web 服務器的期望赃绊!
如果你處在一種棘手的情形并準備好應對后果,重寫響應頭部是解決問題的有效方法羡榴。例如碧查,你可以修復服務器配置錯誤的 Cache-Control
響應頭部,以實現(xiàn)更好的響應緩存:
/** 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 服務器上的對應修復時效果最佳忠售!
4.6 Availability — 可用性
OkHttp 的攔截器需要在 OkHttp 2.2 或更高版本上才能使用。不幸的是迄沫,攔截器不能與 OkUrlFactory
或基于它構建的庫一起使用稻扬,包括 Retrofit ≤ 1.8
和 Picasso ≤ 2.4
。
五羊瘩、HTTPS
OkHttp 試圖平衡兩個相互競爭的問題:
連接到盡可能多的主機泰佳。包括運行最新版本
boringssl
的高級主機和較少運行舊版本OpenSSL
的過時主機。連接的安全性尘吗。包括使用證書驗證遠程 Web 服務器以及使用強密碼交換隱私數(shù)據(jù)逝她。
在協(xié)商與 HTTPS 服務器的連接時,OkHttp 需要知道要提供哪些 TLS 版本和密碼套件睬捶。希望連接最大化的客戶端將包括過時的 TLS 版本和弱設計的密碼套件汽绢。想要安全性最大化的客戶端將僅限于最新的 TLS 版本和最強的密碼套件。
ConnectionSpec
實現(xiàn)了特定的安全性與連接性決策侧戴。
OkHttp 包含四個內(nèi)置連接規(guī)范:
RESTRICTED_TLS
是一種安全配置宁昭,旨在滿足更嚴格的合規(guī)性要求。MODERN_TLS
是一種連接到現(xiàn)代 HTTPS 服務器的安全配置酗宋。COMPATIBLE_TLS
是一種安全配置积仗,可連接到安全的但非當前的 HTTPS 服務器。CLEARTEXT
是一種不安全的配置蜕猫,用于http://
URLs寂曹。
這些非嚴格地遵循 Google 云端策略中設置的模型。
默認情況下,OkHttp 將嘗試進行 MODERN_TLS
連接隆圆。但是漱挚,通過配置客戶端的連接規(guī)范,如果 MODERN_TLS
配置失敗渺氧,你可以回退到 COMPATIBLE_TLS
連接旨涝。
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();
每個規(guī)范中的 TLS 版本和密碼套件都可以隨每個版本而變化。例如侣背,在 OkHttp 2.2 中白华,我們放棄了對 SSL 3.0 的支持以應對 POODLE
攻擊。在 OkHttp 2.3 中贩耐,我們放棄了對 RC4
的支持弧腥。與你的桌面 Web 瀏覽器一樣,保持 OkHttp 處于最新狀態(tài)是保證安全的最佳方式潮太。
你可以使用一組自定義 TLS 版本和密碼套件來構建自己的連接規(guī)范管搪。例如,下面的配置僅限于三個備受推崇的密碼套件铡买。它的缺點是它需要 Android 5.0+ 和類似的現(xiàn)代 Web 服務器抛蚤。
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();
5.1 Certificate Pinning — 證書鎖定
默認情況下,OkHttp 信任主機平臺的證書認證機構寻狂。此策略可最大限度地提高連接性,但它可能會受到諸如 2011 DigiNotar attack
等證書認證機構的攻擊朋沮。它還假定你的 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));
}
}
5.2 Customizing Trusted Certificates — 自定義可信任證書
下面的代碼示例顯示了如何使用自定義集來替換主機平臺的證書認證蒂胞。如上所述,如果沒有得到服務器的 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.
}
六、Events
利用事件可以讓你捕獲應用程序的 HTTP 任務的指標赴叹。
使用事件可以監(jiān)控:
應用程序發(fā)起的 HTTP 任務的大小和頻率鸿染。如果你發(fā)起的網(wǎng)絡任務太多,或者任務太大乞巧,你應該知道它涨椒!
這些任務在底層網(wǎng)絡上的執(zhí)行性能。如果網(wǎng)絡性能不足,則需要改進網(wǎng)絡或減少使用網(wǎng)絡蚕冬。
6.1 EventListener
新建 EventListener
的子類和覆寫你感興趣的事件方法免猾。在沒有重定向或重試的成功 HTTP 調用中,以下流程圖描述了該事件序列:
這里有一個事件監(jiān)聽器示例囤热,它使用時間戳來打印每個事件猎提。
class PrintingEventListener extends EventListener {
private long callStartNanos;
private void printEvent(String name) {
long nowNanos = System.nanoTime();
if (name.equals("callStart")) {
callStartNanos = nowNanos;
}
long elapsedNanos = nowNanos - callStartNanos;
System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
}
@Override public void callStart(Call call) {
printEvent("callStart");
}
@Override public void callEnd(Call call) {
printEvent("callEnd");
}
@Override public void dnsStart(Call call, String domainName) {
printEvent("dnsStart");
}
@Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
printEvent("dnsEnd");
}
...
}
我們發(fā)出了一對請求:
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
// Consume and discard the response body.
response.body().source().readByteString();
}
System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
// Consume and discard the response body.
response.body().source().readByteString();
}
監(jiān)聽器打印了相應的事件:
REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd
注意:第二個請求沒有觸發(fā)連接事件。它重用了第一個請求的連接赢乓,從而顯著提高了性能忧侧。
6.2 EventListener.Factory
在前面的示例中,我們使用了一個字段 callStartNanos
來跟蹤每個事件的花費時間牌芋。這很方便蚓炬,但如果多個任務同時執(zhí)行,它將無法工作躺屁。為了適應這種情況肯夏,可以使用 Factory
為每個 Call
創(chuàng)建一個新的 EventListener
實例。這允許每個監(jiān)聽器保持特定于任務的狀態(tài)犀暑。
以下 sample factory(示例工廠)
為每個任務創(chuàng)建唯一 ID驯击,并使用該 ID 區(qū)分日志消息中的任務。
class PrintingEventListener extends EventListener {
public static final Factory FACTORY = new Factory() {
final AtomicLong nextCallId = new AtomicLong(1L);
@Override public EventListener create(Call call) {
long callId = nextCallId.getAndIncrement();
System.out.printf("%04d %s%n", callId, call.request().url());
return new PrintingEventListener(callId, System.nanoTime());
}
};
final long callId;
final long callStartNanos;
public PrintingEventListener(long callId, long callStartNanos) {
this.callId = callId;
this.callStartNanos = callStartNanos;
}
private void printEvent(String name) {
long elapsedNanos = System.nanoTime() - callStartNanos;
System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
}
@Override public void callStart(Call call) {
printEvent("callStart");
}
@Override public void callEnd(Call call) {
printEvent("callEnd");
}
...
}
我們可以使用此監(jiān)聽器來競爭一對并發(fā) HTTP 請求:
Request washingtonPostRequest = new Request.Builder()
.url("https://www.washingtonpost.com/")
.build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
...
});
Request newYorkTimesRequest = new Request.Builder()
.url("https://www.nytimes.com/")
.build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
...
});
在家庭 WiFi 上進行這場比賽耐亏,結果顯示徊都,Times(0002)
比 Post(0001)
稍早完成:
0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd
EventListener.Factory
還可以設置為僅捕獲一部分任務的指標。以下這個隨機捕獲 10% 的指標:
class MetricsEventListener extends EventListener {
private static final Factory FACTORY = new Factory() {
@Override public EventListener create(Call call) {
if (Math.random() < 0.10) {
return new MetricsEventListener(call);
} else {
return EventListener.NONE;
}
}
};
...
}
6.3 Events with Failures — 失敗事件
操作失敗時广辰,會調用失敗方法暇矫。當與服務器建立連接失敗時將調用 connectFailed()
;當 HTTP 調用不斷失敗時將調用 callFailed()
择吊。發(fā)生故障時李根,start event(啟動事件)
可能沒有相應的 end event(結束事件)
。
6.4 Events with Retries and Follow-Ups — 重試和后續(xù)跟蹤事件
OkHttp 具有可恢復性几睛,可以自動從某些連接故障中恢復房轿。在這種情況下,connectFailed()
事件不是終點所森,而且后面不會跟隨 callFailed()
囱持。嘗試重試時,事件監(jiān)聽器將收到多個相同類型的事件焕济。
單個 HTTP 調用可能需要進行后續(xù)請求以處理身份驗證洪唐,重定向和 HTTP 層超時。在這種情況下吼蚁,可以嘗試多個連接凭需,請求和響應问欠。后續(xù)請求是單個任務可能觸發(fā)相同類型的多個事件的另一個原因。
6.5 Availability — 可用性
Events 在 OkHttp 3.11 中作為公共 API 提供粒蜈。未來版本可能會引入新的事件類型顺献;你將需要覆寫相應的方法來處理它們。