美文网首页
06-NSURLSession

06-NSURLSession

作者: AlanGe | 来源:发表于2017-08-28 00:37 被阅读39次

    一、NSURLSession

    1、简介

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

    结构图

    • 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,则需要按下图修改对应的路径。
    
    ESC
    # 保存退出
    :wq
    # 切换目录,可以使用鼠标拖拽的方式
    $ cd 保存put脚本的目录
    # 以管理员权限运行put配置脚本
    $ sudo ./put
    
    设置两次密码: 123456
    如果MAC系统是10.11,则会在用户根目录下生成三个文件,如下图:
    

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

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

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

    相关文章

      网友评论

          本文标题:06-NSURLSession

          本文链接:https://www.haomeiwen.com/subject/dufwdxtx.html