天天看點

okHttp 網絡請求詳解

 使用android studio 導入httpclient庫的時候一定不要忘記okio庫,下面為android studio使用httpclient時導入的三個主要庫

implementation files('libs/httpclient-4.5.jar')
    implementation files('libs/httpcore-4.4.1.jar')
    implementation 'com.squareup.okio:okio:1.11.0'
           

為了更好的了解OKHttp,強烈推薦先看一下http的整個請求過程~ 文加圖, 了解Http請求與響應,提升軟實力。

1. 曆史上Http請求庫優缺點

在講述OkHttp之前, 我們看下沒有OkHttp的時代, 我們是如何完成http請求的. 

在沒有OkHttp的日子, 我們使用

HttpURLConnection

或者

HttpClient

. 那麼這兩者都有什麼優缺點呢? 為什麼不在繼續使用下去呢? 

HttpClient

是Apache基金會的一個開源網絡庫, 功能十分強大, API數量衆多, 但是正是由于龐大的API數量使得我們很難在不破壞相容性的情況下對它進行更新和擴充, 是以Android團隊在提升和優化HttpClient方面的工作态度并不積極. 

HttpURLConnection

是一種多用途, 輕量極的HTTP用戶端, 提供的API比較簡單, 可以容易地去使用和擴充. 不過在Android 2.2版本之前, 

HttpURLConnection

一直存在着一些令人厭煩的bug. 比如說對一個可讀的InputStream調用close()方法時,就有可能會導緻連接配接池失效了。那麼我們通常的解決辦法就是直接禁用掉連接配接池的功能:

private void disableConnectionReuseIfNecessary() {    
    // 這是一個2.2版本之前的bug    
    if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {    
        System.setProperty("http.keepAlive", "false");    
    }    
}    
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

是以, 一般的推薦是在2.2之前, 使用

HttpClient

, 因為其bug較少. 在2.2之後, 推薦使用

HttpURLConnection

, 因為API簡單, 體積小, 并且有壓縮和緩存機制, 并且Android團隊後續會繼續優化

HttpURLConnection

.

但是, 上面兩個類庫和

OkHttp

比起來就弱爆了, 因為OkHttp不僅具有高效的請求效率, 并且提供了很多開箱即用的網絡疑難雜症解決方案. 

- 支援HTTP/2, HTTP/2通過使用多路複用技術在一個單獨的TCP連接配接上支援并發, 通過在一個連接配接上一次性發送多個請求來發送或接收資料 

- 如果HTTP/2不可用, 連接配接池複用技術也可以極大減少延時 

- 支援GZIP, 可以壓縮下載下傳體積 

- 響應緩存可以直接避免重複請求 

- 會從很多常用的連接配接問題中自動恢複 

- 如果您的伺服器配置了多個IP位址, 當第一個IP連接配接失敗的時候, OkHttp會自動嘗試下一個IP 

- OkHttp還處理了代理伺服器問題和SSL握手失敗問題

使用 OkHttp 無需重寫您程式中的網絡代碼。OkHttp實作了幾乎和java.net.HttpURLConnection一樣的API。如果你用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 子產品。

還有一個好消息, 從Android 4.4起, 其

HttpURLConnection

的内部實作已經變為

OkHttp

, 您可以參考這兩個網頁:爆棧網和Twitter.

2. OkHttp類與http請求響應的映射

在講解OkHttp使用之前, 再看下我們Http請求和響應都有哪些部分組成.

2.1 http請求

okHttp 網絡請求詳解

是以一個類庫要完成一個http請求, 需要包含 

請求方法

請求位址

請求協定

請求頭

請求體

這五部分. 這些都在

okhttp3.Request

的類中有展現, 這個類正是代表http請求的類. 看下圖: 

okHttp 網絡請求詳解

其中

HttpUrl

類代表

請求位址

String method

代表

請求方法

Headers

代表請求頭, 

RequestBody

代表請求體. 

Object tag

這個是用來取消http請求的标志, 這個我們先不管. 這裡也許你在疑惑, 

請求協定

呢? 為什麼沒有請求協定對應的類. 且聽我慢慢道來, 下面就會講到這個問題.

2.1.1 請求協定的協商更新

目前, Http/1.1在全世界大範圍的使用中, 直接廢棄跳到http/2肯定不現實. 不是每個使用者的浏覽器都支援http/2的, 也不是每個伺服器都打算支援http/2的, 如果我們直接發送http/2格式的協定, 伺服器又不支援, 那不是挂掉了! 總不能維護一個全世界的網站清單, 表示哪些支援http/2, 哪些不支援? 

為了解決這個問題, 從稍高層次上來說, 就是為了更友善地部署新協定, HTTP/1.1 引入了 Upgrade 機制. 這個機制在 RFC7230 的「6.7 Upgrade」這一節中有較長的描述. 

簡單說來, 就是先問下你支援http/2麼? 如果你支援, 那麼接下來我就用http/2和你聊天. 如果你不支援, 那麼我還是用原來的http/1.1和你聊天.

1.用戶端在請求頭部中指定

Connection

Upgrade

兩個字段發起 HTTP/1.1 協定更新. HTTP/2 的協定名稱是 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>
           
  • 1
  • 2
  • 3
  • 4
  • 5

2.如果服務端不同意更新或者不支援 Upgrade 所列出的協定,直接忽略即可(當成 HTTP/1.1 請求,以 HTTP/1.1 響應).

HTTP/1.1  OK
Content-Length: 243
Content-Type: text/html

...
           
  • 1
  • 2
  • 3
  • 4
  • 5

如果服務端同意更新,那麼需要這樣響應:

HTTP/1.1  Switching Protocols
Connection: Upgrade
Upgrade: h2c

[ HTTP/ connection ... ]
           
  • 1
  • 2
  • 3
  • 4
  • 5

HTTP Upgrade 響應的狀态碼是 101,并且響應正文可以使用新協定定義的資料格式。

這樣就可以完成從http/1.1更新到http/2了. 同樣也可以從http/1.1更新到WebSocket.

這樣, 你就了解了為什麼OkHttp沒有指定具體請求協定了吧. 因為OkHttp使用了請求協定的協商更新, 無論是1.1還是2, 都先隻以1.1來發送, 并在發送的資訊頭裡包含協定更新字段. 接下來就看伺服器是否支援協定更新了. OkHttp使用的協定更新字段是

ALPN

, 如果有興趣, 可以更深入的查閱相關資料.

2.1.2 OkHttp請求

接下來我們構造一個http請求, 并檢視請求具體内容.

final Request request = new Request.Builder().url("https://github.com/").build();
           
  • 1

我們看下在記憶體中, 這個請求是什麼樣子的, 是否如我們上文所說和

請求方法

請求位址

請求頭

請求體

一一對應. 

okHttp 網絡請求詳解

2.2 http響應

我們看下一個http響應由哪些部分組成, 先看下響應組成圖: 

okHttp 網絡請求詳解

可以看到大體由

應答首行

應答頭

應答體

構成. 但是

應答首行

表達的資訊過多, 

HTTP/1.1

表示

通路協定

200

是響應碼, 

OK

是描述狀态的消息. 根據單一職責, 我們不應該把這麼多内容用一個

應答首行

來表示. 這樣的話, 我們的響應就應該由

通路協定

響應碼

描述資訊

響應頭

響應體

來組成.

2.2.1 OkHttp響應

我們看下OkHttp庫怎麼表示一個響應: 

okHttp 網絡請求詳解

可以看到

Response

類裡面有

Protocol

代表

請求協定

int code

代表

響應碼

String message

代表

描述資訊

Headers

代表

響應頭

ResponseBody

代表

響應體

. 當然除此之外, 還有

Request

代表持有的請求, 

Handshake

代表SSL/TLS握手協定驗證時的資訊, 這些額外資訊我們暫時不問.

有了剛才說的OkHttp響應的類組成, 我們看下OkHttp請求後響應在記憶體中的内容:

final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();
           
  • 1
  • 2
okHttp 網絡請求詳解

可以看到和我們的分析十分一緻.

講了OkHttp裡的請求類和響應類, 我們接下來就可以直接講述OkHttp的使用方法了.

3 HTTP GET

3.1 同步GET

同步GET的意思是一直等待http請求, 直到傳回了響應. 在這之間會阻塞程序, 是以通過get不能在Android的主線程中執行, 否則會報錯.

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 = ; i < responseHeaders.size(); i++) {
      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
    }

    System.out.println(response.body().string());
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

OkHttpClient實作了

Call.Factory

接口, 是Call的工廠類, Call負責發送執行請求和讀取響應. 

Request代表Http請求, 通過Request.Builder輔助類來建構. 

client.newCall(request)通過傳入一個http request, 傳回一個Call調用. 然後執行execute()方法, 同步獲得 

Response代表Http請求的響應. response.body()是ResponseBody類, 代表響應體, 可以通過responseBody.string()獲得字元串的表達形式, 或responseBody.bytes()獲得位元組數組的表達形式, 這兩種形式都會把文檔加入到記憶體. 也可以通過responseBody.charStream()和responseBody.byteStream()傳回流來處理.

上述代碼完成的功能是下載下傳一個檔案, 列印他的響應頭, 以string形式列印響應體. 

響應體的string()方法對于小文檔來說十分友善高效. 但是如果響應體太大(超過1MB), 應避免使用 string()方法, 因為它會将把整個文檔加載到記憶體中. 

對于超過1MB的響應body, 應使用流的方式來處理響應body. 這和我們處理xml文檔的邏輯是一緻的, 小檔案可以載入記憶體樹狀解析, 大檔案就必須流式解析.

3.2 異步GET

異步GET是指在另外的工作線程中執行http請求, 請求時不會阻塞目前的線程, 是以可以在Android主線程中使用. 

下面是在一個工作線程中下載下傳檔案, 當響應可讀時回調Callback接口. 當響應頭準備好後, 就會調用Callback接口, 是以讀取

響應體

時可能會阻塞. 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(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 = ; i < responseHeaders.size(); i++) {
          System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
        }

        System.out.println(response.body().string());
      }
    });
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

4 HTTP POST

4.1 Post方式送出String

下面是使用HTTP POST送出請求到服務. 這個例子送出了一個markdown文檔到web服務, 以HTML方式渲染markdown. 因為整個請求體都在記憶體中, 是以避免使用此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());
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

4.2 Post方式送出流

以流的方式POST送出請求體. 請求體的内容由流寫入産生. 這個例子是流直接寫入Okio的BufferedSink. 你的程式可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來擷取. OkHttp的底層對流和位元組的操作都是基于Okio庫, Okio庫也是Square開發的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便于使用的接口來操作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 = ; i <= ; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = ; 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());
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

4.3 Post方式送出檔案

以檔案作為請求體是十分簡單的。

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());
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

4.4 Post方式送出表單

使用FormEncodingBuilder來建構和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();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4.5 Post方式送出分塊請求

MultipartBody.Builder可以建構複雜的請求體, 與HTML檔案上傳形式相容. 多塊請求體中每塊請求都是一個請求體, 可以定義自己的請求頭. 這些請求頭可以用來描述這塊請求, 例如它的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());
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

5. 其他用法

5.1 提取響應頭

典型的HTTP頭像是一個

Map<String, String>

 : 每個字段都有一個或沒有值. 但是一些頭允許多個值, 像Guava的Multimap. 

例如: HTTP響應裡面提供的Vary響應頭, 就是多值的. OkHttp的api試圖讓這些情況都适用. 

當寫請求頭的時候, 使用header(name, value)可以設定唯一的name、value. 如果已經有值, 舊的将被移除, 然後添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的). 

當讀取響應頭時, 使用header(name)傳回最後出現的name、value. 通常情況這也是唯一的name、value. 如果沒有值, 那麼header(name)将傳回null. 如果想讀取字段對應的所有值, 使用headers(name)會傳回一個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"));
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

5.2 使用Gson來解析JSON響應

Gson是一個在JSON和Java對象之間轉換非常友善的api庫. 這裡我們用Gson來解析Github API的JSON響應. 

注意: ResponseBody.charStream()使用響應頭Content-Type指定的字元集來解析響應體. 預設是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;
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

5.3 響應緩存

為了緩存響應, 你需要一個你可以讀寫的緩存目錄, 和緩存大小的限制. 這個緩存目錄應該是私有的, 不信任的程式應不能讀取緩存内容. 

一個緩存目錄同時擁有多個緩存通路是錯誤的. 大多數程式隻需要調用一次new OkHttp(), 在第一次調用時配置好緩存, 然後其他地方隻需要調用這個執行個體就可以了. 否則兩個緩存示例互相幹擾, 破壞響應緩存, 而且有可能會導緻程式崩潰. 

響應緩存使用HTTP頭作為配置. 你可以在請求頭中添加Cache-Control: max-stale=3600 , OkHttp緩存會支援. 你的服務通過響應頭确定響應緩存多長時間, 例如使用Cache-Control: max-age=9600.

private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize =  *  * ; // 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));
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

如果需要阻值response使用緩存, 使用

CacheControl.FORCE_NETWORK

. 如果需要阻值response使用網絡, 使用

CacheControl.FORCE_CACHE

警告: 如果你使用

FORCE_CACHE

, 但是response要求使用網絡, OkHttp将會傳回一個

504 Unsatisfiable Request

響應.

5.3.1 Force a Network Response

有些時候, 比如使用者剛剛點選

重新整理

按鈕, 這時必須跳過緩存, 直接從伺服器抓取資料. 為了強制全面重新整理, 我們需要添加

no-cache

指令:

  • 1

這樣就可以強制每次請求直接發送給源伺服器, 而不經過本地緩存版本的校驗, 常用于需要确認認證的應用和嚴格要求使用最新資料的應用.

5.3.2 Force a Cache Response

有時你會想立即顯示資源. 這樣即使在背景正下載下傳着最新資源, 你的用戶端仍然可以先顯示原有資源, 畢竟有個東西顯示比沒有東西顯示要好. 

如果需要限制讓請求優先使用本地緩存資源, 需要增加

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
 }
}
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

5.4 取消一個Call

使用Call.cancel()可以立即停止掉一個正在執行的call. 如果一個線程正在寫請求或者讀響應, 将會引發IOException. 當call沒有必要的時候, 使用這個api可以節約網絡資源. 例如當使用者離開一個應用時, 不管同步還是異步的call都可以取消. 

你可以通過tags來同時取消多個請求. 當你建構一請求時, 使用RequestBuilder.tag(tag)來配置設定一個标簽, 之後你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的call.

private final ScheduledExecutorService executor = Executors.newScheduledThreadPool();
  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  second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in  second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / f);
      }
    }, , TimeUnit.SECONDS);

    try {
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / f);
      Response response = call.execute();
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / f, e);
    }
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

5.5 逾時

沒有響應時使用逾時結束call. 沒有響應的原因可能是客戶點連結問題、伺服器可用性問題或者這之間的其他東西. OkHttp支援連接配接逾時, 讀取逾時和寫入逾時.

private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(, TimeUnit.SECONDS)
        .writeTimeout(, TimeUnit.SECONDS)
        .readTimeout(, 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);
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5.6 每個call的配置

使用OkHttpClient, 所有的HTTP Client配置包括代理設定、逾時設定、緩存設定. 當你需要為單個call改變配置的時候, 調用

OkHttpClient.newBuilder()

. 這個api将會傳回一個builder, 這個builder和原始的client共享相同的連接配接池, 分發器和配置. 

下面的例子中,我們讓一個請求是500ms的逾時、另一個是3000ms的逾時。

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  second delay.
        .build();

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(, 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(, 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);
    }
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

5.7 處理驗證

這部分和HTTP AUTH有關.

5.7.1 HTTP AUTH

使用HTTP AUTH需要在server端配置http auth資訊, 其過程如下: 

- 用戶端發送http請求 

- 伺服器發現配置了http auth, 于是檢查request裡面有沒有”Authorization”的http header 

- 如果有, 則判斷Authorization裡面的内容是否在使用者清單裡面, Authorization header的典型資料為”Authorization: Basic jdhaHY0=”, 其中Basic表示基礎認證, jdhaHY0=是base64編碼的”user:passwd”字元串. 如果沒有,或者使用者密碼不對,則傳回http code 401頁面給用戶端. 

- 标準的http浏覽器在收到401頁面之後, 應該彈出一個對話框讓使用者輸入帳号密碼; 并在使用者點确認的時候再次送出請求, 這次請求裡面将帶上Authorization header.

一次典型的通路場景是:

  • 浏覽器發送http請求(沒有Authorization header)
  • 伺服器端傳回401頁面
  • 浏覽器彈出認證對話框
  • 使用者輸入帳号密碼,并點确認
  • 浏覽器再次發出http請求(帶着Authorization header)
  • 伺服器端認證通過,并傳回頁面
  • 浏覽器顯示頁面

5.7.2 OkHttp認證

OkHttp會自動重試未驗證的請求. 當響應是

401 Not Authorized

時,

Authenticator

會被要求提供證書. Authenticator的實作中需要建立一個新的包含證書的請求. 如果沒有證書可用, 傳回null來跳過嘗試. 

使用

Response.challenges()

來獲得任何

authentication challenges

的 schemes 和 realms. 當完成一個

Basic challenge

, 使用

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());
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

當認證無法工作時, 為了避免多次重試, 你可以傳回空來放棄認證. 例如, 當

exact credentials

已經嘗試過, 你可能會直接想跳過認證, 可以這樣做:

if (credential.equals(response.request().header("Authorization"))) {
    return null; // If we already failed with these credentials, don't retry.
   }
           
  • 1
  • 2
  • 3

當重試次數超過定義的次數, 你若想跳過認證, 可以這樣做:

if (responseCount(response) >= ) {
    return null; // If we've failed 3 times, give up.
  }

  private int responseCount(Response response) {
    int result = ;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }
           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

這樣, 對OkHttp的使用我們就講完了, 下一節會講OkHttp内部實作.

謝謝下列文章: 

http://www.blogjava.net/yongboy/archive/2015/03/18/423570.html 

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html 

http://www.jianshu.com/p/aad5aacd79bf 

https://imququ.com/post/protocol-negotiation-in-http2.html 

http://blog.csdn.net/wwwsq/article/details/7255062

繼續閱讀