理一理Android多檔案上傳那點事
多檔案上傳是客戶端與服務端兩個的事,客戶端負責傳送,服務端負責接收
我們都知道客戶端與伺服器只是通過http協議進行交流,那麼http協議應該會對上傳檔案有所規範
你可以根據這些規範來自己拼湊請求頭,可以用使用已經封裝好的框架,如Okhttp3
一、先理一理表單點提交點的時候發生了什麼?
1.客戶端的請求(requst)
請求頭會有: Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC
其中 boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC
可看做是分界線
表單中的資料會和請求體對應,比如只有一個<input/>標籤,裡面是字串
//===================描述String:<input type="text"/>============== ------WebKitFormBoundary5sGoxdCHIEYZKCMC Content-Disposition:form-data;name="KeyName" Content-Type: text/plain;charset="utf-8" [String資料XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX] ------WebKitFormBoundary5sGoxdCHIEYZKCMC
比如只有一個<input type="file"/>標籤,裡面是二進位制檔案流:file stream
//===================描述file:<input type="file"/>================ ------WebKitFormBoundary5sGoxdCHIEYZKCMC Content-Disposition:form-data;name="KeyName";filename="xxx.xxx" Content-Type: 對應MimeTypeMap [file stream] ------WebKitFormBoundary5sGoxdCHIEYZKCMC
這便是客戶端的請求
2.客戶端的接收和處理
服務端會受到客戶端的請求,然後根據指定格式對請求體進行解析
然後是檔案你就可以在服務端儲存,儲存成功便是成功上傳成功,下面是SpringBoot對上傳的處理:
/** * 多檔案上傳(包括一個) * * @param files 上傳的檔案 * @return 上傳反饋資訊 */ @PostMapping(value = "/upload") public @ResponseBody ResultBean uploadImg(@RequestParam("file") List<MultipartFile> files) { StringBuilder result = new StringBuilder(); for (MultipartFile file : files) { if (file.isEmpty()) { return ResultHandler.error("Upload Error"); } String fileName = file.getOriginalFilename();//獲取名字 String path = "F:/SpringBootFiles/imgs/"; File dest = new File(path + "/" + fileName); if (!dest.getParentFile().exists()) { //判斷檔案父目錄是否存在 dest.getParentFile().mkdir(); } try { file.transferTo(dest); //儲存檔案 result.append(fileName).append("上傳成功!\n"); } catch (IllegalStateException | IOException e) { e.printStackTrace(); result.append(fileName).append("上傳失敗!\n"); } } return ResultHandler.ok(result.toString()); }
所以檔案上傳,需要服務端和客戶端的配合,缺一不可
二、okhttp模擬表單檔案上傳檔案
1.單檔案上傳

單檔案上傳.png
/** * 模擬表單上傳檔案:通過MultipartBody */ private void doUpload() { File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk"); RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file); //1.獲取OkHttpClient物件 OkHttpClient okHttpClient = new OkHttpClient(); //2.獲取Request物件 RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", file.getName(), fileBody) .build(); Request request = new Request.Builder() .url(Cons.BASE_URL + "upload") .post(requestBody).build(); //3.將Request封裝為Call物件 Call call = okHttpClient.newCall(request); //4.執行Call call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "onFailure: " + e); } @Override public void onResponse(Call call, Response response) throws IOException { String result = response.body().string(); Log.e(TAG, "onResponse: " + result); runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show()); } }); }
addFormDataPart原始碼跟蹤
可見底層也是根據 Content-Disposition:form-data;name=XXX
來拼湊的請求體
/** Add a form data part to the body. */ public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) { return addPart(Part.createFormData(name, filename, body)); } public static Part createFormData(String name, @Nullable String filename, RequestBody body) { if (name == null) { throw new NullPointerException("name == null"); } StringBuilder disposition = new StringBuilder("form-data; name="); appendQuotedString(disposition, name); if (filename != null) { disposition.append("; filename="); appendQuotedString(disposition, filename); } return create(Headers.of("Content-Disposition", disposition.toString()), body); }
2.如何監聽上傳進度:
該類是網上流傳的方案之一,思路是每次服務端write的時候對寫出的進度值進行累加

okhttp-post模擬表單上傳檔案到伺服器.png
/** * 作者:張風捷特烈<br/> * 時間:2018/10/16 0016:13:44<br/> * 郵箱:[email protected]<br/> * 說明:監聽上傳進度的請求體 */ public class CountingRequestBody extends RequestBody { protected RequestBody delegate;//請求體的代理 private Listener mListener;//進度監聽 public CountingRequestBody(RequestBody delegate, Listener listener) { this.delegate = delegate; mListener = listener; } protected final class CountingSink extends ForwardingSink { private long byteWritten;//已經寫入的大小 private CountingSink(Sink delegate) { super(delegate); } @Override public void write(@NonNull Buffer source, long byteCount) throws IOException { super.write(source, byteCount); byteWritten += byteCount; mListener.onReqProgress(byteWritten, contentLength());//每次寫入觸發回撥函式 } } @Nullable @Override public MediaType contentType() { return delegate.contentType(); } @Override public long contentLength() { try { return delegate.contentLength(); } catch (IOException e) { e.printStackTrace(); return -1; } } @Override public void writeTo(@NonNull BufferedSink sink) throws IOException { CountingSink countingSink = new CountingSink(sink); BufferedSink buffer = Okio.buffer(countingSink); delegate.writeTo(buffer); buffer.flush(); } /////////////----------進度監聽介面 public interface Listener { void onReqProgress(long byteWritten, long contentLength); } }
使用:
//對請求體進行包裝成CountingRequestBody CountingRequestBody countingRequestBody = new CountingRequestBody( requestBody, (byteWritten, contentLength) -> { Log.e(TAG, "doUpload: " + byteWritten + "/" + contentLength); if (byteWritten == contentLength) { runOnUiThread(()->{ mIdBtnUploadPic.setText("UpLoad OK"); }); } else { runOnUiThread(()->{ mIdBtnUploadPic.setText(byteWritten + "/" + contentLength); }); } }); Request request = new Request.Builder() .url(Cons.BASE_URL + "upload") .post(countingRequestBody).build();//使用CountingRequestBody進行請求

捕捉上傳進度
3.多檔案的上傳
也就是多加幾個檔案到請求體
/** * 模擬表單上傳檔案:通過MultipartBody */ private void doUpload() { File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk"); File file2 = new File(Environment.getExternalStorageDirectory(), "DCIM/Screenshots/Screenshot_2018-1 RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file); RequestBody fileBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file2); //1.獲取OkHttpClient物件 OkHttpClient okHttpClient = new OkHttpClient(); //2.獲取Request物件 RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", file.getName(), fileBody) .addFormDataPart("file", file2.getName(), fileBody2) .build(); CountingRequestBody countingRequestBody = new CountingRequestBody( requestBody, (byteWritten, contentLength) -> { Log.e(TAG, "doUpload: " + byteWritten + "/" + contentLength); if (byteWritten == contentLength) { runOnUiThread(()->{ mIdBtnUploadPic.setText("UpLoad OK"); }); } else { runOnUiThread(()->{ mIdBtnUploadPic.setText(byteWritten + "/" + contentLength); }); } }); Request request = new Request.Builder() .url(Cons.BASE_URL + "upload") .post(countingRequestBody).build(); //3.將Request封裝為Call物件 Call call = okHttpClient.newCall(request); //4.執行Call call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "onFailure: " + e); } @Override public void onResponse(Call call, Response response) throws IOException { String result = response.body().string(); Log.e(TAG, "onResponse: " + result); runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show()); } }); }
三、直接傳輸二進位制流:
也就是直接把流post在請求裡,在服務端對request獲取輸入流is,再寫到伺服器上
1.Android端:
private void doPostFile() {//向伺服器傳入二進位制流 File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk"); //1.獲取OkHttpClient物件 OkHttpClient okHttpClient = new OkHttpClient(); //2.構造Request--任意二進位制流:application/octet-stream Request request = new Request.Builder() .url(Cons.BASE_URL + "postfile") .post(RequestBody.create(MediaType.parse("application/octet-stream"), file)) .post(new FormBody.Builder().add("name", file.getName()).build()) .build(); //3.將Request封裝為Call物件 Call call = okHttpClient.newCall(request); //4.執行Call call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "onFailure: " + e); } @Override public void onResponse(Call call, Response response) throws IOException { String result = response.body().string(); Log.e(TAG, "onResponse: " + result); runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show()); } }); }
SpringBoot端:
@PostMapping(value = "/postfile") public @ResponseBody ResultBean postFile(@RequestParam(value = "name") String name, HttpServletRequest request) { String result = ""; ServletInputStream is = null; FileOutputStream fos = null; try { File file = new File("F:/SpringBootFiles/imgs", name); fos = new FileOutputStream(file); is = request.getInputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = is.read(buf)) != -1) { fos.write(buf, 0, len); } result = "SUCCESS"; } catch (IOException e) { e.printStackTrace(); result = "ERROR"; } finally { try { if (is != null) { is.close(); } if (fos != null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); } } return ResultHandler.ok(result); }