OkHttp使用完全教程

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)求頭部中指定ConnectionUpgrade兩個(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末绑蔫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泵额,更是在濱河造成了極大的恐慌配深,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫁盲,死亡現(xiàn)場(chǎng)離奇詭異篓叶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門缸托,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)左敌,“玉大人,你說我怎么就攤上這事嗦董∧富眩” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵京革,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我幸斥,道長(zhǎng)匹摇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任甲葬,我火速辦了婚禮廊勃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘经窖。我一直安慰自己坡垫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布画侣。 她就那樣靜靜地躺著冰悠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪配乱。 梳的紋絲不亂的頭發(fā)上溉卓,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音搬泥,去河邊找鬼桑寨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛忿檩,可吹牛的內(nèi)容都是我干的尉尾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼燥透,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼沙咏!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起兽掰,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芭碍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后孽尽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窖壕,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瞻讽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸳吸。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖速勇,靈堂內(nèi)的尸體忽然破棺而出晌砾,到底是詐尸還是另有隱情,我是刑警寧澤烦磁,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布养匈,位于F島的核電站,受9級(jí)特大地震影響都伪,放射性物質(zhì)發(fā)生泄漏呕乎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一陨晶、第九天 我趴在偏房一處隱蔽的房頂上張望猬仁。 院中可真熱鬧,春花似錦先誉、人聲如沸湿刽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)诈闺。三九已至,卻和暖如春漱病,著一層夾襖步出監(jiān)牢的瞬間买雾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工杨帽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漓穿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓注盈,卻偏偏與公主長(zhǎng)得像晃危,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子老客,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理僚饭,服務(wù)發(fā)現(xiàn),斷路器胧砰,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,161評(píng)論 25 707
  • Okhttp使用指南與源碼分析 標(biāo)簽(空格分隔): Android 使用指南篇# 為什么使用okhttp### A...
    背影殺手不太冷閱讀 8,154評(píng)論 2 119
  • 參考Android網(wǎng)絡(luò)請(qǐng)求心路歷程Android Http接地氣網(wǎng)絡(luò)請(qǐng)求(HttpURLConnection) 一...
    合肥黑閱讀 21,279評(píng)論 7 63
  • 一個(gè)好武器 這半年來(lái)鳍鸵, 學(xué)到了一個(gè)升級(jí)大腦的好武器,它幫助我節(jié)約了很多時(shí)間 尉间,提高了效率偿乖,讓自己精神狀態(tài)良好击罪, 而...
    絕版小貝Anni閱讀 232評(píng)論 0 0