理解Android客戶端POST請求參數

理解Android客戶端POST請求參數

我們都知道,我們的客戶端通過HTTP向服務器發送的post請求實質都是在拼接一個form表單。我們一般會使用下面幾種方式進行post
1. 提交參數
2. 提交文件
3. 即提交參數也提交文件
本文也將就這三種方式的請求進行分析

提交參數的請求

如我們使用OkHttp發起一個post請求,我們需要自己構建一個FormBody表單。使用方法如下:

FormBody.Builder builder = new FormBody.Builder();
builder.add("uid", "10059");
builder.add("token", "J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-");
FormBody body = builder.build();

Request request = new Request.Builder()
     .post(body)
     .url(URL_BASE)
     .build();
mClient.newCall(request).enqueue(new Callback() {..}

其中不重要的回調部分已經省略。可以看出我們簡單拼接了一個uid和一個token的Params參數。我們通過攔截器獲得請求長這個樣子:

D/HttpLogInfo: --> POST https://beta.goldenalpha.com.cn/fundworks/sys/getBanners http/1.1
D/HttpLogInfo: Content-Type: application/x-www-form-urlencoded
D/HttpLogInfo: Content-Length: 48
D/HttpLogInfo: uid=10059&token=J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-

這樣的一個請求表現成form表單格式如下

<form method="post"action="https://beta.goldenalpha.com.cn/fundworks/sys/getBanners" enctype="text/plain">
    <inputtype="text" name="uid" value=10059>
    <inputtype="text" name="uid" value=“J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-> 
</form>


----- 下面是發出去的請求的樣子
POST / HTTP/1.1
Content-Type:application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Content-Length: 48
Connection: Keep-Alive
Cache-Control: no-cache

uid=10059&token=J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-

對於普通的Form POST請求,頭信息每行一條,空行之後便是請求體 也就是我們添加的Params

  1. Content-Length註明內容長度。

  2. Content-Type是application/x-www-form-urlencoded,這意味着消息內容會經過URL編碼,就像在GET請 求時URL裏的QueryString那樣。

  3. Accept-Encoding表示是瀏覽器發給服務器,聲明瀏覽器支持的編碼類型也就是服務返回的時候的編碼格式必須是client支持的不然就會出現亂碼等情況。常見的是gzip格式。參考Accept-Encoding

  4. Cache-Control 是緩存處理字段,服務器將會根據該字段判斷此次請求是否需要進行緩存,OkHttp也可以配置該字段。但是由於Android的緩存大部分是在本地做的,所以這裏也不研究這個字段, 該字段常見的取值有 private、no-cache、max-age、must-revalidate等,默認爲private。 參考Cache-control

看完了請求我們再來看看請求結果是什麼樣的

Connection →keep-alive
Content-Encoding →gzip
Content-Type →application/json;charset=utf-8
Date →Thu, 19 Jan 2017 08:08:12 GMT
Transfer-Encoding →chunked
Vary →Accept-Encoding

{"status":200,"message":"獲取banner成功","debug":null,"attachment":{"banners":[{"qbid":1,"title":"test","summary":"test","url":"","rank":1,"status":1,"image":"http://source.goldenalpha.com.cn/S0XsJR7GHaeIpz","createTime":1482407860000}]}}

可以看到響應和請求一樣也包括響應頭和響應體,響應頭包含的信息有

  1. Content-Encoding 響應採用的編碼形式,該形式是在請求中Accept-Encoding的內容選擇一種編碼方式進行編碼的。客戶端拿到這個響應將通過該方式進行解碼 參考HTTP 協議中的 Content-Encoding

  2. Content-Type 指定請求和響應的HTTP內容類型。如果未指定 ContentType,默認爲text/html。application/json是指響應體內是一個json,charset表示的是該json的編碼方式爲 utf-8

  3. Transfer-Encoding →chunked 表示分塊傳輸編碼,通常,HTTP應答消息中發送的數據是整個發送的,Content-Length消息頭字段表示數據的長度。然而,使用分塊傳輸編碼,數據分解成一系列數據塊,並以一個或多個塊發送,這樣服務器可以發送數據而不需要預先知道發送內容的總大小。通常數據塊的大小是一致的,但也不總是這種情況。如果一個HTTP消息(請求消息或應答消息)的Transfer-Encoding消息頭的值爲chunked,那麼,消息體由數量未定的塊組成,並以最後一個大小爲0的塊爲結束。參考維基百科分塊傳輸編碼

  4. 空一行以後就是響應體的真正內容了


Content-Type enctype MediaType等概念

通過上述的post提交參數請求分析,我們梳理了一個請求到響應的具體過程。這樣方便我們梳理更復雜的提交文件以及混合提交方式。 需要關注的是Content-Type和enctype這兩個字段,因爲在這3種請求中這兩個字段會有所差異。其實在web中是通過enctype來規定請求表單數據的編碼格式,而在Android中是通過setContentType來設置的。這兩種形式在HTTP中都表現爲Content-type.

這裏的Content-type類型遵循了MIME協議對的傳輸類型。也就是我們的參數類型MediaType多媒體類型:

常見的MediaType有
1. URLencoded: application/x-www-form-urlencoded
2. Multipart: multipart/form-data
3. JSON: application/json
4. XML: text/xml
5. 純文本: text/plain
6. 二進制流數據 application/octet-stream
參考HTTP 表單編碼 enctype

一個完整的Content-type表示方式爲:Content-Type: [type]/[subtype]; parameter 也就是我們在響應頭中看到的樣式。如果我們不已完整的形式填寫。則不同的客戶端會有不同的默認值。

  1. type有下面的形式

    1. Text:用於標準化地表示的文本信息,文本消息可以是多種字符集和或者多種格式的;

    2. Multipart:用於連接消息體的多個部分構成一個消息,這些部分可以是不同類型的數據;

    3. Application:用於傳輸應用程序數據或者二進制數據;

    4. Message:用於包裝一個E-mail消息;

    5. Image:用於傳輸靜態圖片數據;

    6. Audio:用於傳輸音頻或者音聲數據;

    7. Video:用於傳輸動態影像數據,可以是與音頻編輯在一起的視頻數據格式。

  2. subtype用於指定type的具體形式,MIME使用Internet Assigned Numbers Authority (IANA)作爲中心的註冊機制來管理這些值。 如果想了解到底有哪些組合方式請參考Media Types

  3. parameter 常用於指定附加信息,一般是用於來指定文本的編碼格式如UTF-8

  4. MIME根據type制定了默認的subtype,當客戶端不能確定消息的subtype的情況下,消息被看作默認的subtype進行處理。Text默認是text/plain,Application默認是application/octet-stream而Multipart默認情況下被看作multipart/mixed。

  5. 事實上我們在Android中的上傳下載操作設置的contenttype多爲application/octet-stream 即爲二進制流。

  6. 對於multipart的請求方式,還可以單獨設置part的content-type的。稍後再上傳文件的時候回詳細講。


post請求提交文件

這裏爲什麼要把上傳文件但單獨列出來,是因爲post上傳文件可以有兩種方式一種是使用FileBody 一種是是使用MultipartBody 兩者的不同之處是請求體中的數據類型,前者是隻有文件,後者是還可以包含一些text參數。

同樣貼出File請求的辦法

File mfile = new File("/storage/sdcard0/alpha/image/1484209275141.jpg");

String sha1_ = MD5Util.sha1(mfile);
String size = String.valueOf(mfile.length());
String uid = "10060";
String token = "8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-";
String url = "......falphaupload/upload/upload.json";
url = url + "?" + "sha1" + "=" + sha1_ + "&" + "size" + "=" + size + "&" + "uid" + "=" + uid + "&" + "token" + "=" + token
      + "&" + "vinfo" + "=" + "AA_samsung_001_30000" + "&" + "suffix" + "=" + "jpg" + "&" + "type" + "=" + "1" + "&" + "plat" + "=" + "0";

RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), mfile);
Request request = new Request.Builder()
      .post(requestBody)
      .url(url)
      .build();

mClient.newCall(request).enqueue(new Callback() {

Log顯示:

請求
D/HttpLogInfo: --> POST https://beta.goldenalpha.com.cn/falphaupload/upload/upload.json?sha1=1F179D204C4D89533240E51EF8CCCA5D6E08A0F5&size=27748&uid=10060&token=8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-&vinfo=AA_samsung_001_30000&suffix=jpg&type=1&plat=0 http/1.1
D/HttpLogInfo: Content-Type: application/octet-stream
D/HttpLogInfo: Content-Length: 27748
D/HttpLogInfo: --> END POST (binary 27748-byte body omitted)

響應
D/HttpLogInfo: Server: nginx/1.0.15
D/HttpLogInfo: Date: Thu, 19 Jan 2017 09:54:12 GMT
D/HttpLogInfo: Content-Type: application/json;charset=UTF-8
D/HttpLogInfo: Transfer-Encoding: chunked
D/HttpLogInfo: Connection: keep-alive
D/HttpLogInfo: Vary: Accept-Encoding
D/HttpLogInfo: {"status":200,"message":"成功","debug":null,"attachment":{"mid":8426}}
D/HttpLogInfo: <-- END HTTP (72-byte body)

由log可以看出請求的Content-Type 爲application/octet-stream意思就是請求體內包含的信息爲二進制流數據。也就是說我們的文件被編碼爲二進制的形式發送給服務器。

值得注意的是,我們這裏RequestBody內就添加了一個file,而沒有其他的key value,然後我們使用MediaType.parse("application/octet-stream")設置我們請求體的Content-Type。我們可以嘗試把該字段改成其他的類型如image/jpeg,但是請求不會成功。其他的Content—Type類型可以參考Content—Type對照表


Post上傳文件及參數

上述請求的時候我們的請求體只攜帶了一個文件,而且只能攜帶一個。如果我們單純的想上傳一個圖片或者文件的時候我們可以通過上述方法構建一個請求體。但是日常的開發中我們這樣的需求並不能滿足我們的需求,而且我們也不希望手動拼接如上的url這麼多參數。那麼採用multipart/form-data的請求方式就此誕生了。

我們發送一個multipart/form-data的表單格式大體如下:

<form method="post"action="url" enctype=”multipart/form-data”>
    <inputtype="text" name="desc">
    <inputtype="file" name="pic">
 </form>

其中的enctype就是我們上述所說的content-type屬性。那麼我們通過攔截器攔截請求後大體會看出該請求的數據爲:

POST  HTTP/1.1
Accept-Language: zh-cn,zh;q=0.5
Accept-Charset: GBK,utf-8
Connection: keep-alive
Content-Length: 60408
Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC

--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="desc"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

[......][......][......][......]...........................
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="pic"; filename="photo.jpg"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

[圖片二進制數據]
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--

我們來分析一下上述數據的內容:
我們可以看multipart的請求體有多塊數據構成,每一塊都有他自己的Content-Type,Content-Transfer-Encoding,Content-Disposition。而且在請求頭中我們也發現了boundary這個字段。--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--看起來像分隔線的東西就好像是boundary的值。

這個boundary根據RFC 1867定義,是一段數據的“分割邊界”,這個“邊界數據”不能在內容其他地方出現,一般來說使用一段從概率上說“幾乎不可能”的數據即可。不同的瀏覽器及webkite實現的方式不太相同,但是這個數據是webkit隨機生成的。而不是遍歷請求體後生成的,雖然是隨機生成,但是絕大多數條件下他也是不能和請求體中的字節重複的。如果你發現有重複的話,那麼請去買彩票。

選擇了這個邊界之後,webkite便把它放在Content-Type 裏面傳遞給服務器,服務器根據此邊界解析數據。下面的數據便根據boundary劃分段,每一段便是一項數據。

每個field被分成小部分,而且包含一個value是”form-data”的”Content-Disposition”的頭部;一個”name”屬性對應field的名稱,文件的話還會多出一個filename的屬性。

接下來我們看下在OkHttp中該請求應該如何構造:

   File mFile = new File("/storage/sdcard0/alpha/image/1484209275141.jpg");
   String sha1_ = MD5Util.sha1(mFile);
   String size = String.valueOf(mFile.length());
   String uid = "10060";
   String token = "8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-";
   String url = "..../falphaupload/upload/upload.json";

   MultipartBody.Builder builder = new MultipartBody.Builder();
   builder.addFormDataPart("sha1",sha1_);
   builder.addFormDataPart("size",size);
   builder.addFormDataPart("suffix","jpg");
   builder.addFormDataPart("type","1");
   builder.addFormDataPart("uid",uid);
   builder.addFormDataPart("plat","0");
   builder.addFormDataPart("token",token);
   builder.addFormDataPart("vinfo","AA_samsung_001_30000");
   builder.addFormDataPart("temp.jpg",mFile.getAbsolutePath(),RequestBody.create(MediaType.parse("application/octet-stream"),mFile));

   MultipartBody multipartBody = builder.build();

   Request request = new Request.Builder()
           .post(multipartBody)
           .url(url)
           .build();

   mClient.newCall(request).enqueue(new Callback() {
       @Override
       public void onFailure(Call call, IOException e) {

       }

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

       }
   });

我們看到OkHttp提供給你麼MultipartBody的構造器Builder,我們通過構造器可以調用addFormDataPart方法來構建我們的表單。該方法有如下兩個重載方法。

/** Add a form data part to the body. */
public Builder addFormDataPart(String name, String value) {
 return addPart(Part.createFormData(name, value));
}

/** Add a form data part to the body. */
public Builder addFormDataPart(String name, String filename, RequestBody body) {
 return addPart(Part.createFormData(name, filename, body));
}

接下來Part.createFormData應該就是構造我們對應的上述表單數據中的每一段數據了,進入源一看果真不出意外,form-data name Content-Disposition 這幾個熟悉的字樣就映入眼簾了。至於具體怎麼拼接的有興趣的可以自己在深入研究,畢竟我們已經知道他最終的樣子。

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

爲什麼會寫這篇文章

至於爲什麼要整理這篇文章,是因爲最近詢問我使用OkHttp上傳圖片或者文件的小夥伴有點多,因爲現在的項目的框架,還是我們公司的老大自己搭的HttpClient的框架,對於OkHttp的瞭解也不多,所以就研究了一下,以後也可能使用這個框架來替代掉公司的項目框架。

其中有一個同學也問了我使用Client上傳圖片和使用OkHttp上傳圖片的區別。那麼就以他們公司的上傳框架代碼分析下如何從HttpClient 轉到 OkHttp

下面是使用HttpClient構造請求的方法:

HttpClient包中各種各樣的body: FileBody StringBody MultiBody 其實就是對應了常用的三種請求方式。而這幾個類在OkHttp中也能找到他的身影,我們知道OkHttp的RequestBody有三種構造參數:

  1. public static RequestBody create(MediaType contentType, String content) {} — Stringbody

  2. public static RequestBody create(final MediaType contentType, final File file) {} — FileBody

  3. 而MultiBody 在OkHttp3.0以後單獨出來一個RequestBody的子類,它的 public Builder addFormDataPart(String name, String filename, RequestBody body) {} 以及public Builder addFormDataPart(String name, String value) {} 通過這兩個方法 我們基本上可以構造任何請求體。


下面來看下具體實現:

       HttpEntity entity = null;
        HttpPost httpPostUpLoadFile = new HttpPost(uri);
        //這裏拼接了一個Http請求的請求頭 就是我們上邊分析的 Accept-Charset: GBK,utf-8 Content-Type:multipart/form-data; boundary=
        httpPostUpLoadFile.addHeader("Accept", "application/json");
        // boundary可以是任意字符,但是必須和MultipartEntity的boundary相同,否則就會報錯
        httpPostUpLoadFile.addHeader("Content-Type", "multipart/form-data;boundary=-");
        try {
            // 與header的boundary一致,否則報錯
            MultipartEntity multiEntity = new MultipartEntity(HttpMultipartMode.STRICT, "-", Charset.forName(HTTP.UTF_8));

            //拼接params參數 在Client包中稱作StringBody請求體 其實就是Content—Type爲text/plan的字段 new Stringbody對應的請求頭內容
            //Content-Disposition: form-data;name="desc"
            //Content-Type: text/plain; charset=UTF-8
            for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext(); ) {
                String name = iter.next();
                String value = params.get(name);
                multiEntity.addPart(name, new StringBody(value, Charset.forName(HTTP.UTF_8)));

            }
            //這裏是上傳文件
            if (files != null) {
                for (int i = 0; i < files.size(); i++) {
                    File file = new File(files.get(i));
                    //拼接一個文件的請求體 在client中被稱爲FileBody 也就是Content-Type爲application/octet-stream的請求體

                    //Content-Disposition: form-data;name="pic"; filename="photo.jpg"
                    //Content-Type: application/octet-stream
                    //Content-Transfer-Encoding: binary
                    ContentBody cbFile = new FileBody(file);

                    //addpart的兩個參數一個是filename 一個是file 請求體
                    multiEntity.addPart("fjList" + "[" + i + "].file", cbFile);
                }
            }
            if (images != null) {

                // 圖片參數 圖片的話求file是相同的了唯一不同的就是FileBody的Content—Type可以更進一步的指定爲image/jpeg 其實實質也是而二進制傳輸,
                // 但是這個字段需要跟服務器約定好,如果服務器解析的image/jpeg你傳application/octet-stream服務器可能解析不到

                for (int i = 0; i < images.size(); i++) {
                    File f = new File(images.get(i));
                    if (f.exists()) {
                        FileBody fp = new FileBody(f, "image/jpeg");
                        multiEntity.addPart("fjList" + "[" + i + "].file", fp);
                    }
                }
            }
            //接下來就是發送請求 這裏不再贅述了

            httpPostUpLoadFile.setEntity(multiEntity);
            // 創建客戶端
            HttpClient httpClient = getNewHttpClient();
            // 執行請求獲得響應
            HttpResponse response = httpClient.execute(httpPostUpLoadFile);
            // 解析響應對象
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                entity = response.getEntity();
                String json = "";
                if (entity != null) {
                    json = EntityUtils.toString(entity, "utf-8");
                    System.out.println(json);
                    return json;
                }
            }
        } catch (Exception e) {
            System.out.println(e.toString());
        }
        return uri;

這裏是修改爲OkHttp後的請求操作,主要區別就是在與請求體的封裝方法。

public static String getEntity2(String uri, Map<String, String> params, ArrayList<String> images, ArrayList<String> files) {

        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        OkHttpClient okHttpClient = builder.build();

        MultipartBody.Builder multiBuilder = new MultipartBody.Builder("-");
        //添加key value 請求體
        for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext(); ) {
            String name = iter.next();
            String value = params.get(name);
            multiBuilder.addPart(RequestBody.create(MediaType.parse("text/plain"), value)); //對於text/plain okhttp默認的Charset就是UTF-8所以不用手動設置
            //multiBuilder.addFormDataPart(name,value); 這種方式也可以 爲了方便理解寫成上邊的添加請求體方式
        }

        //這裏是上傳文件
        if (files != null) {
            for (int i = 0; i < files.size(); i++) {
                File file = new File(files.get(i));
                //這兩種方式都可以,但是服務器對於file的名字有要求的時候就需要用第一種了
                multiBuilder.addFormDataPart("fjList" + "[" + i + "].file", file.getAbsolutePath(), RequestBody.create(MediaType.parse("application/octet-stream"), file));
                //multiBuilder.addPart(RequestBody.create(MediaType.parse("application/octet-stream"),file));
            }
        }

        if (images != null) {

            for (int i = 0; i < images.size(); i++) {
                File f = new File(images.get(i));
                if (f.exists()) {
                    multiBuilder.addFormDataPart("fjList" + "[" + i + "].file", f.getAbsolutePath(), RequestBody.create(MediaType.parse("image/jpeg"), f));
                }
            }
        }

        //構造請求體完成
        MultipartBody multipartBody = multiBuilder.build();

        Request.Builder requestBuilder = new Request.Builder();
        // okHttp的請求頭是在Request.Builder添加的
        requestBuilder.addHeader("Accept", "application/json");
        requestBuilder.addHeader("Content-Type", "multipart/form-data;boundary=-");
        Request request = requestBuilder
                .post(multipartBody)
                .url(uri)
                .build();

        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //響應失敗操作
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //響應完成操作
            }
        });

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