OkHttp使用完全教程
標(biāo)簽 : Http請(qǐng)求, 類庫(kù)
blog : http://blog.csdn.net/oncealong/article/details/52096477
上一節(jié)我們講述了Http請(qǐng)求的過程, 這一節(jié)我們就講述下OkHttp是怎么完成Http請(qǐng)求的.
(為了更好的理解這節(jié)內(nèi)容,強(qiáng)烈推薦上一節(jié)文加圖, 理解Http請(qǐng)求與響應(yīng))
1. 歷史上Http請(qǐng)求庫(kù)優(yōu)缺點(diǎn)
在講述OkHttp之前, 我們看下沒有OkHttp的時(shí)代, 我們是如何完成http請(qǐng)求的.
在沒有OkHttp的日子, 我們使用HttpURLConnection
或者HttpClient
. 那么這兩者都有什么優(yōu)缺點(diǎn)呢? 為什么不在繼續(xù)使用下去呢?
HttpClient
是Apache基金會(huì)的一個(gè)開源網(wǎng)絡(luò)庫(kù), 功能十分強(qiáng)大, API數(shù)量眾多, 但是正是由于龐大的API數(shù)量使得我們很難在不破壞兼容性的情況下對(duì)它進(jìn)行升級(jí)和擴(kuò)展, 所以Android團(tuán)隊(duì)在提升和優(yōu)化HttpClient方面的工作態(tài)度并不積極.
HttpURLConnection
是一種多用途, 輕量極的HTTP客戶端, 提供的API比較簡(jiǎn)單, 可以容易地去使用和擴(kuò)展. 不過在Android 2.2版本之前, HttpURLConnection
一直存在著一些令人厭煩的bug. 比如說對(duì)一個(gè)可讀的InputStream調(diào)用close()方法時(shí)夫否,就有可能會(huì)導(dǎo)致連接池失效了蛾默。那么我們通常的解決辦法就是直接禁用掉連接池的功能:
private void disableConnectionReuseIfNecessary() {
// 這是一個(gè)2.2版本之前的bug
if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
}
因此, 一般的推薦是在2.2之前, 使用HttpClient
, 因?yàn)槠鋌ug較少. 在2.2之后, 推薦使用HttpURLConnection
, 因?yàn)锳PI簡(jiǎn)單, 體積小, 并且有壓縮和緩存機(jī)制, 并且Android團(tuán)隊(duì)后續(xù)會(huì)繼續(xù)優(yōu)化HttpURLConnection
.
但是, 上面兩個(gè)類庫(kù)和OkHttp
比起來(lái)就弱爆了, 因?yàn)镺kHttp不僅具有高效的請(qǐng)求效率, 并且提供了很多開箱即用的網(wǎng)絡(luò)疑難雜癥解決方案.
- 支持HTTP/2, HTTP/2通過使用多路復(fù)用技術(shù)在一個(gè)單獨(dú)的TCP連接上支持并發(fā), 通過在一個(gè)連接上一次性發(fā)送多個(gè)請(qǐng)求來(lái)發(fā)送或接收數(shù)據(jù)
- 如果HTTP/2不可用, 連接池復(fù)用技術(shù)也可以極大減少延時(shí)
- 支持GZIP, 可以壓縮下載體積
- 響應(yīng)緩存可以直接避免重復(fù)請(qǐng)求
- 會(huì)從很多常用的連接問題中自動(dòng)恢復(fù)
- 如果您的服務(wù)器配置了多個(gè)IP地址, 當(dāng)?shù)谝粋€(gè)IP連接失敗的時(shí)候, OkHttp會(huì)自動(dòng)嘗試下一個(gè)IP
- OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題
使用 OkHttp 無(wú)需重寫您程序中的網(wǎng)絡(luò)代碼这敬。OkHttp實(shí)現(xiàn)了幾乎和java.net.HttpURLConnection一樣的API戳葵。如果你用了 Apache HttpClient焕檬,則OkHttp也提供了一個(gè)對(duì)應(yīng)的okhttp-apache 模塊岩灭。
還有一個(gè)好消息, 從Android 4.4起, 其HttpURLConnection
的內(nèi)部實(shí)現(xiàn)已經(jīng)變?yōu)?code>OkHttp, 您可以參考這兩個(gè)網(wǎng)頁(yè):爆棧網(wǎng)和Twitter.
2. OkHttp類與http請(qǐng)求響應(yīng)的映射
在講解OkHttp使用之前, 再看下我們Http請(qǐng)求和響應(yīng)都有哪些部分組成.
2.1 http請(qǐng)求
所以一個(gè)類庫(kù)要完成一個(gè)http請(qǐng)求, 需要包含
請(qǐng)求方法
, 請(qǐng)求地址
, 請(qǐng)求協(xié)議
, 請(qǐng)求頭
, 請(qǐng)求體
這五部分. 這些都在okhttp3.Request
的類中有體現(xiàn), 這個(gè)類正是代表http請(qǐng)求的類. 看下圖:其中
HttpUrl
類代表請(qǐng)求地址
, String method
代表請(qǐng)求方法
, Headers
代表請(qǐng)求頭, RequestBody
代表請(qǐng)求體. Object tag
這個(gè)是用來(lái)取消http請(qǐng)求的標(biāo)志, 這個(gè)我們先不管. 這里也許你在疑惑, 請(qǐng)求協(xié)議
呢? 為什么沒有請(qǐng)求協(xié)議對(duì)應(yīng)的類. 且聽我慢慢道來(lái), 下面就會(huì)講到這個(gè)問題.
2.1.1 請(qǐng)求協(xié)議的協(xié)商升級(jí)
目前, Http/1.1在全世界大范圍的使用中, 直接廢棄跳到http/2肯定不現(xiàn)實(shí). 不是每個(gè)用戶的瀏覽器都支持http/2的, 也不是每個(gè)服務(wù)器都打算支持http/2的, 如果我們直接發(fā)送http/2格式的協(xié)議, 服務(wù)器又不支持, 那不是掛掉了! 總不能維護(hù)一個(gè)全世界的網(wǎng)站列表, 表示哪些支持http/2, 哪些不支持?
為了解決這個(gè)問題, 從稍高層次上來(lái)說, 就是為了更方便地部署新協(xié)議, HTTP/1.1 引入了 Upgrade 機(jī)制. 這個(gè)機(jī)制在 RFC7230 的「6.7 Upgrade」這一節(jié)中有詳細(xì)描述.
簡(jiǎn)單說來(lái), 就是先問下你支持http/2么? 如果你支持, 那么接下來(lái)我就用http/2和你聊天. 如果你不支持, 那么我還是用原來(lái)的http/1.1和你聊天.
1.客戶端在請(qǐng)求頭部中指定Connection
和Upgrade
兩個(gè)字段發(fā)起 HTTP/1.1 協(xié)議升級(jí). HTTP/2 的協(xié)議名稱是 h2c, 代表 HTTP/2 ClearText.
GET / HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
2.如果服務(wù)端不同意升級(jí)或者不支持 Upgrade 所列出的協(xié)議瞄摊,直接忽略即可(當(dāng)成 HTTP/1.1 請(qǐng)求,以 HTTP/1.1 響應(yīng)).
HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html
...
如果服務(wù)端同意升級(jí)裸删,那么需要這樣響應(yīng):
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ HTTP/2 connection ... ]
HTTP Upgrade 響應(yīng)的狀態(tài)碼是 101八拱,并且響應(yīng)正文可以使用新協(xié)議定義的數(shù)據(jù)格式。
這樣就可以完成從http/1.1升級(jí)到http/2了. 同樣也可以從http/1.1升級(jí)到WebSocket.
這樣, 你就了解了為什么OkHttp沒有指定具體請(qǐng)求協(xié)議了吧. 因?yàn)镺kHttp使用了請(qǐng)求協(xié)議的協(xié)商升級(jí), 無(wú)論是1.1還是2, 都先只以1.1來(lái)發(fā)送, 并在發(fā)送的信息頭里包含協(xié)議升級(jí)字段. 接下來(lái)就看服務(wù)器是否支持協(xié)議升級(jí)了. OkHttp使用的協(xié)議升級(jí)字段是ALPN
, 如果有興趣, 可以更深入的查閱相關(guān)資料.
2.1.2 OkHttp請(qǐng)求
接下來(lái)我們構(gòu)造一個(gè)http請(qǐng)求, 并查看請(qǐng)求具體內(nèi)容.
final Request request = new Request.Builder().url("https://github.com/").build();
我們看下在內(nèi)存中, 這個(gè)請(qǐng)求是什么樣子的, 是否如我們上文所說和請(qǐng)求方法
, 請(qǐng)求地址
, 請(qǐng)求頭
, 請(qǐng)求體
一一對(duì)應(yīng).
2.2 http響應(yīng)
我們看下一個(gè)http響應(yīng)由哪些部分組成, 先看下響應(yīng)組成圖:
可以看到大體由
應(yīng)答首行
, 應(yīng)答頭
, 應(yīng)答體
構(gòu)成. 但是應(yīng)答首行
表達(dá)的信息過多, HTTP/1.1
表示訪問協(xié)議
, 200
是響應(yīng)碼, OK
是描述狀態(tài)的消息. 根據(jù)單一職責(zé), 我們不應(yīng)該把這么多內(nèi)容用一個(gè)應(yīng)答首行
來(lái)表示. 這樣的話, 我們的響應(yīng)就應(yīng)該由訪問協(xié)議
, 響應(yīng)碼
, 描述信息
, 響應(yīng)頭
, 響應(yīng)體
來(lái)組成.
2.2.1 OkHttp響應(yīng)
我們看下OkHttp庫(kù)怎么表示一個(gè)響應(yīng):
可以看到
Response
類里面有Protocol
代表請(qǐng)求協(xié)議
, int code
代表響應(yīng)碼
, String message
代表描述信息
, Headers
代表響應(yīng)頭
, ResponseBody
代表響應(yīng)體
. 當(dāng)然除此之外, 還有Request
代表持有的請(qǐng)求, Handshake
代表SSL/TLS握手協(xié)議驗(yàn)證時(shí)的信息, 這些額外信息我們暫時(shí)不問.
有了剛才說的OkHttp響應(yīng)的類組成, 我們看下OkHttp請(qǐng)求后響應(yīng)在內(nèi)存中的內(nèi)容:
final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();
可以看到和我們的分析十分一致.
講了OkHttp里的請(qǐng)求類和響應(yīng)類, 我們接下來(lái)就可以直接講述OkHttp的使用方法了.
3 HTTP GET
3.1 同步GET
同步GET的意思是一直等待http請(qǐng)求, 直到返回了響應(yīng). 在這之間會(huì)阻塞進(jìn)程, 所以通過get不能在Android的主線程中執(zhí)行, 否則會(huì)報(bào)錯(cuò).
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());
}
OkHttpClient實(shí)現(xiàn)了Call.Factory
接口, 是Call的工廠類, Call負(fù)責(zé)發(fā)送執(zhí)行請(qǐng)求和讀取響應(yīng).
Request代表Http請(qǐng)求, 通過Request.Builder輔助類來(lái)構(gòu)建.
client.newCall(request)通過傳入一個(gè)http request, 返回一個(gè)Call調(diào)用. 然后執(zhí)行execute()方法, 同步獲得
Response代表Http請(qǐng)求的響應(yīng). response.body()是ResponseBody類, 代表響應(yīng)體, 可以通過responseBody.string()獲得字符串的表達(dá)形式, 或responseBody.bytes()獲得字節(jié)數(shù)組的表達(dá)形式, 這兩種形式都會(huì)把文檔加入到內(nèi)存. 也可以通過responseBody.charStream()和responseBody.byteStream()返回流來(lái)處理.
上述代碼完成的功能是下載一個(gè)文件, 打印他的響應(yīng)頭, 以string形式打印響應(yīng)體.
響應(yīng)體的string()方法對(duì)于小文檔來(lái)說十分方便高效. 但是如果響應(yīng)體太大(超過1MB), 應(yīng)避免使用 string()方法, 因?yàn)樗鼤?huì)將把整個(gè)文檔加載到內(nèi)存中.
對(duì)于超過1MB的響應(yīng)body, 應(yīng)使用流的方式來(lái)處理響應(yīng)body. 這和我們處理xml文檔的邏輯是一致的, 小文件可以載入內(nèi)存樹狀解析, 大文件就必須流式解析.
3.2 異步GET
異步GET是指在另外的工作線程中執(zhí)行http請(qǐng)求, 請(qǐng)求時(shí)不會(huì)阻塞當(dāng)前的線程, 所以可以在Android主線程中使用.
下面是在一個(gè)工作線程中下載文件, 當(dāng)響應(yīng)可讀時(shí)回調(diào)Callback接口. 當(dāng)響應(yīng)頭準(zhǔn)備好后, 就會(huì)調(diào)用Callback接口, 所以讀取響應(yīng)體
時(shí)可能會(huì)阻塞. OkHttp現(xiàn)階段不提供異步api來(lái)接收響應(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(Request request, Throwable throwable) {
throwable.printStackTrace();
}
@Override public void onResponse(Response response) throws IOException {
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());
}
});
}
4 HTTP POST
4.1 Post方式提交String
下面是使用HTTP POST提交請(qǐng)求到服務(wù). 這個(gè)例子提交了一個(gè)markdown文檔到web服務(wù), 以HTML方式渲染markdown. 因?yàn)檎麄€(gè)請(qǐng)求體都在內(nèi)存中, 因此避免使用此api提交大文檔(大于1MB).
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
4.2 Post方式提交流
以流的方式POST提交請(qǐng)求體. 請(qǐng)求體的內(nèi)容由流寫入產(chǎn)生. 這個(gè)例子是流直接寫入Okio的BufferedSink. 你的程序可能會(huì)使用OutputStream, 你可以使用BufferedSink.outputStream()來(lái)獲取. OkHttp的底層對(duì)流和字節(jié)的操作都是基于Okio庫(kù), Okio庫(kù)也是Square開發(fā)的另一個(gè)IO庫(kù), 填補(bǔ)I/O和NIO的空缺, 目的是提供簡(jiǎn)單便于使用的接口來(lái)操作IO.
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}
private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
4.3 Post方式提交文件
以文件作為請(qǐng)求體是十分簡(jiǎn)單的乘粒。
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
File file = new File("README.md");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
4.4 Post方式提交表單
使用FormEncodingBuilder來(lái)構(gòu)建和HTML<form>
標(biāo)簽相同效果的請(qǐng)求體. 鍵值對(duì)將使用一種HTML兼容形式的URL編碼來(lái)進(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());
}
4.5 Post方式提交分塊請(qǐng)求
MultipartBody.Builder可以構(gòu)建復(fù)雜的請(qǐng)求體, 與HTML文件上傳形式兼容. 多塊請(qǐng)求體中每塊請(qǐng)求都是一個(gè)請(qǐng)求體, 可以定義自己的請(qǐng)求頭. 這些請(qǐng)求頭可以用來(lái)描述這塊請(qǐng)求, 例如它的Content-Disposition. 如果Content-Length和Content-Type可用的話, 他們會(huì)被自動(dòng)添加到請(qǐ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());
}
5. 其他用法
5.1 提取響應(yīng)頭
典型的HTTP頭像是一個(gè)Map<String, String>
: 每個(gè)字段都有一個(gè)或沒有值. 但是一些頭允許多個(gè)值, 像Guava的Multimap.
例如: HTTP響應(yīng)里面提供的Vary響應(yīng)頭, 就是多值的. OkHttp的api試圖讓這些情況都適用.
當(dāng)寫請(qǐng)求頭的時(shí)候, 使用header(name, value)可以設(shè)置唯一的name、value. 如果已經(jīng)有值, 舊的將被移除, 然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
當(dāng)讀取響應(yīng)頭時(shí), 使用header(name)返回最后出現(xiàn)的name伤塌、value. 通常情況這也是唯一的name灯萍、value. 如果沒有值, 那么header(name)將返回null. 如果想讀取字段對(duì)應(yīng)的所有值, 使用headers(name)會(huì)返回一個(gè)list.
為了獲取所有的Header, Headers類支持按index訪問.
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"));
}
5.2 使用Gson來(lái)解析JSON響應(yīng)
Gson是一個(gè)在JSON和Java對(duì)象之間轉(zhuǎn)換非常方便的api庫(kù). 這里我們用Gson來(lái)解析Github API的JSON響應(yīng).
注意: ResponseBody.charStream()使用響應(yīng)頭Content-Type指定的字符集來(lái)解析響應(yīng)體. 默認(rèn)是UTF-8.
private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
for (Map.Entry<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;
}
5.3 響應(yīng)緩存
為了緩存響應(yīng), 你需要一個(gè)你可以讀寫的緩存目錄, 和緩存大小的限制. 這個(gè)緩存目錄應(yīng)該是私有的, 不信任的程序應(yīng)不能讀取緩存內(nèi)容.
一個(gè)緩存目錄同時(shí)擁有多個(gè)緩存訪問是錯(cuò)誤的. 大多數(shù)程序只需要調(diào)用一次new OkHttp(), 在第一次調(diào)用時(shí)配置好緩存, 然后其他地方只需要調(diào)用這個(gè)實(shí)例就可以了. 否則兩個(gè)緩存示例互相干擾, 破壞響應(yīng)緩存, 而且有可能會(huì)導(dǎo)致程序崩潰.
響應(yīng)緩存使用HTTP頭作為配置. 你可以在請(qǐng)求頭中添加Cache-Control: max-stale=3600 , OkHttp緩存會(huì)支持. 你的服務(wù)通過響應(yīng)頭確定響應(yīng)緩存多長(zhǎng)時(shí)間, 例如使用Cache-Control: max-age=9600.
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();
client.setCache(cache);
}
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));
}
如果需要阻值response使用緩存, 使用CacheControl.FORCE_NETWORK
. 如果需要阻值response使用網(wǎng)絡(luò), 使用CacheControl.FORCE_CACHE
.
警告: 如果你使用FORCE_CACHE
, 但是response要求使用網(wǎng)絡(luò), OkHttp將會(huì)返回一個(gè)504 Unsatisfiable Request
響應(yīng).
5.3.1 Force a Network Response
有些時(shí)候, 比如用戶剛剛點(diǎn)擊刷新
按鈕, 這時(shí)必須跳過緩存, 直接從服務(wù)器抓取數(shù)據(jù). 為了強(qiáng)制全面刷新, 我們需要添加no-cache
指令:
connection.addRequestProperty("Cache-Control", "no-cache");
這樣就可以強(qiáng)制每次請(qǐng)求直接發(fā)送給源服務(wù)器, 而不經(jīng)過本地緩存版本的校驗(yàn), 常用于需要確認(rèn)認(rèn)證的應(yīng)用和嚴(yán)格要求使用最新數(shù)據(jù)的應(yīng)用.
5.3.2 Force a Cache Response
有時(shí)你會(huì)想立即顯示資源. 這樣即使在后臺(tái)正下載著最新資源, 你的客戶端仍然可以先顯示原有資源, 畢竟有個(gè)東西顯示比沒有東西顯示要好.
如果需要限制讓請(qǐng)求優(yōu)先使用本地緩存資源, 需要增加only-if-cached
指令:
try {
connection.addRequestProperty("Cache-Control", "only-if-cached");
InputStream cached = connection.getInputStream();
// the resource was cached! show it
catch (FileNotFoundException e) {
// the resource was not cached
}
}
5.4 取消一個(gè)Call
使用Call.cancel()可以立即停止掉一個(gè)正在執(zhí)行的call. 如果一個(gè)線程正在寫請(qǐng)求或者讀響應(yīng), 將會(huì)引發(fā)IOException. 當(dāng)call沒有必要的時(shí)候, 使用這個(gè)api可以節(jié)約網(wǎng)絡(luò)資源. 例如當(dāng)用戶離開一個(gè)應(yīng)用時(shí), 不管同步還是異步的call都可以取消.
你可以通過tags來(lái)同時(shí)取消多個(gè)請(qǐng)求. 當(dāng)你構(gòu)建一請(qǐng)求時(shí), 使用RequestBuilder.tag(tag)來(lái)分配一個(gè)標(biāo)簽, 之后你就可以用OkHttpClient.cancel(tag)來(lái)取消所有帶有這個(gè)tag的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);
}
}
5.5 超時(shí)
沒有響應(yīng)時(shí)使用超時(shí)結(jié)束call. 沒有響應(yīng)的原因可能是客戶點(diǎn)鏈接問題、服務(wù)器可用性問題或者這之間的其他東西. OkHttp支持連接超時(shí), 讀取超時(shí)和寫入超時(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);
}
5.6 每個(gè)call的配置
使用OkHttpClient, 所有的HTTP Client配置包括代理設(shè)置每聪、超時(shí)設(shè)置旦棉、緩存設(shè)置. 當(dāng)你需要為單個(gè)call改變配置的時(shí)候, 調(diào)用OkHttpClient.newBuilder()
. 這個(gè)api將會(huì)返回一個(gè)builder, 這個(gè)builder和原始的client共享相同的連接池, 分發(fā)器和配置.
下面的例子中,我們讓一個(gè)請(qǐng)求是500ms的超時(shí)药薯、另一個(gè)是3000ms的超時(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);
}
}
5.7 處理驗(yàn)證
這部分和HTTP AUTH有關(guān).
5.7.1 HTTP AUTH
使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:
- 客戶端發(fā)送http請(qǐng)求
- 服務(wù)器發(fā)現(xiàn)配置了http auth, 于是檢查request里面有沒有"Authorization"的http header
- 如果有, 則判斷Authorization里面的內(nèi)容是否在用戶列表里面, Authorization header的典型數(shù)據(jù)為"Authorization: Basic jdhaHY0=", 其中Basic表示基礎(chǔ)認(rèn)證, jdhaHY0=是base64編碼的"user:passwd"字符串. 如果沒有,或者用戶密碼不對(duì)童本,則返回http code 401頁(yè)面給客戶端.
- 標(biāo)準(zhǔn)的http瀏覽器在收到401頁(yè)面之后, 應(yīng)該彈出一個(gè)對(duì)話框讓用戶輸入帳號(hào)密碼; 并在用戶點(diǎn)確認(rèn)的時(shí)候再次發(fā)出請(qǐng)求, 這次請(qǐng)求里面將帶上Authorization header.
一次典型的訪問場(chǎng)景是:
- 瀏覽器發(fā)送http請(qǐng)求(沒有Authorization header)
- 服務(wù)器端返回401頁(yè)面
- 瀏覽器彈出認(rèn)證對(duì)話框
- 用戶輸入帳號(hào)密碼真屯,并點(diǎn)確認(rèn)
- 瀏覽器再次發(fā)出http請(qǐng)求(帶著Authorization header)
- 服務(wù)器端認(rèn)證通過,并返回頁(yè)面
- 瀏覽器顯示頁(yè)面
5.7.2 OkHttp認(rèn)證
OkHttp會(huì)自動(dòng)重試未驗(yàn)證的請(qǐng)求. 當(dāng)響應(yīng)是401 Not Authorized
時(shí)穷娱,Authenticator
會(huì)被要求提供證書. Authenticator的實(shí)現(xiàn)中需要建立一個(gè)新的包含證書的請(qǐng)求. 如果沒有證書可用, 返回null來(lái)跳過嘗試.
使用Response.challenges()
來(lái)獲得任何authentication challenges
的 schemes 和 realms. 當(dāng)完成一個(gè)Basic challenge
, 使用Credentials.basic(username, password)
來(lái)解碼請(qǐng)求頭.
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());
}
當(dāng)認(rèn)證無(wú)法工作時(shí), 為了避免多次重試, 你可以返回空來(lái)放棄認(rèn)證. 例如, 當(dāng)exact credentials
已經(jīng)嘗試過, 你可能會(huì)直接想跳過認(rèn)證, 可以這樣做:
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
當(dāng)重試次數(shù)超過定義的次數(shù), 你若想跳過認(rèn)證, 可以這樣做:
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
這樣, 對(duì)OkHttp的使用我們就講完了, 下一節(jié)會(huì)講OkHttp內(nèi)部實(shí)現(xiàn).
謝謝下列文章:
http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html
http://www.reibang.com/p/aad5aacd79bf
https://imququ.com/post/protocol-negotiation-in-http2.html
http://blog.csdn.net/wwwsq/article/details/7255062