原文地址:http://xixinfei.iteye.com/blog/2002017
含義 ENCTYPE="multipart/form-data" 說(shuō)明:
通過(guò) http 協(xié)議上傳文件 rfc1867協(xié)議概述,jsp 應用舉例,客戶(hù)端發(fā)送內容構造
1、概述在最初的 http 協(xié)議中,沒(méi)有上傳文件方面的功能。 rfc1867 (http://www.ietf.org/rfc/rfc1867.txt) 為 http 協(xié)議添加了這個(gè)功能??蛻?hù)端的瀏覽器,如 Microsoft IE, Mozila, Opera 等,按照此規范將用戶(hù)指定的文件發(fā)送到服務(wù)器。服務(wù)器端的網(wǎng)頁(yè)程序,如 php, asp, jsp 等,可以按照此規范,解析出用戶(hù)發(fā)送來(lái)的文件。Microsoft IE, Mozila, Opera 已經(jīng)支持此協(xié)議,在網(wǎng)頁(yè)中使用一個(gè)特殊的 form 就可以發(fā)送文件。絕大部分 http server ,包括 tomcat ,已經(jīng)支持此協(xié)議,可接受發(fā)送來(lái)的文件。各種網(wǎng)頁(yè)程序,如 php, asp, jsp 中,對于上傳文件已經(jīng)做了很好的封裝。
2、上傳文件的實(shí)例:用 servelet 實(shí)現(http server 為 tomcat 4.1.24)1. 在一個(gè) html 網(wǎng)頁(yè)中,寫(xiě)一個(gè)如下的form :
<form enctype="multipart/form-data" action="http://192.168.29.65/UploadFile" method=post>
load multi files :<br>
<input name="userfile1" type="file"><br>
<input name="userfile2" type="file"><br>
<input name="userfile3" type="file"><br> <input name="userfile4" type="file"><br>
text field :<input type="text" name="text" value="text"><br>
<input type="submit" value="提交"><input type=reset></form>
用戶(hù)可以選擇多個(gè)文件,填寫(xiě)表單其它項,點(diǎn)擊“提交”按鈕后就開(kāi)始上傳給 http://192.168.29.65/upload_file/UploadFile
這是一個(gè) servelet 程序注意 enctype="multipart/form-data", method=post, type="file" 。根據 rfc1867, 這三個(gè)屬性是必須的。multipart/form-data 是新增的編碼類(lèi)型,以提高二進(jìn)制文件的傳輸效率。具體的解釋請參閱 rfc18672. 服務(wù)端 servelet 的編寫(xiě)現在第三方的 http upload file 工具庫很多。Jarkata 項目本身就提供了fileupload 包http://jakarta.apache.org/commons/fileupload/ 。
文件上傳、表單項處理、效率問(wèn)題基本上都考慮到了。在 Struts 中就使用了這個(gè)包,不過(guò)是用 Struts 的方式另行封裝了一次。這里我們直接使用 fileupload 包。至于Struts 中的用法,請參閱 Struts 相關(guān)文檔。這個(gè)處理文件上傳的 servelet 主要代碼如下:
public void doPost( HttpServletRequest request, HttpServletResponse response )
{
DiskFileUpload diskFileUpload = new DiskFileUpload(); // 允許文件最大長(cháng)度
diskFileUpload.setSizeMax( 100*1024*1024 ); // 設置內存緩沖大小
diskFileUpload.setSizeThreshold( 4096 ); // 設置臨時(shí)目錄
diskFileUpload.setRepositoryPath( "c:/tmp" );
List fileItems = diskFileUpload.parseRequest( request );
Iterator iter = fileItems.iterator(); for( ; iter.hasNext(); )
{
FileItem fileItem = (FileItem) iter.next();
if( fileItem.isFormField() ) { // 當前是一個(gè)表單項
out.println( "form field : " + fileItem.getFieldName() + ", " + fileItem.getString() );
} else {
// 當前是一個(gè)上傳的文件
String fileName = fileItem.getName();
fileItem.write( new File("c:/uploads/"+fileName) );
}
}}
為簡(jiǎn)略起見(jiàn),異常處理,文件重命名等細節沒(méi)有寫(xiě)出。3、 客戶(hù)端發(fā)送內容構造假設接受文件的網(wǎng)頁(yè)程序位于 http://192.168.29.65/upload_file/UploadFile.假設我們要發(fā)送一個(gè)二進(jìn)制文件、一個(gè)文本框表單項、一個(gè)密碼 框表單項。文件名為 E:\s ,其內容如下:(其中的XXX代表二進(jìn)制數據,如 01 02 03)abbXXXccc 客戶(hù)端應該向 192.168.29.65 發(fā)送如下內容:
POST /upload_file/UploadFile HTTP/1.1
Accept: text/plain, */*
Accept-Language: zh-cn
Host: 192.168.29.65:80
Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6
User-Agent: Mozilla/4.0 (compatible; OpenOffice.org)
Content-Length: 424
Connection: Keep-Alive -----------------------------7d33a816d302b6
Content-Disposition:form-data;
name="userfile1";
filename="E:\s"Content-Type:
application/octet-stream abbXXXccc
-----------------------------7d33a816d302b6
Content-Disposition: form-data;
name="text1" foo
-----------------------------7d33a816d302b6
Content-Disposition: form-data;
name="password1" bar
-----------------------------7d33a816d302b6--
(上面有一個(gè)回車(chē))此內容必須一字不差,包括最后的回車(chē)。
注意:Content-Length: 424 這里的424是紅色內容的總長(cháng)度(包括最后的回車(chē))
注意這一行:Content-Type: multipart/form-data; boundary=---------------------------7d33a816d302b6
根據 rfc1867, multipart/form-data是必須的.---------------------------7d33a816d302b6 是分隔符,分隔多個(gè)文件、表單項。
其中33a816d302b6 是即時(shí)生成的一個(gè)數字,用以確保整個(gè)分隔符不會(huì )在文件或表單項的內容中出現。前面的 ---------------------------7d 是 IE 特有的標志。
Mozila 為---------------------------71用手工發(fā)送這個(gè)例子,在上述的 servlet 中檢驗通過(guò)。
使用POST發(fā)送數據
以POST方式發(fā)送數據主要是為了向服務(wù)器發(fā)送較大量的客戶(hù)端的數據,它不受URL的長(cháng)度限制。POST請求將數據以URL編碼的形式放在 HTTP正文中,字段形式為fieldname=value,用&分隔每個(gè)字段。注意所有的字段都被作為字符串處理。實(shí)際上我們要做的就是模擬瀏 覽器POST一個(gè)表單。以下是IE發(fā)送一個(gè)登陸表單的POST請求:
POST http://127.0.0.1/login.do HTTP/1.0
Accept: image/gif, image/jpeg, image/pjpeg, */*
Accept-Language: en-us,zh-cn;q=0.5
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Length: 28
\r\n
username=admin&password=1234
要在MIDP應用程序中模擬瀏覽器發(fā)送這個(gè)POST請求,首先設置HttpConnection的請求方式為POST:
hc.setRequestMethod(HttpConnection.POST);
然后構造出HTTP正文:
byte[] data = "username=admin&password=1234".getBytes();
并計算正文長(cháng)度,填入Content-Type和Content-Length:
hc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
hc.setRequestProperty("Content-Length", String.valueOf(data.length));
然后打開(kāi)OutputStream將正文寫(xiě)入:
OutputStream output = hc.openOutputStream();
output.write(data);
需要注意的是,數據仍需要以URL編碼格式編碼,由于MIDP庫中沒(méi)有J2SE中與之對應的URLEncoder類(lèi),因此,需要自己動(dòng)手編寫(xiě) 這個(gè)encode()方法,可以參考java.net.URLEncoder.java的源碼。剩下的便是讀取服務(wù)器響應,代碼與GET一致,這里就不再 詳述。
使用multipart/form-data發(fā)送文件
如果要在MIDP客戶(hù)端向服務(wù)器上傳文件,我們就必須模擬一個(gè)POST multipart/form-data類(lèi)型的請求,Content-Type必須是multipart/form-data。
以multipart/form-data編碼的POST請求格式與application/x-www-form-urlencoded完全不同,multipart/form-data需要首先在HTTP請求頭設置一個(gè)分隔符,例如ABCD:
hc.setRequestProperty("Content-Type", "multipart/form-data; boundary=ABCD");
然后,將每個(gè)字段用“--分隔符”分隔,最后一個(gè)“--分隔符--”表示結束。例如,要上傳一個(gè)title字段"Today"和一個(gè)文件C:\1.txt,HTTP正文如下:
--ABCD
Content-Disposition: form-data; name="title"
\r\n
Today
--ABCD
Content-Disposition: form-data; name="1.txt"; filename="C:\1.txt"
Content-Type: text/plain
\r\n
<這里是1.txt文件的內容>
--ABCD--
\r\n
請注意,每一行都必須以\r\n結束,包括最后一行。如果用Sniffer程序檢測IE發(fā)送的POST請求,可以發(fā)現IE的分隔符類(lèi)似于 ---------------------------7d4a6d158c9,這是IE產(chǎn)生的一個(gè)隨機數,目的是防止上傳文件中出現分隔符導致服務(wù)器 無(wú)法正確識別文件起始位置。我們可以寫(xiě)一個(gè)固定的分隔符,只要足夠復雜即可。
發(fā)送文件的POST代碼如下:
String[] props = ... // 字段名
String[] values = ... // 字段值
byte[] file = ... // 文件內容
String BOUNDARY = "---------------------------7d4a6d158c9"; // 分隔符
StringBuffer sb = new StringBuffer();
// 發(fā)送每個(gè)字段:
for(int i=0; i
sb = sb.append("--");
sb = sb.append(BOUNDARY);
sb = sb.append("\r\n");
sb = sb.append("Content-Disposition: form-data; name=\""+ props[i] + "\"\r\n\r\n");
sb = sb.append(URLEncoder.encode(values[i]));
sb = sb.append("\r\n");
}
// 發(fā)送文件:
sb = sb.append("--");
sb = sb.append(BOUNDARY);
sb = sb.append("\r\n");
sb = sb.append("Content-Disposition: form-data; name=\"1\"; filename=\"1.txt\"\r\n");
sb = sb.append("Content-Type: application/octet-stream\r\n\r\n");
byte[] data = sb.toString().getBytes();
byte[] end_data = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
// 設置HTTP頭:
hc.setRequestProperty("Content-Type", MULTIPART_FORM_DATA + "; boundary=" + BOUNDARY);
hc.setRequestProperty("Content-Length", String.valueOf(data.length + file.length + end_data.length));
// 輸出:
output = hc.openOutputStream();
output.write(data);
output.write(file);
output.write(end_data);
// 讀取服務(wù)器響應:
// TODO...
最近項目中用到的一個(gè)是用一個(gè)頁(yè)面接收c程序post過(guò)來(lái)的一斷字符串..總接收不到值...
我用C#寫(xiě)一個(gè)測試可以正常接收到值.
最后抓包比較
查資料得:
application/x-www-form-urlencoded: 窗體數據被編碼為名稱(chēng)/值對。這是標準的編碼格式。multipart/form-data: 窗體數據被編碼為一條消息,頁(yè)上的每個(gè)控件對應消息中的一個(gè)部分。 text/plain: 窗體數據以純文本形式進(jìn)行編碼,其中不含任何控件或格式字符。
補充
form 的enctype屬性為編碼方式,常用有兩種:application/x-www-form-urlencoded和multipart/form- data,默認為application/x-www-form-urlencoded。 當action為get時(shí)候,瀏覽器用x-www-form-urlencoded的編碼方式把form數據轉換成一個(gè)字串(name1=value1& amp;name2=value2...),然后把這個(gè)字串a(chǎn)ppend到url后面,用?分割,加載這個(gè)新的url。 當action為post時(shí)候,瀏覽器把form數據封裝到http body中,然后發(fā)送到server。 如果沒(méi)有type=file的控件,用默認的application/x-www-form-urlencoded就可以了。 但是如果有type=file的話(huà),就要用到multipart/form-data了。瀏覽器會(huì )把整個(gè)表單以控件為單位分割,并為每個(gè)部分加上 Content-Disposition(form-data或者file),Content-Type(默認為text/plain),name(控件 name)等信息,并加上分割符(boundary)。
boundary 是客戶(hù)端瀏覽器隨機生成的你可以不用提取。提交數據的時(shí)候設置一個(gè)串給他并用該串來(lái)分隔數據就可以了
聯(lián)系客服