OkHttp 源碼簡要分析 (Request 詳解)

我們知道,http 請求分爲三個部分, 請求行、請求頭和請求體;對應的消息也分爲三個部分:響應行、響應頭和響應體。以前使用 HttpURLConnection 時,我們很容易設置消息頭及參數,它內部是封裝了 Socket 供我們使用。補充一點,我們知道網絡運輸層是由 TCP 和 UDP 構成的,TCP 建立連接,安全可靠,以流傳輸數據,沒有大小限制,速度慢;UDP 是不建立連接,每次傳遞數據限制在64k內,數據容易丟失,但是速度快。TCP 和 UDP 都是依據Socket來生效的,而 http 則是建立在 TCP 基礎上產生的。 舉個 HttpURLConnection 的 get 和 pos 請求的例子

    private void get(){
        try {
            //子線程中執行請求
            String age = "20", address = "SH";
            URL url = new URL("http://baidu.com" + "?age=" + age + "&address=" + address);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);
            if (connection.getResponseCode() == 200) {
                InputStream inputStream = connection.getInputStream();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void post() {
        try {
            //子線程中執行請求
            String age = "20", address = "SH";
            URL url = new URL("http://baidu.com");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            String content = "age=" + URLEncoder.encode(age) + "&address=" + URLEncoder.encode(address);//數據編解碼
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//設置請求頭
            connection.setRequestProperty("Content-Length", content.length() + "");
            connection.setDoOutput(true);
            OutputStream outputStream = connection.getOutputStream();
            outputStream.write(content.getBytes());
            if (connection.getResponseCode() == 200) {
                InputStream inputStream = connection.getInputStream();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


通過對比,明顯可以看出get和post請求的設置方式不一樣,由於 HttpURLConnection 封裝的比較好,我們直接設置就行了,接下來看看 OkHttp,OkHttp 是個網絡請求框架,支持異步和同步請求,也支持 get 、post 及壓縮上傳等,舉個栗子

    private void okhttp() {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String string = response.body().string();
                Log.e("onResponse", string);
            }
        });
    }

    private void okhttpPost() {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .build();

        RequestBody body = new FormBody.Builder()
                .add("useName", "老老")
                .add("usePwd", "321")
                .build();

        Request request = new Request.Builder()
                .url("https://kp.dftoutiao.com/announcement")
                .header("User-Agent", "OkHttp Example")
                .addHeader("Accept", "application/json; q=0.5")
                .post(body)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    if(response.body() != null){
                        String responseBody = response.body().string();
                        Log.i("onResponse"," onResponse    "  +  responseBody);

                    }
                }
            }
        });

    }

    private void okhttpPostGizp(String content) {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request request =  chain.request().newBuilder()
                                .header("Content-Encoding","gzip")
                                .build();
                        return chain.proceed(request);
                    }
                })
                .build();

        RequestBody requestBody = new RequestBody() {

            @Override
            public MediaType contentType() {
                return  MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8");
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
                gzipSink.writeUtf8(content);
                gzipSink.flush();
                gzipSink.close();
            }
        };

        Request request = new Request.Builder()
                .url("https://test.upstring.cn")
                .post(requestBody)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });

    }

今天着重看看請求相關的代碼,先看看 Request 這個類

public final class Request {
  private final HttpUrl url;
  private final String method;
  private final Headers headers;
  private final RequestBody body;
  private final Object tag;

  private volatile CacheControl cacheControl; // Lazily initialized.

    ...
}

用到了 Builder 模式,這個模式適合有大量參數需要設置的bean,提高寫作效率及觀賞性。 HttpUrl 對應的是請求行,即所謂的url;method 對應的是請求格式,比如是 get 還是 post;headers 是消息頭,比如 User-Agent 等;body 是請求體,get方式沒有,它是post的,裏面包含一些請求參數,get方式的請求參數是拼接在url後面;tag 是用來做標識的,根據標識來找到請求的 request,可以取消請求等;cacheControl 是告訴服務端緩存模式,no-cache表示不使用緩存。

先看看 HttpUrl 這個類,它說白了就是對 url 做了詳細的拆分的工具類,對外提供各種細節,具體操作都是java代碼,我直接舉個例子

    private static void testHttpUrl() {
        HttpUrl parsed = HttpUrl.parse("https://translate.google.cn/?view=home&op=translate&sl=auto&tl=zh-CN&text=老大");
        System.out.println("scheme:  " +  parsed.scheme() + "\n"  +
                        "query:   " +  parsed.query() + "\n"  +
                        "encodedQuery:  " +  parsed.encodedQuery() + "\n"  +
                        "host:    " +  parsed.host() + "\n"  +
                        "port:    " +  parsed.port() + "\n"
                );
    }


打印結果是

scheme:  https
query:   view=home&op=translate&sl=auto&tl=zh-CN&text=老大
encodedQuery:  view=home&op=translate&sl=auto&tl=zh-CN&text=%E8%80%81%E5%A4%A7
host:    translate.google.cn
port:    443

這裏基本就是我們需要的各種值了。如果我們想在外部添加參數,可以使用 Builder 模式中的 addQueryParameter() 、addEncodedQueryParameter() 方法,區別就是傳入的參數是否已經轉碼,默認會用 URLEncoder.encode(value, "utf-8" ) 來轉碼,防範中文出錯。

請求方式 method 默認是 "GET",如果有傳入 RequestBody 則變爲 "POST",也可以通過暴露的方法設置它的值;

Headers 這個也比較簡單,裏面是個字符串數組對象,用來存儲頭部信息的 key 和 value,它只能是字符串,不能是漢字,如果需要則先 URLEncoder 轉碼。


最後看看 RequestBody 這個類,它是個抽象類,裏面有抽象方法和靜態方法

    public abstract class RequestBody {
        public abstract MediaType contentType();
        public long contentLength() throws IOException {
            return -1;
        }
        public abstract void writeTo(BufferedSink sink) throws IOException;
        ...
    }

這裏面有兩個比較關鍵的類,一個是 MediaType, 一個是 Okio 中的 Sink,Okio 前面幾章講過了,這裏就不多說了,不動Okio的話,OkHttp 基本就很難弄懂了;看看 MediaType 這個類

public final class MediaType {
      private final String mediaType;
      private final String type;
      private final String subtype;
      private final String charset;
    
      private MediaType(String mediaType, String type, String subtype, String charset) {
        this.mediaType = mediaType;
        this.type = type;
        this.subtype = subtype;
        this.charset = charset;
      }

      public static MediaType parse(String string) {
            Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
            if (!typeSubtype.lookingAt()) return null;
            String type = typeSubtype.group(1).toLowerCase(Locale.US);
            String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
        
            String charset = null;
            Matcher parameter = PARAMETER.matcher(string);
            for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
              parameter.region(s, string.length());
              if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
        
              String name = parameter.group(1);
              if (name == null || !name.equalsIgnoreCase("charset")) continue;
              String charsetParameter = parameter.group(2) != null
                  ? parameter.group(2)  // Value is a token.
                  : parameter.group(3); // Value is a quoted string.
              if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
                throw new IllegalArgumentException("Multiple different charsets: " + string);
              }
              charset = charsetParameter;
            }
        
            return new MediaType(string, type, subtype, charset);
      }
    
      public static MediaType parse(String string) {
            Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
            if (!typeSubtype.lookingAt()) return null;
            String type = typeSubtype.group(1).toLowerCase(Locale.US);
            String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
        
            String charset = null;
            Matcher parameter = PARAMETER.matcher(string);
            for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
              parameter.region(s, string.length());
              if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
        
              String name = parameter.group(1);
              if (name == null || !name.equalsIgnoreCase("charset")) continue;
              String charsetParameter = parameter.group(2) != null
                  ? parameter.group(2)  // Value is a token.
                  : parameter.group(3); // Value is a quoted string.
              if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
                throw new IllegalArgumentException("Multiple different charsets: " + string);
              }
              charset = charsetParameter;
            }
        
            return new MediaType(string, type, subtype, charset);
      }
  
      ...
}

這個類中有幾個屬性,比較核心的就是 parse() 方法,這裏會把傳進去的值按照正則去切分,分別賦值給幾個屬性,例如 
 MediaType type = MediaType.parse("application/x-www-form-urlencoded;charset=utf-8"); 其中,它  type : application;   subtype : x-www-form-urlencoded;    charset : UTF-8;    mediaType : application/x-www-form-urlencoded;charset=utf-8。

重新回到 RequestBody 中,發現最終會執行

  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);
      }
    };
  }


方法,這個方法中對應也是需要 MediaType 和 Okio 才能理解,我們看看上文中的例子 FormBody ,看看它有什麼特別的。FormBody 中有個內部類 Builder,對外提供的添加參數的方法,也是兩個,一個是直接添加,另一個添加已經編碼過的值,然後通過 Builer 模式創建 FormBody 對象,看看 FormBody 中的三個抽象方法:

  private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");

  @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);
  }

contentType() 方法中返回的是靜態對象 CONTENT_TYPE,這個是固定的;另外兩個方法都調用了同一個方法,區別就是參數不一樣

  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;
  }

這裏不得不感慨,OkHttp 的作者真是把 Okio 用到了極致,writeOrCountBytes(null, true) 時,此時創建了 Buffer 對象,然後把參數都添加了進去,重點是它通過 buffer.size() 算出參數的長度,此時是以字節作爲個數的,然後把 Buffer 清空; writeOrCountBytes(sink, false) 中是傳入一個 sink 對象,獲取它內部的 Buffer 對象,把參數添加到 Buffer 中。這裏真的是很巧妙。RequestBody 還有個子類,是 MultipartBody,這個暫不分析。


Cache-Control: no-cache 這個意思是不用緩存,每次都用最新的。

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