1. 程式人生 > >Android利用HttpURLConnection實現檔案上傳

Android利用HttpURLConnection實現檔案上傳

普通Java應用

瀏覽器在上傳的過程中是將檔案以流的形式提交到伺服器端的,如果直接使用Servlet獲取上傳檔案的輸入流然後再解析裡面的請求引數是比較麻煩,所以Java中Web端可以用的上傳元件有兩種:

  • FileUpload【操作比較複雜】struts上傳的功能就是基於這個實現的。


		try{
		
			//1.得到解析器工廠
			DiskFileItemFactory factory = new DiskFileItemFactory();
			
			//2.得到解析器
			ServletFileUpload upload = new ServletFileUpload(factory);
			
			//3.判斷上傳表單的型別
			if(!upload.isMultipartContent(request)){
				//上傳表單為普通表單,則按照傳統方式獲取資料即可
				return;
			}
			
			//為上傳表單,則呼叫解析器解析上傳資料
			List<FileItem> list = upload.parseRequest(request);  //FileItem
			
			//遍歷list,得到用於封裝第一個上傳輸入項資料fileItem物件
			for(FileItem item : list){
				
				if(item.isFormField()){
					//得到的是普通輸入項
					String name = item.getFieldName();  //得到輸入項的名稱
					String value = item.getString();
					System.out.println(name + "=" + value);
				}else{
					//得到上傳輸入項
					String filename = item.getName();  //得到上傳檔名  C:\Documents and Settings\ThinkPad\桌面\1.txt
					filename = filename.substring(filename.lastIndexOf("\\")+1);
					InputStream in = item.getInputStream();   //得到上傳資料
					int len = 0;
					byte buffer[]= new byte[1024];
					
					
					String savepath = this.getServletContext().getRealPath("/upload");
					FileOutputStream out = new FileOutputStream(savepath + "\\" + filename);  //向upload目錄中寫入檔案
					while((len=in.read(buffer))>0){
						out.write(buffer, 0, len);
					}
					
					in.close();
					out.close();
				}
			}
		
		}catch (Exception e) {
			e.printStackTrace();
		}

 

  • SamrtUpload【操作比較簡單】


        //例項化元件
        SmartUpload smartUpload = new SmartUpload();

        //初始化上傳操作
        smartUpload.initialize(this.getServletConfig(), request, response);


        try {

            //上傳準備
            smartUpload.upload();

            //對於普通資料,單純到request物件是無法獲取得到提交引數的。也是需要依賴smartUpload
            String password = smartUpload.getRequest().getParameter("password");
            System.out.println(password);

            //上傳到uploadFile資料夾中
            smartUpload.save("uploadFile");


        } catch (SmartUploadException e) {
            e.printStackTrace();
        }

此時瀏覽器端只需要利用file元件和form就可以提交檔案。

而如果不是webapp,而是Http客戶端,可以利用Httpclient,其API操作也比較簡單。

Android應用

Android 6中移除了HttpClient元件,推薦使用HttpURLConnection API, 需要手動按照格式去實現檔案上傳:上傳檔案資料是經過MIME協議進行分割的,表單進行了二進位制封裝。也就是說:getParameter()無法獲取得到上傳檔案的資料。

下述程式碼的BOUNDARY可以是隨機字串。

private static final String BOUNDARY = "----WebKitFormBoundaryT1HoybnYeFOGFlBR";
 
	/**
	 * 
	 * @param params
	 *            傳遞的普通引數
	 * @param uploadFile
	 *            需要上傳的檔名
	 * @param fileFormName
	 *            需要上傳檔案表單中的名字
	 * @param newFileName
	 *            上傳的檔名稱,不填寫將為uploadFile的名稱
	 * @param urlStr
	 *            上傳的伺服器的路徑
	 * @throws IOException
	 */
	public void uploadForm(Map<String, String> params, String fileFormName,
			File uploadFile, String newFileName, String urlStr)
			throws IOException {
		if (newFileName == null || newFileName.trim().equals("")) {
			newFileName = uploadFile.getName();
		}
 
		StringBuilder sb = new StringBuilder();
		/**
		 * 普通的表單資料
		 */
		for (String key : params.keySet()) {
			sb.append("--" + BOUNDARY + "\r\n");
			sb.append("Content-Disposition: form-data; name=\"" + key + "\""
					+ "\r\n");
			sb.append("\r\n");
			sb.append(params.get(key) + "\r\n");
		}
		/**
		 * 上傳檔案的頭
		 */
		sb.append("--" + BOUNDARY + "\r\n");
		sb.append("Content-Disposition: form-data; name=\"" + fileFormName
				+ "\"; filename=\"" + newFileName + "\"" + "\r\n");
		sb.append("Content-Type: image/jpeg" + "\r\n");// 如果伺服器端有檔案型別的校驗,必須明確指定ContentType
		sb.append("\r\n");
 
		byte[] headerInfo = sb.toString().getBytes("UTF-8");
		byte[] endInfo = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8");
		System.out.println(sb.toString());
		URL url = new URL(urlStr);
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setRequestMethod("POST");
		conn.setRequestProperty("Content-Type",
				"multipart/form-data; boundary=" + BOUNDARY);
		conn.setRequestProperty("Content-Length", String
				.valueOf(headerInfo.length + uploadFile.length()
						+ endInfo.length));
		conn.setDoOutput(true);
 
		OutputStream out = conn.getOutputStream();
		InputStream in = new FileInputStream(uploadFile);
		out.write(headerInfo);
 
		byte[] buf = new byte[1024];
		int len;
		while ((len = in.read(buf)) != -1)
			out.write(buf, 0, len);
 
		out.write(endInfo);
		in.close();
		out.close();
		if (conn.getResponseCode() == 200) {
			System.out.println("上傳成功");
		}
 
	}

基本原理:

使用HttpUrlConnection上傳有一個很致命的問題就是,當上傳檔案很大時,會發生記憶體溢位,手機分配給我們app的記憶體更小,所以就更需要解決這個問題,於是我們可以使用Socket模擬POST進行HTTP檔案上傳。 

	/**
	 * 
	 * @param params
	 *            傳遞的普通引數
	 * @param uploadFile
	 *            需要上傳的檔名
	 * @param fileFormName
	 *            需要上傳檔案表單中的名字
	 * @param newFileName
	 *            上傳的檔名稱,不填寫將為uploadFile的名稱
	 * @param urlStr
	 *            上傳的伺服器的路徑
	 * @throws IOException
	 */
	public void uploadFromBySocket(Map<String, String> params,
			String fileFormName, File uploadFile, String newFileName,
			String urlStr) throws IOException {
		if (newFileName == null || newFileName.trim().equals("")) {
			newFileName = uploadFile.getName();
		}
 
		StringBuilder sb = new StringBuilder();
		/**
		 * 普通的表單資料
		 */
 
		if (params != null)
			for (String key : params.keySet()) {
				sb.append("--" + BOUNDARY + "\r\n");
				sb.append("Content-Disposition: form-data; name=\"" + key
						+ "\"" + "\r\n");
				sb.append("\r\n");
				sb.append(params.get(key) + "\r\n");
			}                                                                                                                                                  else{ab.append("\r\n");}
		/**
		 * 上傳檔案的頭
		 */
		sb.append("--" + BOUNDARY + "\r\n");
		sb.append("Content-Disposition: form-data; name=\"" + fileFormName
				+ "\"; filename=\"" + newFileName + "\"" + "\r\n");
		sb.append("Content-Type: image/jpeg" + "\r\n");// 如果伺服器端有檔案型別的校驗,必須明確指定ContentType
		sb.append("\r\n");
 
		byte[] headerInfo = sb.toString().getBytes("UTF-8");
		byte[] endInfo = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8");
 
		System.out.println(sb.toString());
 
		URL url = new URL(urlStr);
		Socket socket = new Socket(url.getHost(), url.getPort());
		OutputStream os = socket.getOutputStream();
		PrintStream ps = new PrintStream(os, true, "UTF-8");
 
		// 寫出請求頭
		ps.println("POST " + urlStr + " HTTP/1.1");
		ps.println("Content-Type: multipart/form-data; boundary=" + BOUNDARY);
		ps.println("Content-Length: "
				+ String.valueOf(headerInfo.length + uploadFile.length()
						+ endInfo.length));
		ps.println("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
 
		InputStream in = new FileInputStream(uploadFile);
		// 寫出資料
		os.write(headerInfo);
 
		byte[] buf = new byte[1024];
		int len;
		while ((len = in.read(buf)) != -1)
			os.write(buf, 0, len);
 
		os.write(endInfo);
 
		in.close();
		os.close();
	}

如果覺得以上API過於複雜,可以使用okhttp,甚至是封裝好的第三方框架: volleyRetrofit。 retrofit依賴於okhttp。

//通過“addFormDataPart”可以新增多個上傳的檔案。
public  class OkHttpCallBackWrap {
    public void post(String url) throws IOException{
                File file = new File("D:/app/dgm/3.mp4");
                RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
                RequestBody requestBody = new MultipartBody.Builder()
                        .setType(MultipartBody.FORM) 
                        .addFormDataPart("application/octet-stream", "1.mp4", fileBody)
                        .build();
                Request request = new Request.Builder()
                        .url(url)
                        .post(requestBody)
                        .build();

                final okhttp3.OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
                OkHttpClient okHttpClient  = httpBuilder
                        //設定超時
                        .connectTimeout(100, TimeUnit.SECONDS)
                        .writeTimeout(150, TimeUnit.SECONDS)
                        .build();
                okHttpClient.newCall(request).enqueue(new Callback() {

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        System.out.println(response.body().string());
                    }

                    @Override
                    public void onFailure(Call arg0, IOException e) {
                        // TODO Auto-generated method stub
                        System.out.println(e.toString());

                    }

                });
            }
        }

檔案上傳的注意點

  (1)、為保證伺服器安全,上傳檔案應該放在外界無法直接訪問的目錄下,比如放於WEB-INF目錄下。

  (2)、為防止檔案覆蓋的現象發生,要為上傳檔案產生一個唯一的檔名。

  (3)、為防止一個目錄下面出現太多檔案,要使用hash演算法打散儲存。

  (4)、要限制上傳檔案的最大值。

  (5)、要限制上傳檔案的型別,在收到上傳檔名時,判斷後綴名是否合法。

       (6)、大檔案的上傳也可以實現為斷點上傳,需要服務端配合實現,和斷點下載實現類似,也採用RandomAccessFile來做檔案移動。

檔案上傳的方式:1.multipart/from-data方式上傳。2.binary方式上傳。multipart上傳方式html的上傳方式程式碼這中上傳方式是我們最常用的上傳方式。比如我們使用網頁上傳檔案,其中html程式碼大致為這樣:<formmethod="post"enctype="multipart/form-data"action="/upload/single">。本文講的也是這種方式。

multipart/form-data方式的一個重要組成部分請求頭Content-Type必須為: Content-Type:multipart/form-data;boundarty=一個32位元組的隨機數,用來分割每個part。 然後每個Part之間用“雙橫槓”加bundary來分割,最後一個Part分割符末尾也要加“雙橫槓”。

每個Part中必須包含Content-Disposition欄位來註明欄位檔名等資訊,也可以包含Content-Type來說明檔案的MeidaType。

檔案下載API

最後提供一個檔案下載的Servlet實現:直接設定響應response的響應頭和輸出流即可。Content-Disposition (以附件形式傳輸)

package me.gacl.web.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DownLoadServlet extends HttpServlet {

    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //得到要下載的檔名
        String fileName = request.getParameter("filename");  //23239283-92489-阿凡達.avi
        fileName = new String(fileName.getBytes("iso8859-1"),"UTF-8");
        //上傳的檔案都是儲存在/WEB-INF/upload目錄下的子目錄當中
        String fileSaveRootPath=this.getServletContext().getRealPath("/WEB-INF/upload");
        //通過檔名找出檔案的所在目錄
        String path = findFileSavePathByFileName(fileName,fileSaveRootPath);
        //得到要下載的檔案
        File file = new File(path + "\\" + fileName);
        //如果檔案不存在
        if(!file.exists()){
            request.setAttribute("message", "您要下載的資源已被刪除!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        //處理檔名
        String realname = fileName.substring(fileName.indexOf("_")+1);
        //設定響應頭,控制瀏覽器下載該檔案
        response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(realname, "UTF-8"));
        //讀取要下載的檔案,儲存到檔案輸入流
        FileInputStream in = new FileInputStream(path + "\\" + fileName);
        //建立輸出流
        OutputStream out = response.getOutputStream();
        //建立緩衝區
        byte buffer[] = new byte[1024];
        int len = 0;
        //迴圈將輸入流中的內容讀取到緩衝區當中
        while((len=in.read(buffer))>0){
            //輸出緩衝區的內容到瀏覽器,實現檔案下載
            out.write(buffer, 0, len);
        }
        //關閉檔案輸入流
        in.close();
        //關閉輸出流
        out.close();
    }
    
    /**
    * @Method: findFileSavePathByFileName
    * @Description: 通過檔名和儲存上傳檔案根目錄找出要下載的檔案的所在路徑
    * @Anthor:孤傲蒼狼
    * @param filename 要下載的檔名
    * @param saveRootPath 上傳檔案儲存的根目錄,也就是/WEB-INF/upload目錄
    * @return 要下載的檔案的儲存目錄
    */ 
    public String findFileSavePathByFileName(String filename,String saveRootPath){
        int hashcode = filename.hashCode();
        int dir1 = hashcode&0xf;  //0--15
        int dir2 = (hashcode&0xf0)>>4;  //0-15
        String dir = saveRootPath + "\\" + dir1 + "\\" + dir2;  //upload\2\3  upload\3\5
        File file = new File(dir);
        if(!file.exists()){
            //建立目錄
            file.mkdirs();
        }
        return dir;
    }
    
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}