文件分片上传与分片下载

[toc]

前言

iOS m3u8本地缓存播放(控制下载并发、暂停恢复)

传统上传 VS 分片上传

传统上传方法的问题 分片上传的优点
大文件上传耗时长,容易导致超时。 将大文件拆分成较小的分片,更快更可靠地上传。
占用服务器和网络带宽资源,可能影响其他用户的访问速度。 监控并显示上传进度,提高用户体验。
如果上传中断,需要重新上传整个文件,效率低下。 充分利用浏览器的并发上传能力,减轻服务器负载。
难以显示和控制上传进度。 实现断点续传功能,避免重新上传已上传的分片。

传统文件下载 VS 文件分片下载

文件分片下载是一种通过将大文件拆分成较小的片段(分片)并同时下载它们来提高文件下载效率的技术。

问题/技术 传统文件下载 文件分片下载
长时间等待 用户可能需要等待很长时间才能开始使用大文件 只需下载第一个分片,客户端就可以开始使用文件
网络拥堵 如果网络带宽被大文件下载占用,其他用户可能会遇到下载速度慢的问题 可以使用多个并行请求来下载分片,充分利用带宽并提高整体下载速度
难以恢复下载 如果网络故障或用户中断,整个文件必须重新下载 如果下载被中断,只需重新下载未完成的分片,而不是整个文件
下载效率 下载速度较慢,特别是在网络不稳定或速度较慢的情况下 通过将大文件拆分成较小的片段并同时下载,提高文件下载效率
并行下载 不支持 支持,可以使用多个并行请求来下载分片
下载管理 整个文件作为一个整体进行下载 每个分片可以单独管理和下载,提供更好的灵活性

一、分片上传的实现方法

1、获取文件所有切片/分片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)。切片方法 [fileHandle readDataOfLength:chunkSize];

2、上传分片:提交时除了文件data,同时传入:总片数、当前是第几片。

3、上传所有分片后,有时候这里会额外调用服务器API告诉后台已完成全部上传。实际上后台自己也可以根据上传个数是否等于总数判断。服务端得到所有的数据片后合并数据。

其他注意点:

如果要上传的文件是录制的视频或者从系统相册中选择的视频,则该视频文件的路径必须是app的相对路径。缺点视频可被从相册中删除。

1. 定义分片上传的辅助类

首先,定义一个辅助类来管理分片上传的状态和数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// FileChunk.m 文件切片的数据模型
@interface FileChunk : NSObject
@property (nonatomic, strong) ChunkFileModel *fileModel; // 切片来源于哪个文件(包含文件路径、文件大小,每个分片大小,分片总数)
@property (nonatomic, assign) NSUInteger index; // 数据块的位置(为了中断后的重新上传时候,过滤掉已上传完成的切片)
@property (nonatomic, assign) NSUInteger offset; // 数据块的起始位置(可能不同大小的文件,切片大小不一样,data的offset就不一样)
@property (nonatomic, strong) NSData *data; // 数据块内容
@property (nonatomic, strong) NSURLResponse *response; // 上传响应(会包含每片的上传状态)

// 获取文件单个切片:按指定的每个切片大小切
- (FileChunk *)initWithFileHandle(NSFileHandle *)fileHandle chunkSize:(NSUInteger)chunkSize chunkIndex:(NSUInteger)chunkIndex {
self = [super init];
if (self) {
chunk.data = [fileHandle readDataOfLength:chunkSize];// 执行后文件句柄的内部偏移量会根据读取的数据长度自动向前移动
chunk.offset = [fileHandle offsetInFile]; // 返回当前文件的偏移量,即文件句柄的当前位置
chunk.index = chunkIndex;
}
return self;
}

// 要上传的切片的要设置给 HTTPHeaderField:@"Content-Range"
- (NSString *)uploadContentRangeString {
return [NSString stringWithFormat:@"bytes %@-%@/total-size", @(chunk.offset), @(chunk.offset + chunk.data.length)];
}
@end



// FileChunkHelper.m
+ (void)addFinishSaveChunk:(FileChunk *)chunk forFilePath:(NSString *)filePath {
// 以key: chunk.belongfileMD5 保存已上传完成的切片索引 [0, 1, 2, chunk.index, ...]
}
+ (void)getFinishSaveChunkForFilePath:(NSString *)filePath {
// 以key: chunk.belongfileMD5 获取已上传完成的切片索引 [0, 1, 2, chunk.index, ...]
}

// 获取文件所有切片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)
+ (NSArray *)chunksFromFilePath:(NSString *)filePath
chunkSize:(NSUInteger)chunkSize
withoutChunkIndexs:(NSArray<NSUInteger>)chunkIndexs
{
NSMutableArray<FileChunk *> *chunks = [NSMutableArray array];

NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
[fileHandle seekFileOffset:0]; // 先定位到文件头部
int chunkIndex = 0;
FileChunk *chunk = [[FileChunk alloc] initWithFileHandle:fileHandle chunkSize:chunkSize chunkIndex:chunkIndex];
while (chunk.data) {
chunkIndex++;
if ([chunkIndexs contains chunk.index]) { // 为了中断后的重新上传时候,过滤掉已上传完成的切片
continue;
}
[chunks addObject:chunk];
}
[fileHandle closeFile];

return chunks;
}

// 单个分片的上传方法
+ (void)uploadChunk:(FileChunk *)chunk
uploadId:(NSString *)uploadId
completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"your-upload-chunk-api-endpoint"]];
request.HTTPMethod = @"PUT";
request.HTTPBody = chunk.data;
[request setValue:chunk.uploadContentRangeString forHTTPHeaderField:@"Content-Range"];

NSURLResponse *response = nil;
NSError *error = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if (error) {
completionHandler(NO, error);
return;
}

chunk.response = response; // 如果上传成功,更新状态
[self addFinishSaveChunk:chunk forFilePath:chunk.filePath];
completionHandler(YES, nil);
}

+ (void)uploadChunks:(NSArray<FileChunk *> *)chunks
withUploadId:(NSString *)uploadId
completionHandler:(void (^)())completionHandler
{
dispatch_group_t group = dispatch_group_create();
NSMutableArray *uploadTasks = [NSMutableArray array];
for (FileChunk *chunk in chunks) {
dispatch_group_enter(group);
[FileChunkHelper uploadChunk:chunk uploadId:uploadId completionHandler:^(BOOL success,NSError *error) {
completionHandler(success, error);
dispatch_group_leave(group);
}];

[uploadTasks addObject:@{@"task": @"uploadChunk", @"chunk": chunk}];
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
completionHandler();
});
}

2. 分片上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 请求文件是否上传过。(未上传过:返回用于上传的upload;正在上传中:返回upload;已上传过:返回url)
- (void)requestUploadInfoForFilePath:(NSString *)filePath
{
NSDictionary *parameters = @{
@"file_name": @"filename.txt", // 替换为实际文件名
@"file_size": @([FileChunkHelper getFileSize:filePath]), // 调用方法获取文件大小
@"part_size": @(partSize)
};
if (resultModel.uploadFinish) {
NSString *url = resultModel.url;
return;
}

NSString *uploadId = resultModel.uploadId;
//NSArray<NSInteger> finishChunkIndexs = resultModel.finishChunkIndexs; // 个人感觉不需要后台返回未完成的
// 1、获取文件所有切片:按指定的每个切片大小切(中断后的重新上传时,要过滤掉已上传完成的切片)
NSUInteger chunkSize = 1024 * 1024; // 1M
NSArray<NSUInteger> chunkIndexs = [FileChunkHelper getFinishSaveChunkForFilePath:filePath];
NSArray<FileChunk *> *chunks = [FileChunkHelper chunksFromFilePath:filePath chunkSize:chunkSize withoutChunkIndexs:chunkIndexs];
// 2、上传分片:提交时除了文件data,同时传入、总片数、当前是第几片
[FileChunksUpload uploadChunks:chunks withUploadId:uploadId completionHandler:^(void){
// 3、上传所有分片后,有时候这里会额外调用服务器API告诉后台已完成全部上传。实际上后台自己也可以根据上传个数是否等于总数判断
}];
}

二、文件分段下载

iOS 文件分段下载

iOS文件下载,断点续传,后台下载.

NSURLSessionDataTask 方式断点续传之下载

原理:
每次向服务器下载数据的时候,告诉服务器从整个文件数据流的某个还未下载的位置开始下载,然后服务器就返回从那个位置开始的数据流.通过设置请求头Range可以指定每次从服务器下载数据包的大小.

Range示例
bytes = 0-499 从0到499的头500个字节
bytes = 500-999 从500到999第二个500个字节
bytes = 1000- 从1000字节以后所有的字节
bytes = -500 最后500个字节
bytes = 0-499,500-999 同时指定几个范围

1
2
3
4
>  /// 下次请求的时候,设置请求头,每次从本地取数据把保存好的下载长度放进请求头Range中
> NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
> [request setValue:range forHTTPHeaderField:@"Range"];
>

数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>  /// 初始化:通过NSOutputStream流向文件写出数据 ,append是否向尾部追加下载的数据
> self.stream = [[NSOutputStream alloc] initToFileAtPath:filePath append:YES];
>
> // 获取到服务器信息后,打开流,block回调允许接收下载数据
> [self.stream open];
> completionHandler(NSURLSessionResponseAllow);
>
> // 下载数据的回调:写入数据
> [self.stream write:[data bytes] maxLength:data.length];
>
> // 下载成功和失败都会来
> [self.stream close];
> self.stream = nil;
>

大文件视频上传:断点续传

End