转自:http://www.cnblogs.com/kenshincui/p/4042190.html
扩展--文件分段下载
通过前面的演示大家应该对于iOS的Web请求有了大致的了解,可以通过代理方法接收数据也可以直接通过静态方法接收数据,但是实际开发中更推荐使用静态方法。关于前面的文件下载示例,更多的是希望大家了解代理方法接收响应数据的过程,实际开发中也不可能使用这种方法进行文件下载。这种下载有个致命的问题:不适合进行大文件分段下载。因为代理方法在接收数据时虽然表面看起来是每次读取一部分响应数据,事实上它只有一次请求并且也只接收了一次服务器响应,只是当响应数据较大时系统会重复调用数据接收方法,每次将已读取的数据拿出一部分交给数据接收方法。这样一来对于上G的文件进行下载,如果中途暂停的话再次请求还是从头开始下载,不适合大文件断点续传(另外说明一点,上面NSURLConnection示例中使用了NSMutableData进行数据接收和追加只是为了方便演示,实际开发建议直接写入文件)。
实际开发文件下载的时候不管是通过代理方法还是静态方法执行请求和响应,我们都会分批请求数据,而不是一次性请求数据。假设一个文件有1G,那么只要每次请求1M的数据,请求1024次也就下载完了。那么如何让服务器每次只返回1M的数据呢?
在网络开发中可以在请求的头文件中设置一个range信息,它代表请求数据的大小。通过这个字段配合服务器端可以精确的控制每次服务器响应的数据范围。例如指定bytes=0-1023,然后在服务器端解析Range信息,返回该文件的0到1023之间的数据的数据即可(共1024Byte)。这样,只要在每次发送请求控制这个头文件信息就可以做到分批请求。
当然,为了让整个数据保持完整,每次请求的数据都需要逐步追加直到整个文件请求完成。但是如何知道整个文件的大小?其实在前面的文件下载演示中大家可以看到,可以通过头文件信息获取整个文件大小。但是这么做的话就必须请求整个数据,这样分段下载就没有任何意义了。所幸在WEB开发中我们还有另一种请求方法“HEAD”,通过这种请求服务器只会响应头信息,其他数据不会返回给客户端,这样一来整个数据的大小也就可以得到了。下面给出完整的程序代码,关键的地方已经给出注释(为了简化代码,这里没有使用代理方法):
KCMainViewController.m
// // KCMainViewController.m // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #define kUrl @"http://192.168.1.208/FileDownload.aspx" #define kFILE_BLOCK_SIZE (1024) //每次1KB @interface KCMainViewController (){ UITextField *_textField; UIButton *_button; UIProgressView *_progressView; UILabel *_label; long long _totalLength; long long _loadedLength; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ //地址栏 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"1.jpg"; // _textField.text=@"1.jpg"; [self.view addSubview:_textField]; //进度条 _progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)]; [self.view addSubview:_progressView]; //状态显示 _label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)]; _label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; [self.view addSubview:_label]; //下载按钮 _button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)]; [_button setTitle:@"下载" forState:UIControlStateNormal]; [_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_button addTarget:self action:@selector(downloadFileAsync) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_button]; } #pragma mark 更新进度 -(void)updateProgress{
//注意:对界面进行操作的操作要放到主线程里 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (_loadedLength==_totalLength) { _label.text=@"下载完成"; }else{ _label.text=@"正在下载..."; } [_progressView setProgress:(double)_loadedLength/_totalLength]; }]; } #pragma mark 取得请求链接 -(NSURL *)getDownloadUrl:(NSString *)fileName{ NSString *urlStr=[NSString stringWithFormat:@"%@?file=%@",kUrl,fileName]; urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; return url; } #pragma mark 取得保存地址(保存在沙盒缓存目录) -(NSString *)getSavePath:(NSString *)fileName{
//获取沙盒中“最后一个”文件的路径
NSString *path=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
return [path stringByAppendingPathComponent:fileName];//设定文件名称 } #pragma mark 文件追加 -(void)fileAppend:(NSString *)filePath data:(NSData *)data{ //以可写方式打开文件 NSFileHandle *fileHandle=[NSFileHandle fileHandleForWritingAtPath:filePath]; //如果存在文件则追加,否则创建 if (fileHandle) { [fileHandle seekToEndOfFile]; [fileHandle writeData:data]; [fileHandle closeFile];//关闭文件 }else{ [data writeToFile:filePath atomically:YES];//创建文件 } } #pragma mark 取得文件大小 -(long long)getFileTotlaLength:(NSString *)fileName{ NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f]; //设置为头信息请求 [request setHTTPMethod:@"HEAD"]; NSURLResponse *response; NSError *error; //注意这里使用了同步请求,直接将文件大小返回 [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if (error) { NSLog(@"detail error:%@",error.localizedDescription); } //取得内容长度,这里使用属性 return response.expectedContentLength; } #pragma mark 下载指定块大小的数据 -(void)downloadFile:(NSString *)fileName startByte:(long long)start endByte:(long long)end{ NSString *range=[NSString stringWithFormat:@"Bytes=%lld-%lld",start,end];//注意range的格式Bytes=xx-yy NSLog(@"%@",range); // NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName]]; NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f]; //通过请求头设置数据请求范围 [request setValue:range forHTTPHeaderField:@"Range"]; NSURLResponse *response; NSError *error; //注意这里使用同步请求,避免文件块追加顺序错误,同步请求就是必须先执行完当前请求才能进行其他操作 NSData *data= [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if(!error){ NSLog(@"dataLength=%lld",(long long)data.length); [self fileAppend:[self getSavePath:fileName] data:data]; } else{ NSLog(@"detail error:%@",error.localizedDescription); } } #pragma mark 文件下载 -(void)downloadFile{ _totalLength=[self getFileTotlaLength:_textField.text]; _loadedLength=0; long long startSize=0; long long endSize=0; //分段下载 while(startSize< _totalLength){ endSize=startSize+kFILE_BLOCK_SIZE-1; if (endSize>_totalLength) { endSize=_totalLength-1; } [self downloadFile:_textField.text startByte:startSize endByte:endSize]; //更新进度 _loadedLength+=(endSize-startSize)+1; [self updateProgress]; startSize+=kFILE_BLOCK_SIZE; } } #pragma mark 异步下载文件 -(void)downloadFileAsync{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self downloadFile]; }); } @end
运行效果:
下载文件的生成过程:
分段下载的过程实现并不复杂,主要是需要配合后台进行响应进行操作。针对不同的开发技术,服务器端处理方式稍有差别,但是基本原理是一样的,那就是读取Range信息,按需提供相应数据。