06-NSURLSession

一、NSURLSession

1、简介

  • iOS7.0 推出,用于替代 NSURLConnection。
  • 支持后台运行的网络任务。
  • 暂停,停止,重启网络任务,不需要NSOperation 封装。 > 请求可以使用同样的 配置容器。
  • 不同的 session 可以使用不同的私有存储。
  • block 和代理可以同时起作用。
  • 直接从文件系统上传,下载。

结构图

06-NSURLSession_第1张图片
  • 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,则需要按下图修改对应的路径。
06-NSURLSession_第2张图片
ESC
# 保存退出
:wq
# 切换目录,可以使用鼠标拖拽的方式
$ cd 保存put脚本的目录
# 以管理员权限运行put配置脚本
$ sudo ./put

设置两次密码: 123456
如果MAC系统是10.11,则会在用户根目录下生成三个文件,如下图:
06-NSURLSession_第3张图片

注意:要在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公司研发,主要提供了通讯双方的身份认证和加密通信方法。现在广泛应用于互联网上安全敏感通讯。

06-NSURLSession_第4张图片
  • 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协议是通过非对称密钥机制保证双方身份认证,并完成建立连接,在实际数据通信时通过对称密钥机制保障数据安全性

06-NSURLSession_第5张图片

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
    • 提供了完善的错误解决方案
    • 使用非常简单
  • 官网地址
    *https://github.com/AFNetworking/AFNetworking

  • 学习第三方框架的步骤

  • 获取框架

    • 克隆代码 $ git clone https://github.com/AFNetworking/AFNetworking.git
    • 更新代码 $ git pull
  • 运行演示程序

  • 编写测试程序

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 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);
    }
}

你可能感兴趣的:(06-NSURLSession)