當(dāng)上傳或者下載的文件比較大的時候损痰,如果中途網(wǎng)絡(luò)斷掉或者后臺前端出現(xiàn)問題福侈,導(dǎo)致下載中斷酒来,作為用戶來說,我肯定希望下次登陸的時候接著上次下載上傳的進(jìn)度繼續(xù)上傳或者下載肪凛,而不是從頭開始堰汉。這就需要前端和后臺支持?jǐn)帱c續(xù)傳。
斷點分片上傳
大體思路是這樣的:
前端:假定我們每個分片的大小是1Mb伟墙,每次前端上傳的時候翘鸭,先讀取一個分片大小的數(shù)據(jù),然后詢問后臺該分片是否已經(jīng)上傳戳葵,如果沒有上傳就乓,則將該分片上傳,如果上傳過了拱烁,那么就seek 一個分片大小的offset生蚁,讀取下一個分片繼續(xù)同樣的操作,直到所有分片上傳完成戏自。
后端:我們將接收到的一個一個的分片文件單獨放到一個以文件md5命名的文件夾下邦投,當(dāng)前端詢問分片是否存在的時候,只要到該文件夾下查找對應(yīng)的臨時文件是否存在即可擅笔。當(dāng)所有的分片文件都上傳后志衣,再合并所有的臨時文件即可屯援。
先看前端的實現(xiàn):
@interface LFUpload : NSObject
-(void)uploadFile:(NSString *)fileName withType:(NSString*)fileType progress:(Progress)progress;
@end
這里,我們將上傳單獨放到一個類里面
static int offset = 1024*1024; // 每片的大小是1Mb
@interface LFUpload()
@property (nonatomic, assign) NSInteger truncks;
@property (nonatomic, copy) NSString *fileMD5;
@property (nonatomic, copy) Progress progress;
@end
@implementation LFUpload
-(void)uploadFile:(NSString *)fileName withType:(NSString *)fileType progress:(Progress)progress {
self.progress = progress;
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
self.truncks = fileData.length % offset == 0 ? fileData.length/offset : fileData.length/offset + 1; // 計算該文件的總分片數(shù)
self.fileMD5 = [FileUtil getMD5ofFile:filePath]; // 獲取文件md5值并傳給后臺
[self checkTrunck:1]; // 檢查第一個分片是否已經(jīng)上傳
}
// 檢查分片是否已經(jīng)上傳
-(void)checkTrunck:(NSInteger)currentTrunck {
if (currentTrunck > self.truncks) {
self.progress(100, NO); // 標(biāo)示上傳完成
return;
}
__weak typeof(self) weakSelf = self;
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
[params setValue:self.fileMD5 forKey:@"md5file"];
[params setValue:@(currentTrunck) forKey:@"chunk"];
[[LFNetManager defaultManager] POST:@"https://192.168.1.57:443/checkChunk" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *code = [responseObject objectForKey:@"code"];
if ([code isEqualToString:@"0002"]) { //分片未上傳念脯,下面開始上傳該分片數(shù)據(jù)
[weakSelf uploadTrunck:currentTrunck];
} else { // 分片已經(jīng)上傳過了狞洋,直接更新上傳進(jìn)度
CGFloat progressFinished = currentTrunck * 1.0/self.truncks; self.progress(progressFinished, NO);
[weakSelf checkTrunck:currentTrunck + 1];
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
self.progress(0, YES);
}];
}
// 上傳分片
-(void)uploadTrunck:(NSInteger)currentTrunck {
__weak typeof(self) weakSelf = self;
[[LFNetManager defaultManager] POST:@"https://192.168.1.57:443/upload"
parameters:nil
constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"myFilm" ofType:@"mp4"];
NSData *data;
NSFileHandle *readHandler = [NSFileHandle fileHandleForReadingAtPath:filePath];
// 偏移到這個分片開始的位置,并讀取數(shù)據(jù)
[readHandler seekToFileOffset:offset * (currentTrunck - 1)];
data = [readHandler readDataOfLength:offset];
[formData appendPartWithFileData:data name:@"file" fileName:@"myFilm.mp4" mimeType:@"application/mp4"];
// md5File和二, 后臺需要的參數(shù)
NSData *md5FileData = [self.fileMD5 dataUsingEncoding:NSUTF8StringEncoding];
[formData appendPartWithFormData:md5FileData name:@"md5File"];
// truncks徘铝, 后臺需要直到分片總數(shù),來判斷上傳是否完成
NSData *truncksData = [[NSString stringWithFormat:@"%ld", (long)self.truncks] dataUsingEncoding:NSUTF8StringEncoding];
[formData appendPartWithFormData:truncksData name:@"truncks"];
// currentTrunck惯吕,告知后臺當(dāng)前上傳的分片索引
NSData *trunckData = [[NSString stringWithFormat:@"%ld", (long)currentTrunck] dataUsingEncoding:NSUTF8StringEncoding];
[formData appendPartWithFormData:trunckData name:@"currentTrunck"];
} progress:^(NSProgress * _Nonnull uploadProgress) {
CGFloat progressInThisTrunck = (1.0 * uploadProgress.completedUnitCount) / (uploadProgress.totalUnitCount * self.truncks); // 當(dāng)前分片中已上傳數(shù)據(jù)占文件總數(shù)據(jù)的百分比
CGFloat progressFinished = (currentTrunck - 1) * 1.0/self.truncks; // 已經(jīng)完成的進(jìn)度
self.progress(progressInThisTrunck + progressFinished, NO);
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[weakSelf checkTrunck:currentTrunck + 1];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
self.progress(0, YES);
}];
}
@end
再來看后臺的實現(xiàn):
- 檢查分片是否已經(jīng)上傳
@PostMapping("checkChunk")
@ResponseBody
public ResponseCommon checkChunk(@RequestParam(value = "md5file") String md5file, @RequestParam(value = "chunk") Integer chunk) {
ResponseCommon responseCommon = new ResponseCommon();
// 這里通過判斷分片對應(yīng)的文件存不存在來判斷分片有沒有上傳
try {
File path = new File(ResourceUtils.getURL("classpath:").getPath());
File fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5file + "/" + chunk + ".tmp");
if (fileUpload.exists()) {
responseCommon.setCode("0001");
responseCommon.setMsg("分片已上傳");
} else {
responseCommon.setCode("0002");
responseCommon.setMsg("分片未上傳");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
responseCommon.setCode("1111");
responseCommon.setMsg(e.getLocalizedMessage());
}
return responseCommon;
}
- 分片上傳
@PostMapping("upload")
@ResponseBody
public ResponseCommon upload(@RequestParam(value = "file") MultipartFile file,
@RequestParam(value = "md5File") String md5File,
@RequestParam(value = "truncks") Integer truncks,
@RequestParam(value = "currentTrunck") Integer currentTrunck) {
ResponseCommon responseCommon = new ResponseCommon();
try {
File path = new File(ResourceUtils.getURL("classpath:").getPath());
File fileUpload;
if (truncks == 1) {
fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5File + "/1.tmp");
} else {
fileUpload = new File(path.getAbsolutePath(), "static/uploaded/" + md5File + "/" + currentTrunck + ".tmp");
}
if (!fileUpload.exists()) {
fileUpload.mkdirs();
}
file.transferTo(fileUpload);
if (currentTrunck == truncks) {
boolean result = this.merge(truncks, md5File, file.getOriginalFilename());
if (result) {
responseCommon.setCode("0000");
responseCommon.setMsg("上傳成功");
} else {
responseCommon.setCode("1111");
responseCommon.setMsg("文件合并失敗");
}
} else {
responseCommon.setCode("0000");
responseCommon.setMsg("上傳成功");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
responseCommon.setCode("1111");
responseCommon.setMsg(e.getLocalizedMessage());
} catch (IOException e) {
e.printStackTrace();
responseCommon.setCode("1111");
responseCommon.setMsg(e.getLocalizedMessage());
}
return responseCommon;
}
- 合并所有分片臨時文件
public boolean merge(@RequestParam(value = "truncks") Integer truncks,
@RequestParam(value = "md5File") String md5File,
@RequestParam(value = "fileName") String fileName) {
ResponseCommon responseCommon = new ResponseCommon();
FileOutputStream outputStream = null;
try {
File path = new File(ResourceUtils.getURL("classpath:").getPath());
File md5FilePath = new File(path.getAbsolutePath(), "static/uploaded/" + md5File);
File finalFile = new File(path.getAbsolutePath(), "static/uploaded/" + fileName);
outputStream = new FileOutputStream(finalFile);
byte[] buffer = new byte[1024];
for (int i=1; i<=truncks; i++) {
String chunckFile = i + ".tmp";
File tmpFile = new File(md5FilePath + "/" + chunckFile);
InputStream inputStream = new FileInputStream(tmpFile);
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
inputStream.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return false;
} catch (IOException e) {
e.printStackTrace();
return false;
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return true;
}
這里惕它,我們只是大體實現(xiàn)一下,不必在意細(xì)節(jié)和規(guī)范废登。
斷點分片下載
這里的邏輯和上傳的邏輯正好是反過來的
前端先到后臺獲取總分片數(shù)淹魄,然后循環(huán)遍歷,檢查本地對應(yīng)文件夾下是否有對應(yīng)的分片臨時文件堡距,如果有甲锡,表示之前下載過,如果沒有羽戒,就到后臺請求下載對應(yīng)的分片文件缤沦,當(dāng)所有分片下載完成后,再合并所有的分片臨時文件即可
后臺易稠,這里要提供文件的總分片數(shù)缸废,以及和之前前端一樣的可以seek到某個分片,單獨提供給前端下載驶社,另外企量,考慮到gzip的問題,最好提供一個單獨的接口告知前端整個文件的大小亡电,當(dāng)然届巩,這里,因為我們知道每個分片的大小是1Mb份乒,以及所有的分片數(shù)恕汇,為了簡單起見,就簡單粗糙的計算下下載進(jìn)度或辖。
先看后端實現(xiàn):
- 獲取該文件的所有分片數(shù)
@GetMapping("getTruncks")
@ResponseBody
public ResponseCommon getTruncks() {
ResponseCommon responseCommon = new ResponseCommon();
responseCommon.setCode("0000");
try {
File file = new File("/Users/archerlj/Desktop/Untitled/myFilm.mp4");
Long fileLength = file.length();
Long trunck = fileLength/(1024 * 1024);
if (fileLength%(1024*1024) == 0) {
responseCommon.setTruncks(trunck.intValue());
} else {
responseCommon.setTruncks(trunck.intValue() + 1);
}
String fileMD5 = DigestUtils.md5DigestAsHex(new FileInputStream(file));
responseCommon.setFileMD5(fileMD5);
responseCommon.setCode("0000");
} catch (IOException e) {
e.printStackTrace();
responseCommon.setCode("1111");
responseCommon.setMsg(e.getLocalizedMessage());
}
return responseCommon;
}
- 分片下載接口
@GetMapping("downloadFile")
@ResponseBody
public void downloadFile(Integer trunck, HttpServletRequest request, HttpServletResponse response) {
try {
RandomAccessFile file = new RandomAccessFile("/Users/archerlj/Desktop/Untitled/myFilm.mp4", "r");
long offset = (trunck - 1) * 1024 * 1024;
file.seek(offset);
byte[] buffer = new byte[1024];
int len = 0;
int allLen = 0;
for (int i=0; i<1024; i++) {
len = file.read(buffer);
if (len == -1) {
break;
}
allLen += len;
response.getOutputStream().write(buffer, 0, len);
file.seek(offset + (1024 * (i + 1)));
}
file.close();
response.setContentLength(allLen);
response.flushBuffer();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
前端實現(xiàn):
同樣瘾英,這里我們單獨提供一個類:
typedef void(^DownloadProgress)(CGFloat progress, Boolean error);
@interface LFDownload : NSObject
-(void)downloadWithCallBack:(DownloadProgress)callback;
@end
@interface LFDownload()
@property (nonatomic, copy) DownloadProgress progress;
@property (nonatomic, assign) NSInteger allTruncks;
@property (nonatomic, assign) NSInteger currentTrunck;
@property (nonatomic, copy) NSString *fileMD5;
@end
@implementation LFDownload
-(void)downloadWithCallBack:(DownloadProgress)callback {
self.progress = callback;
[self getTruncks];
[self getProgress]; // 這里我們單獨來計算下載進(jìn)度
}
-(void)getTruncks {
__weak typeof(self) weakSelf = self;
[[LFNetManager defaultManager] GET:@"https://192.168.1.57:443/getTruncks" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if ([[responseObject valueForKey:@"code"] isEqualToString:@"0000"]) {
weakSelf.allTruncks = [[responseObject valueForKey:@"truncks"] integerValue];
weakSelf.fileMD5 = [responseObject valueForKey:@"fileMD5"];
if ([self checkMD5FilePath]) {
[self downloadWithTrunck:1];
} else {
weakSelf.progress(0, YES);
}
} else {
weakSelf.progress(0, YES);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
weakSelf.progress(0, YES);
}];
}
// 檢查存放temp文件的文件夾是否存在
-(BOOL)checkMD5FilePath {
NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir = YES;
BOOL fileMD5PathExists = [fileManager fileExistsAtPath:path isDirectory:&isDir];
if (!fileMD5PathExists) {
// 如果不存在,就創(chuàng)建文件夾
NSError *error;
[fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"下載失敗");
return NO;
}
}
return YES;
}
// 下載完成孝凌,合并所有temp文件
-(void)mergeTempFiles {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *finalFilePath = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"myFilm.mp4"];
BOOL isDir = NO;
// 檢查合并的最終文件是否存在方咆,不存在就新建一個文件
if (![fileManager fileExistsAtPath:finalFilePath isDirectory:&isDir]) {
BOOL result = [fileManager createFileAtPath:finalFilePath contents:nil attributes:nil];
if (!result) {
self.progress(0, YES);
NSLog(@"文件合并失敗");
return;
}
}
NSFileHandle *outFileHandler = [NSFileHandle fileHandleForUpdatingAtPath:finalFilePath];
NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
// 開始循環(huán)讀取每個分片臨時文件
for (int i=1; i<= self.allTruncks; i++) {
NSString *fileName = [NSString stringWithFormat:@"%d.temp", i];
NSString *tempFilePath = [path stringByAppendingPathComponent:fileName];
NSFileHandle *inFileHandler = [NSFileHandle fileHandleForReadingAtPath:tempFilePath];
int offsetIndex = 0;
BOOL end = NO;
// 這里每個分片文件,我們每次讀取 1024 byte蟀架, 循環(huán)讀取瓣赂,直到讀取的data長度為0
while (!end) {
[inFileHandler seekToFileOffset:1024 * offsetIndex];
NSData *data = [inFileHandler readDataOfLength:1024];
if (data.length == 0) {
end = YES;
} else {
// 將最終文件seek到最后榆骚,并將數(shù)據(jù)寫入
[outFileHandler seekToEndOfFile];
[outFileHandler writeData:data];
}
offsetIndex += 1;
}
[inFileHandler closeFile];
}
[outFileHandler closeFile];
NSLog(@"文件合并成功");
}
-(void)downloadWithTrunck:(NSInteger)currentTrunck {
if (currentTrunck > self.allTruncks) {
NSLog(@"下載完成");
self.progress(1.0, NO);
[self mergeTempFiles];
return;
}
__weak typeof(self) weakSelf = self;
NSString *urlStr = [NSString stringWithFormat:@"https://192.168.1.57:443/downloadFile?trunck=%ld", (long)currentTrunck];
NSURL *url = [NSURL URLWithString:urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSString *fileName = [NSString stringWithFormat:@"%ld.temp", (long)currentTrunck];
NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:self.fileMD5];
NSString *tempFilePath = [path stringByAppendingPathComponent:fileName];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir = NO;
BOOL tempFileExists = [fileManager fileExistsAtPath:tempFilePath isDirectory:&isDir];
if (tempFileExists) { // 文件已經(jīng)下載了
NSLog(@"%ld.temp 已經(jīng)下載過了", currentTrunck);
self.progress(currentTrunck * 1.9 /self.allTruncks, NO);
[weakSelf downloadWithTrunck:currentTrunck + 1];
} else { // 開始下載文件
self.currentTrunck = currentTrunck;
NSURLSessionDownloadTask *task = [[LFNetManager defaultSessionManager] downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
/* 由于后臺使用gzip壓縮的原因,導(dǎo)致http header中沒有Content-Length字段煌集,所以這里不能獲取到下載進(jìn)度 */
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
NSLog(@"%@", tempFilePath);
return [NSURL fileURLWithPath:tempFilePath];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
if (error) {
weakSelf.progress(0, YES);
} else {
[weakSelf downloadWithTrunck:currentTrunck + 1];
}
}];
[task resume];
}
}
// 我們自己在回調(diào)中計算進(jìn)度
-(void)getProgress {
__weak typeof(self) weakSelf = self;
[[LFNetManager defaultSessionManager] setDownloadTaskDidWriteDataBlock:^(NSURLSession * _Nonnull session, NSURLSessionDownloadTask * _Nonnull downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
// 這里總長度應(yīng)該從后臺單獨獲取妓肢,這里取一個大致的值,假設(shè)所有的trunk都有1Mb苫纤,其實碉钠,只有最后一個沒有1Mb
long allBytes = 1024 * 1024 * self.allTruncks;
long downloadedBytes = bytesWritten + 1024 * 1024 * (self.currentTrunck - 1); // 當(dāng)前trunk已經(jīng)下載的大小 + 前面已經(jīng)完成的trunk的大小
weakSelf.progress(downloadedBytes * 1.0 / allBytes, NO);
}];
}
@end
文件下載完成以后,我們可以在XCode中卷拘,到Window->Devices and Simulators中喊废,將app的container文件下載到電腦上,查看下載的文件:
這里只考慮了分片斷點續(xù)傳栗弟,其實文件比較大的時候污筷,考慮多線程上傳應(yīng)該會更快一些,可以用NSOperationQueue來簡單的將文件分片分成多個任務(wù)上傳下載乍赫,這里就要控制下多線程下臨界區(qū)的問題了瓣蛀,這里主要是分片索引和進(jìn)度的多線程訪問問題。當(dāng)然雷厂,如果是多個文件上傳下載惋增,將每個文件單獨分配任務(wù)的話就簡單多了。
詳細(xì)的代碼請參考下面鏈接: