下載LOFTER客戶端
IOS Http斷點續傳淺析
http實現斷點續傳的關鍵地方就是在httprequest中加入“Range”頭。
//設置Range頭,值:bytes=x-y;x:開始字節,y:結束字節,不指定則為文件末尾
[request addValue:@"bytes=500-" forHTTPHeaderField:@"Range"];
如果服務器正確響應的話,就可以順利續傳;如果服務器不支持,那就只能用其它方法了。
經過測試,服務器的不支持分為兩種情況:
1.完全沒響應
如果不處理會導致文件無法下載。
測試地址:http://dl_dir.qq.com/qqfile/qq/QQforMac/QQ_V2.4.2.dmg
發送請求後,過一段時間直接進入了didFailWithError的delegate;錯誤信息為time out。
針對這種情況可以做出的處理是:增加一個是否支持斷點續傳的標志。
具體:
第一次請求,開始字節為0,不用發送Range頭,可以正常下載;
當下載中斷,開始第二次請求,開始字節不為0,發送range頭;
如果進入didFailWithError的delegate,就標明此鏈接不可以斷點續傳,每次請求前都清除緩存,保證開始的字節為0,不發送Range頭。
2.無論發送Range的值是多少,服務器都會重新下載。
如果不處理,會導致續傳過的文件出錯。
測試地址:https://github.com/CocoaPods/CocoaPods/archive/master.zip
這種情況的處理方案是:
第一次收到響應的時候,就把文件的總大小記錄下來;
以後每次收到響應的時候都比較一下下載長度和總大小是不是一樣;
如果一樣而且又存在緩存;就表明屬於這種情況了;直接刪掉緩存,重新下載。
下面是用NSURLConnection實現http斷點續傳的實例:
針對上面兩種做了簡單的處理,回調函數還有待添加
MXDownload.h文件:
#import <Foundation/Foundation.h>
@interface MXDownload : NSObject
//文件名路徑
@property (nonatomic, readonly) NSString *filePath;
//是否正在下載的標志
@property (nonatomic, readonly) BOOL downloading;
//初始化
- (id)initWithUrlString:(NSString *)urlString;
//兩個狀態
- (void)start;
- (void)stop;
//清除緩存
- (void)clearCache;
@end
MXDownload.m文件:
#import "MXDownload.h"
#import "NSString+MX.h"
#define FILE_INFO_PLIST [NSString pathWithName:@"MXDownload/fileInfo.plist" directory:NSCachesDirectory]
@interface MXDownload (){
NSURLConnection *_urlConnection;
NSString *_urlString;
BOOL _downloading,_didAddRange,_shouldResume;
NSString *_fileName,*_filePath, *_tempFilePath;
NSFileHandle *_fileHandle;
unsigned long long _fileOffset,_fileSize;
}
@end
@implementation MXDownload
@synthesize downloading = _downloading;
@synthesize filePath = _filePath;
//初始化,順便設置下載文件和下載臨時文件路徑
- (id)initWithUrlString:(NSString *)urlString{
self = [super init];
if (self){
_urlString = urlString;
_shouldResume = YES;
if (_urlString) {
_fileName = [_urlString MD5];
_filePath = [NSString pathWithName:[NSString stringWithFormat:@"MXDownload/%@",_fileName] directory:NSCachesDirectory];
_tempFilePath = [NSString stringWithFormat:@"%@.temp",_filePath];
}
}
return self;
}
//開始下載
- (void)start{
//如果正在下載,中斷
if (_downloading) return;
//沒有url,也中斷
if (!_urlString) return;
//臨時文件句柄
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:_tempFilePath];
//獲取本次請求下載開始的位置,如果文件不存在,就是0
_fileOffset = _fileHandle ? [_fileHandle seekToEndOfFile] : 0;
//初始化請求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:_urlString]];
//設置緩存策略,很重要,因為文件是自己儲存的,和緩存無關,所以要忽略緩存
//要不然第二次請求會出錯
[request setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
//最關鍵地方,設置Range頭,值:bytes=x-y;x:開始字節,y:結束字節,不指定則為文件末尾
_didAddRange = NO;
if (_fileOffset != 0 && _shouldResume) {
[request addValue:[NSString stringWithFormat:@"bytes=%llu-",_fileOffset] forHTTPHeaderField:@"Range"];
_didAddRange = YES;
}
_urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[_urlConnection start];
_downloading = YES;
}
//結束下載
- (void)stop{
[_urlConnection cancel];
_urlConnection = nil;
[_fileHandle closeFile];
_downloading = NO;
}
//清除文件
- (void)clearCache{
if (_downloading) [self stop];
[[NSFileManager defaultManager] removeItemAtPath:_filePath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
}
#pragma mark -
#pragma mark NSURLConnectionDelegate
//接收到響應
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
//本次請求回來的文件大小
long long fileLength = response.expectedContentLength;
if (fileLength == NSURLResponseUnknownLength) [self stop];
NSData *existFileData = [[NSData alloc] initWithContentsOfFile:_filePath];
//檢查文件是否已下載完成
if (existFileData && existFileData.length == fileLength) {
NSLog(@"之前已經下載好了");
[self stop];
}
else{
//保存文件的總大小
if (!_didAddRange){
NSMutableDictionary *dic = [NSMutableDictionary new];
[dic addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile:FILE_INFO_PLIST]];
[dic setValue:[NSNumber numberWithLongLong:fileLength] forKey:_fileName];
[dic writeToFile:FILE_INFO_PLIST atomically:YES];
}
NSFileManager *fileManager = [NSFileManager defaultManager];
//先清除掉舊的文件
[fileManager removeItemAtPath:_filePath error:nil];
//如果此次請求回來的大小等於文件的總大小而且臨時文件又存在,則刪除臨時文件
//解決每次請求都是重新開始的問題
NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:FILE_INFO_PLIST];
BOOL isTotalLength = fileLength == [[dic valueForKey:_fileName] longLongValue];
if ([fileManager fileExistsAtPath:_tempFilePath] && isTotalLength){
[fileManager removeItemAtPath:_tempFilePath error:nil];
}
//重新創建文件
if (![fileManager fileExistsAtPath:_tempFilePath]){
[fileManager createFileAtPath:_tempFilePath contents:nil attributes:nil];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:_tempFilePath];
_fileOffset = 0;
}
_fileSize = fileLength + _fileOffset;
//用_fileOffset可以檢查是重新下載還是繼續下載
NSLog(@"%@",_fileOffset ? @"繼續下載" : @"開始下載");
}
}
//不斷接收到數據
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)aData{
//寫入文件
[_fileHandle writeData:aData];
_fileOffset = [_fileHandle offsetInFile];
NSLog(@"下載進度: %lld / %lld",_fileOffset,_fileSize);
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self stop];
//如果不支持續傳,刪掉臨時文件再試一次
if (_shouldResume) {
_shouldResume = NO;
[[NSFileManager defaultManager] removeItemAtPath:_tempFilePath error:nil];
[self start];
}
}
//完成
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
[[NSFileManager defaultManager] moveItemAtPath:_tempFilePath toPath:_filePath error:nil];
NSLog(@"下載完成");
[self stop];
}
@end
調用:
- (IBAction)startDownLoad:(id)sender{
if (_downloader == nil){
// _downloader = [[MXDownload alloc] initWithUrlString:@"https://github.com/CocoaPods/CocoaPods/archive/master.zip"];
_downloader = [[MXDownload alloc] initWithUrlString:@"http://dl_dir.qq.com/qqfile/qq/QQforMac/QQ_V2.4.2.dmg"];
// _downloader = [[MXDownload alloc] initWithUrlString:@"http://192.168.50.19:8080/vcont/wb.mp3"];
}
if (_downloader.downloading) {
[_downloader stop];
}
else{
[_downloader start];
}
}