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,甚至是封裝好的第三方框架: volley, Retrofit。 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);
}
}