IOS基础:网络请求(下)

2020-10-22  本文已影响0人  时光啊混蛋_97boy

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

续文见上篇 IOS基础:网络请求(上)

四、断点续传和后台下载(基于AFNetworking)

1、简介

如果是小文件的下载,比如图片和文字之类的,我们可以直接请求源地址,然后一次下载完毕。但是如果是下载较大的音频和视频文件,不可能一次下载完毕,用户可能下载一段时间,关闭程序,回家接着下载。这个时候,就需要实现断点续传的功能。让用户可以随时暂停下载,下次开始下载,还能接着上次的下载的进度。

在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这也需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载以后未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。

使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。

断点续传的过程如下:

  1. 断点续传需要在下载过程中记录每条线程的下载进度;
  2. 每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库;
  3. 在每次向文件中写入数据之后,在数据库中更新下载进度;
  4. 下载完成之后删除数据库中下载记录。

如何自己简单的封装一个断点续传的类,实现如下功能:

2、原理介绍

a、服务器支持断点续传功能

如果使用断点续传,不仅仅是客户端的工作,还需要服务器支持断点续传功能,否则无法生成正确的resumeData。因为要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。

如何验证服务器是否支持断点续传呢? 可以通过下载文件的时候的响应头来查看,只有满足以下条件,才能恢复下载:

一个最简单的断点续传实现大概如下:

b、下载请求

AFURLSessionManager开始一个下载的时候,有两个方法,分别是开始下载(重头下载),继续下载(需要传入一个resumeData)。

- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                             destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;

当我们需要下载的时候,主动去调用downloadTaskWithRequest方法,根据下载地址url,去判断本地是否有resumeData,这个类似SDWebImage的实现,我们自己去维护这个对应关系,可以存到本地一个plist文件里面,键值对对应url:resumeData

开始下载的时候,去判断当前是否存在这个url:resumeData,存在的话看一下长度是否大于零(当用户开始下载的时候,如果什么都没下载到,就中断下载。此时resumeDatanil,如果直接写入plist是会崩溃的,可以写一个NSString对象,虽然不是一个Data类型,但是length都可以使用,不会有太大影响。downloadTaskWithRequest去开辟新任务。

    // 开始下载的时候,去判断当前是否存在这个url:resumeData
    if (!data)
    {
        // 当用户开始下载的时候,如果什么都没下载到,就会中断下载
        // 此时resumeData是nil,如果直接写入plist是会崩溃的
        // 可以写一个NSString对象,虽然不是一个Data类型,但是length都可以使用,不会有太大影响
        NSString *emptyData = [NSString stringWithFormat:@""];
        [self.downLoadHistoryDictionary setObject:emptyData forKey:key];
    }

length大于零就使用resumeData调用downloadTaskWithResumeData这个方法,去继续我们的下载任务,否则调用:

- (NSURLSessionDownloadTask *)AFDownLoadFileWithUrl:(NSString*)urlHost
                                           progress:(DowningProgress)progress
                                       fileLocalUrl:(NSURL *)localUrl
                                            success:(DonwLoadSuccessBlock)success
                                            failure:(DownLoadfailBlock)failure{
    
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlHost]];
    NSURLSessionDownloadTask   *downloadTask = nil;
    NSData *downLoadHistoryData = [self.downLoadHistoryDictionary   objectForKey:urlHost];

    if (downLoadHistoryData.length > 0 ) 
    {
        NSLog(@"使用旧任务");
        downloadTask = [self.manager downloadTaskWithResumeData:downLoadHistoryData progress:^(NSProgress * _Nonnull downloadProgress) {
        ....
    } 
    else 
    {
        NSLog(@"开辟 新任务");
        downloadTask = [self.manager    downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
        ....
    }
    // 启动下载任务
    [downloadTask resume];
    return downloadTask;
}

下载进度和成功、失败的回调:

typedef void (^DonwLoadSuccessBlock)(NSURL* fileUrlPath ,NSURLResponse* response);
typedef void (^DownLoadfailBlock)(NSError* error ,NSInteger statusCode);
typedef void (^DowningProgress)(CGFloat progress);

- (NSURLSessionDownloadTask *)AFDownLoadFileWithUrl:(NSString *)urlHost
                                           progress:(DowningProgress)progress
                                       fileLocalUrl:(NSURL *)localUrl
                                            success:(DonwLoadSuccessBlock)success
                                            failure:(DownLoadfailBlock)failure
{
    ...    
    if (downLoadHistoryData.length > 0)
    {
        // 其length大于零就使用resumeData调用downloadTaskWithResumeData这个方法,去继续我们的下载任务
        NSLog(@"继续旧任务");
        downloadTask = [self.manager downloadTaskWithResumeData:downLoadHistoryData progress:^(NSProgress * _Nonnull downloadProgress) {
            // typedef void (^DowningProgress)(CGFloat progress);
            // 调用下载进度block
            if (progress)
            {
                progress(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
                NSLog(@"下载进度 %F",(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount));
            }
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            // 返回本地存储路径
            return localUrl;
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            NSLog(@"AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块");
            
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            
            // 特殊处理404,删除垃圾文件
            if (httpResponse.statusCode == 404)
            {
                [[NSFileManager defaultManager] removeItemAtURL:filePath error:nil];
            }
            
            if (error)
            {
                // typedef void (^DownLoadfailBlock)(NSError* error ,NSInteger statusCode);
                // 调用下载失败block
                if (failure)
                {
                    //将下载失败存储起来,提交到下面的的网络监管类里面
                    failure(error, httpResponse.statusCode);
                }
            }
            else
            {
                // typedef void (^DonwLoadSuccessBlock)(NSURL* fileUrlPath ,NSURLResponse* response);
                // 调用下载成功block
                if (success)
                {
                    //将下载成功存储起来,提交到下面的的网络监管类里面
                    success(filePath, response);
                }
            }
        }];
    }     
    ...
}


- (IBAction)startNew:(id)sender
{
    // 正处于下载之中则直接返回
    if (self.isDownLoading)
    {
        return;
    }
    
    self.isDownLoading = YES;
    // 根据封装的断点续传工具类创建下载任务
    NSURLSessionDownloadTask *tempTask = [[NetworkServerDownLoadTool sharedTool] AFDownLoadFileWithUrl:self.downLoadUrl progress:^(CGFloat progress) {
        // 回到主线程刷新显示进度
        dispatch_async(dispatch_get_main_queue(), ^{
            self.Progress.progress = progress;
            NSLog(@"当前的进度 = %f",progress);
            self.progressLabel.text = [NSString stringWithFormat:@"进度:%.3f",progress];
        });
    } fileLocalUrl:self.fileUrl success:^(NSURL *fileUrlPath, NSURLResponse *response) {
        NSLog(@"下载成功 下载的文档路径是 %@, ",fileUrlPath);
    } failure:^(NSError *error, NSInteger statusCode) {
        NSLog(@"下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载");
    }];
    self.downloadTask = tempTask;
}
c、本地缓存

问题:在控制器中开始下载功能,可以获取当前下载进度。当我退出这个页面再次进入的时候,我如何自动获取最新的下载进度?

解答:将下载功能交给一个单例管理类管理,下载的功能由他控制,进入下载页面的时候,读取他的信息,然后显示UI,开始和暂停以及完成下载由他管理操作,再次进入也是如此。存储最新数据的方式是把这些任务task放到容器(数组或者字典)里面存起来,等进来的时候,再来取了。

单例:

// 获取到网络请求单例对象
// 将下载功能交给一个单例管理类管理,下载的功能由他控制,再次进入下载页面的时候,读取他的信息,然后显示UI
static NetworkServerDownLoadTool* tool = nil;
+ (instancetype)sharedTool
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool =  [[self alloc] init];
    });
    return tool;
}

存到本地文件的路径:

        // 文件本地存储路径
        NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        self.fileHistoryPath = [path stringByAppendingPathComponent:@"fileDownLoadHistory.plist"];
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.fileHistoryPath])// 文件已经存在
        {
            // 用之前存在的文件来初始化downLoadHistoryDictionary,即是之前的下载历史记录
            self.downLoadHistoryDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:self.fileHistoryPath];
        }
        else
        {
            // 新创建downLoadHistoryDictionary
            self.downLoadHistoryDictionary = [NSMutableDictionary dictionary];
            // 将dictionary中的数据写入plist文件中,此时也新建了plist文件
            [self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:YES];
        }
        NSLog(@"处于%s方法中,self.manager.session = %@ 支持后台下载/上传",__func__,self.manager.session);

存到本地的方法:

// 将key和resumeData存到本地,用作下载续传时候用到的值
- (void)saveHistoryWithKey:(NSString *)key DownloadTaskResumeData:(NSData *)data
{
    // 开始下载的时候,去判断当前是否存在这个url:resumeData
    if (!data)
    {
        // 当用户开始下载的时候,如果什么都没下载到,就会中断下载
        // 此时resumeData是nil,如果直接写入plist是会崩溃的
        // 可以写一个NSString对象,虽然不是一个Data类型,但是length都可以使用,不会有太大影响
        NSString *emptyData = [NSString stringWithFormat:@""];
        [self.downLoadHistoryDictionary setObject:emptyData forKey:key];
    }
    else
    {
        // 有数据就直接写入即可
        [self.downLoadHistoryDictionary setObject:data forKey:key];
    }
    
    // 将下载好的部分存储到本地
    [self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:NO];
    NSLog(@"文件路径%@",self.fileHistoryPath);
}

取出来用:

            // 判断是否有错误,有错误说明下载没有完成就被中断了
            if (error)
            {
                if (error.code == -1001)
                {
                    NSLog(@"下载出错,请查看一下网络是否正常");
                }
                
                // 可以通过key:NSURLSessionDownloadTaskResumeData去获取续传时候的data,因为AF源码中将其存储进入了这个key中
                NSData *resumeData = [error.userInfo objectForKey:@"NSURLSessionDownloadTaskResumeData"];
                
                // 当我退出这个页面再次进入的时候,如何自动获取最新的下载进度?
                // 将urlHost和resumeData存到本地,用作下载续传时候用到的值
                [weakSelf saveHistoryWithKey:urlHost DownloadTaskResumeData:resumeData];
            }

下载完成后删除:

            else
            {
                NSLog(@"下载全部完成才会进入这里");
                // 判断当前是否存在这个urlHost:resumeData,存在的话看一下长度是否大于零
                if ([weakSelf.downLoadHistoryDictionary valueForKey:urlHost])
                {
                    // 下载完成移除存储容器中的内容
                    [weakSelf.downLoadHistoryDictionary removeObjectForKey:urlHost];
                    // AF在下载的时候(其实是调用的系统的下载方法),会将文件先下载到沙盒目录下的temp文件夹中,生成一个后缀为tmp的文件
                    // 下载完的时候,系统会将tmp文件删除掉释放占用的内存,然后移动文件到我们下载时设置的路径下,这样下载任务就完成了
                    [weakSelf.downLoadHistoryDictionary writeToFile:weakSelf.fileHistoryPath atomically:YES];
                }
            }
d、暂停任务

暂停具体某个下载任务:

// 暂停下载任务
- (IBAction)pause:(id)sender
{
    // 可以在这里存储resumeData ,也可以去NetworkServerDownLoadTool里面,根据那个通知去处理,都会回调
    if (self.isDownLoading)
    {
        [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
            NSLog(@"已经下载好的data = %@", resumeData);
        }];
    }
    self.isDownLoading = NO;
}

暂停所有下载任务,能够获取到每个任务的resumeData

- (IBAction)cancel:(id)sender
{
    if (self.isDownLoading)
    {
        NSLog(@"暂停所有下载任务,能够获取到每个任务的resumeData");
        [[NetworkServerDownLoadTool sharedTool] stopAllDownLoadTasks];
    }
    self.isDownLoading = NO;
}

// 可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法
- (void)stopAllDownLoadTasks
{
    if ([[self.manager downloadTasks] count] == 0)
    {
        return;
    }
    
    //停止所有的下载
    for (NSURLSessionDownloadTask *task in  [self.manager downloadTasks])
    {
        // 处于允许状态中的下载任务
        if (task.state == NSURLSessionTaskStateRunning)
        {
            // 取消掉
            [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                NSLog(@"可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法");
            }];
        }
    }
}
e、后台下载
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.quarkdata.emm"];
        configuration.HTTPMaximumConnectionsPerHost = 8;
        
        //设置请求超时为10秒钟
        configuration.timeoutIntervalForRequest = 30;
        //在蜂窝网络情况下是否继续请求(上传或下载)
        configuration.allowsCellularAccess = YES;
        
        self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

初始化manger的时候,配置那个后台配置的时候已经实现了后台下载的过程,你可以尝试下调回前台,发现你的下载也在继续,只是这个时候,你的session被操作系统接过去了,操作系统去管理了。一旦你下载完成,就会通知你(如果你的app还活着的话)。那么你的app死了的时候怎么办呢?操作系统会帮我们持有这个session,然后把ResumeData存着,等你的app再次复活的时候,并且再次创建NSURLSessionConfiguration,有后台任务并且后台的backgroundSessionConfigurationWithIdentifier一致的话,操作系统会去调用你的原来的NSURLSessionTaskDelegate代理中的taskdidCompleteWithError的方法。

由于我们的使用的是AF封装的,所以那个代理被AF接到并且提供了回调APIBlock,同时也通过通知处理了,所以还是会在我们之前监听的那个通知里面,和我们app活着的时候一样去发送那个AFNetworkingTaskDidCompleteNotification的通知,所以我们原来的处理逻辑并不需要改变。

上面讲的是我们进入后台,app慢慢的被系统杀死或者没杀死的时候。那么,如果我正在下载着,app崩溃,或者被用户手动强退怎么办? 经过验证其实是一样的,出现这个情况后,操作系统监听到被杀死或者被中断,也会帮我们持有这个session,然后把崩溃的一瞬间的ResumeData存着,等待我们再次创建相同后缀的session,那时候在调用回调---->af再发通知---->我们再存起来。

f、HTTPMaximumConnectionsPerHost并发属性的使用场景

如果我们在一个网络请求并发很多的app内,共用一个session,且未设置最大并发数的时候,尤其是包含一些请求响应时间不给力的请求(甚至说使用下载任务),可能会影响我们的其他的网络请求,这个时候,可以通过设置最大并发数来增加并发数(讨论的是一个服务器域名的 ),就是迅雷或者腾讯视频的多任务下载。

问题:对于我们在高并发的请求,期望快速做出反应的时候,除了设置HTTPMaximumConnectionsPerHost的值为一个我们能够接受的比较大的值外,是否也可以创建2个甚至多个NSURLSession来增加并发呢?

为此进行了一个验证:
基于AFNetworking创建了2个manager,分别对应不同的config,通过打印mangersession实例,发现的确不是一个session

// NetworkServerDownLoadTool 
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
 
// TestNetworkServerDownLoadTool
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

输出结果为:

2020-07-28 13:46:42.213683+0800 Demo[84680:23730915] 处于-[NetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLSessionLocal: 0x7ff51de08580>  
2020-07-28 13:46:43.209552+0800 Demo[84680:23730915] 处于-[TestNetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLSessionLocal: 0x7ff51de04e40> 

完整Demo如下:

#import "MaximumConnectionsViewController.h"
#import "NetworkServerDownLoadTool.h"
#import "TestNetworkServerDownLoadTool.h"

@interface MaximumConnectionsViewController ()

@property (nonatomic,copy) NSString *downLoadUrl;// 下载地址
@property (nonatomic,strong) NSURL *fileUrl;// 本地保存地址
@property (nonatomic,strong) NSURLSessionDownloadTask *downloadTask;// 下载任务
@property (nonatomic,assign) BOOL isDownLoading;
@property (nonatomic,strong) NSArray *urlArr;// url数组
@property (nonatomic,strong) NSMutableArray *localArray;// taskIdentifier并发数组

@end

@implementation MaximumConnectionsViewController

#pragma mark - Life Circle

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"NSURLSession高并发测试";
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.downLoadUrl = @"https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4";
    self.urlArr = [NSMutableArray arrayWithObjects:self.downLoadUrl,self.downLoadUrl,nil];
    self.localArray = [NSMutableArray array];
    
    // timer每隔1秒去创建一个task
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(event) userInfo:nil repeats:YES];
}

#pragma mark - Event

static int single = 0;
- (void)event
{
    NSString *localPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [localPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%dxiejiapei.mp4",single]];
    NSURL *url = [NSURL fileURLWithPath:filePath isDirectory:NO];
  
    // 2个session混合循环创建task,查看task的创建记录
    if (single % 2 == 0)
    {
        [self downLoadWithTask:self.urlArr[1] FileUrl:url];
    }
    else
    {
        [self testdownLoadWithTask:self.urlArr[1] FileUrl:url];
    }
    
    single++;
}

#pragma mark - 下载任务

- (void)downLoadWithTask:(NSString *)url FileUrl:(NSURL *)fileuUrl
{
    __block  NSURLSessionDownloadTask *tempTask;
    tempTask = [[NetworkServerDownLoadTool sharedTool] AFDownLoadFileWithUrl:url progress:^(CGFloat progress) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            // 根据taskIdentifier来判断是否之前已经包含该task,不包含则增加一个task并发
            NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
            if (![self.localArray containsObject:str])
            {
                [self.localArray addObject:str];
                NSLog(@"增加一个并发 taskIdentifier = %@ 总数 %lu %s",str,(unsigned long)self.localArray.count,__func__);
            }
        });
    } fileLocalUrl:fileuUrl success:^(NSURL *fileUrlPath, NSURLResponse *response) {
        NSLog(@"下载成功");
        
        // 下载成功移除一个task并发
        NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
        if ([self.localArray containsObject:str])
        {
            [self.localArray removeObject:str];
            NSLog(@"下载成功移除一个并发 taskIdentifier = %@",str);
        }
        
    } failure:^(NSError *error, NSInteger statusCode) {
        NSLog(@"下载失败,下载的data被downLoad工具暂存了");
        
        // 下载失败,移除一个并发
        NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
        if ([self.localArray containsObject:str])
        {
            [self.localArray removeObject:str];
            NSLog(@"下载失败,移除一个并发 taskIdentifier = %@",str);
        }
    }];
}

- (void)testdownLoadWithTask:(NSString *)url FileUrl:(NSURL *)fileuUrl
{
    __block  NSURLSessionDownloadTask *tempTask;
    tempTask = [[TestNetworkServerDownLoadTool sharedTool] AFDownLoadFileWithUrl:url progress:^(CGFloat progress) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            // 根据taskIdentifier来判断是否之前已经包含该task,不包含则增加一个task并发
            NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
            if (![self.localArray containsObject:str])
            {
                [self.localArray addObject:str];
                NSLog(@"增加一个并发 taskIdentifier = %@ 总数 %lu %s",str,(unsigned long)self.localArray.count,__func__);
            }
        });
    } fileLocalUrl:fileuUrl success:^(NSURL *fileUrlPath, NSURLResponse *response) {
        NSLog(@"下载成功");
        
        // 下载成功移除一个task并发
        NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
        if ([self.localArray containsObject:str])
        {
            [self.localArray removeObject:str];
            NSLog(@"下载成功移除一个并发 taskIdentifier = %@",str);
        }
        
    } failure:^(NSError *error, NSInteger statusCode) {
        NSLog(@"下载失败,下载的data被downLoad工具暂存了");
        
        // 下载失败,移除一个并发
        NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)tempTask.taskIdentifier];
        if ([self.localArray containsObject:str])
        {
            [self.localArray removeObject:str];
            NSLog(@"下载失败,移除一个并发 taskIdentifier = %@",str);
        }
    }];
}

@end

ephemeralSessionConfigurationdefaultSessionConfiguration混合,并且使用默认值的时候,并发数量却没有按照我们想象的,各自持有一个并发数,而是按照顺序创建task,直到满足6个默认值,就停止了:

2020-07-28 13:54:21.838852+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 1 总数 1 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:38.780335+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 2 总数 2 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:39.260323+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 3 总数 3 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:40.412324+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 4 总数 4 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:40.844415+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 5 总数 5 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:44.515921+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 6 总数 6 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2

可见,无论创建多少个NSURLSessionsession之间共用的都是一个最大并发的配置,最后一次的配置为所有session使用的最终的配置。

那么创建2个甚至多个NSURLSession是否还有什么意义呢? 苹果官方解释如下:

官网URL Loading System模块介绍

创建多个Session的目的,不是为了增加并发,而是为了对不同的Task使用不同的策略来实现更符合我们需求的交互。

3、Demo演示

NetworkServerDownLoadTool.h
#import "NetworkServerDownLoadTool.h"
#import <AFNetworking.h>

@interface NetworkServerDownLoadTool ()

@property (nonatomic,copy) NSString *fileHistoryPath;// 文件本地存储路径

@end

@implementation NetworkServerDownLoadTool

// 获取到网络请求单例对象
// 将下载功能交给一个单例管理类管理,下载的功能由他控制,再次进入下载页面的时候,读取他的信息,然后显示UI
static NetworkServerDownLoadTool* tool = nil;
+ (instancetype)sharedTool
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool =  [[self alloc] init];
    });
    return tool;
}

#pragma mark - Life Circle

// 初始化
- (instancetype)init
{
    self = [super init];
    if (self)
    {
        // 创建一个SessionConfiguration对象,其允许HTTP和HTTPS在后台进行下载或者上传
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.quarkdata.emm"];
        
        // 如果是做下载工具或者使用的默认的defaultConfig的话,iOS默认的限制同一个服务器tcp连接的并发数量限制
        // 否则会发现 ,无论创建多少下载任务,都是4个或者6个在运行,其他的在排队,甚至还最后直接超时了
        // 默认配置下,iOS对于同一个IP服务器的并发最大为4,OS X为6
        // 如果用户设置了最大并发数,则按照用户设置的最大并发数执行(我设置最大20,最小为1,均可以执行)
        configuration.HTTPMaximumConnectionsPerHost = 8;
        
        // 设置请求超时为10秒钟
        configuration.timeoutIntervalForRequest = 10;
        
        // 在蜂窝网络情况下是否继续请求(上传或下载)
        configuration.allowsCellularAccess = NO;
        
        // 创建SessionManager对象
        self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
        
        // task完成时的回调
        __weak typeof(self) weakSelf = self;
        [self.manager setTaskDidCompleteBlock:^(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSError * _Nullable error) {
            
            NSLog(@"manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:%@",task);
            // 下载地址
            NSString *urlHost = [task.currentRequest.URL absoluteString];
            
            // 判断是否有错误,有错误说明下载没有完成就被中断了
            if (error)
            {
                if (error.code == -1001)
                {
                    NSLog(@"下载出错,请查看一下网络是否正常");
                }
                
                // 可以通过key:NSURLSessionDownloadTaskResumeData去获取续传时候的data,因为AF源码中将其存储进入了这个key中
                NSData *resumeData = [error.userInfo objectForKey:@"NSURLSessionDownloadTaskResumeData"];
                
                // 当我退出这个页面再次进入的时候,如何自动获取最新的下载进度?
                // 将urlHost和resumeData存到本地,用作下载续传时候用到的值
                [weakSelf saveHistoryWithKey:urlHost DownloadTaskResumeData:resumeData];
            }
            else
            {
                NSLog(@"下载全部完成才会进入这里");
                // 判断当前是否存在这个urlHost:resumeData,存在的话看一下长度是否大于零
                if ([weakSelf.downLoadHistoryDictionary valueForKey:urlHost])
                {
                    // 下载完成移除存储容器中的内容
                    [weakSelf.downLoadHistoryDictionary removeObjectForKey:urlHost];
                    // AF在下载的时候(其实是调用的系统的下载方法),会将文件先下载到沙盒目录下的temp文件夹中,生成一个后缀为tmp的文件
                    // 下载完的时候,系统会将tmp文件删除掉释放占用的内存,然后移动文件到我们下载时设置的路径下,这样下载任务就完成了
                    [weakSelf.downLoadHistoryDictionary writeToFile:weakSelf.fileHistoryPath atomically:YES];
                }
            }
        }];
        
        // 文件本地存储路径
        NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        self.fileHistoryPath = [path stringByAppendingPathComponent:@"fileDownLoadHistory.plist"];
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.fileHistoryPath])// 文件已经存在
        {
            // 用之前存在的文件来初始化downLoadHistoryDictionary,即是之前的下载历史记录
            self.downLoadHistoryDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:self.fileHistoryPath];
        }
        else
        {
            // 新创建downLoadHistoryDictionary
            self.downLoadHistoryDictionary = [NSMutableDictionary dictionary];
            // 将dictionary中的数据写入plist文件中,此时也新建了plist文件
            [self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:YES];
        }
        NSLog(@"处于%s方法中,self.manager.session = %@ 支持后台下载/上传",__func__,self.manager.session);
    }
    return self;
}

#pragma mark - 存储文件到本地

// 将key和resumeData存到本地,用作下载续传时候用到的值
- (void)saveHistoryWithKey:(NSString *)key DownloadTaskResumeData:(NSData *)data
{
    // 开始下载的时候,去判断当前是否存在这个url:resumeData
    if (!data)
    {
        // 当用户开始下载的时候,如果什么都没下载到,就会中断下载
        // 此时resumeData是nil,如果直接写入plist是会崩溃的
        // 可以写一个NSString对象,虽然不是一个Data类型,但是length都可以使用,不会有太大影响
        NSString *emptyData = [NSString stringWithFormat:@""];
        [self.downLoadHistoryDictionary setObject:emptyData forKey:key];
    }
    else
    {
        // 有数据就直接写入即可
        [self.downLoadHistoryDictionary setObject:data forKey:key];
    }
    
    // 将下载好的部分存储到本地
    [self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:NO];
}

#pragma mark - 网络下载 downloadTask
/**
文件下载
@param urlHost 下载地址
@param progress 下载进度
@param localUrl 本地存储路径
@param success 下载成功
@param failure 下载失败
@return downLoadTask
*/
- (NSURLSessionDownloadTask *)AFDownLoadFileWithUrl:(NSString *)urlHost
                                           progress:(DowningProgress)progress
                                       fileLocalUrl:(NSURL *)localUrl
                                            success:(DonwLoadSuccessBlock)success
                                            failure:(DownLoadfailBlock)failure
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlHost]];
    NSURLSessionDownloadTask *downloadTask = nil;
    // 取出之前下载好的部分
    NSData *downLoadHistoryData = [self.downLoadHistoryDictionary objectForKey:urlHost];
    NSLog(@"之前下载好的部分数据长度为 %ld",downLoadHistoryData.length);
    
    if (downLoadHistoryData.length > 0)
    {
        // 其length大于零就使用resumeData调用downloadTaskWithResumeData这个方法,去继续我们的下载任务
        NSLog(@"继续旧任务");
        downloadTask = [self.manager downloadTaskWithResumeData:downLoadHistoryData progress:^(NSProgress * _Nonnull downloadProgress) {
            // typedef void (^DowningProgress)(CGFloat progress);
            // 调用下载进度block
            if (progress)
            {
                progress(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
                NSLog(@"下载进度 %F",(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount));
            }
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            // 返回本地存储路径
            return localUrl;
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            NSLog(@"AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块");
            
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            
            // 特殊处理404,删除垃圾文件
            if (httpResponse.statusCode == 404)
            {
                [[NSFileManager defaultManager] removeItemAtURL:filePath error:nil];
            }
            
            if (error)
            {
                // typedef void (^DownLoadfailBlock)(NSError* error ,NSInteger statusCode);
                // 调用下载失败block
                if (failure)
                {
                    //将下载失败存储起来,提交到下面的的网络监管类里面
                    failure(error, httpResponse.statusCode);
                }
            }
            else
            {
                // typedef void (^DonwLoadSuccessBlock)(NSURL* fileUrlPath ,NSURLResponse* response);
                // 调用下载成功block
                if (success)
                {
                    //将下载成功存储起来,提交到下面的的网络监管类里面
                    success(filePath, response);
                }
            }
        }];
    }
    else// 否则调用downloadTaskWithRequest去开辟新任务
    {
        NSLog(@"开辟新任务");
        downloadTask = [self.manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
            // typedef void (^DowningProgress)(CGFloat progress);
            // 调用下载进度block
            if (progress)
            {
                progress(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
                NSLog(@"下载进度 %F",(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount));
            }
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            // 返回本地存储路径
            return localUrl;
        } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
            NSLog(@"downloadTaskWithRequest的completionHandler得到回调");
            
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            
            // 特殊处理404,删除垃圾文件
            // 404页面是HTTP协议响应状态码,当我们的用户对网站某个不存在的页面进行访问时
            if (httpResponse.statusCode == 404)
            {
                [[NSFileManager defaultManager] removeItemAtURL:filePath error:nil];
            }
            
            if (error)
            {
                // typedef void (^DownLoadfailBlock)(NSError* error ,NSInteger statusCode);
                // 调用下载失败block
                if (failure)
                {
                    //将下载失败存储起来,提交到appDelegate的的网络监管类里面
                    failure(error, httpResponse.statusCode);
                }
            }
            else
            {
                // typedef void (^DonwLoadSuccessBlock)(NSURL* fileUrlPath ,NSURLResponse* response);
                // 调用下载成功block
                if (success)
                {
                    //将下载成功存储起来,提交到appDelegate的的网络监管类里面
                    success(filePath, response);
                }
            }
        }];
    }
    // 启动下载任务
    [downloadTask resume];
    return downloadTask;
}

// 可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法
- (void)stopAllDownLoadTasks
{
    if ([[self.manager downloadTasks] count] == 0)
    {
        return;
    }
    
    //停止所有的下载
    for (NSURLSessionDownloadTask *task in  [self.manager downloadTasks])
    {
        // 处于允许状态中的下载任务
        if (task.state == NSURLSessionTaskStateRunning)
        {
            // 取消掉
            [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                NSLog(@"可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法");
            }];
        }
    }
}

点击开始下载,输出结果为:

2020-07-28 09:36:54.961071+0800 合并两个有序链表[83999:23608303] 处于-[NetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLBackgroundSession: 0x7fa2ac6104e0> 支持后台下载/上传
2020-07-28 09:36:54.961199+0800 Demo[83999:23608303] 之前下载好的部分数据长度为 0
2020-07-28 09:36:54.961275+0800 Demo[83999:23608303] 开辟新任务
2020-07-28 09:36:55.237086+0800 Demo[83999:23608922] 下载进度 0.000018
2020-07-28 09:36:55.237111+0800 Demo[83999:23608303] 当前的进度 = 0.000018
...
2020-07-28 09:36:58.188459+0800 Demo[83999:23608919] 下载进度 0.008947
2020-07-28 09:36:58.188491+0800 Demo[83999:23608303] 当前的进度 = 0.008947

点击暂停,输出结果为:

2020-07-28 09:36:58.453978+0800 Demo[83999:23608919] 已经下载好的data = {length = 5286, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00001404 }
2020-07-28 09:36:58.458186+0800 Demo[83999:23608303] downloadTaskWithRequest的completionHandler得到回调
2020-07-28 09:36:58.458200+0800 Demo[83999:23608917] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask <B190D11A-AE07-4C81-B8BE-F55DE48516FD>.<1>
2020-07-28 09:36:58.458366+0800 Demo[83999:23608303] 下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载

AF在下载的时候(其实是调用的系统的下载方法),会将文件先下载到沙盒目录下的temp文件夹中,生成一个后缀为tmp的文件

断点续传

其中resumeData的内容为:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <data>YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICV8QG05TS2V5ZWRBcmNoaXZlUm9vdE9iamVjdEtleYABrxAUCwwjJCUmJygpKissMDg7PD0+P0BVJG51bGzTDQ4PEBkiV05TLmtleXNaTlMub2JqZWN0c1YkY2xhc3OoERITFBUWFxiAAoADgASABYAGgAeACIAJqBobHB0eHyAhgAqAC4ANgA6AD4AQgBGAEoATXxAbTlNVUkxTZXNzaW9uUmVzdW1lQnl0ZVJhbmdlXxAgTlNVUkxTZXNzaW9uUmVzdW1lQ3VycmVudFJlcXVlc3RfECFOU1VSTFNlc3Npb25SZXN1bWVPcmlnaW5hbFJlcXVlc3RfEBdOU1VSTFNlc3Npb25Eb3dubG9hZFVSTF8QIk5TVVJMU2Vzc2lvblJlc3VtZUluZm9UZW1wRmlsZU5hbWVfEB9OU1VSTFNlc3Npb25SZXN1bWVCeXRlc1JlY2VpdmVkXxAdTlNVUkxTZXNzaW9uUmVzdW1lSW5mb1ZlcnNpb25fECROU1VSTFNlc3Npb25SZXN1bWVTZXJ2ZXJEb3dubG9hZERhdGVYMzYzOTU1Mi3SLQ8uL1dOUy5kYXRhTxELUmJwbGlzdDAw1AECAwQFBgcKWCR2ZXJzaW9uWSRhcmNoaXZlclQkdG9wWCRvYmplY3RzEgABhqBfEA9OU0tleWVkQXJjaGl2ZXLRCAlfEBtOU0tleWVkQXJjaGl2ZVJvb3RPYmplY3RLZXmAAa8QHwsMQFVbXGJjZGU9ZkFnaHx9fn+AgYKDhIWGh4iJio9VJG51bGzfECUNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0MzMzODk6MzM5PT4/QDlBQj1DQEVGM0U+QEtAS04+UEIzU18QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzE4XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMjBfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xOV8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzIxXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfNF8QGGJvdW5kSW50ZXJmYWNlSWRlbnRpZmllcl8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzZfEB5hbGxvd3NDb25zdHJhaW5lZE5ldHdvcmtBY2Nlc3NfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfOF8QGl9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3BzXxAUYWxsb3dlZFByb3RvY29sVHlwZXNfEBpwYXlsb2FkVHJhbnNtaXNzaW9uVGltZW91dF8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzNWJGNsYXNzXxAecmVxdWlyZXNTaG9ydENvbm5lY3Rpb25UaW1lb3V0XxAcYWxsb3dzRXhwZW5zaXZlTmV0d29ya0FjY2Vzc1IkMF8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzVfEBBzdGFydFRpbWVvdXRUaW1lUiQxXxAhc2NoZW1lV2FzVXBncmFkZWREdWVUb0R5bmFtaWNIU1RTXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMFIkMl8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzdfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xMF8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzExWmlnbm9yZUhTVFNfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xMl8QEnByZXZlbnRIU1RTU3RvcmFnZV8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzEzXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMl8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzE0XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTVfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial85XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTZfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xN4ANgACADoAAgACAAIAJEACAA4AAgAAjAAAAAAAAAACAB4AeCBACgAgQCQiAAhAWgACAAoAHCIAKCIAKgAaAB4ALgAiAAIAMCNNWG1czWVpXTlMuYmFzZVtOUy5yZWxhdGl2ZYAAgAWABF8Qjmh0dHBzOi8vd3d3LmFwcGxlLmNvbS8xMDUvbWVkaWEvY24vaXBob25lLXgvMjAxNy8wMWRmNWI0My0yOGU0LTQ4NDgtYmYyMC00OTBjMzRhOTI2YTcvZmlsbXMvZmVhdHVyZS9pcGhvbmUteC1mZWF0dXJlLWNuLTIwMTcwOTEyXzEyODB4NzIwaC5tcDTSXV5fYFokY2xhc3NuYW1lWCRjbGFzc2VzVU5TVVJMol9hWE5TT2JqZWN0I0BOAAAAAAAAEAAJECQT//////////9TR0VU02lqG2tze1dOUy5rZXlzWk5TLm9iamVjdHOnbG1ub3BxcoAPgBCAEYASgBOAFIAVp3R1dnd4eXqAFoAXgBiAGYAagBuAHIAdVkFjY2VwdFhfX2hoYWFfX1VSYW5nZVhJZi1SYW5nZVpVc2VyLUFnZW50XxAPQWNjZXB0LUxhbmd1YWdlXxAPQWNjZXB0LUVuY29kaW5nUyovKl8RAeQNCg0KWW5Cc2FYTjBNRERXQVFJREJBVUdCd2tMRFE4UldsVnpaWEl0UVdkbGJuUlZVbUZ1WjJWV1FXTmpaWEIwWHhBUFFXTmpaWEIwTFV4aGJtZDFZV2RsV0VsbUxWSmhibWRsWHhBUFFXTmpaWEIwTFVWdVkyOWthVzVub1FoZkVHc2xSVFVsT1RBbE9EZ2xSVFVsUWprbFFqWWxSVFFsUWpnbFFUUWxSVFFsUWpnbFFVRWxSVFlsT1VNbE9Ea2xSVFVsUWtFbE9FWWxSVGtsT1RNbFFrVWxSVGdsUVRFbFFUZ3ZNU0JEUms1bGRIZHZjbXN2TVRFeU9DNHdMakVnUkdGeWQybHVMekU1TGpVdU1LRUtYbUo1ZEdWelBUTTJNemsxTlRJdG9ReFRLaThxb1E1VlpXNHRkWE9oRUY4UUhWUjFaU3dnTVRJZ1UyVndJREl3TVRjZ01ESTZNakk2TXpNZ1IwMVVvUkpmRUJGbmVtbHdMQ0JrWldac1lYUmxMQ0JpY2dBSUFCVUFJQUFtQUMwQVB3QklBRm9BWEFES0FNd0Eyd0RkQU9FQTR3RHBBT3NCQ3dFTkFBQUFBQUFBQWdFQUFBQUFBQUFBRXdBQUFBQUFBQUFBQUFBQUFBQUFBU0U9XmJ5dGVzPTM2Mzk1NTItXxAdVHVlLCAxMiBTZXAgMjAxNyAwMjoyMjozMyBHTVRfEGslRTUlOTAlODglRTUlQjklQjYlRTQlQjglQTQlRTQlQjglQUElRTYlOUMlODklRTUlQkElOEYlRTklOTMlQkUlRTglQTElQTgvMSBDRk5ldHdvcmsvMTEyOC4wLjEgRGFyd2luLzE5LjUuMFVlbi11c18QEWd6aXAsIGRlZmxhdGUsIGJy0l1ei4xfEBNOU011dGFibGVEaWN0aW9uYXJ5o42OYV8QE05TTXV0YWJsZURpY3Rpb25hcnlcTlNEaWN0aW9uYXJ50l1ekJFfEBNOU011dGFibGVVUkxSZXF1ZXN0o5KTYV8QE05TTXV0YWJsZVVSTFJlcXVlc3RcTlNVUkxSZXF1ZXN0AAgAEQAaACQAKQAyADcASQBMAGoAbACOAJQA4QEEAScBSgFtAY8BqgHMAe0CDwIxAk4CZQKCAqQCqwLMAusC7gMQAyMDJgNKA2wDbwORA7QD1wPiBAUEGgQ9BF8EggSlBMcE6gUNBQ8FEQUTBRUFFwUZBRsFHQUfBSEFIwUsBS4FMAUxBTMFNQU3BTgFOgU8BT4FQAVCBUMFRQVGBUgFSgVMBU4FUAVSBVQFVQVcBWQFcAVyBXQFdgYHBgwGFwYgBiYGKQYyBjsGPQY+BkAGSQZNBlQGXAZnBm8GcQZzBnUGdwZ5BnsGfQaFBocGiQaLBo0GjwaRBpMGlQacBqUGqwa0Br8G0QbjBucIzwjeCP4JbAlyCYYJiwmhCaUJuwnICc0J4wnnCf0AAAAAAAACAQAAAAAAAACUAAAAAAAAAAAAAAAAAAAKCoAM0jEyMzRaJGNsYXNzbmFtZVgkY2xhc3Nlc11OU011dGFibGVEYXRhozU2N11OU011dGFibGVEYXRhVk5TRGF0YVhOU09iamVjdNItDzkvTxCgYnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICV8QG05TS2V5ZWRBcmNoaXZlUm9vdE9iamVjdEtleYAAoQtVJG51bGwIERokKTI3SUxqbG4AAAAAAAABAQAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAdIAMXxCOaHR0cHM6Ly93d3cuYXBwbGUuY29tLzEwNS9tZWRpYS9jbi9pcGhvbmUteC8yMDE3LzAxZGY1YjQzLTI4ZTQtNDg0OC1iZjIwLTQ5MGMzNGE5MjZhNy9maWxtcy9mZWF0dXJlL2lwaG9uZS14LWZlYXR1cmUtY24tMjAxNzA5MTJfMTI4MHg3MjBoLm1wNF8QHENGTmV0d29ya0Rvd25sb2FkX1J6aEVNVC50bXASAEBEixAFXxAdVHVlLCAxMiBTZXAgMjAxNyAwMjoyMjozMyBHTVTSMTJBQl8QE05TTXV0YWJsZURpY3Rpb25hcnmjQ0Q3XxATTlNNdXRhYmxlRGljdGlvbmFyeVxOU0RpY3Rpb25hcnkACAARABoAJAApADIANwBJAEwAagBsAIMAiQCQAJgAowCqALMAtQC3ALkAuwC9AL8AwQDDAMwAzgDQANIA1ADWANgA2gDcAN4A/AEfAUMBXQGCAaQBxAHrAfQB+QIBDVcNWQ1eDWkNcg2ADYQNkg2ZDaINpw5KDkwO3Q78DwEPAw8jDygPPg9CD1gAAAAAAAACAQAAAAAAAABFAAAAAAAAAAAAAAAAAAAPZQ==</data>
</plist>

可以看到,虽然resumeData是一个data类型,但是本质是一个xml文件,其中包括下载的url,当前接受的文件大小,第几次下载,还有临时文件名等信息。

点击继续下载,输出结果为:

2020-07-28 09:41:26.595378+0800 Demo[83999:23608303] 之前下载好的部分数据长度为 5286
2020-07-28 09:41:26.595550+0800 Demo[83999:23608303] 继续旧任务
2020-07-28 09:41:26.710125+0800 Demo[83999:23608921] 下载进度 0.008947
2020-07-28 09:41:26.710158+0800 Demo[83999:23608303] 当前的进度 = 0.008947
...

回到Home,停一段时间后再回到APP,输出结果为:

2020-07-28 09:41:57.916591+0800 Demo[82972:23456508] 下载进度 0.251849
2020-07-28 09:41:57.916628+0800 Demo[82972:23440779] 当前的进度 = 0.251849
2020-07-28 09:41:12.191663+0800 Demo[82972:23456509] 下载进度 0.401489
2020-07-28 09:41:12.191682+0800 Demo[82972:23440779] 当前的进度 = 0.401489

可以看到直接从0.2跳到了0.4,说明可以继续之前的进行下载。同理,回到上个视图再进入,或者APP突然闪退、崩溃等也可以继续。但是如果把APP删除了则会把tmp文件删除,一切从头开始。点击暂停所有下载任务后的输出结果:

2020-07-28 09:51:16.688357+0800 Demo[83999:23617896] 下载进度 0.199756
2020-07-28 09:51:16.688377+0800 Demo[83999:23608303] 当前的进度 = 0.199756
2020-07-28 09:51:16.804138+0800 Demo[83999:23608303] 暂停所有下载任务,能够获取到每个任务的resumeData
2020-07-28 09:51:16.809438+0800 Demo[83999:23612187] 可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法
2020-07-28 09:51:16.810733+0800 Demo[83999:23608303] AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块
2020-07-28 09:51:16.810744+0800 Demo[83999:23612187] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask <813BFDFE-A94F-40B9-AC82-22314BB896B6>.<3>
2020-07-28 09:51:16.810850+0800 Demo[83999:23608303] 下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载

点击继续,直到下载完成,输出结果为:

2020-07-28 09:58:51.482124+0800 Demo[83999:23620038] 下载进度 0.999901
2020-07-28 09:58:51.482203+0800 Demo[83999:23608303] 当前的进度 = 0.999901
2020-07-28 09:58:51.520497+0800 Demo[83999:23620047] 下载进度 1.000000
2020-07-28 09:58:51.520564+0800 Demo[83999:23608303] 当前的进度 = 1.000000
2020-07-28 09:58:51.523004+0800 Demo[83999:23620047] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask <4A717F1B-E655-4FAD-9F4F-39009B43CED5>.<4>
2020-07-28 09:58:51.523157+0800 Demo[83999:23620047] 下载全部完成才会进入这里
2020-07-28 09:58:51.523347+0800 Demo[83999:23608303] AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块
2020-07-28 09:58:51.523914+0800 Demo[83999:23608303] 下载成功 下载的文档路径是 file:///Users/xiejiapei/Library/Developer/CoreSimulator/Devices/4B1F0517-19D5-45E0-A0F4-7E4B55191EE0/data/Containers/Data/Application/59BBB0D2-AE9A-4666-98A2-36D815FDBA75/Documents/iphoneX.mp4, 

下载完的时候,系统会将tmp文件删除掉释放占用的内存,然后移动文件到我们下载时设置的路径下,这样下载任务就完成了。

下载完成

五、拼接URL进行网络请求

整个流程分为计算签名-->拼接URL-->再进行网络请求三部分,将其封装在一个请求方法中,传入参数和相对路径的URL即可拿到返回数据,其中返回数据的方式是通过block属性来进行的。

// 网络请求方法
- (void)startRequest:(NSDictionary *_Nullable)parameters pathUrl:(NSString *)pathUrl {
}

1、计算签名

为什么要有计算签名这个步骤的原因解释:网络传输并非绝对安全可靠。以微信支付举例,一旦支付请求被中间人拦截并恶意篡改(如利用DNS欺骗),就会画风突变。这种场景下就需要信息摘要技术了。信息摘要把明文内容按某种规则生成一段哈希值,即使明文消息只改动了一点点,生成的结果也会完全不同。MD5 (Message -digest algorithm 5)就是信息摘要的一种实现,它可以从任意长度的明文字符串生成128位的哈希值。

网络传输并非绝对安全可靠

摘要哈希生成的正确姿势是什么样呢?分三步:

  1. 收集相关业务参数,这里以金额和目标账户举例。
  2. 按照规则,把参数名和参数值拼接成一个字符串,同时把给定的密钥也拼接起来。之所以需要密钥,是因为攻击者也可能获知拼接规则。
  3. 利用 MD5 算法,从原文生成哈希值。MD5 生成的哈希值是 128 位的二进制数,也就是 32 位的十六进制数。

其中MD5算法生成签名需要的东西:服务商一般会给你一个appidappkey;同时这两个参数服务商也会保存,这两个形成了你的唯一标识。appid通过网络传输,而appkey是不在网络上进行传输的,只在生成签名时使用,所以安全性还是比较高的。

MD5算法生成签名的流程:
1、除去加密数组中的空值和签名参数
2、对数组排序
3、把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
4、加上appkey值,对形成的数据进行MD5加密,生成签名

利用 MD5 算法,从原文生成哈希值

第三方支付平台如何验证请求的签名?同样分三步:

  1. 发送方和请求方约定相同的字符串拼接规则,约定相同的密钥。
  2. 第三方平台接到支付请求,按规则拼接业务参数和密钥,利用 MD5 算法生成Sign
  3. 用第三方平台自己生成的Sign和请求发送过来的 Sign做对比,如果两个Sign值一模一样,则签名无误,如果两个Sign 值不同,则信息做了篡改。这个过程叫做验签。

使用MD5算法以后,发起支付请求的画风会变成什么样呢? 我们来看一看:

使用MD5算法以后,发起支付请求的画风

计算签名的具体代码实现如下:
a、创建一个可变字典,用来容纳用于计算签名的各种参数,首先放入cid,注意cid可以先写死,但是要带入计算部分。

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
//可以先写死,但是要带入计算部分
dict[@"cid"] = @"000001";

b、接着放入用户标识uiduid从沙盒中获取,第一次登陆后存储uid到本地沙盒,并且保存登录状态,其中保存的方法如下:

// content为网络请求返回的内容
NSString *uid = content[@"uid"];
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [path stringByAppendingPathComponent:@"uid.plist"];
NSMutableDictionary *plistDict = [[NSMutableDictionary alloc] init];
[plistDict setValue:uid forKey:@"uid"];// 存储uid到本地沙盒
[plistDict setValue:@(1) forKey:@"statusCode"]; // 保存登录状态,防止跳转界面时重复登录
[plistDict writeToFile:filePath atomically:YES];

从沙盒中获取uid放入用于计算签名的字典:

//uid用户标识
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [path stringByAppendingPathComponent:@"uid.plist"];
NSDictionary *plistDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
dict[@"uid"] = plistDict[@"uid"];

c、将请求参数放入用于计算签名的字典中。首先需要将请求参数进行序列化为JSON数据,接着将NSData数据以utf-8解码为字符串,但是注意如果NSData数据受损,则会返回nil,最后字符串放入用于计算签名的字典中。

    //将请求参数放入用于计算签名的字典中
    if (parameters.count > 0) {
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];
        NSString *qValue = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        dict[@"q"] = qValue;
    }

d、前面的准备工作都是为了这一步的计算签名。首先我们要根据用于计算签名的字典中元素个数创建个可变数组,用于放入用“=”拼接字典的keyvalue后字符串。

    NSMutableArray *dictArray = [NSMutableArray arrayWithCapacity:dict.count];
    for (NSString *key in dict.allKeys) {
        NSString *string = [NSString stringWithFormat:@"%@=%@", key, dict[key]];
        [dictArray addObject:string];
    }

接着对放入数组中的字符串进行排序,将排好序的字符串用";"进行拼接成为一个完整的字符串。

    NSArray *array = [dictArray sortedArrayUsingSelector:@selector(compare:)];
    NSString *signStr = [array componentsJoinedByString:@";"];

最后在这个字符串中放入我们的签名密钥。

// 密钥可以写在这
signStr = [NSString stringWithFormat:@"%@%@", signStr, @"mUwmfk6viCFCWydSogtH"];

大功告成!将这个计算后的签名字符串加密后放入之前我们创建的用于容纳计算签名的各种元素的字典中,作为其中一个元素存在。

dict[@"sign"] = [self doMD5String:signStr];

注意到,这里使用了MD5进行加密,先引入MD5加密所需的框架:

#import <CommonCrypto/CommonCrypto.h>

辅助常量:

@property (nullable, readonly) const char *UTF8String NS_RETURNS_INNER_POINTER;
typedef uint32_t CC_LONG;       /* 32 bit unsigned integer */
extern unsigned char *CC_MD5(const void *data, CC_LONG len, unsigned char *md);
#define CC_MD5_DIGEST_LENGTH    16          /* digest length in bytes */

核心算法:

- (NSString *)doMD5String:(NSString *)string {
    //1: 将字符串转换成C语言的字符串(因为:MD5加密是基于C的)
    const char *cStr = [string UTF8String];
    //2: 初始化一个字符串数组,用来存放MD5加密后的数据
    unsigned char result[CC_MD5_DIGEST_LENGTH];
    //3: 计算MD5的值
    //参数一: 表示要加密的字符串
    //参数二: 表示要加密字符串的长度
    //参数三: 表示接受结果的数组
    CC_MD5(cStr, (CC_LONG)strlen(cStr), result);
    
    // 从保存结果的数组中,取出值进行加密
    int first = abs([self bytesToInt:result offset:0]);
    int second = abs([self bytesToInt:result offset:4]);
    int third = abs([self bytesToInt:result offset:8]);
    int fourth = abs([self bytesToInt:result offset:12]);
    
    // 将加密后的值拼接后赋值给字符串
    NSString *stirng = [NSString stringWithFormat:@"%d%d%d%d", first, second, third, fourth];
    // 返回加密结果
    return stirng;
}

其中用到的bytesToInt方法如下:这个设计到MD5 算法底层原理,比较复杂略过。

// 从保存结果的数组中,取出值进行加密
- (int)bytesToInt:(Byte[])src offset:(int)offset {
    int value;
    value = (int)(((src[offset] & 0xFF) << 24) | ((src[offset + 1] & 0xFF) << 16) | ((src[offset + 2] & 0xFF) << 8) |
                  (src[offset + 3] & 0xFF));
    return value;
}

2、拼接请求参数的URL

a、首先还是先生成请求参数字符串,第一个放入的还是我们的cid

//cid 只拼接了一次,上次是用于计算签名
NSString *parametersURL = @"cid=000001";

b、接着放入我们网络请求方法中传入的参数,这是我们的主体部分。

if (dict[@"q"]) { 
    parametersURL = [parametersURL stringByAppendingFormat:@"&q=%@",[dict[@"q"]  stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]]; 
}

此处存在一个小问题,因为网络请求会拼接中文参数,用户名登陆等很多地方会用到中文,所以需要针对中文进行编码和解码,举两个比较清晰的例子:

编码:
NSString* hStr =@"你好啊";
NSString* hString = [hStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSLog(@"hString === %@",hString); // hString === %E4%BD%A0%E5%A5%BD%E5%95%8A
解码:
NSString*str3 =@"\u5982\u4f55\u8054\u7cfb\u5ba2\u670d\u4eba\u5458\uff1f";
NSString*str5 = [str3 stringByRemovingPercentEncoding];
NSLog(@"str5 ==== %@",str5);// str5 ==== 如何联系客服人员?

c、然后放入我们的uid,注意uid当我们第一次登录的时候是不存在的,第一次登录后才保存到了沙盒中。

if (dict[@"uid"]) {
    parametersURL = [parametersURL stringByAppendingFormat:@"&uid=%@",dict[@"uid"]];
}

d、最后放入我们千辛万苦得来的签名。

parametersURL = [parametersURL stringByAppendingFormat:@"&sign=%@", dict[@"sign"]];

e、完美,万事俱备,只欠东风。

NSString *strURL = [NSString stringWithFormat:@"https://apiproxytest.ucarinc.com/ucarincapiproxy/action/th/api%@?%@",pathUrl,parametersURL];
NSLog(@"%@",strURL);

将请求URL的共同部分提取出来作为baseURL即这里的https://apiproxytest.ucarinc.com/ucarincapiproxy/action/th/api,然后将我们本次请求方法中传入的具体url拼接到baseURL后面,最后再放入我们历经九九八十一难得到的无上至宝——parametersURL,注意到这里使用的是GET的请求方法。
f、将请求URL字符串转变为NSURL类型,打完收工。

NSURL *url = [NSURL URLWithString:strURL];

3、进行网络请求

这里使用系统的NSURLSession来实现简单的GET请求。

    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSDictionary *resDict;
        NSLog(@"请求完成...");
        if (!error) {// 请求成功
            // 此处返回的数据是JSON格式的,因此使用NSJSONSerialization进行反序列化处理,解析服务器返回的数据
            resDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            // 网络请求数据在其他线程,拿到数据后需要返回主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                // block属性,用于回传数据
                self.transDataBlock(resDict);
            });
        } else {// 请求失败
            NSLog(@"error: %@", error.localizedDescription);
        }
    }];
    [task resume];

其中的block属性如下:

typedef void (^TransDataBlock)(NSDictionary *  content);

@property (copy, nonatomic) TransDataBlock transDataBlock;

4、调用网络请求方法

既然我们实现了刚才的网络请求方法,那么在实际的业务场景中我们应该怎么去调用它呢?
a、首先创建我们的请求参数,例如:

NSArray *keysArr = [[NSArray alloc] initWithObjects:@"telephone",@"password",nil];
NSArray *valuesArr = [[NSArray alloc] initWithObjects:self.phoneNumber,self.password,nil];
NSDictionary *dict = [NSDictionary dictionaryWithObjects:valuesArr forKeys:keysArr];

b、接着通过URLRequest 传入请求参数和相对地址发起网络请求,再使用block的方式拿到请求结果处理业务逻辑,注意避免循环引用,要是用__weak修饰符。

self.urlRequest = [[URLRequest alloc] init];
__weak LoginViewController *weakSelf = self;
self.urlRequest.transDataBlock = ^(NSDictionary * _Nonnull content) {
}
[self.urlRequest startRequest:dict pathUrl:@"/home/login"];

c、处理业务逻辑之前,首先需要先判断响应结果的状态码是否正常,这里的测试接口比较特殊判断了两次,一般一次就OK了。

NSInteger errorStatusCode = [content[@"code"] integerValue];
if (errorStatusCode == 1) {
     NSDictionary *dict = [NSDictionary dictionaryWithDictionary:content [@"content"]];
     weakSelf.statusCode = [dict[@"code"] integerValue];
      if (self.statusCode == 1) {
      } else {
        NSLog(@"%@",content[@"msg"]);
      } 
}

d、为了完整,把业务逻辑也列举出来下。

//获取接口数据
NSDictionary *memberRe = [NSDictionary dictionaryWithDictionary:dict[@"re"]];

//保存登录状态,获取uid.....

//通过接口刷新Tab的badgeValue的值.....
delegate.shoppingCartNavigationController.tabBarItem.badgeValue = num;

//刷新页面
[weakSelf.tableView reloadData];

Demo

Demo在我的Github上,欢迎下载。
NetworkRequestDemo

参考文献

最详细的MD5签名的原理和流程
漫画趣解MD5算法
Http协议请求和响应报文字段详解
iOS 如何防止https抓包(中间人攻击),及charles抓包原理
如何用 Charles 抓 HTTPS 的包?
HTTP学习笔记
iOS网络NSURLConnection使用详解
iOS学习笔记-----URLSession的使用
iOS学习笔记-----AFNetworking的使用

上一篇 下一篇

猜你喜欢

热点阅读