1. 程式人生 > >OkHttp3原始碼詳解(一Request類)

OkHttp3原始碼詳解(一Request類)


           上次在哪篇部落格看到這句話,我們不重複造輪子不表示我們不需要知道輪子該怎麼造及如何更好的造,我覺得很有道理

對於用到專案的某個開源專案我們不應該只停留在會用的階段,廢話不多說,直接開始okhttp原始碼的學習之路

        從上至下我們先從Request類開始閱讀

       一.Request

        每一次網路請求都是一個Request,Request是對url,method,header,body的封裝,也是對Http協議中請求行,請求頭,實體內容的封裝

       
      Request request = new Request
                        .Builder()
                        .url(url)
                        .post(body)
                        .addHeader("Accept","*/*")
                        .cacheContro()
                       .build();

 通常我們通過構建折模式來構建一個Request,對於構建者在OkHttp的原始碼中到處都可以看到,大家可以自己去翻一翻,我們來看一下Request裡的程式碼,Request主要有如下屬性
  private final HttpUrl url;//請求url封裝
  private final String method;//請求方法
  private final Headers headers;//請求頭
  private final RequestBody body;//請求體,也就是http協議的實體內容
  private final Object tag;//被請求的標籤

  private volatile URL javaNetUrl; // Lazily initialized.
  private volatile URI javaNetUri; // Lazily initialized.
  private volatile CacheControl cacheControl; // 快取控制的封裝

  1.HttpUrl

HttpUrl主要用來規範普通的url連線,並且解析url的組成部分

我們來看一下url的構成

scheme://username:[email protected]:port/pathSegment/pathSegment?queryParameter#fragment;

HttpUrl url = HttpUrl.parse("https://www.google.com/search?q=maplejaw");

通過構造者模式來常見:

HttpUrl url = new HttpUrl.Builder()
        .scheme("https")
        .host("www.google.com")
        .addPathSegment("search")
        .addQueryParameter("q", "maplejaw")
        .build();

   

2.Headers

Headers用於配置請求頭,對於請求頭配置大家一定不陌生吧,比如Content-Type,User-Agent和Cache-Control等等

建立Headers也有兩種方式。如下:
(1)of()建立:傳入的陣列必須是偶數對,否則會丟擲異常。

Headers.of("name1","value1","name2","value2",.....);
  還可以使用它的過載方法of(Map<String,String> map)方法來建立

(2)構建者模式建立

 Headers mHeaders=new Headers.Builder()
            .set("name1","value1")//set表示name1是唯一的,會覆蓋掉已經存在的
            .add("name2","value2")//add不會覆蓋已經存在的頭,可以存在多個
            .build();

我們來看一下Header的內部,原始碼就不貼上了很簡單,Headers內部是通過一個數組來儲存header private final String[] namesAndValues;大家可能會有這樣的疑問,為什麼不用Map而用陣列呢?因為Map的Key是唯一的,而header要求不唯一

另外,陣列方便取陣列嗎?很方便,我們來看著兩個方法

/** Returns the field at {@code position} or null if that is out of range. */
  public String name(int index) {
    int nameIndex = index * 2;
    if (nameIndex < 0 || nameIndex >= namesAndValues.length) {
      return null;
    }
    return namesAndValues[nameIndex];
  }

  /** Returns the value at {@code index} or null if that is out of range. */
  public String value(int index) {
    int valueIndex = index * 2 + 1;
    if (valueIndex < 0 || valueIndex >= namesAndValues.length) {
      return null;
    }
    return namesAndValues[valueIndex];
  }

最後通過toString方法轉變成String,方便寫入請求頭,
<span style="font-size:14px;"> @Override public String toString() {
    StringBuilder result = new StringBuilder();
    for (int i = 0, size = size(); i < size; i++) {
      result.append(name(i)).append(": ").append(value(i)).append("\n");
    }
    return result.toString();
  }</span>

2.CacheControl

Cache-Control對應請求頭中Cache-Control中的值,我們先來看一下Http協議中Cache-Control

   ( 1)  Cache-Control:

     Cache-Control指定請求和響應遵循的快取機制。在請求訊息或響應訊息中設定Cache-Control並不會修改另一個訊息處理過程中的快取處理過程。請求時的快取指令有下幾種:

  • Public:所有內容都將被快取(客戶端和代理伺服器都可快取)。
  • Private:內容只快取到私有快取中(僅客戶端可以快取,代理伺服器不可快取)
  • no-cache:請求或者響應訊息不能快取
  • no-store:不使用快取,也不儲存快取
  • max-age:快取的內容將在指定時間(秒)後失效, 這個選項只在HTTP 1.1可用, 並如果和Last-Modified一起使用時, 優先順序較高
    在 xxx 秒後,瀏覽器重新發送請求到伺服器,指定時間(秒)內,客戶端會直接返回cache而不會發起網路請求,若過期會自動發起網路請求
  • min-fresh:指示客戶端可以接收響應時間小於當前時間加上指定時間的響應。
  • max-stale:指示客戶端可以接收超出超時期間的響應訊息。如果指定max-stale訊息的值,那麼客戶機可以接收超出超時期指定值之內的響應訊息。
   (2)CacheControl類

①常用的函式

 final CacheControl.Builder builder = new CacheControl.Builder();
            builder.noCache();//不使用快取,全部走網路
            builder.noStore();//不使用快取,也不儲存快取
            builder.onlyIfCached();//只使用快取
            builder.noTransform();//禁止轉碼
            builder.maxAge(10, TimeUnit.MILLISECONDS);//指示客戶機可以接收生存期不大於指定時間的響應。
            builder.maxStale(10, TimeUnit.SECONDS);//指示客戶機可以接收超出超時期間的響應訊息
            builder.minFresh(10, TimeUnit.SECONDS);//指示客戶機可以接收響應時間小於當前時間加上指定時間的響應。
            CacheControl cache = builder.build();//cacheControl

②CacheControl的兩個常量:

 public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();//不使用快取
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();//只使用快取

     ③請求時如何使用:
final CacheControl.Builder builder = new CacheControl.Builder();
            builder.maxAge(10, TimeUnit.MILLISECONDS);
            CacheControl cache = builder.build();
            final Request request = new Request.Builder().cacheControl(cache).url(requestUrl).build();
            final Call call = mOkHttpClient.newCall(request);//
            call.enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    failedCallBack("訪問失敗", callBack);
                    Log.e(TAG, e.toString());
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    if (response.isSuccessful()) {
                        String string = response.body().string();
                        Log.e(TAG, "response ----->" + string);
                        successCallBack((T) string, callBack);
                    } else {
                        failedCallBack("伺服器錯誤", callBack);
                    }
                }
            });
            return call;
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
 
     以上如果Cache沒有過去會直接返回cache而不會去發起網路請求,若過期自動發起網路請求,注意:如果您使用FORCE_CACHE和網路的響應需求,OkHttp則會返回一個504提示,告訴你不可滿足請求響應,所以我們加一個判斷在沒有網路的情況下使用
     //判斷網路是否連線
        boolean connected = NetworkUtil.isConnected(context);
         if (!connected) {
             request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
          }
     以上這些就是CacheControl類的學習,原始碼沒必要看了,雖然很長但是比較簡單,也就是通過方法來選擇使用哪種快取模式而已

3.RequestBody

requestBody也就是請求實體內容,我們先來看一下如何來構建一個RequestBody

(1)Request.create()方法建立

  public static final MediaType TEXT = MediaType.parse("text/plain; charset=utf-8");
     public static final MediaType STREAM = MediaType.parse("application/octet-stream");
     public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

     //構建字串請求體
     RequestBody body1 = RequestBody.create(TEXT, string);
     //構建位元組請求體
     RequestBody body2 = RequestBody.create(STREAM, byte);
     //構建檔案請求體
     RequestBody body3 = RequestBody.create(STREAM, file);
     //post上傳json
     RequestBody body4 = RequestBody.create(JSON, json);//json為String型別的

     //將請求體設定給請求方法內
     Request request = new Request.Builder()
       .url(url)
       .post(xx)// xx表示body1,body2,body3,body4中的某一個
       .build();


(2)構建表單請求體,提交鍵值對(OkHttp3沒有FormEncodingBuilder這個類,替代它的是功能更加強大的FormBody:)
 //構建表單RequestBody
     RequestBody formBody=new FormBody.Builder()
                 .add("name","maplejaw")
                 .add("age","18")
                  ...     
                 .build();

 (3)構建分塊表單請求體:(OkHttp3取消了MultipartBuilder,取而代之的是MultipartBody.Builder()
                  既可以新增表單,又可以也可以新增檔案等二進位制資料)
public static final MediaType STREAM = MediaType.parse("application/octet-stream");
      //構建表單RequestBody
      RequestBody multipartBody=new MultipartBody.Builder()
                .setType(MultipartBody.FORM)//指明為 multipart/form-data 型別
                .addFormDataPart("age","20") //新增表單資料
                .addFormDataPart("avatar","111.jpg",RequestBody.create(STREAM,file)) //新增檔案,其中avatar為表單名,111.jpg為檔名。
                .addPart(..)//該方法用於新增RequestBody,Headers和新增自定義Part,一般來說以上已經夠用
                .build();



   知道了RequestBody的建立,我們來看一下原始碼

   RequestBody也就是請求實體內容,對於一個Get請求時沒有實體內容的,Post提交才有,而且瀏覽器與伺服器通訊時基本上只有表單上傳才會用到POST提交,所以RequestBody其實也就是封裝了瀏覽器表單上傳時對應的實體內容,對於實體內容是什麼樣還不清楚的可以去看一下我的一篇部落格Android的Http協議的通訊詳解

  OkHttp3中RequestBody有三種建立方式

  ①方式一:

 public static RequestBody create(MediaType contentType, String content) {
    Charset charset = Util.UTF_8;
    if (contentType != null) {
      charset = contentType.charset();//MediaType的為請求頭中的ContentType建立方式:public static final MediaType TEXT = 
                                      //MediaType.parse("text/plain; charset=utf-8")
      if (charset == null) {
        charset = Util.UTF_8;<span style="font-family:Microsoft YaHei;">//如果contentType中沒有指定charset,預設使用UTF-8</span>
        contentType = MediaType.parse(contentType + "; charset=utf-8");
      }
    }
    byte[] bytes = content.getBytes(charset);
    return create(contentType, bytes);
  }
   最終會呼叫下面的方法
 /** Returns a new request body that transmits {@code content}. */
  public static RequestBody create(final MediaType contentType, final byte[] content,
      final int offset, final int byteCount) {
    if (content == null) throw new NullPointerException("content == null");
    Util.checkOffsetAndCount(content.length, offset, byteCount);
    return new RequestBody() {
      @Override public MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return byteCount;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content, offset, byteCount);
      }
    };
  }
   ②方式二:FormBody表單建立,我們來看一下

    FormBody用於普通post表單上傳鍵值對,我們先來看一下建立的方法,再看原始碼

 RequestBody formBody=new FormBody.Builder()
                 .add("name","maplejaw")
                 .add("age","18")
                  ...     
                 .build();

  也就是簡單的鍵值對,通過構建者模式得到FormBody物件,我們來看一下原始碼z
 private static final MediaType CONTENT_TYPE =
  MediaType.parse("application/x-www-form-urlencoded");<span style="font-family:Microsoft YaHei;">//ContentType,請求頭中</span>

  private final List<String> encodedNames;
  private final List<String> encodedValues;

  private FormBody(List<String> encodedNames, List<String> encodedValues) {
    this.encodedNames = Util.immutableList(encodedNames);
    this.encodedValues = Util.immutableList(encodedValues);
  }

  /** The number of key-value pairs in this form-encoded body. */
  public int size() {
    return encodedNames.size();
  }

  public String encodedName(int index) {
    return encodedNames.get(index);
  }

  public String name(int index) {
    return percentDecode(encodedName(index), true);
  }

  public String encodedValue(int index) {
    return encodedValues.get(index);
  }

  public String value(int index) {
    return percentDecode(encodedValue(index), true);
  }

  @Override public MediaType contentType() {
    return CONTENT_TYPE;
  }

  @Override public long contentLength() {
    return writeOrCountBytes(null, true);
  }

  @Override public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }
<span style="font-family:Microsoft YaHei;">   </span>
  private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
    long byteCount = 0L;

    Buffer buffer;
    if (countBytes) {
      buffer = new Buffer();
    } else {
      buffer = sink.buffer();
    }

    for (int i = 0, size = encodedNames.size(); i < size; i++) {
      if (i > 0) buffer.writeByte('&');
      buffer.writeUtf8(encodedNames.get(i));
      buffer.writeByte('=');
      buffer.writeUtf8(encodedValues.get(i));
    }

    if (countBytes) {
      byteCount = buffer.size();
      buffer.clear();
    }

    return byteCount;
  }
   我們主要來看一下方法writeOrCountBytes通過writeOrCountBytes來計算請求體大小和將請求體寫入BufferedSink。

至於BufferSink和Buffer類,這兩個類是Okio中的類,Buffer相當於一個快取區,BufferedSink相當於OutputStream,它擴充套件了

OutputStream的功能,Okio的完整原始碼我後續也會寫部落格

   ③方式三:MultipartBody分塊表單建立

  MultipartBody, 既可以新增表單,又可以也可以新增檔案等二進位制資料,我們就看幾個重要的方法

 public static Part createFormData(String name, String filename, RequestBody body) {
      if (name == null) {
        throw new NullPointerException("name == null");
      }
      StringBuilder disposition = new StringBuilder("form-data; name=");
      appendQuotedString(disposition, name);

      if (filename != null) {
        disposition.append("; filename=");
        appendQuotedString(disposition, filename);
      }

      return create(Headers.of("Content-Disposition", disposition.toString()), body);
    }

  我們來看這個方法,我們是addPart還是addFormDataPart最終都走到了這個方法,封裝成一個Part物件,也就是實體內容中

的Content-Disposition跟檔案二進位制流或者鍵值對的值

  MultipartBody和FormBody大體上相同,主要區別在於writeOrCountBytes方法,分塊表單主要是將每個塊的大小進行累加來求出請求體大小,如果其中有一個塊沒有指定大小,就會返回-1。所以分塊表單中如果包含檔案,預設是無法計算出大小的,除非你自己給檔案的RequestBody指定contentLength。

  private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
    long byteCount = 0L;

    Buffer byteCountBuffer = null;
    if (countBytes) {
      //如果是計算大小的話,就new個
      sink = byteCountBuffer = new Buffer();
    }
    //迴圈塊
    for (int p = 0, partCount = parts.size(); p < partCount; p++) {
      Part part = parts.get(p);
      //獲取每個塊的頭
      Headers headers = part.headers;
      //獲取每個塊的請求體
      RequestBody body = part.body;

      //寫 --xxxxxxxxxx 邊界     
      sink.write(DASHDASH);
      sink.write(boundary);
      sink.write(CRLF);

      //寫塊的頭
      if (headers != null) {
        for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
          sink.writeUtf8(headers.name(h))
              .write(COLONSPACE)
              .writeUtf8(headers.value(h))
              .write(CRLF);
        }
      }

      //寫塊的Content_Type
      MediaType contentType = body.contentType();
      if (contentType != null) {
        sink.writeUtf8("Content-Type: ")
            .writeUtf8(contentType.toString())
            .write(CRLF);
      }

      //寫塊的大小
      long contentLength = body.contentLength();
      if (contentLength != -1) {
        sink.writeUtf8("Content-Length: ")
            .writeDecimalLong(contentLength)
            .write(CRLF);
      } else if (countBytes) {
        // We can't measure the body's size without the sizes of its components.
        //如果有個塊沒有這名大小,就返回-1.
        byteCountBuffer.clear();
        return -1L;
      }

      sink.write(CRLF);

      //如果是計算大小就累加,否則寫入BufferedSink
      if (countBytes) {
        byteCount += contentLength;
      } else {
        body.writeTo(sink);
      }

      sink.write(CRLF);
    }

 //寫 --xxxxxxxxxx-- 結束邊界
    sink.write(DASHDASH);
    sink.write(boundary);
    sink.write(DASHDASH);
    sink.write(CRLF);


    if (countBytes) {
      byteCount += byteCountBuffer.size();
      byteCountBuffer.clear();
    }

    return byteCount;
  }

  好了OkHttp Request類的原始碼也差不讀就這麼多,比較簡單,從簡單的來嘛從淺到深,後面的部落格將繼續深入的學習OkHttp的原始碼,我這個人比較較真喜歡追究看原始碼弄懂到底是這麼回事,這兩天把大學裡面的計算機網路的書又翻出來看了看,以前在學習的時候還是基礎沒打牢啊
 

相關推薦

OkHttp3原始碼(Request)

           上次在哪篇部落格看到這句話,我們不重複造輪子不表示我們不需要知道輪子該怎麼造及如何更好的造,我覺得很有道理 對於用到專案的某個開源專案我們不應該只停留在會用的階段,廢話不多說,直接開始okhttp原始碼的學習之路         從上至下我們先從Re

openTSDB原始碼之Deferred簡單示例2

openTSDB原始碼詳解之Deferred類簡單示例2 1.示例2 1.1 程式碼 程式程式碼如下: public static void test2() { try { //注意這個時候由 dfd -> dfd List(lstDfd)。但是其型

openTSDB原始碼之Deferred程式碼簡單示例1

openTSDB原始碼詳解之Deferred類程式碼簡單示例1 1.示例1 1.1 程式碼 /** * simplest with only 1 defer * 最簡單的,僅僅只有1個defer */ public static void test

JDK原始碼之File

JDK原始碼詳解之File類 1. 類釋義 2. 類方法 listFiles() File[] listFiles() Returns an array of abstract pathnames denoting the files in the dir

Jdk原始碼之`ProcessBuilder()`

Jdk原始碼詳解之ProcessBuilder()類 1.ProcessBuilder類 2.方法簡介 構造器ProcessBuilder /** Constructs a process builder with the specif

Hadoop原始碼之DBOutputFormat

Hadoop 原始碼詳解之 DBOutputFormat 類 1. 類釋義 A OutputFormat that sends the reduce output to a SQL table. 一種將Reduce 輸出到一個SQL表中的輸出格式。 DB

Hadoop原始碼之Mapper

Hadoop原始碼詳解之Mapper類 1. 類釋義 Maps input key/value pairs to a set of intermediate key/value pairs. 將輸入的鍵值對應成一系列的中間鍵值對 Maps are the

Hadoop原始碼之FileOutputFormat

Hadoop 原始碼詳解之FileOutputFormat 類 1. 類釋義 A base class for OutputFormats that read from FileSystems. 一個類從FileSystems讀取 用於OutputFormats 【實在翻

Hadoop 原始碼之FileInputFormat

Hadoop 原始碼詳解之FileInputFormat類【updating…】 1. 類釋義 A base class for file-based InputFormats. 針對基於檔案的 InputFormats 一個基類 FileInputFo

Hadoop原始碼之Job

Hadoop原始碼詳解之Job類 1. 原始碼 包:org.apache.hadoop.mapreduce 繼承的介面有:AutoCloseable,JobContext,org.apache.hadoop.mapreduce.MRJobConfig

補間動畫Animation

補間動畫(Tween animation)是通過在兩個關鍵幀之間補充漸變的動畫效果來實現的。 Android系統提供了四個補間動畫的類,分別是AlphaAnimation、RotateAnimation、ScaleAnimation和TranslateAnimation,另

三:python 對象:數字(上)

結果 dom 運行 精度 升級 方法 函數 般的 代碼 一:python 的數字類型: a)整數和浮點數 b)復數 c)固定精度的十進制數 d)有理分數 e)集合 f)布爾類型 g)無窮的整數精度 h)各種數字內置函數和模塊 二:各種數字類型的詳解   1,數字常量:pyt

python的方法,普通方法和靜態方法

http 接下來 方法調用 pytho 類和實例 訪問類 靜態 都是 com   首先形式上的區別,實例方法隱含的參數為類實例self,而類方法隱含的參數為類本身cls。 靜態方法無隱含參數,主要為了類實例也可以直接調用靜態方法。   所以邏輯上,類方法被類調用,實例方法

06.RTSP原始碼

6.7.rtsp傳輸原始碼分析1 6.8.rtsp傳輸原始碼分析2 6.9.rtsp傳輸原始碼分析3 6.10.rtsp傳輸原始碼分析4 6.11.rtsp傳輸實戰分析 6.12.直接傳送與環狀buffer傳送 6.13.rtsp分包傳送h264的原始碼分析 rtp_timestamp:http

java集合原始碼-LinkedList(4)-基於JDK8

LinkedList 裡面還有個具有新增功能的函式,上回學漏了,這回補上。 它就是linkBefore()------在一個非空節點前,插入資料 這裡打上個斷點 點選下一步。先把size除二  ,去比較。具體的這個node()方法,我們之前學過,這裡跳過。這個方法,其實就

java集合原始碼-ArrayList(2)

上次關於ArrayList的結構沒有做總結。這次還是補充在自己部落格裡面吧。 ArrayList繼承自一個抽象類。實現了四個介面。 AbstractList繼承自AbstractCollection。AbstractCollection繼承自Object。 ArrayL

死磕Netty原始碼之記憶體分配()(PooledByteBufAllocator)

前言 為了避免頻繁的記憶體分配給系統帶來負擔以及GC對系統性能帶來波動,Netty4使用了記憶體池來管理記憶體的分配和回收,Netty記憶體池參考了Slab分配和Buddy分配思想。Slab分配是將記憶體分割成大小不等的記憶體塊,在使用者執行緒請求時根據請求的

死磕Netty原始碼之Reactor執行緒模型()NioEventLoop的啟動

前言 Netty中最核心的就是Reactor執行緒,對應Netty中的程式碼就是NioEventLoop。NioEventLoop是通過NioEventLoopGroup進行維護的,所以在介紹NioEventLoop前我們先介紹一下NioEventLoopGr

spring的BeanFactory和ApplicationContext原始碼

轉自http://www.sandzhang.com/blog/2011/04/10/Spring-BeanFactory-ApplicationContext-Detail-1/ 版本:spring-framework-3.0.5.RELEASE Spring的最核心的部分就是BeanFactory了,

java集合原始碼-ArrayList(5)

上次,測試了java集合類支援遍歷方式的效率比較,今天再通過斷電除錯,去ArrayList底層的迭代器做了什麼。 首先在迭代器這裡打上斷電,(在實際中變數ArrayList最後別用迭代器,因為他很慢) 可以看到這個iterator()方法返回值是一個迭代器,函式體是r