Java 檔案分塊上傳客戶端原始碼
阿新 • • 發佈:2019-02-07
本部落格介紹如何進行檔案的分塊上傳。本文側重介紹客戶端,伺服器端請參考部落格《Java 檔案分塊上傳伺服器端原始碼》。建議讀者朋友在閱讀本文程式碼前先了解一下 MIME 協議。
所謂分塊上傳並非把大檔案進行物理分塊,然後挨個上傳,而是依次讀取大檔案的一部分檔案流進行上傳。分塊,倒不如說分流比較切實。本文通過一個專案中的示例,說明使用 Apache 的 HttpComponents/HttpClient 對大檔案進行分塊上傳的過程。示例使用的版本是 HttpComponents Client 4.2.1。本文僅以一小 demo 功能性地解釋 HttpComponents/HttpClient 分塊上傳,沒有考慮 I/O 關閉、多執行緒等資源因素,讀者可以根據自己的專案酌情處理。
本文核心思想及流程:以 100 MB 大小為例,大於 100 MB 的進行分塊上傳,否則整塊上傳。對於大於 100 MB 的檔案,又以 100 MB 為單位進行分割,保證每次以不大於 100 MB 的大小進行上傳。比如 304 MB 的一個檔案會分為 100 MB、100 MB、100 MB、4 MB 等四塊依次上傳。第一次讀取 0 位元組開始的 100 MB 個位元組,上傳;第二次讀取第 100 MB 位元組開始的 100 MB 個位元組,上傳;第三次讀取第 200 MB 位元組開始的 100 MB 個位元組,上傳;第四次讀取最後剩下的 4 MB 個位元組進行上傳。
自定義的 ContentBody 原始碼如下,其中定義了流的讀取和輸出:
package com.defonds.rtupload.common.util.block; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import org.apache.http.entity.mime.content.AbstractContentBody; import com.defonds.rtupload.GlobalConstant; public class BlockStreamBody extends AbstractContentBody { //給MultipartEntity看的2個引數 private long blockSize = 0;//本次分塊上傳的大小 private String fileName = null;//上傳檔名 //writeTo需要的3個引數 private int blockNumber = 0, blockIndex = 0;//blockNumber分塊數;blockIndex當前第幾塊 private File targetFile = null;//要上傳的檔案 private BlockStreamBody(String mimeType) { super(mimeType); // TODO Auto-generated constructor stub } /** * 自定義的ContentBody構造子 * @param blockNumber分塊數 * @param blockIndex當前第幾塊 * @param targetFile要上傳的檔案 */ public BlockStreamBody(int blockNumber, int blockIndex, File targetFile) { this("application/octet-stream"); this.blockNumber = blockNumber;//blockNumber初始化 this.blockIndex = blockIndex;//blockIndex初始化 this.targetFile = targetFile;//targetFile初始化 this.fileName = targetFile.getName();//fileName初始化 //blockSize初始化 if (blockIndex < blockNumber) {//不是最後一塊,那就是固定大小了 this.blockSize = GlobalConstant.CLOUD_API_LOGON_SIZE; } else {//最後一塊 this.blockSize = targetFile.length() - GlobalConstant.CLOUD_API_LOGON_SIZE * (blockNumber - 1); } } @Override public void writeTo(OutputStream out) throws IOException { byte b[] = new byte[1024];//暫存容器 RandomAccessFile raf = new RandomAccessFile(targetFile, "r");//負責讀取資料 if (blockIndex == 1) {//第一塊 int n = 0; long readLength = 0;//記錄已讀位元組數 while (readLength <= blockSize - 1024) {//大部分位元組在這裡讀取 n = raf.read(b, 0, 1024); readLength += 1024; out.write(b, 0, n); } if (readLength <= blockSize) {//餘下的不足 1024 個位元組在這裡讀取 n = raf.read(b, 0, (int)(blockSize - readLength)); out.write(b, 0, n); } } else if (blockIndex < blockNumber) {//既不是第一塊,也不是最後一塊 raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳過前[塊數*固定大小 ]個位元組 int n = 0; long readLength = 0;//記錄已讀位元組數 while (readLength <= blockSize - 1024) {//大部分位元組在這裡讀取 n = raf.read(b, 0, 1024); readLength += 1024; out.write(b, 0, n); } if (readLength <= blockSize) {//餘下的不足 1024 個位元組在這裡讀取 n = raf.read(b, 0, (int)(blockSize - readLength)); out.write(b, 0, n); } } else {//最後一塊 raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳過前[塊數*固定大小 ]個位元組 int n = 0; while ((n = raf.read(b, 0, 1024)) != -1) { out.write(b, 0, n); } } //TODO 最後不要忘掉關閉out/raf } @Override public String getCharset() { // TODO Auto-generated method stub return null; } @Override public String getTransferEncoding() { // TODO Auto-generated method stub return "binary"; } @Override public String getFilename() { // TODO Auto-generated method stub return fileName; } @Override public long getContentLength() { // TODO Auto-generated method stub return blockSize; } }
在自定義的 HttpComponents/HttpClient 工具類 HttpClient4Util 裡進行分塊上傳的封裝:
public static String restPost(String serverURL, File targetFile,Map<String, String> mediaInfoMap){ String content =""; try { DefaultHttpClient httpClient = new DefaultHttpClient(); HttpPost post = new HttpPost(serverURL +"?"); httpClient.getParams().setParameter("http.socket.timeout",60*60*1000); MultipartEntity mpEntity = new MultipartEntity(); List<String> keys = new ArrayList<String>(mediaInfoMap.keySet()); Collections.sort(keys, String.CASE_INSENSITIVE_ORDER); for (Iterator<String> iterator = keys.iterator(); iterator.hasNext();) { String key = iterator.next(); if (StringUtils.isNotBlank(mediaInfoMap.get(key))) { mpEntity.addPart(key, new StringBody(mediaInfoMap.get(key))); } } if(targetFile!=null&&targetFile.exists()){ ContentBody contentBody = new FileBody(targetFile); mpEntity.addPart("file", contentBody); } post.setEntity(mpEntity); HttpResponse response = httpClient.execute(post); content = EntityUtils.toString(response.getEntity()); httpClient.getConnectionManager().shutdown(); } catch (Exception e) { e.printStackTrace(); } System.out.println("=====RequestUrl==========================\n" +getRequestUrlStrRest(serverURL, mediaInfoMap).replaceAll("&fmt=json", "")); System.out.println("=====content==========================\n"+content); return content.trim(); }
其中 "file" 是分塊上傳伺服器對分塊檔案引數定義的名字。細心的讀者會發現,整塊檔案上傳直接使用 Apache 官方的 InputStreamBody,而分塊才使用自定義的 BlockStreamBody。
最後呼叫 HttpClient4Util 進行上傳:
public static Map<String, String> uploadToDrive(
Map<String, String> params, String domain) {
File targetFile = new File(params.get("filePath"));
long targetFileSize = targetFile.length();
int mBlockNumber = 0;
if (targetFileSize < GlobalConstant.CLOUD_API_LOGON_SIZE) {
mBlockNumber = 1;
} else {
mBlockNumber = (int) (targetFileSize / GlobalConstant.CLOUD_API_LOGON_SIZE);
long someExtra = targetFileSize
% GlobalConstant.CLOUD_API_LOGON_SIZE;
if (someExtra > 0) {
mBlockNumber++;
}
}
params.put("blockNumber", Integer.toString(mBlockNumber));
if (domain != null) {
LOG.debug("Drive---domain=" + domain);
LOG.debug("drive---url=" + "http://" + domain + "/sync"
+ GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH);
} else {
LOG.debug("Drive---domain=null");
}
String responseBodyStr = HttpClient4Util.getRest("http://" + domain
+ "/sync" + GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH, params);
ObjectMapper mapper = new ObjectMapper();
DrivePreInfo result;
try {
result = mapper.readValue(responseBodyStr, ArcDrivePreInfo.class);
} catch (IOException e) {
LOG.error("Drive.preUploadToArcDrive error.", e);
throw new RtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO
}
// JSONObject jsonObject = JSONObject.fromObject(responseBodyStr);
if (Integer.valueOf(result.getRc()) == 0) {
int uuid = result.getUuid();
String upsServerUrl = result.getUploadServerUrl().replace("https",
"http");
if (uuid != -1) {
upsServerUrl = upsServerUrl
+ GlobalConstant.CLOUD_API_UPLOAD_PATH;
params.put("uuid", String.valueOf(uuid));
for (int i = 1; i <= mBlockNumber; i++) {
params.put("blockIndex", "" + i);
HttpClient4Util.restPostBlock(upsServerUrl, targetFile,
params);//
}
}
} else {
throw new RtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO
}
return null;
}
其中 params 這個 Map 裡封裝的是伺服器分塊上傳所需要的一些引數,而上傳塊數也在這裡進行確定。
本文中的示例經本人測試能夠上傳大檔案成功,諸如 *.mp4 的檔案上傳成功沒有出現任何問題。如果讀者朋友測試時遇到問題無法上傳成功,請在部落格後跟帖留言,大家共同交流下。本文示例肯定還存在很多不足之處,如果讀者朋友發現還請留言指出,筆者先行謝過了。