okHttp使用和原理分析

基本用法

  1. GET

在okHttp,每次網絡請求就是一個Request,在Request裏填寫我們需要的url、header等其他參數,再通過OkHttpClien構造出Call,Call內部傳入請求參數,得到回覆,並將結果告訴調用者。

  • 通過同步的方法去操作網絡請求
    同步方法是在當前線程的,所以要開一個子線程去訪問網絡

  • 通過異步的方式:
    在同步的基礎上將execute改成enqueue,並且傳入回調接口,但接口回調回來的代碼是在非UI線程的,因此如果有更新UI的操作記得用Handler或者其他方式在主線程操作。
    異步請求是有條件限制的,默認最多64個請求,而同一個請求默認最多同時存在5個

響應體的 string() 方法對於小文檔來說十分方便、高效。但是如果響應體太大(超過1MB),應避免適應 string()方法 ,因爲他會將把整個文檔加載到內存中。對於超過1MB的響應body,應使用流的方式來處理body。如果希望獲得返回的二進制字節數組,則調用response.body().bytes();如果你想拿到返回的inputStream,則調用response.body().byteStream().

總結:先通過Request.Builder()來構建請求,包括URL;再通過OkHttpClient對象調用newCall(request)通過同步或異步方法發送請求。

  1. POST請求

傳入header或者post參數都是傳到Request裏面,最後的調用方式和GET方法一樣。

  • 提交鍵值對

總結:先創建FormBody.Builder對象builder,然後通過builder.add(key,value)傳入參數,再通過builder.build()方法創建RequestBody對象,接着與get請求類似,構建請求。

  • 提交json數據
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);     //用create()構造RequestBody請求體
      Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
      Response response = client.newCall(request).execute();
    f (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException("Unexpected code " + response);
    } 
}
  • 提交string數據(和提交json類似)
    只需把上面第一句改一下,然後將JSON改爲MEDIA_TYPE_MARKDOWN,json改爲要提交的字符串。
public static final MediaType MEDIA_TYPE_MARKDOWN  = MediaType.parse("text/x-markdown; charset=utf-8");
RequestBody body = RequestBody.create(MEDIA_TYPE_MARKDOWN ,"string to send");

總結:請求體的構建使用RequestBody.create(MediaType,data)

  • 以流的方式POST提交請求,這個例子是流直接寫入Okio的BufferedSink。
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());
}
  • 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());
}
  • POST提交分塊(IMGUR_CLIENT_ID 是本例所需)
private static final String IMGUR_CLIENT_ID = "9199fdef135c122";
  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());
    }
  }
  • 使用緩存
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)         //創建client對象時指明要緩存
            .build();
}

public void run() throws Exception {
    Request request = new Request.Builder()
            .url("http://publicobject.com/helloworld.txt")
            .build();

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

        response1Body = response1.body().string();
    Log.e("response1 normal: ",response1Body+" N");
    Log.e("response1 cache: ",response1.cacheResponse()+" X");  //緩存響應
    Log.e("response1 network: ",response1.networkResponse()+" Y");   //網絡請求響應


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

        response2Body = response2.body().string();
    Log.e("response2 normal: ",response2Body+" N2");
    Log.e("response2 cache: ",response2.cacheResponse()+" X2");
    Log.e("response2 network: ",response2.networkResponse()+" Y2");

okhttp框架全局必須只有一個OkHttpClient實例(new OkHttpClient()),並在第一次創建實例的時候,配置好緩存路徑和大小。只要設置的緩存,okhttp默認就會自動使用緩存功能。
cache域在我們構造OkHttpClient的時候是沒有被初始化的,因此如果我們沒有通過調用Builder的cache方法設置cache值的話,該方法返回的對象實際上是一個不支持任何緩存操作的對象,所以說該對象的所有方法爲空。因此如果需要OkHttpClient支持緩存,需要我們寫一個Cache對象並在構造OkHttpClient的時候將其傳給OkHttpClient。

各個主要類的作用:

  • OkHttpClient:通信的客戶端,用來統一管理髮起請求與解析響應。
  • Call:Call是一個接口,它是HTTP請求的抽象描述,具體實現類是RealCall,它由CallFactory創建。
  • Request:請求,封裝請求的具體信息,例如:url、header等。
  • RequestBody:請求體,用來提交流、表單等請求信息。
  • Response:HTTP請求的響應,獲取響應信息,例如:響應header等。
  • ResponseBody:HTTP請求的響應體,被讀取一次以後就會關閉,所以我們重複調用responseBody.string()獲取請求結果是會報錯的。
  • Interceptor:Interceptor是請求攔截器,負責攔截並處理請求,它將網絡請求、緩存、透明壓縮等功能都統一起來,每個功能都是一個Interceptor,所有的Interceptor最 終連接成一個Interceptor.Chain。典型的責任鏈模式實現。
  • StreamAllocation:用來控制Connections與Streas的資源分配與釋放。
  • RouteSelector:選擇路線與自動重連。
  • RouteDatabase:記錄連接失敗的Route黑名單。

相關問題和原理

  1. 這個庫的優缺點

優點:

  • OkHttp 提供了對最新的 HTTP 協議版本 HTTP/2 和 SPDY 的支持,這使得對同一個主機發出的所有請求都可以共享相同的套接字連接
  • 如果 HTTP/2 和 SPDY 不可用,OkHttp 會使用連接池來複用連接以提高效率。
  • OkHttp 提供了對 GZIP 的默認支持來降低傳輸內容的大小
  • OkHttp 也提供了對 HTTP 響應的緩存機制,可以避免不必要的網絡請求。
  • 當網絡出現問題時,OkHttp 會自動重試一個主機的多個 IP 地址。

缺點:

  • 使用的時候仍然需要自己再做一層請求封裝。
  1. 實現原理
    在這裏插入圖片描述

OkHttp內部的請求流程

使用OkHttp會在請求的時候初始化一個Call的實例,然後執行它的execute()方法或enqueue()方法,內部最後都會執行到getResponseWithInterceptorChain()方法,這個方法裏面通過攔截器組成的責任鏈,依次經過用戶自定義普通攔截器、重試攔截器、橋接攔截器、緩存攔截器、連接攔截器和用戶自定義網絡攔截器以及訪問服務器攔截器等攔截處理過程,來獲取到一個響應並交給用戶

各個攔截器的作用

  • interceptors:用戶自定義攔截器
  • retryAndFollowUpInterceptor:負責失敗重試以及重定向
  • BridgeInterceptor:請求時,對必要的Header進行一些添加,接收響應時,移除必要的Header
  • CacheInterceptor:負責讀取緩存直接返回(根據請求的信息和緩存的響應的信息來判斷是否存在緩存可用)、更新緩存
  • ConnectInterceptor:負責和服務器建立連接,利用 Okio 對 Socket 的讀寫操作進行封裝。
  • networkInterceptors:用戶定義網絡攔截器
  • CallServerInterceptor:負責向服務器發送請求數據、從服務器讀取響應數據

getResponseWithInterceptorChain()響應責任鏈

OkHttp的這種攔截器鏈採用的是責任鏈模式,這樣的好處是將請求的發送和處理分開,並且可以動態添加中間的處理方實現對請求的處理、短路等操作。

dispatcher線程池

在Okhttp中,構建了一個核心爲[0, Integer.MAX_VALUE]的線程池,它不保留任何最小線程數,隨時創建更多的線程數,當線程空閒時只能活60秒,它使用了一個不存儲元素的阻塞工作隊列,一個叫做"OkHttp Dispatcher"的線程工廠。如果當前還能執行一個併發請求,則加入 runningAsyncCalls ,立即執行,否則加入 readyAsyncCalls 隊列

  • 調度線程池Disptcher實現了高併發,低阻塞的實現。
  • 採用隊列Deque作爲緩存,先進先出的順序執行。
  • 任務在try/finally中調用了finished函數,控制任務隊列的執行順序,而不是採用鎖,減少了編碼複雜性提高性能。

ConnectionPool鏈接池

即對於同一主機的多個請求,共用一個Socket連接,而不是每次發送完HTTP請求就關閉底層的Socket,這樣就實現了連接池的概念。

  • 判斷連接是否可用,不可用則從ConnectionPool獲取連接,ConnectionPool無連接,創建新連接,握手,放入ConnectionPool。
  • 它是一個Deque,add添加Connection,使用線程池負責定時清理緩存。
  • 使用連接複用省去了進行 TCP 和 TLS 握手的一個過程。
  1. Volley與OkHttp的對比:

Volley:

  • 支持HTTPS。
  • 支持緩存、異步請求,不支持同步請求
  • 協議類型是Http/1.0, Http/1.1,網絡傳輸使用的是 HttpUrlConnection/HttpClient,數據讀寫使用的IO

OkHttp:

  • 支持HTTPS。
  • 支持緩存、異步請求、同步請求
  • 協議類型是Http/1.0, Http/1.1, SPDY, Http/2.0, WebSocket,網絡傳輸使用的是封裝的Socket,數據讀寫使用的NIO(Okio)。 SPDY協議類似於HTTP,但旨在縮短網頁的加載時間和提高安全性。SPDY協議通過壓縮、多路複用和優先級來縮短加載時間。

原理分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章