前言
分塊上傳和斷點下載很像,就是講文件分為多份來傳輸鼎姐,從而實現(xiàn)暫停和繼續(xù)傳輸洁墙。區(qū)別是斷點下載的進度保存在客戶端蛹疯,ey往是寫入數(shù)據(jù)庫,分塊上傳的進度保存在服務器热监,每次可以通過文件的md5請求服務器捺弦,來獲取最新的上傳偏移量。但是這樣明顯效率偏低孝扛,客戶端可以把offSet保存在內(nèi)存列吼,每上傳一塊文件服務器返回下一次的offSet。只不過這個offSet不需要保存在數(shù)據(jù)庫苦始,每次app關(guān)閉在打開繼續(xù)上傳可以請求服務器寞钥,獲取最新偏移量。
分塊上傳原理
1.客戶端向服務端申請文件的上傳地址
a. 如果上傳過陌选,直接返回uuid (快速上傳)
b. 沒上傳過理郑,返回 上傳地址url + 上傳偏移量offset
下面上傳一段31M大小的mp4文件,申請上傳地址服務端返回offSet = 0表示文件沒有上傳過咨油,需要從頭開始上傳
2.客戶端對本地文件進行分塊您炉,比如10M為一塊chunk
上傳第一塊:
3.客戶端以標準表單方式,上傳 offset 到 offset+chunk的文件分塊役电,每次上傳完服務端返回新的offset赚爵,客戶端更新offset值并繼續(xù)下一次上傳,如此循環(huán)。
上傳最后一塊:
4.最后服務端返回文件uuid冀膝,代表整個文件上傳成功
基于Okhttp的實現(xiàn)
Okhttp已經(jīng)支持表單形式的文件上傳唁奢,剩下的關(guān)鍵就是:
構(gòu)造分塊文件的RequestBody,對本地文件分塊窝剖,和服務端約定相關(guān)header麻掸,保存offset實現(xiàn)分塊上傳
構(gòu)造RequestBody
繼承之前實現(xiàn)的進度監(jiān)聽RequestBody:
public class MDProgressRequestBody extends FileProgressRequestBody {
protected final byte[] content;
public MDProgressRequestBody(byte[] content, String contentType , ProgressListener listener) {
this.content = content;
this.contentType = contentType;
this. listener = listener;
}
@Override
public long contentLength() {
return content.length;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
int offset = 0 ;
//計算分塊數(shù)
count = (int) ( content.length / SEGMENT_SIZE + (content.length % SEGMENT_SIZE != 0?1:0) );
for( int i=0; i < count; i++ ) {
int chunk = i != count -1 ? SEGMENT_SIZE : content.length - offset;
sink.buffer().write(content, offset, chunk );//每次寫入SEGMENT_SIZE 字節(jié)
sink.buffer().flush();
offset += chunk;
listener.transferred( offset );
}
}
}
注意這個RequestBody傳入Byte數(shù)組,從而實現(xiàn)了對文件的分塊上傳赐纱。
對文件分塊
上面的RequestBody支持傳輸Byte數(shù)組论笔,那么如何把文件切割成byte[]:
/**
* 文件分塊工具
* @param offset 起始偏移位置
* @param file 文件
* @param blockSize 分塊大小
* @return 分塊數(shù)據(jù)
*/
public static byte[] getBlock(long offset, File file, int blockSize) {
byte[] result = new byte[blockSize];
RandomAccessFile accessFile = null;
try {
accessFile = new RandomAccessFile(file, "r");
accessFile.seek(offset);
int readSize = accessFile.read(result);
if (readSize == -1) {
return null;
} else if (readSize == blockSize) {
return result;
} else {
byte[] tmpByte = new byte[readSize];
System.arraycopy(result, 0, tmpByte, 0, readSize);
return tmpByte;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (accessFile != null) {
try {
accessFile.close();
} catch (IOException e1) {
}
}
}
return null;
}
基于OkHttp的分塊上傳
關(guān)鍵就是構(gòu)造Request對象:
protected Request generateRequest(String url) {
// 獲取分塊數(shù)據(jù),按照每次10M的大小分塊上傳
final int CHUNK_SIZE = 10 * 1024 * 1024;
//切割文件為10M每份
byte[] blockData = FileUtil.getBlock(offset, new File(fileInfo.filePath), CHUNK_SIZE);
if (blockData == null) {
throw new RuntimeException(String.format("upload file get blockData faild千所,filePath:%s , offest:%d", fileInfo.filePath, offset));
}
curBolckSize = blockData.length;
// 分塊上傳狂魔,客戶端和服務端約定,name字段傳文件分塊的始偏移量
String formData = String.format("form-data;name=%s; filename=file", offset);
RequestBody filePart = new MDProgressRequestBody(blockData, "application/octet-stream ", this);
MultipartBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addPart(Headers.of("Content-Disposition", formData), filePart)
.build();
// 創(chuàng)建Request對象
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
return request;
}
用OkHttp執(zhí)行上傳:
上傳開始前調(diào)用獲取上傳地址的接口淫痰,從而獲取初始offSet最楷,然后開始上傳:
```java
while (offset < fileInfo.fileSize) {
//doUpload是阻塞式方法,必須返回結(jié)果后才下一次調(diào)用
int result = doUpload(url); // readResponse()會修正偏移量
if (result != STATUS_RETRY) {
return result;
}
}
定義文件上傳的執(zhí)行方法doUpload:(和上文OkHttp監(jiān)聽進度的文件上傳一樣待错,只是不過構(gòu)造的Request不同)
protected int doUpload(String url){
try {
OkHttpClient httpClient = OkHttpClientMgr.Instance().getOkHttpClient();
call = httpClient.newCall( generateRequest(url) );
Response response = call.execute();
if (response.isSuccessful()) {
sbFileUUID = new StringBuilder();
return readResponse(response,sbFileUUID);
} else( ... ) { // 重試
return STATUS_RETRY;
}
} catch (IOException ioe) {
LogUtil.e(LOG_TAG, "exception occurs while uploading file!",ioe);
}
return isCancelled() ? STATUS_CANCEL : STATUS_FAILED_EXIT;
}
這里的readRespones讀取服務端結(jié)果籽孙,更新offSet數(shù)值:
// 解析服務端響應結(jié)果
protected int readResponse(Response response, StringBuilder sbFileUUID) {
int exitStatus = STATUS_FAILED_EXIT;
ResponseBody body = response.body();
if (body == null) {
LogUtil.e(LOG_TAG, "readResponse body is null!", new Throwable());
return exitStatus;
}
try {
String content = body.string();
JSONObject jsonObject = new JSONObject(content);
if (jsonObject.has("uuid")) { // 上傳成功,返回UUID
String uuid = jsonObject.getString("uuid");
if (uuid != null && !uuid.isEmpty()) {
sbFileUUID.append(uuid);
exitStatus = STATUS_SUCCESS;
} else {
LogUtil.e(LOG_TAG, "readResponse fileUUID return empty! ");
}
} else if (jsonObject.has("offset")) { // 分塊上傳完成火俄,返回新的偏移量
long newOffset = (long) jsonObject.getLong("offset");
if (newOffset != offset + curBolckSize) {
LogUtil.e(LOG_TAG, "readResponse offest-value exception ! ");
} else {
offset = newOffset; // 分塊數(shù)據(jù)上傳完成犯建,修正偏移
exitStatus = STATUS_RETRY;
}
} else {
LogUtil.e(LOG_TAG, "readResponse unexpect data , no offest、uuid field !");
}
} catch (Exception ex) {
LogUtil.e(LOG_TAG, "readResponse exception occurs!", ex);
}
return exitStatus;
}
說明
1.offSet值是保存在服務端的瓜客,比如中途上傳失敗了适瓦,下次繼續(xù)上傳,調(diào)用申請上傳地址接口谱仪,服務端會返回最新的offSet告訴你從哪開始上傳玻熙。
2.本文方案不支持多線程分塊上傳,必須按照文件切割的順序疯攒,依次上傳