網絡編程之使用HttpClient上傳文件的客戶端和服務器

1.1客戶端:

 HttpClient常用HttpGet和HttpPost這兩個類,分別對應Get方式和Post方式。

           無論是使用HttpGet,還是使用HttpPost,都必須通過如下3步來訪問HTTP資源。

           1.創建HttpGetHttpPost對象,將要請求的URL通過構造方法傳入HttpGetHttpPost對象。

           2.使用DefaultHttpClient類的execute方法發送HTTP GETHTTP POST請求,並返回HttpResponse對象。

           3.通過HttpResponse接口的getEntity方法返回響應信息,並進行相應的處理。

package com.qianfeng.uploadfile;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * 文件上傳的客戶端
 * @author qq
 *
 */
public class FileUpload {

	public FileUpload() {
		
	}
	
	public void fileUpload( final String url,
			                final File file,
			                final HashMap<String,String> params,
			                final String encode,
			                final CallBack callBack
			                )
	{
		new Thread(new Runnable(){

			@Override
			public void run() {
				//使用HttpPost方法提交HTTP POST請求
				HttpPost post = new HttpPost(url);
				//使用DefaultHttpClient類的execute方法發送HTTP GET或HTTP POST請求,並返回HttpResponse對象。
				HttpClient client = new DefaultHttpClient();
				//通過HttpResponse接口的getEntity方法返回響應信息,並進行相應的處理。
				HttpResponse response = null;
				/*
				 * 在HttpCient4.3之前上傳文件主要使用MultipartEntity這個類,
				 * 但現在這個類已經不在推薦使用了。
				 * 隨之替代它的類是MultipartEntityBuilder。
				 * */
				MultipartEntity entity =new MultipartEntity();
				//把文件轉換成流對象FileBody 
				FileBody body = new FileBody(file);
				FormBodyPart part = new FormBodyPart("form",body);
				//Map通過entrySet()方法來遍歷Map集合
				for(Map.Entry<String,String> entry:params.entrySet())
				{
					try {
						entity.addPart(entry.getKey(),new StringBody(entry.getValue()));
						
					} catch (UnsupportedEncodingException e) {
						e.printStackTrace();
					}
				}
				entity.addPart(part);//添加數據
			
				post.setEntity(entity);//設置請求參數
				
				try {
					//使用DefaultHttpClient類的execute方法發送HTTP POST請求,並返回HttpResponse對象
					response = client.execute(post);
					if(response.getStatusLine().getStatusCode()==200)//響應碼200請求成功
					{
						//JSON解析
						String jsonString = EntityUtils.toString(response.getEntity(),encode);
						JSONObject jsonObj = new JSONObject(jsonString);
						callBack.getResult(jsonObj);//回調函數
					}
				} catch (ClientProtocolException e) {
					
					e.printStackTrace();
				} catch (IOException e) {
					
					e.printStackTrace();
				} catch (JSONException e) {
					
					e.printStackTrace();
				}
				
				
			}
			
		}).start();
		
	}

}
interface CallBack
{
	void getResult(JSONObject obj);
}

1.2服務器:

一個文件上傳請求的消息實體由一系列根據 RFC1867("Form-based File Upload in HTML".)編碼的項目(文本參數和文件參數)組成。自己編程來解析獲取這些數據是非常麻煩的,還需要了解RFC1867規範對請求數據編碼的相關知識。FileUpload 可以幫助我們解析這樣的請求,將每一個項目封裝成一個實現了FileItem接口的對象,並以列表的形式返回。所以,我們只需要瞭解FileUpload的API如何使用即可,不用管它們的底層實現。
讓我們來看一個簡單文件上傳處理代碼:將UploadServlet發佈在服務器上。
package com.qianfeng.servlet;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.json.JSONObject;

public class UploadServlet extends HttpServlet {

	/**
	 * Destruction of the servlet. <br>
	 */
	public void destroy() {
		super.destroy(); // Just puts "destroy" string in log
		// Put your code here
	}

	/**
	 * The doGet method of the servlet. <br>
	 *
	 * This method is called when a form has its tag value method equals to get.
	 * 
	 * @param request the request send by the client to the server
	 * @param response the response send by the server to the client
	 * @throws ServletException if an error occurred
	 * @throws IOException if an error occurred
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		response.setContentType("text/html;charset=utf-8");
		request.setCharacterEncoding("utf-8");
		response.setCharacterEncoding("utf-8");
		PrintWriter out = response.getWriter();
		//實現上傳的類
		DiskFileItemFactory factory = new DiskFileItemFactory();//磁盤對象
		
		ServletFileUpload upload = new ServletFileUpload(factory);//聲明解析request對象
		upload.setFileSizeMax(2*1024*1024);//設置每個文件最大爲2M
		upload.setSizeMax(4*1024*1024);//設置一共最多上傳4M
		
		try {
			List<FileItem> list = upload.parseRequest(request);//解析
			for(FileItem item:list){//判斷FileItem類對象封裝的數據是一個普通文本表單字段,還是一個文件表單字段,如果是普通表單字段則返回true,否則返回false。
				if(!item.isFormField()){
					//獲取文件名
					String fileName = item.getName();
					//獲取服務器端路徑
					String file_upload_loader =this.getServletContext().getRealPath("\\upload");
					System.out.println("上傳文件存放路徑:"+file_upload_loader);
					//將FileItem對象中保存的主體內容保存到某個指定的文件中。
					item.write(new File(file_upload_loader+File.separator+fileName));
				}else{
					if(item.getFieldName().equalsIgnoreCase("username")){
						String username = item.getString("utf-8");//將FileItem對象中保存的數據流內容以一個字符串返回
						System.out.println(username);
					}
					if(item.getFieldName().equalsIgnoreCase("password")){
						String password = item.getString("utf-8");
						System.out.println(password);
					}
				}
			}
			//返回響應碼(ResultCode)和響應值(ResultMsg)簡單的JSON解析
			ResultMessage message = new ResultMessage(1,"上傳成功");
			JSONObject json = new JSONObject();
			JSONObject jsonObject = new JSONObject();
			json.put("ResultCode",message.getResultCode());
			json.put("ResultMsg",message.getResultMsg());
			jsonObject.put("upload",json);
			//System.out.println(jsonObject.toString());
			out.print(jsonObject.toString());
			
		} catch (FileUploadException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			out.close();
		}
	}

	/**
	 * The doPost method of the servlet. <br>
	 *
	 * This method is called when a form has its tag value method equals to post.
	 * 
	 * @param request the request send by the client to the server
	 * @param response the response send by the server to the client
	 * @throws ServletException if an error occurred
	 * @throws IOException if an error occurred
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		doGet(request,response);
	}

	/**
	 * Initialization of the servlet. <br>
	 *
	 * @throws ServletException if an error occurs
	 */
	public void init() throws ServletException {
		// Put your code here
	}

}
寫一個ResultMessage類來接收服務器反饋的請求成功數據(包含了JSON解析功能,可以省略這一步):

package com.qianfeng.servlet;

public class ResultMessage {

	private int resultCode;
    private String resultMsg;
    
    
	public ResultMessage() {
		// TODO Auto-generated constructor stub
	}
	
	public ResultMessage(int resultCode, String resultMsg) {
		super();
		this.resultCode = resultCode;
		this.resultMsg = resultMsg;
	}
	public int getResultCode() {
		return resultCode;
	}

	public void setResultCode(int resultCode) {
		this.resultCode = resultCode;
	}

	public String getResultMsg() {
		return resultMsg;
	}

	public void setResultMsg(String resultMsg) {
		this.resultMsg = resultMsg;
	}
	

}

針對以上使用到的幾個類做詳細介紹:

1.3、FileItem接口
org.apache.commons.fileupload.disk.DiskFileItem實現了FileItem接口,用來封裝單個表單字段元素的數據。通過調用FileItem 定義的方法可以獲得相關表單字段元素的數據。我們不需要關心DiskFileItem的具體實現,在程序中可以採用FileItem接口類型來對DiskFileItem對象進行引用和訪問。FileItem類還實現了Serializable接口,以支持序列化操作。
FileItem類常用的方法:
1.  boolean isFormField()方法
isFormField方法用於判斷FileItem類對象封裝的數據是一個普通文本表單字段,還是一個文件表單字段,如果是普通表單字段則返回true,否則返回false。
2.  String getName()方法 
getName方法用於獲得文件上傳字段中的文件名,即表單字段元素描述頭中的filename屬性值,如“C:\Documents and Settings\All Users\Documents\My Pictures\示例圖片\Sunset.jpg”。如果FileItem類對象對應的是普通表單字段,getName方法將返回null。即使用戶沒有通過網頁表單中的文件字段傳遞任何
文件,但只要設置了文件表單字段的name屬性,瀏覽器也會將文件字段的信息傳遞給服務器,只是文件名和文件內容部分都爲空,但這個表單字段仍然對應一個FileItem對象,此時,getName方法返回結果爲空字符串"",讀者在調用Apache文件上傳組件時要注意考慮這個情況。
注意:上面的數據包是通過IE提交,所以是完整的路徑和名稱。如 
C:\Documents and Settings\All Users\Documents\My Pictures\示例圖片\Sunset.jpg。如果是其它瀏覽器,如火狐和Chromium,則僅僅是名字,沒有路徑,如Sunset.jpg。
3.  String getFieldName()方法
getFieldName方法用於返回表單字段元素描述頭的name屬性值,也是表單標籤name屬性的值。例如“name=file1”中的“file1”。
4.  void write(File file)方法
write方法用於將FileItem對象中保存的主體內容保存到某個指定的文件中。如果FileItem對象中的主體內容是保存在某個臨時文件中,該方法順利完成後,臨時文件有可能會被清除。該方法也可將普通表單字段內容寫入到一個文件中,但它主要用途是將上傳的文件內容保存在本地文件系統中。
5.  String getString()方法
getString方法用於將FileItem對象中保存的數據流內容以一個字符串返回,它有兩個重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding) throws java.io.UnsupportedEncodingException
前者使用缺省的字符集編碼將主體內容轉換成字符串,後者使用參數指定的字符集編碼將主體內容轉換成字符串。如果在讀取普通表單字段元素的內容時出現了中文亂碼現象,請調用第二個getString方法,併爲之傳遞正確的字符集編碼名稱。
6.  String getContentType()方法
getContentType 方法用於獲得上傳文件的類型,即表單字段元素描述頭屬性“Content-Type”的值,如“image/jpeg”。如果FileItem類對象對應的是普通表單字段,該方法將返回null。
7.  boolean isInMemory()方法
isInMemory方法用來判斷FileItem對象封裝的數據內容是存儲在內存中,還是存儲在臨時文件中,如果存儲在內存中則返回true,否則返回false。
8.  void delete()方法
delete方法用來清空FileItem類對象中存放的主體內容,如果主體內容被保存在臨時文件中,delete方法將刪除該臨時文件。儘管當FileItem對象被垃圾收集器收集時會自動清除臨時文件,但及時調用delete方法可以更早的清除臨時文件,釋放系統存儲資源。另外,當系統出現異常時,仍有可能造成有的臨時文件被永久保存在了硬盤中。
9.  InputStream getInputStream()方法
    以流的形式返回上傳文件的數據內容。
10. long getSize()方法
返回該上傳文件的大小(以字節爲單位)。

1.4、DiskFileItemFactory
此類將請求消息實體中的每一個項目封裝成單獨的DiskFileItem (FileItem接口的實現) 對象的任務由 org.apache.commons.fileupload.FileItemFactory 接口的默認實現 org.apache.commons.fileupload.disk.DiskFileItemFactory 來完成。當上傳的文件項目比較小時,直接保存在內存中(速度比較快),比較大時,以臨時文件的形式,保存在磁盤臨時文件夾(雖然速度慢些,但是內存資源是有限的)。
屬性:
1) public static final int DEFAULT_SIZE_THRESHOLD :
將文件保存在內存還是磁盤臨時文件夾的默認臨界值,值爲10240,即10kb。
2) private File repository:
用於配置在創建文件項目時,當文件項目大於臨界值時使用的臨時文件夾,默認採用系統默認的臨時文件路徑,可以通過系統屬性 java.io.tmpdir 
獲取。代碼:System.getProperty("java.io.tmpdir");
3) private int sizeThreshold:
用於保存將文件保存在內存還是磁盤臨時文件夾的臨界值。
構造方法:
1) public DiskFileItemFactory():
採用默認臨界值和系統臨時文件夾構造文件項工廠對象。
2) public DiskFileItemFactory(int sizeThreshold,File repository):
採用參數指定臨界值和系統臨時文件夾構造文件項工廠對象。
其他方法:
1、FileItem createItem() 方法
根據DiskFileItemFactory相關配置將每一個請求消息實體項目創建 成DiskFileItem 實例,並返回。該方法從來不需要我們親自調用,FileUpload組件在解析請求時內部使用。
2、void setSizeThreshold(int sizeThreshold)
Apache文件上傳組件在解析上傳數據中的每個字段內容時,需要臨時保存解析出的數據,以便在後面進行數據的進一步處理(保存在磁盤特定位置或插入數據庫)。因爲Java虛擬機默認可以使用的內存空間是有限的,超出限制時將會拋出“java.lang.OutOfMemoryError”錯誤。如果上傳的文件
很大,例如800M的文件,在內存中將無法臨時保存該文件內容,Apache文件上傳組件轉而採用臨時文件來保存這些數據;但如果上傳的文件很小,例如600個字節的文件,顯然將其直接保存在內存中性能會更加好些。
3、setSizeThreshold
方法用於設置是否將上傳文件已臨時文件的形式保存在磁盤的臨界值(以字節爲單位的int值),如果從沒有調用該方法設置此臨界值,將會採用系統默認值10KB。對應的getSizeThreshold() 方法用來獲取此臨界值。
4、void setRepository(File repository)
setRepositoryPath方法用於設置當上傳文件尺寸大於setSizeThreshold方法設置的臨界值時,將文件以臨時文件形式保存在磁盤上的存放目錄。有一個對應的獲得臨時文件夾的 File getRespository() 方法。
注意:當從沒有調用此方法設置臨時文件存儲目錄時,默認採用系統默認的臨時文件路徑,可以通過系統屬性 java.io.tmpdir 獲取。
如下代碼:
System.getProperty("java.io.tmpdir");
Tomcat系統默認臨時目錄爲“<tomcat安裝目錄>/temp/”。

說明:
使用List<FileItem> list = servletFileUpload.parseRequest(httpServletRequest);方法,則臨時目錄受用戶設置的管理。即,如果用戶設置的臨時目錄爲d:/a,則當文件上傳大於,大於緩衝區設置時會向d:/a下保存臨時文件。如果用戶沒有設置臨時目錄,纔會將臨時文件保存到CATALINA_HOME\temp目錄下。
此種方式保存的臨時文件名爲:upload_2eb46fea_13615ef5327__8000_00000000.tmp
使用FileItemIterator fii = servletFileUpload.getItemIterator(httpServletRequest)方法時,則不受用戶設置臨時目錄的影響。總是會將文件保臨時文件保存到CATALINA_HOME\temp目錄下。
此種情況下保存的臨時文件名爲:hsperfdata_Administrator (這是一個文件夾,用裏面的文件做爲數據交互)
圖示:
 
1.5、ServletFileUpload類
org.apache.commons.fileupload.servlet.ServletFileUpload類是Apache文件上傳組件處理文件上傳的核心高級類(所謂高級就是不需要管底層實現,暴露給用戶的簡單易用的接口)。使用其 parseRequest(HttpServletRequest) 方法可以將通過表單中每一個HTML標籤提交的數據封裝成一個FileItem對象,然後以List列表的形式返回。使用該方法處理上傳文件簡單易用。
如果你希望進一步提高性能,你可以採用 getItemIterator 方法,直接獲得每一個文件項的數據輸入流,對數據做直接處理。
在使用ServletFileUpload對象解析請求時需要根據DiskFileItemFactory對象的屬性 sizeThreshold(臨界值)和repository(臨時目錄) 來決定將解析得到的數據保存在內存還是臨時文件中,如果是臨時文件,保存在哪個臨時目錄中?。所以,我們需要在進行解析工作前構造好DiskFileItemFactory對象,
通過ServletFileUpload對象的構造方法或setFileItemFactory()方法設置 ServletFileUpload對象的fileItemFactory屬性。
ServletFileUpload的繼承結構爲:
 
構造方法:
1) public ServletFileUpload():
構造一個未初始化的實例,需要在解析請求之前先調用setFileItemFactory()方法設置 fileItemFactory屬性。
2) public ServletFileUpload(FileItemFactory fileItemFactory):
構造一個實例,並根據參數指定的FileItemFactory 對象,設置 fileItemFactory屬性。
ServletFileUpload類常用方法:
1.public void setSizeMax(long sizeMax)
方法setSizeMax方法繼承自FileUploadBase類,用於設置請求消息實體內容(即所有上傳數據)的最大尺寸限制,以防止客戶端惡意上傳超大文件來浪費服務器端的存儲空間。其參數是以字節爲單位的long型數字。
在請求解析的過程中,如果請求消息體內容的大小超過了setSizeMax方法的設置值,將會拋出FileUploadBase內部定義的SizeLimitExceededException異常(FileUploadException的子類)。
如:
org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: 
the request was rejected because its size (1649104) exceeds the configured 
maximum (153600)
該方法有一個對應的讀方法:public long getSizeMax()方法。
2.public void setFileSizeMax(long fileSizeMax)
方法setFileSizeMax方法繼承自FileUploadBase類,用於設置單個上傳文件的最大尺寸限制,以防止客戶端惡意上傳超大文件來浪費服務器端的存儲空間。其參數是以字節爲單位的long型數字。該方法有一個對應的讀方法:public long geFileSizeMax()方法。
在請求解析的過程中,如果單個上傳文件的大小超過了setFileSizeMax方法的設置值,將會拋出FileUploadBase內部定義的FileSizeLimitExceededException異常(FileUploadException的子類)。
如:
org.apache.commons.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file1 exceeds its
 maximum permitted size of 51200 characters.
3. public List parseRequest(javax.servlet.http.HttpServletRequest req)
parseRequest 方法是ServletFileUpload類的重要方法,它是對HTTP請求消息體內容進行解析的入口方法。它解析出FORM表單中的每個字段的數據,並將它們分別包裝成獨立的FileItem對象,然後將這些FileItem對象加入進一個List類型的集合對象中返回。
該方法拋出FileUploadException異常來處理諸如文件尺寸過大、請求消息中的實體內容的類型不是“multipart/form-data”、IO異常、請求消息體長度信息丟失等各種異常。每一種異常都是FileUploadException的一個子類型。
4. public FileItemIterator getItemIterator(HttpServletRequest request)
getItemIterator方法和parseRequest 方法基本相同。但是getItemIterator方法返回的是一個迭代器,該迭代器中保存的不是FileItem對象,而是FileItemStream 對象,如果你希望進一步提高性能,你可以採用 getItemIterator 方法,直接獲得每一個文件項的數據輸入流,做底層處理;如果性能不
是問題,你希望代碼簡單,則採用parseRequest方法即可。 
5. public stiatc boolean isMultipartContent(HttpServletRequest req)
isMultipartContent方法方法用於判斷請求消息中的內容是否是“multipart/form-data”類型,是則返回true,否則返回false。isMultipartContent方法是一個靜態方法,不用創建ServletFileUpload類的實例對象即可被調用。
6. getFileItemFactory()和setFileItemFactory(FileItemFactory)
兩個方法繼承自FileUpload類,用於設置和讀取fileItemFactory屬性。
7. public void setProgressListener(ProgressListener pListener)
設置文件上傳進度監聽器。該方法有一個對應的讀取方法:ProgressListener getProgressListener()。
8.public void setHeaderEncoding()方法
在文件上傳請求的消息體中,除了普通表單域的值是文本內容以外,文件上傳字段中的文件路徑名也是文本,在內存中保存的是它們的某種字符集編碼的字節數組,Apache文件上傳組件在讀取這些內容時,必須知道它們所採用的字符集編碼,才能將它們轉換成正確的字符文本返回。
setHeaderEncoding方法繼承自FileUploadBase類,用於設置上面提到的字符編碼。如果沒有設置,則對應的讀方法getHeaderEncoding()方法返回null,將採用HttpServletRequest設置的字符編碼,如果HttpServletRequest的字符編碼也爲null,則採用系統默認字符編碼。可以通過一下語句獲得系統默認字符編碼:
System.getProperty("file.encoding"));
1.7、FileCleanerCleanup清理資源 
    只適用於你使用 DiskFileItem 的情況.換句話說,就是在你處理上傳的數據之前它們被存放在臨時文件中。
    這些臨時文件在不再被使用的時候(如果相應的java.io.File是可回收的則更好)會自動被刪除.這會被org.apache.commons.io.FileCleaningTracker的一個實例啓動的一個收割線程默默執行。
    你的web應用應該使用org.apache.commons.fileupload.FileCleanerCleanup的一個實例.那很簡單,你只要把它加到你的 web.xml 中:
  <listener>
  <listener-class>org.apache.commons.fileupload.servlet.FileCleanerCleanup</listener-class>
  </listener>
然後,在創建了DiskFileItemFactory以後,設置資源回收:
DiskFileItemFactory f = new DiskFileItemFactory();//聲明臨時文件對象
f.setFileCleaningTracker(FileCleanerCleanup.getFileCleaningTracker(this.getServletContext()));
f.setSizeThreshold(1024*8);//設置在內存中最多可以放多少個字節,如果超出這個字節,保存到臨文件中去
f.setRepository(new File("d:/a"));//設置臨時目錄

注意:必須要正常關閉Tomcat服務器。因爲此線程在tomcat終止時會調用清空臨時文件的代碼。
正常關閉,是指執行CATALINA_HOME\bin\shutdown.bat文件。


1.8、進度ProgressListener
這個進度條比較合適於在後臺監控進度,如果在作上傳進度,還是使用ajax更加合適:
示例代碼:
upload.setProgressListener(new ProgressListener() {
double dd = 0;
long len = 0;
//參數1:已經上傳完成的數量
//參數2:總長度
//參數3:第幾個元素從1開始。0爲沒有
public void update(long pBytesRead, long pContentLength, int pItems) {
double persent = pBytesRead*100/pContentLength;
if(dd!=persent){
System.err.println(dd+"%");
dd=persent;
}else if(persent==100){
System.err.println("100%");
}
}
});



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