HTTP POST請求報文格式分析與Java實現文件上傳

在開發中,我們使用的比較多的HTTP請求方式基本上就是GET、POST。其中GET用於從服務器獲取數據,POST主要用於向服務器提交一些表單數據,例如文件上傳等。而我們在使用HTTP請求時中遇到的比較麻煩的事情就是構造文件上傳的HTTP報文格式,這個格式雖說也比較簡單,但也比較容易出錯。今天我們就一起來學習HTTP POST的報文格式以及通過Java來模擬文件上傳的請求。

首先我們來看一個POST的報文請求,然後我們再來詳細的分析它。

POST報文格式

  1. POST /api/feed/ HTTP/1.1  
  2. Accept-Encoding: gzip  
  3. Content-Length: 225873  
  4. Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  5. Host: www.myhost.com  
  6. Connection: Keep-Alive  
  7.   
  8. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  9. Content-Disposition: form-data; name="lng"  
  10. Content-Type: text/plain; charset=UTF-8  
  11. Content-Transfer-Encoding: 8bit  
  12.   
  13. 116.361545  
  14. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  15. Content-Disposition: form-data; name="lat"  
  16. Content-Type: text/plain; charset=UTF-8  
  17. Content-Transfer-Encoding: 8bit  
  18.   
  19. 39.979006  
  20. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  21. Content-Disposition: form-data; name="images"; filename="/storage/emulated/0/Camera/jdimage/1xh0e3yyfmpr2e35tdowbavrx.jpg"  
  22. Content-Type: application/octet-stream  
  23. Content-Transfer-Encoding: binary  
  24.   
  25. 這裏是圖片的二進制數據  
  26. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp--  
POST /api/feed/ HTTP/1.1
Accept-Encoding: gzip
Content-Length: 225873
Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Host: www.myhost.com
Connection: Keep-Alive

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

116.361545
--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Content-Disposition: form-data; name="lat"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

39.979006
--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Content-Disposition: form-data; name="images"; filename="/storage/emulated/0/Camera/jdimage/1xh0e3yyfmpr2e35tdowbavrx.jpg"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

這裏是圖片的二進制數據
--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp--
這裏我們提交的是經度、緯度和一張圖片(圖片數據比較長,而且比較雜亂,這裏省略掉了)。

格式分析

請求頭分析

我們先看報文格式中的第一行:
  1. POST /api/feed/ HTTP/1.1  
POST /api/feed/ HTTP/1.1
這一行就說明了這個請求的請求方式,即爲POST方式,要請求的子路徑爲/api/feed/,例如我們的服務器地址爲www.myhost.com,然後我們的這個請求的完整路徑就是www.myhost.com/api/feed/,最後說明了HTTP協議的版本號爲1.1。
  1. Accept-Encoding: gzip  
  2. Content-Length: 225873  
  3. Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  4. Host: www.myhost.com  
  5. Connection: Keep-Alive  
Accept-Encoding: gzip
Content-Length: 225873
Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Host: www.myhost.com
Connection: Keep-Alive
這幾個header的意思分別爲服務器返回的數據需要使用gzip壓縮、請求的內容長度爲225873、內容的類型爲"multipart/form-data"、請求參數分隔符(boundary)爲OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp、請求的根域名爲www.myhost.com、HTTP連接方式爲持久連接( Keep-Alive)。

其中這裏需要注意的一點是分隔符,即boundary。boundary用於作爲請求參數之間的界限標識,例如參數1和參數2之間需要有一個明確的界限,這樣服務器才能正確的解析到參數1和參數2。但是分隔符並不僅僅是boundary,而是下面這樣的格式:-- + boundary。例如這裏的boundary爲OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,那麼參數分隔符則爲:
  1. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
不管boundary本身有沒有這個"--",這個"--"都是不能省略的。

我們知道HTTP協議採用“請求-應答”模式,當使用普通模式,即非KeepAlive模式時,每個請求/應答客戶和服務器都要新建一個連接,完成之後立即斷開連接(HTTP協議爲無連接的協議);當使用Keep-Alive模式(又稱持久連接、連接重用)時,Keep-Alive功能使客戶端到服務器端的連接持續有效,當出現對服務器的後續請求時,Keep-Alive功能避免了建立或者重新建立連接。

如上圖中,左邊的是關閉Keep-Alive的情況,每次請求都需要建立連接,然後關閉連接;右邊的則是Keep-Alive,在第一次建立請求之後保持連接,然後後續的就不需要每次都建立、關閉連接了,啓用Keep-Alive模式肯定更高效,性能更高,因爲避免了建立/釋放連接的開銷

http 1.0中默認是關閉的,需要在http頭加入"Connection: Keep-Alive",才能啓用Keep-Alive;http 1.1中默認啓用Keep-Alive,如果加入"Connection: close ",才關閉。目前大部分瀏覽器都是用http1.1協議,也就是說默認都會發起Keep-Alive的連接請求了,所以是否能完成一個完整的Keep- Alive連接就看服務器設置情況。


請求實體分析

請求實體其實就是HTTP POST請求的參數列表,每個參數以請求分隔符開始,即-- + boundary。例如下面這個參數。
  1. --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp  
  2. Content-Disposition: form-data; name="lng"  
  3. Content-Type: text/plain; charset=UTF-8  
  4. Content-Transfer-Encoding: 8bit  
  5.   
  6. 116.361545  
--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
Content-Disposition: form-data; name="lng"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

116.361545
上面第一行爲--OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,也就是--加上boundary內容,最後加上一個換行 (這個換行不能省略),換行的字符串表示爲"\r\n"。第二行爲Content-Disposition和參數名,這裏的參數名爲lng,即經度。Content-Disposition就是當用戶想把請求所得的內容存爲一個文件的時候提供一個默認的文件名,這裏我們不過多關注。第三行爲Content-Type,即WEB 服務器告訴瀏覽器自己響應的對象的類型,還有指定字符編碼爲UTF-8。第四行是描述的是消息請求(request)和響應(response)所附帶的實體對象(entity)的傳輸形式,簡單文本數據我們設置爲8bit,文件參數我們設置爲binary就行。然後添加兩個換行之後纔是參數的具體內容。例如這裏的參數內容爲116.361545。
注意這裏的每行之間都是使用“\r\n”來換行的,最後一行和參數內容之間是兩個換行。文件參數也是一樣的格式,只是文件參數的內容是字節流。
這裏要注意一下,普通文本參數和文件參數有如下兩個地方的不同,因爲其內容本身的格式是不一樣的。
普通參數:
  1. Content-Type: text/plain; charset=UTF-8  
  2. Content-Transfer-Encoding: 8bit  
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
文件參數:
  1. Content-Type: application/octet-stream  
  2. Content-Transfer-Encoding: binary  
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

參數實體的最後一行是: --加上boundary加上--,最後換行,這裏的 格式即爲: --OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp--。

模擬文件上傳請求

  1.     public static void uploadFile(String fileName) {  
  2.         try {  
  3.   
  4.             // 換行符  
  5.             final String newLine = "\r\n";  
  6.             final String boundaryPrefix = "--";  
  7.             // 定義數據分隔線  
  8.             String BOUNDARY = "========7d4a6d158c9";  
  9.             // 服務器的域名  
  10.             URL url = new URL("www.myhost.com");  
  11.             HttpURLConnection conn = (HttpURLConnection) url.openConnection();  
  12.             // 設置爲POST情  
  13.             conn.setRequestMethod("POST");  
  14.             // 發送POST請求必須設置如下兩行  
  15.             conn.setDoOutput(true);  
  16.             conn.setDoInput(true);  
  17.             conn.setUseCaches(false);  
  18.             // 設置請求頭參數  
  19.             conn.setRequestProperty("connection""Keep-Alive");  
  20.             conn.setRequestProperty("Charsert""UTF-8");  
  21.             conn.setRequestProperty("Content-Type""multipart/form-data; boundary=" + BOUNDARY);  
  22.   
  23.             OutputStream out = new DataOutputStream(conn.getOutputStream());  
  24.   
  25.             // 上傳文件  
  26.             File file = new File(fileName);  
  27.             StringBuilder sb = new StringBuilder();  
  28.             sb.append(boundaryPrefix);  
  29.             sb.append(BOUNDARY);  
  30.             sb.append(newLine);  
  31.             // 文件參數,photo參數名可以隨意修改  
  32.             sb.append("Content-Disposition: form-data;name=\"photo\";filename=\"" + fileName  
  33.                     + "\"" + newLine);  
  34.             sb.append("Content-Type:application/octet-stream");  
  35.             // 參數頭設置完以後需要兩個換行,然後纔是參數內容  
  36.             sb.append(newLine);  
  37.             sb.append(newLine);  
  38.   
  39.             // 將參數頭的數據寫入到輸出流中  
  40.             out.write(sb.toString().getBytes());  
  41.   
  42.             // 數據輸入流,用於讀取文件數據  
  43.             DataInputStream in = new DataInputStream(new FileInputStream(  
  44.                     file));  
  45.             byte[] bufferOut = new byte[1024];  
  46.             int bytes = 0;  
  47.             // 每次讀1KB數據,並且將文件數據寫入到輸出流中  
  48.             while ((bytes = in.read(bufferOut)) != -1) {  
  49.                 out.write(bufferOut, 0, bytes);  
  50.             }  
  51.             // 最後添加換行  
  52.             out.write(newLine.getBytes());  
  53.             in.close();  
  54.   
  55.             // 定義最後數據分隔線,即--加上BOUNDARY再加上--。  
  56.             byte[] end_data = (newLine + boundaryPrefix + BOUNDARY + boundaryPrefix + newLine)  
  57.                     .getBytes();  
  58.             // 寫上結尾標識  
  59.             out.write(end_data);  
  60.             out.flush();  
  61.             out.close();  
  62.   
  63.             // 定義BufferedReader輸入流來讀取URL的響應  
  64. //            BufferedReader reader = new BufferedReader(new InputStreamReader(  
  65. //                    conn.getInputStream()));  
  66. //            String line = null;  
  67. //            while ((line = reader.readLine()) != null) {  
  68. //                System.out.println(line);  
  69. //            }  
  70.   
  71.         } catch (Exception e) {  
  72.             System.out.println("發送POST請求出現異常!" + e);  
  73.             e.printStackTrace();  
  74.         }  
  75.     }  
    public static void uploadFile(String fileName) {
        try {

            // 換行符
            final String newLine = "\r\n";
            final String boundaryPrefix = "--";
            // 定義數據分隔線
            String BOUNDARY = "========7d4a6d158c9";
            // 服務器的域名
            URL url = new URL("www.myhost.com");
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 設置爲POST情
            conn.setRequestMethod("POST");
            // 發送POST請求必須設置如下兩行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 設置請求頭參數
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("Charsert", "UTF-8");
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);

            OutputStream out = new DataOutputStream(conn.getOutputStream());

            // 上傳文件
            File file = new File(fileName);
            StringBuilder sb = new StringBuilder();
            sb.append(boundaryPrefix);
            sb.append(BOUNDARY);
            sb.append(newLine);
            // 文件參數,photo參數名可以隨意修改
            sb.append("Content-Disposition: form-data;name=\"photo\";filename=\"" + fileName
                    + "\"" + newLine);
            sb.append("Content-Type:application/octet-stream");
            // 參數頭設置完以後需要兩個換行,然後纔是參數內容
            sb.append(newLine);
            sb.append(newLine);

            // 將參數頭的數據寫入到輸出流中
            out.write(sb.toString().getBytes());

            // 數據輸入流,用於讀取文件數據
            DataInputStream in = new DataInputStream(new FileInputStream(
                    file));
            byte[] bufferOut = new byte[1024];
            int bytes = 0;
            // 每次讀1KB數據,並且將文件數據寫入到輸出流中
            while ((bytes = in.read(bufferOut)) != -1) {
                out.write(bufferOut, 0, bytes);
            }
            // 最後添加換行
            out.write(newLine.getBytes());
            in.close();

            // 定義最後數據分隔線,即--加上BOUNDARY再加上--。
            byte[] end_data = (newLine + boundaryPrefix + BOUNDARY + boundaryPrefix + newLine)
                    .getBytes();
            // 寫上結尾標識
            out.write(end_data);
            out.flush();
            out.close();

            // 定義BufferedReader輸入流來讀取URL的響應
//            BufferedReader reader = new BufferedReader(new InputStreamReader(
//                    conn.getInputStream()));
//            String line = null;
//            while ((line = reader.readLine()) != null) {
//                System.out.println(line);
//            }

        } catch (Exception e) {
            System.out.println("發送POST請求出現異常!" + e);
            e.printStackTrace();
        }
    }

使用Apache Httpmime上傳文件

  1. /** 
  2.  * @param fileName 圖片路徑 
  3.  */  
  4. public static void uploadFileWithHttpMime(String fileName) {  
  5.   
  6.     // 定義請求url  
  7.     String uri = "www.myhost.com";  
  8.   
  9.     // 實例化http客戶端  
  10.     HttpClient httpClient = new DefaultHttpClient();  
  11.     // 實例化post提交方式  
  12.     HttpPost post = new HttpPost(uri);  
  13.   
  14.     // 添加json參數  
  15.     try {  
  16.         // 實例化參數對象  
  17.         MultipartEntity params = new MultipartEntity();  
  18.   
  19.         // 圖片文本參數  
  20.         params.addPart("textParams"new StringBody(  
  21.                 "{'user_name':'我的用戶名','channel_name':'卻道明','channel_address':'(123.4,30.6)'}",  
  22.                 Charset.forName("UTF-8")));  
  23.   
  24.         // 設置上傳文件  
  25.         File file = new File(fileName);  
  26.         // 文件參數內容  
  27.         FileBody fileBody = new FileBody(file);  
  28.         // 添加文件參數  
  29.         params.addPart("photo", fileBody);  
  30.         params.addPart("photoName"new StringBody(file.getName()));  
  31.         // 將參數加入post請求體中  
  32.         post.setEntity(params);  
  33.   
  34.         // 執行post請求並得到返回對象 [ 到這一步我們的請求就開始了 ]  
  35.         HttpResponse resp = httpClient.execute(post);  
  36.   
  37.         // 解析返回請求結果  
  38.         HttpEntity entity = resp.getEntity();  
  39.         InputStream is = entity.getContent();  
  40.         BufferedReader reader = new BufferedReader(new InputStreamReader(is));  
  41.         StringBuffer buffer = new StringBuffer();  
  42.         String temp;  
  43.   
  44.         while ((temp = reader.readLine()) != null) {  
  45.             buffer.append(temp);  
  46.         }  
  47.   
  48.         System.out.println(buffer);  
  49.   
  50.     } catch (UnsupportedEncodingException e) {  
  51.         e.printStackTrace();  
  52.     } catch (ClientProtocolException e) {  
  53.         e.printStackTrace();  
  54.     } catch (IOException e) {  
  55.         e.printStackTrace();  
  56.     } catch (IllegalStateException e) {  
  57.         e.printStackTrace();  
  58.     }  
  59.   
  60. }  
    /**
     * @param fileName 圖片路徑
     */
    public static void uploadFileWithHttpMime(String fileName) {

        // 定義請求url
        String uri = "www.myhost.com";

        // 實例化http客戶端
        HttpClient httpClient = new DefaultHttpClient();
        // 實例化post提交方式
        HttpPost post = new HttpPost(uri);

        // 添加json參數
        try {
            // 實例化參數對象
            MultipartEntity params = new MultipartEntity();

            // 圖片文本參數
            params.addPart("textParams", new StringBody(
                    "{'user_name':'我的用戶名','channel_name':'卻道明','channel_address':'(123.4,30.6)'}",
                    Charset.forName("UTF-8")));

            // 設置上傳文件
            File file = new File(fileName);
            // 文件參數內容
            FileBody fileBody = new FileBody(file);
            // 添加文件參數
            params.addPart("photo", fileBody);
            params.addPart("photoName", new StringBody(file.getName()));
            // 將參數加入post請求體中
            post.setEntity(params);

            // 執行post請求並得到返回對象 [ 到這一步我們的請求就開始了 ]
            HttpResponse resp = httpClient.execute(post);

            // 解析返回請求結果
            HttpEntity entity = resp.getEntity();
            InputStream is = entity.getContent();
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            StringBuffer buffer = new StringBuffer();
            String temp;

            while ((temp = reader.readLine()) != null) {
                buffer.append(temp);
            }

            System.out.println(buffer);

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }

    }
HttpMime.jar下載地址下載httpClient的壓縮包即可,httpmime.jar包含在其中。

轉載:http://blog.csdn.net/bboyfeiyu/article/details/41863951

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