06-NSURLSession

weixin_33913332發表於2017-08-28

一、NSURLSession

1、簡介

  • iOS7.0 推出,用於替代 NSURLConnection。
  • 支援後臺執行的網路任務。
  • 暫停,停止,重啟網路任務,不需要NSOperation 封裝。 > 請求可以使用同樣的 配置容器。
  • 不同的 session 可以使用不同的私有儲存。
  • block 和代理可以同時起作用。
  • 直接從檔案系統上傳,下載。

結構圖

2229471-954e2d3280b9e73d.jpeg
  • 1、為了方便程式設計師使用,蘋果提供了一個全域性 session
  • 2、所有的 任務(Task)都是由 session 發起的
  • 3、所有的任務預設是掛起的,需要 resume
  • 4、使用 NSURLSession 後,NSURLRequest 通常只用於 指定 HTTP 請求方法,而其他的額外資訊都是通過 NSUSLSessionConfiguration 設定

2、程式碼演練--獲取JSON資料

#pragma mark - 詳細的寫法
// 載入資料
- (void)loadData{
    // 1.建立 url
    NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
    // 2.為了程式設計師方便開發,蘋果提供了一個全域性的 session 物件
    // 獲取全域性的會話物件
    // 在 session 中,request絕大多數情況下是可以省略的
    NSURLSession *session = [NSURLSession sharedSession];
    // 3.獲得資料任務物件
    // **** 所有的任務都是由 session 發起的,不要通過 alloc init 建立任務物件
    // **** 任務的回撥預設的就是非同步的
    // **** 如果需要更新 UI,需要注意主執行緒回撥
    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        id result =[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
        NSLog(@"result = %@ ---%@",result,[NSThread currentThread]);
    }];
    // 4.繼續任務
    [task resume];
}

#pragma mark - 簡化的寫法
- (void)loadData2{
    // 1.建立 url
    NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
    // 2.執行任務
    [self dataTask:url finished:^(id obj) {
        NSLog(@"obj = %@ -- %@",obj,[NSThread currentThread]);
    }];
}

- (void)dataTask:(NSURL *)url finished:(void (^)(id obj))finished{
    NSAssert(finished != nil, @"必須傳人回撥");
    // **** 所以的任務都是由 session 發起的,不要通過 alloc init 建立任務物件
    // **** 任務的回撥預設的就是非同步的
    // **** 如果需要更新 UI,需要注意主執行緒回撥
    [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        id result =[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
//        NSLog(@"result = %@ ---%@",result,[NSThread currentThread]);
        dispatch_async(dispatch_get_main_queue(), ^{
            finished(result);
        });
    }] resume];
}

二、下載和解壓縮

1、NSURLSession下載檔案

- (void)download{
    // 1.建立下載 url
    NSURL *url = [NSURL URLWithString:@"http://localhost/1234.mp4"];
    NSLog(@"開始下載");
    // 2.開始下載
    [[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
        NSLog(@"下載完成");
        // 非同步解壓縮
        
    }] resume];
}

細節:

  • 從Xcode6.0 到 Xcode 6.3 記憶體佔用一直很高,差不多是檔案大小
    的 2.5 倍。
  • 檔案下載完成後會被自動刪除!思考為什麼?
    • 大多數情況下,下載的檔案型別以‘zip’居多,可以節約使用者的流量。
    • 下載完成後解壓縮。
    • 壓縮包就可以刪除。

2、解壓縮zip包

- (void)download{
    // 1.建立下載 url
    NSURL *url = [NSURL URLWithString:@"http://localhost/itcast/images.zip"];
    NSLog(@"開始下載");
    // 2.開始下載
    [[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
        NSLog(@"下載完成");
        // 獲得沙盒快取資料夾路徑
         NSString *cacheDir = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"pkxing"];
        // 非同步解壓縮
        // location.path:沒有‘協議頭’的路徑
        // location.absoluteString:包含 ‘協議頭‘’的完成 url 路徑
        // 注意:解壓縮路徑最好自己新建一個專屬的資料夾,因為 zip 包中可能有多個檔案。
        [SSZipArchive unzipFileAtPath:location.path toDestination:cacheDir delegate:self];
    }] resume];
}

#pragma mark - SSZipArchiveDelegate 代理方法
/**
 *  跟蹤解壓縮排度
 *
 *  @param loaded 已經解壓的大小
 *  @param total  要解壓的檔案大小
 */
- (void)zipArchiveProgressEvent:(NSInteger)loaded total:(NSInteger)total {
    NSLog(@"%f",(CGFloat)loaded/ total);
}

壓縮
// 將指定路徑下的檔案打包到指定的 zip 檔案中
[SSZipArchive createZipFileAtPath:@"/users/pkxing/desktop/abc.zip" withFilesAtPaths:@[@"/users/pkxing/desktop/多贏商城.ipa"]];
    // 將指定的資料夾下所有的檔案打包到指定的zip 檔案中
[SSZipArchive createZipFileAtPath:@"/users/pkxing/desktop/abc.zip" withContentsOfDirectory:@"/users/pkxing/desktop/課件PPT模板"];

3、下載進度

  • 1、監聽如何監聽下載進度?

    • 通知/代理/block/KVO(監聽屬性值,極少用在這種情況)
      • 檢視是否可以使用有代理:進入標頭檔案, 從 interface 向上滾兩下檢視是否有對應的協議。
      • 檢視是否可以使用有通知:看標頭檔案底部。
      • 檢視是否可以使用有block:通常和方法在一起。
  • 2、要監聽session下載進度使用的是代理,此時不能使用'sharedSession'方法建立會話物件

    • 使用 sharedSession 獲得的物件是全域性的物件。
    • 多處地方呼叫獲得的物件都是同一個會話物件,而代理又是一對一的。
  • 3、如果發起的任務傳遞了completionHandler回撥,不會觸發代理方法。

- (void)download{
    // 1.建立下載 url
    NSURL *url = [NSURL URLWithString:@"http://localhost/1234.mp4"];
    NSLog(@"開始下載");
    // 2.開始下載
    /*
    [[self.session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
        NSLog(@"下載完成");
    }] resume];
     */
    [[self.session downloadTaskWithURL:url] resume];
}

#pragma mark - NSURLSessionDownloadDelegate
/**
 *  下載完畢回撥
 *
 *  @param session      會話物件
 *  @param downloadTask 下載任務物件
 *  @param location     下載路徑
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"location = %@",location);
}
/**
 *  接收到資料回撥
 *
 *  @param session                   會話物件
 *  @param downloadTask              下載任務物件
 *  @param bytesWritten              本次下載位元組數
 *  @param totalBytesWritten         已經下載位元組數
 *  @param totalBytesExpectedToWrite 總大小位元組數
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    // 計算進度
    CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;
    NSLog(@"progress = %f---%@",progress,[NSThread currentThread]);
}
/**
 *  續傳代理方法:沒有什麼用
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
    NSLog(@"---%s",__func__);
}

#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
    if (_session == nil) {
        // 建立會話配置物件
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        /**
         引數:
         configuration:會話配置,大多使用預設的。
         delegate:代理,一般是控制器
         delegateQueue:代理回撥的佇列,可以傳入 nil.
         */
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}

4、佇列的選擇

#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
    if (_session == nil) {
        // 建立會話配置物件
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}

引數:
configuration:會話配置,大多使用預設的。
delegate:代理,一般是控制器。
delegateQueue:代理回撥的佇列。
可以傳人 nil,傳人 nil 等價於[[NSOperationQueue alloc] init]。
傳人[NSOperationQueue mainQueue],表示代理方法在主佇列非同步執行。
如果代理方法中沒有耗時操作,則選擇主佇列,有耗時操作,則選擇非同步佇列。
下載本身是由一個獨立的執行緒完成。無論選擇什麼佇列,都不會影響主執行緒。

5、暫停和繼續01

/**
 *  開始下載
 */
- (IBAction)start{
    // 1.建立下載 url
    NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.0.3_setup.1435732931.dmg"];
    self.task = [self.session downloadTaskWithURL:url];
    [self.task resume];
}

/**
 *  暫停下載
 */
- (IBAction)pause{
    // 只有執行的任務才需要掛起
    if (self.task.state == NSURLSessionTaskStateRunning) {
        NSLog(@"pause = %@",self.task);
        [self.task suspend];
    }
}
/**
 *  繼續下載
 */
- (IBAction)resume{
    // 只有被掛起的任務才需要繼續
    if (self.task.state == NSURLSessionTaskStateSuspended) {
        NSLog(@"resume = %@",self.task);
        [self.task resume];
    }
}

#pragma mark - NSURLSessionDownloadDelegate
/**
 *  下載完畢回撥
 *
 *  @param session      會話物件
 *  @param downloadTask 下載任務物件
 *  @param location     下載路徑
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"location = %@",location);
}
/**
 *  接收到資料回撥
 *
 *  @param session                   會話物件
 *  @param downloadTask              下載任務物件
 *  @param bytesWritten              本次下載位元組數
 *  @param totalBytesWritten         已經下載位元組數
 *  @param totalBytesExpectedToWrite 總大小位元組數
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    // 計算進度
    CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;
    NSLog(@"progress = %f---%@",progress,[NSThread currentThread]);
    // 回到主執行緒更新進度條
    dispatch_async(dispatch_get_main_queue(), ^{
        self.progressView.progress = progress;
    });
}
/**
 *  續傳代理方法:沒有什麼用
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
    NSLog(@"---%s",__func__);
}

#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
    if (_session == nil) {
        // 建立會話配置物件
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        /**
         引數:
         configuration:會話配置,大多使用預設的。
         delegate:代理,一般是控制器
         delegateQueue:代理回撥的佇列,可以傳入 nil.
         */
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}


NSURLConnection和 NSURLSessionTask 對比
NSURLConnection 不能掛起,只能開始和取消,一旦取消,如果需要再次啟動,需要新建connection
NSURLSessionTask 可以掛起/繼續/取消/完成

6、暫停和繼續02

// 記錄續傳資料
@property(nonatomic,strong) NSData *resumeData;
/**
 *  開始下載
 */
- (IBAction)start{
    // 1.建立下載 url
    NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.0.3_setup.1435732931.dmg"];
    self.task = [self.session downloadTaskWithURL:url];
    [self.task resume];r
}

/**
 *  暫停下載
 */
- (IBAction)pause{
    // 如果任務已經被取消,不希望再次執行 block
// 在 oc中,可以給 nil 物件傳送任何訊息
    [self.task cancelByProducingResumeData:^(NSData *resumeData) {
        NSLog(@"length = %zd",resumeData.length);
        // 記錄續傳資料
        self.resumeData = resumeData;
        // 清空任務
        self.task = nil;
    }];
}

/**
 *  繼續下載
 */
- (IBAction)resume{
    // 如果沒有續傳資料
    if(self.resumeData == nil){
        NSLog(@"沒有續傳資料");
        return;
    }
    // 使用續傳資料開啟續傳下載
    self.task = [self.session downloadTaskWithResumeData:self.resumeData];
    // 清空續傳資料
    self.resumeData = nil;
    
    [self.task resume];
}

resumeData:
該引數包含了繼續下載檔案的位置資訊。也就是說,當你下載了10M得檔案資料,暫停了。那麼你下次繼續下載的時候是從第10M這個位置開始的,而不是從檔案最開始的位置開始下載。因而為了儲存這些資訊,所以才定義了這個NSData型別的這個屬性:resumeData

下載完成後,將檔案移動到指定的資料夾下面

#pragma mark - NSURLSessionDownloadDelegate
/**
 *  下載完畢回撥
 *
 *  @param session      會話物件
 *  @param downloadTask 下載任務物件
 *  @param location     下載路徑
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    // 獲得 cache 資料夾路徑
    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    // 獲得檔名
    NSString *filePath = [cache stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    // 將下載好的檔案移動到指定的資料夾
    [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:filePath error:NULL];
}

7、載入續傳資料

/**
 NSURLSession中,斷點續傳的關鍵點就在 resumeData
 1. 一旦取消任務,在resumeData 中會記錄住當前下載的資訊,格式是 plist 的
 2. 可以將續傳資料寫入磁碟
 3. 程式重新執行,從磁碟載入 resumeData,修改其中儲存的`臨時檔名`,因為每一次啟動 路徑會發生變化
 4. 使用 resumeData 開啟一個續傳任務!
 */
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.2.dmg"];
    self.url = url;
    
    // 判斷沙盒中是否有快取資料?如果有,載入快取資料
    self.resumeData = [self loadResumeData:url];
    
    if (self.resumeData != nil) {
        // 使用快取資料新建下載任務
        self.task = [self.session downloadTaskWithResumeData:self.resumeData];
    } else {
        // 如果沒有,直接下載
        self.task = [self.session downloadTaskWithURL:url];
    }
    
    [self.task resume];
}

// 根據 URL 載入沙盒中的快取資料
/**
 如果程式再次執行,NSHomeDirectory會發生變化!在iOS 8.0才會有!
 
 需要解決的,將快取資料中的路徑名修改成正確的
 */
- (NSData *)loadResumeData:(NSURL *)url {
    // 1. 判斷檔案是否存在
    NSString *filePath = [self resumeDataPath:url];
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        // 以字典的方式載入續傳資料
        NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
        
        // 1. 取出儲存的 - 臨時檔案的目錄
        // 臨時目錄/CFNetworkDownload_p78VgR.tmp
        NSString *localPath = dict[@"NSURLSessionResumeInfoLocalPath"];
        NSString *fileName = localPath.lastPathComponent;
        // 計算得到正確的臨時檔案目錄
        localPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
        
        // 重新設定字典的鍵值
        dict[@"NSURLSessionResumeInfoLocalPath"] = localPath;
        
        // 字典轉二進位制資料,序列化(Plist的序列化)
        return [NSPropertyListSerialization dataWithPropertyList:dict format:NSPropertyListXMLFormat_v1_0 options:0 error:NULL];
    }
    
    return nil;
}

/**
 *  儲存續傳資料的檔案路徑
 
 儲存到`臨時資料夾`中 - 儲存成 "url.字串的 md5.~resume"
 */
- (NSString *)resumeDataPath:(NSURL *)url {
    // 1. 取得 md5 的字串
    NSString *fileName = url.absoluteString.md5String;
    // 2. 拼接臨時資料夾
    NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
    // 3. 拼接副檔名,避免衝突
    path = [path stringByAppendingPathExtension:@"~resume"];
    
    NSLog(@"續傳資料檔案路徑 %@", path);
    
    return path;
}


// 暫停
- (IBAction)pause {
    NSLog(@"暫停");
    // 取消下載任務 可以給 nil 傳送任何訊息,不會有任何不良反應
    [self.task cancelByProducingResumeData:^(NSData *resumeData) {
        NSLog(@"續傳資料長度 %tu", resumeData.length);
        
        // 將續傳資料寫入磁碟
        [resumeData writeToFile:[self resumeDataPath:self.url] atomically:YES];
        
        // 記錄續傳資料
        self.resumeData = resumeData;
        
        // 釋放任務
        self.task = nil;
    }];
}

// 繼續
- (IBAction)resume {
    NSLog(@"繼續");
    if (self.resumeData == nil) {
        NSLog(@"沒有續傳資料");
        return;
    }
    self.task = [self.session downloadTaskWithResumeData:self.resumeData];
    // 釋放續傳資料
    self.resumeData = nil;
    // 繼續任務
    [self.task resume];
}

三、WebDav

WebDav伺服器是基於 Apache 的,使用的是 HTTP 協議,可以當作網路檔案伺服器使用。上傳檔案的大小沒有限制。

1、WebDav 的配置

WebDav完全可以當成一個網路共享的檔案伺服器使用!

# 切換目錄
$ cd /etc/apache2
$ sudo vim httpd.conf
# 查詢httpd-dav.conf
/httpd-dav.conf
"刪除行首#"
# 將游標定位到行首
0
# 刪除行首的註釋
x
# 儲存退出
:wq
# 切換目錄
$ cd /etc/apache2/extra
# 備份檔案(只要備份一次就行)
$ sudo cp httpd-dav.conf httpd-dav.conf.bak
# 編輯配置檔案
$ sudo vim httpd-dav.conf
"將Digest修改為Basic"
# 查詢Digest
/Digest
# 進入編輯模式
i
# 返回到命令列模式
如果MAC系統是10.11,則需要按下圖修改對應的路徑。
2229471-7deee8e99c09214b.png
ESC
# 儲存退出
:wq
# 切換目錄,可以使用滑鼠拖拽的方式
$ cd 儲存put指令碼的目錄
# 以管理員許可權執行put配置指令碼
$ sudo ./put

設定兩次密碼: 123456
如果MAC系統是10.11,則會在使用者根目錄下生成三個檔案,如下圖:
2229471-9a762245b822e89c.png

注意:要在Mac 10.10以上配置Web-dav還需要在httpd.conf中開啟以下三個模組

LoadModule dav_module libexec/apache2/mod_dav.so
LoadModule dav_fs_module libexec/apache2/mod_dav_fs.so
LoadModule auth_digest_module libexec/apache2/mod_auth_digest.so

2、WebDav 上傳檔案(PUT)

先上傳圖片,再換成大檔案(視訊),不修改上傳的路徑,讓後面上傳的大檔案覆蓋之前的圖片。
- (void)webDavUpload{
    // 1.URL -- 要上傳檔案的完整網路路徑,包括檔名
    NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
    // 2.建立請求物件
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 2.1 設定請求方法
    request.HTTPMethod = @"PUT";
    // 2.2 設定身份驗證
    [request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
    // 3.上傳
    NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"001.png" withExtension:nil];
    [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromFile:fileUrl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
    }] resume];
}

/**
 *  獲得授權字元字串
 */
- (NSString *)authString{
    NSString *str = @"admin:123456";
    return [@"BASIC " stringByAppendingString:[self base64:str]];
}

/**
 *  將字串進行 base64編碼,返回編碼後的字串
 */
-(NSString *)base64:(NSString *)string{
    // 轉換成二進位制資料
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    // 返回 base64編碼後的字串
    return [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
}

狀態碼
401 Unauthorized:沒有許可權。需要身份驗證
201  新增檔案,建立成功。
204  沒有內容,檔案覆蓋成功,伺服器不知道該告訴我們什麼,所以沒有內容返回。
授權的字串格式
BASIC (admin:123456).base64。其中admin是使用者名稱,123456是密碼。

3、WebDav 上傳進度跟進

/**
 *  獲得授權字元字串
 */
- (NSString *)authString{
    NSString *str = @"admin:123456";
    return [@"BASIC " stringByAppendingString:[self base64:str]];
}

/**
 *  將字串進行 base64編碼,返回編碼後的字串
 */
-(NSString *)base64:(NSString *)string{
    // 轉換成二進位制資料
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    // 返回 base64編碼後的字串
    return [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
}

#pragma mark - 上傳操作
- (void)webDavUpload{
    // 1.URL -- 要上傳檔案的完整網路路徑,包括檔名
    NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
    // 2.建立請求物件
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 2.1 設定請求方法
    request.HTTPMethod = @"PUT";
    // 2.2 設定身份驗證
    [request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
    // 3.上傳
    NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"18-POST上傳檔案演練.mp4" withExtension:nil];
    [[self.session uploadTaskWithRequest:request fromFile:fileUrl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
    }] resume];
}

#pragma mark - NSURLSessionTaskDelegate 代理方法
/**
 *  上傳進度的跟進,只要實現這個方法就可以了
 *  @param bytesSent                本次上傳位元組數
 *  @param totalBytesSent           已經上傳的總位元組數
 *  @param totalBytesExpectedToSend 要上傳檔案的總大小
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    CGFloat progress = (CGFloat)totalBytesSent / totalBytesExpectedToSend;
    NSLog(@"progress = %f",progress);
}

#pragma mark - 懶載入會話
- (NSURLSession *)session {
    if (_session == nil) {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}

4、WebDav 刪除檔案(Delete)

- (void)webDavDelete{
    // 1.URL -- 指定要刪除檔案的 url
    NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.mp4"];
    // 2.建立請求物件
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 2.1 設定請求方法
    request.HTTPMethod = @"DELETE";
    // 2.2 設定身份驗證
    [request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
    // 3.刪除
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
         NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
    }] resume];
}

狀態碼
401 Unauthorized:沒有許可權。需要身份驗證
404   Not Found 檔案不存在
204   刪除成功

5、WebDav GET/HEAD檔案

- (void)webDavGet{
    NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
    [[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        // 將返回的資料寫到檔案
        [data writeToFile:@"/users/pkxing/desktop/abc.mp4" atomically:YES];
        NSLog(@"%@===",response);
    }] resume];
}

/**
 GET & HEAD 都不需要身份驗證,只是獲取資源,不會破壞資源!
 
 PUT & DELETE 會修改伺服器資源,需要有許可權的人執行,需要身份驗證
 */
- (void)webDavHead {
    // 1. URL,要刪除的檔案網路路徑
    NSURL *url = [NSURL URLWithString:@"http://192.168.40.2/uploads/321.png"];
    
    // 2. request
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 2.1 HTTP方法
    request.HTTPMethod = @"HEAD";
    
    // 3. session
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        
        [data writeToFile:@"/Users/apple/Desktop/aa.mp4" atomically:YES];
        
        // *** 不要只跟蹤 data,否則會以為什麼也沒有發生
        NSLog(@"%@ | %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], response);
    }] resume];
}

6、WebDav總結(PUT/DELETE/GET/HEAD)

  • 需要許可權

    • DELETE:刪除資源
    • PUT:新增或修改資源
  • 不需要許可權

    • WebDav的'GET/HEAD' 請求不需要身份驗證,因為對伺服器的資源沒有任何的破壞。
  • 不支援POST上傳,POST方法通常需要指令碼的支援,提交給伺服器的是二進位制資料,同時告訴伺服器資料型別。

四、NSURLSession注意點

The session object keeps a strong reference to the delegate until your app explicitly invalidates the session. If you do not invalidate the session by calling the invalidateAndCancel or resetWithCompletionHandler: method, your app leaks memory.

1、 在什麼時候取消網路會話?
方法一:在viewWillDisappear 取消網路會話
- (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        // 取消網路會話
        [self.session invalidateAndCancel];
        self.session = nil;
}

方法二:在每一個任務完成後,取消會話,不推薦
[self.session finishTasksAndInvalidate];
self.session = nil;
完成任務並取消會話(會話一旦取消就無法再建立任務)
會造成 session 頻繁的銷燬&建立
Attempted to create a task in a session that has been invalidated
錯誤原因:嘗試在一個被取消的會話中建立一個任務。

方法三:建立一個‘網路管理單列’,單獨負責所有的網路訪問操作。

五、NSURLSession--POST上傳

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"001.png" ofType:nil];
    NSDictionary *fileDict = @{@"other.png":[NSData dataWithContentsOfFile:filePath]};
    // 資料引數
    NSDictionary *params = @{@"username":@"zhang"};
    [self upload:fileDict fieldName:@"userfile[]" params:params];
}

// 分割符
#define boundary @"itheima"
#pragma mark - 上傳操作
- (void)upload:(NSDictionary *)fileDict fieldName:(NSString *)fieldName params:(NSDictionary *)params{
    // 1.URL -- 負責上傳檔案的指令碼
    NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/post/upload-m.php"];
    // 2.建立請求物件
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 2.1 設定請求方法
    request.HTTPMethod = @"POST";
    // 設定Content-Type
    //Content-Type multipart/form-data;boundary=傳值NB
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
    [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    
    // 不需要設定請求體 request.HTTPBody
    // 獲得檔案資料
    // session 上傳的資料 通過 fromData 指定
    NSData *fromData = [self fileData:fileDict fieldName:fieldName params:params];
   [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:fromData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
       NSLog(@"%@--%@ -- %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL],response,error);
   }] resume];
}

/**
 *  返回要上傳檔案的檔案二進位制資料
 */
- (NSData *)fileData:(NSDictionary *)fileDict fieldName:(NSString *)fieldName params:(NSDictionary *)params{
    NSMutableData *dataM = [NSMutableData data];
    // 遍歷檔案字典 ---> 檔案資料
    [fileDict enumerateKeysAndObjectsUsingBlock:^(NSString *fileName, NSData *fileData, BOOL *stop) {
        NSMutableString *strM = [NSMutableString string];
        [strM appendFormat:@"--%@\r\n",boundary];
        [strM appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",fieldName,fileName];
        [strM appendString:@"Content-Type: application/octet-stream \r\n\r\n"];
        // 插入 strM
        [dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
        // 插入 檔案二進位制資料
        [dataM appendData:fileData];
        // 插入 \r\n
        [dataM appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    }];
    
    // 遍歷普通引數
    [params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        NSMutableString *strM = [NSMutableString string];
        [strM appendFormat:@"--%@\r\n",boundary];
        [strM appendFormat:@"Content-Disposition: form-data; name=\"%@\" \r\n\r\n",key];
        [strM appendFormat:@"%@",obj];
        // 插入 普通引數
        [dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
    }];
    
    // 插入 結束標記
    NSString *tail = [NSString stringWithFormat:@"\r\n--%@--",boundary];
    [dataM appendData:[tail dataUsingEncoding:NSUTF8StringEncoding]];
    
    return dataM;
}

六、HTTPS

1、HTTPS 原理

Https是基於安全目的的Http通道,其安全基礎由SSL層來保證。最初由netscape公司研發,主要提供了通訊雙方的身份認證和加密通訊方法。現在廣泛應用於網際網路上安全敏感通訊。

2229471-5777d584702d643d.png
  • Https與Http主要區別

    • 協議基礎不同:Https在Http下加入了SSL層,
    • 通訊方式不同:Https在資料通訊之前需要客戶端、伺服器進行握手(身份認證),建立連線後,傳輸資料經過加密,通訊埠443。Http傳輸資料不加密,明文,通訊埠80。
  • SSL協議基礎

    • SSL協議位於TCP/IP協議與各種應用層協議之間,本身又分為兩層:
      • SSL記錄協議(SSL Record Protocol):建立在可靠傳輸層協議(TCP)之上,為上層協議提供資料封裝、壓縮、加密等基本功能。
      • SSL握手協議(SSL Handshake Procotol):在SSL記錄協議之上,用於實際資料傳輸前,通訊雙方進行身份認證、協商加密演算法、交換加密金鑰等。
  • SSL協議通訊過程
    (1) 瀏覽器傳送一個連線請求給伺服器,伺服器將自己的證照(包含伺服器公鑰S_PuKey)、對稱加密演算法種類及其他相關資訊返回客戶端;
    (2) 客戶端瀏覽器檢查伺服器傳送到CA證照是否由自己信賴的CA中心簽發。若是,執行4步;否則,給客戶一個警告資訊:詢問是否繼續訪問。
    (3) 客戶端瀏覽器比較證照裡的資訊,如證照有效期、伺服器域名和公鑰S_PK,與伺服器傳回的資訊是否一致,如果一致,則瀏覽器完成對伺服器的身份認證。
    (4) 伺服器要求客戶端傳送客戶端證照(包含客戶端公鑰C_PuKey)、支援的對稱加密方案及其他相關資訊。收到後,伺服器進行相同的身份認證,若沒有通過驗證,則拒絕連線;
    (5) 伺服器根據客戶端瀏覽器傳送到密碼種類,選擇一種加密程度最高的方案,用客戶端公鑰C_PuKey加密後通知到瀏覽器;
    (6) 客戶端通過私鑰C_PrKey解密後,得知伺服器選擇的加密方案,並選擇一個通話金鑰key,接著用伺服器公鑰S_PuKey加密後傳送給伺服器;
    (7) 伺服器接收到的瀏覽器傳送到訊息,用私鑰S_PrKey解密,獲得通話金鑰key。
    (8) 接下來的資料傳輸都使用該對稱金鑰key進行加密。

上面所述的是雙向認證 SSL 協議的具體通訊過程,伺服器和使用者雙方必須都有證照。由此可見,SSL協議是通過非對稱金鑰機制保證雙方身份認證,並完成建立連線,在實際資料通訊時通過對稱金鑰機制保障資料安全性

2229471-f76aafad28aa92df.png

2、NSURLSession--HTTPS

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 建立 url
    NSURL *url = [NSURL URLWithString:@"https://mail.itcast.cn"];
    // 發起請求
    [[self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSLog(@"data = %@,response = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response);
    }] resume];
}

NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9843)
錯誤原因:沒有信任證照
如何信任證照?
通過代理方法告訴伺服器信任證照

#pragma mark - NSURLSessionTaskDelegate 代理方法
// 收到伺服器發過來的證照後回撥
//  提示:此代理方法中的程式碼是固定的,只要會 cmd+c / cmd + v 就可以了
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
    NSLog(@"protectionSpace - %@",challenge.protectionSpace);
    // 判斷是否是信任伺服器證照
    if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
        // 使用受保護空間的伺服器信任建立憑據
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        // 通過 completionHandler 告訴伺服器信任證照
        completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
}

引數介紹
challenge         '挑戰' 安全質詢,詢問是否信任證照
completionHandler  對證照處置的回撥
       - NSURLSessionAuthChallengeDisposition: 通過該引數告訴伺服器如何處置證照
            NSURLSessionAuthChallengeUseCredential = 0,  使用指定的憑據,即信任伺服器證照
            NSURLSessionAuthChallengePerformDefaultHandling = 1, 預設處理,忽略憑據。
            NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, 整個請求取消,忽略憑據
            NSURLSessionAuthChallengeRejectProtectionSpace = 3, 本次拒絕,下次再試
       - NSURLCredential


#pragma mark - 懶載入 session
- (NSURLSession *)session {
    if (_session == nil) {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}

3、NSURLConnection--HTTPS

 (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 建立 url 物件
    NSURL *url = [NSURL URLWithString:@"https://mail.itcast.cn"];
    // 建立請求物件
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    // 傳送請求
   [NSURLConnection connectionWithRequest:request delegate:self];
}

#pragma mark - NSURLConnection 代理方法
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
    NSLog(@"change = %@",challenge.protectionSpace);
    // 判斷是否是伺服器信任證照
    if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
        // 建立憑據
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        // 傳送信任告訴伺服器信任證照
        [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
    }
}

/**
 *   接收到伺服器響應
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    // 清空資料
    [self.data setData:nil];
}

/**
 *  接收到伺服器返回的資料呼叫
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // 拼接資料
    [self.data appendData:data];
}

/**
 *  請求完畢呼叫
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"self.data = %@",self.data);
}


- (NSMutableData *)data {
    if (_data == nil) {
        _data = [[NSMutableData alloc] init];
    }
    return _data;
}

七、AFNetworking

1、AFN介紹

  • AFN
    • 目前國內開發網路應用使用最多的第三方框架
    • 是專為 MAC OS & iOS 設計的一套網路框架
    • 對 NSURLConnection 和 NSURLSession 做了封裝
    • 提供有豐富的 API
    • 提供了完善的錯誤解決方案
    • 使用非常簡單

2、AFN演練

2.1、AFN--Get/Post
1、GET請求
- (void)get{
    // 建立請求管理器
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    
    // 傳送請求
    [manager GET:@"http://192.168.1.105/demo.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"%@---%@",responseObject,[responseObject class]);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error = %@",error);
    }];
}

2、Get請求
- (void)getLogin01{
    // 建立請求管理器
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    
    // 傳送登入請求
    [manager GET:@"http://192.168.1.105/login.php?username=zhangsan&password=zhang" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"%@---%@",responseObject,[responseObject class]);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error = %@",error);
    }];
}

3、Get請求
- (void)getLogin02{
    // 建立請求管理器
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    
    // 封裝請求引數
    NSDictionary *params = @{@"username":@"張三",@"password":@"zhang"};
    // 傳送登入請求
    [manager GET:@"http://192.168.1.105/login.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"%@---%@",responseObject,[responseObject class]);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error = %@",error);
    }];
}


4、POST請求
- (void)postLogin{
    // 建立請求管理器
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    // 封裝請求引數
    NSDictionary *params = @{@"username":@"zhangsan",@"password":@"zhang"};
    // 傳送登入請求
    [manager POST:@"http://192.168.1.105/login.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"%@---%@",responseObject,[responseObject class]);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error = %@",error);
    }];
}

5、AFN的好處
沒有了 URL 的概念
完成回撥的結果已經做好了序列化
完成回撥在主執行緒,不需要考慮執行緒間的通訊
GET請求的引數可以使用字典封裝,不需要再記住 URL 的拼接格式
不需要新增百分號轉義,中文,特殊字元(空格,&)等。OC增加百分轉義的方法不能轉義所有特殊字元,AFN處理的很好。
POST請求不用設定HTTPMethod/HTTPBody

2.2、AFN--SAX解析

- (void)xml{
    // 建立請求管理器
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    // 設定響應解析器為 xml 解析器
    manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
    // 傳送登入請求
    [manager GET:@"http://192.168.1.105/videos.xml" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"%@---%@",responseObject,[responseObject class]);
        [HMSAXVideo saxParser:responseObject finished:^(NSArray *data) {
            NSLog(@"%@",data);
        }]
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error = %@",error);
    }];
}

2.3、AFN檔案上傳

- (void)postUpload {
    AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
    
    // 上傳
    NSDictionary *params = @{@"username": @"da xiagua"};
    [mgr POST:@"http://localhost/upload/upload-m.php" parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        
        /**
         引數
         1. 本地檔案 URL
         2. name: 負責上傳檔案的欄位名,諮詢公司的後端程式設計師,或者有文件
         3. error
         */
        NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"04.jpg" withExtension:nil];
        [formData appendPartWithFileURL:fileURL name:@"userfile[]" error:NULL];
        
        // 上傳多個檔案
        /**
         引數
         1. 本地檔案 URL
         2. name: 負責上傳檔案的欄位名,諮詢公司的後端程式設計師,或者有文件
         3. fileName: 儲存在伺服器的檔名
         4. mimeType: 告訴伺服器上傳檔案的型別1
         5. error
         */
        NSURL *fileURL2 = [[NSBundle mainBundle] URLForResource:@"AppIcon.jpg" withExtension:nil];
        [formData appendPartWithFileURL:fileURL2 name:@"userfile[]" fileName:@"001.jpg" mimeType:@"application/octet-stream" error:NULL];
    } success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"%@", responseObject);
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"%@", error);
    }];
}

2.4、AFN 檔案下載

- (void)download{
    // 1. url
    NSURL *url = [NSURL URLWithString:@"http://localhost/123.mp4"];
    
    // 2. AFN 上傳
    AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
    
    // 3. request
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 4. 開始下載任務
    // *** 學習第三方框架的好處:可以發現自己的知識空缺點,跟大牛直接學習!
    // iOS 7.0 之後推出的,專門用於跟蹤進度的類,可以跟蹤`進度樹`
    NSProgress *progress = nil;
    [[mgr downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
        
        NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
        NSLog(@"file = %@",targetPath);
        return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
        
    } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
        NSLog(@"response = %@,filePath = %@",response,filePath);
    }] resume];
    // 此處已經獲得進度物件 - `監聽`進度
    NSLog(@"%@", progress);
    
    // KVO監聽進度
    [progress addObserver:self forKeyPath:@"completedUnitCount" options:0 context:nil];
}

// 是所有 KVO 統一呼叫的一個方法,最好判斷一下物件型別
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    //    NSLog(@"%@", object);
    // 判斷物件是否是 NSProgress 物件
    if ([object isKindOfClass:[NSProgress class]]) {
        NSProgress *p = object;
        
        NSLog(@"%lld - %lld", p.completedUnitCount, p.totalUnitCount);
        NSLog(@"%@ - %@", p.localizedDescription, p.localizedAdditionalDescription);
        // *** 顯示百分比
        NSLog(@"%f", p.fractionCompleted);
    }
}