0、寫在前面的話
上篇博客已經是在8月了,期間到底發生了什么,只有我自己知道,反正就是心情特別糟糕,生活狀態工作狀態學習狀態都十分不好,還有心思進取嗎,No!現在狀態好起來了,生活又充滿了希望 :D
前兩周在寫視頻管理相關的功能,說是要在原來的項目上進行拓展。結果今天領導給我說客戶那邊還沒定,只做技術上研究就行了,不用寫具體功能代碼(我都寫了好嗎?)於是突然時間有騰出來,今天整理一下把內容寫一些。
要努力努力,為了更好的人為了更好的生活。
1、斷點續傳的兩種方式
1.1 RandomAccessFile
客戶端給一個已經上傳的位置標記,然后服務器端就可以在指定的位置進行處理。這個斷點位置的讀取,就要用到RandomAccessFile類,該類不同於InputStream和OutputStream,它既可以對文件進行讀也可以進行寫,兩個重要方法:
- long getFilePoint():返回文件記錄指針的當前位置,不指定指針的位置默認是0
- void seek(long pos):設置文件指針偏移,即將文件記錄指針定位到pos位置
至於position位置如何去處理,就看各自的想法了。1)你可以將位置存在瀏覽器(比如localStorage),下次傳輸的時候前端從殘缺位置切割文件 blob.slice() 只傳輸剩余的部分,后端直接接收接着寫入服務器即可;2)也可以前端把文件完整傳輸,同時帶上position參數,由后端通過 RandomAccessFile 在指定位置開始讀取內容即可。
至於客戶端和服務端之間文件的一致性,多使用md5進行校驗。
1.2 分片處理
H5中新增了File API,可以通過使用 slice() 方法生成只有某段文件內容。這個方法就為斷點續傳提供了新的方式,就是分片處理,假設一個文件是100M大小,那么每次傳輸我只需要傳送10M,按序發送10次請求即可。某個分片傳送失敗,那么從這個分片再繼續發送即可,后端則對分片文件進行合並成完整文件。
其實方式和1.1提到的是類似的,不過每次傳輸的數據單位量更大一些,完整文件交給后端進行合並。
2、WebUploader的分片斷點續傳
WebUploader的選項中支持直接開啟分片上傳:
var uploader = WebUploader.Uploader({
swf: 'path_of_swf/Uploader.swf',
// 開起分片上傳
chunked: true,
// 分片大小,默認5M
chunkSize: 5242880,
// 分片出錯后(如網絡原因等造成出錯)的重傳次數
chunkRetry: 2,
// 上傳並發數
threads: 1
});
x
1
var uploader = WebUploader.Uploader({
2
swf: 'path_of_swf/Uploader.swf',
3
4
// 開起分片上傳
5
chunked: true,
6
// 分片大小,默認5M
7
chunkSize: 5242880,
8
// 分片出錯后(如網絡原因等造成出錯)的重傳次數
9
chunkRetry: 2,
10
// 上傳並發數
11
threads: 1
12
});
開啟分片上傳后,插件會自動分片上傳文件,接下來只需要在配置文件跳過和后端處理即可。官方回應在分片發送前會有監聽的事件 uploadBeforeSend,在這個方法的callback里面如果返回的是一個promise,且此promise被reject了,那么此分片就跳過了。(
實際上該方式在自測和咨詢網友時發現,並沒有什么用,即便按照官方說明,分片也沒有跳過,仍然往后端進行了請求發送,同時也附帶有文件)
webUploader.on('uploadBeforeSend', function(block, data){
data.fileMd5 = block.file.wholeMd5;
var deferred = WebUploader.Deferred();
var chunk = data.chunk;
var existChunks = block.file.existChunks;
//后端返回了已存在分片的數組,這里判斷要發送的分片是否已存在
if(existChunks && existChunks.indexOf(chunk) != -1) {
//console.log("分片存在,已跳過:" + chunk);
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise();
});
1
webUploader.on('uploadBeforeSend', function(block, data){
2
data.fileMd5 = block.file.wholeMd5;
3
var deferred = WebUploader.Deferred();
4
var chunk = data.chunk;
5
var existChunks = block.file.existChunks;
6
//后端返回了已存在分片的數組,這里判斷要發送的分片是否已存在
7
if(existChunks && existChunks.indexOf(chunk) != -1) {
8
//console.log("分片存在,已跳過:" + chunk);
9
deferred.reject();
10
} else {
11
deferred.resolve();
12
}
13
return deferred.promise();
14
});
分片是否存在的判斷,也有不同的方式,一種你可以每次計算分片的md5值發送給后端,如果服務器已存在則跳過,否則就發送;另一種就是只向服務器查詢一次獲取已經存在的分片,然后在瀏覽器端進行比對,但如此需要考慮分片是否並發傳輸,進行相應處理。
我采用的方式是:先對文件進行md5計算,在服務器端創建和md5值同名的文件夾,每次上傳的分片存放在對應文件夾,文件名即分片的序號,比如某文件夾中可能存在文件 0, 1, 2, 3... 前端發送分片前請求后端數據,后端將已經存在的分片名數組返回前端,前端進行跳過處理,同時后端在接收分片也要做是否存在的判斷,已存在的話就不再進行讀寫操作,直到最后分片到達,則進行分片的按序合並即可。
public boolean uploadChunk() throws ChunkUploadException {
HttpServletRequest request = ServletActionContext.getRequest();
//封裝源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//獲取同時上傳的文件其他屬性
Map<String, String> params = getVideoParams(request);
if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
throw new ChunkUploadException("文件md5值未傳遞");
}
//存放
File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
if (!temp.exists()) {
try {
VideoUtil.copy(srcFileInfo.getFile(), temp);
} catch (IOException e) {
throw new ChunkUploadException("分片上傳失敗: chunkNum" + params.get("chunk"));
}
}
//如果是最后分片
return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
}
x
1
public boolean uploadChunk() throws ChunkUploadException {
2
HttpServletRequest request = ServletActionContext.getRequest();
3
//封裝源文件信息
4
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
5
//獲取同時上傳的文件其他屬性
6
Map<String, String> params = getVideoParams(request);
7
8
if (params.get("fileMd5") == null || "".equals(params.get("fileMd5"))) {
9
throw new ChunkUploadException("文件md5值未傳遞");
10
}
11
//存放
12
File temp = new File(getTempPath(params.get("fileMd5")) + "/" + srcFileInfo.getCurChunk());
13
if (!temp.exists()) {
14
try {
15
VideoUtil.copy(srcFileInfo.getFile(), temp);
16
} catch (IOException e) {
17
throw new ChunkUploadException("分片上傳失敗: chunkNum" + params.get("chunk"));
18
}
19
}
20
//如果是最后分片
21
return !srcFileInfo.isChunked() || srcFileInfo.getCurChunk() == srcFileInfo.getChunkSize() - 1;
22
}
public String upload() {
boolean isLastChunk = false;
try {
isLastChunk = uploadChunk();
} catch (ChunkUploadException e) {
e.printStackTrace();
AjaxSupport.sendFailText(null, e.getMessage());
return AJAX_RESULT;
}
//不是最后的分片,直接返回成功響應
if (!isLastChunk) {
AjaxSupport.sendSuccessText("chunk uploaded", "success");
return AJAX_RESULT;
}
//最后切片
else {
HttpServletRequest request = ServletActionContext.getRequest();
//封裝源文件信息
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
//獲取同時上傳的文件其他屬性
Map<String, String> params = getVideoParams(request);
//獲取合並文件的文件名
String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();
//合並文件
File tempDir = new File(getTempPath(params.get("fileMd5")));
File[] tempfileArr = tempDir.listFiles();
File storeFile = new File(getStorePath() + "/" + filename);
try {
VideoUtil.merge(tempfileArr, storeFile);
}
...
x
1
public String upload() {
2
boolean isLastChunk = false;
3
try {
4
isLastChunk = uploadChunk();
5
} catch (ChunkUploadException e) {
6
e.printStackTrace();
7
AjaxSupport.sendFailText(null, e.getMessage());
8
return AJAX_RESULT;
9
}
10
11
//不是最后的分片,直接返回成功響應
12
if (!isLastChunk) {
13
AjaxSupport.sendSuccessText("chunk uploaded", "success");
14
return AJAX_RESULT;
15
}
16
//最后切片
17
else {
18
HttpServletRequest request = ServletActionContext.getRequest();
19
//封裝源文件信息
20
FileInfo srcFileInfo = VideoUtil.getUploadFileInfoByStruts(request, "file");
21
//獲取同時上傳的文件其他屬性
22
Map<String, String> params = getVideoParams(request);
23
//獲取合並文件的文件名
24
String filename = UUID.randomUUID().toString() + "." + srcFileInfo.getFileType();
25
26
//合並文件
27
File tempDir = new File(getTempPath(params.get("fileMd5")));
28
File[] tempfileArr = tempDir.listFiles();
29
File storeFile = new File(getStorePath() + "/" + filename);
30
try {
31
VideoUtil.merge(tempfileArr, storeFile);
32
}
33
...
最后,實際上這種方式斷點續傳仍然存在很多細節沒有考慮,比如多線程,同個瀏覽器兩個tab發送同一文件時如何處理?
