目录
一、我们先自己用NSURLSession实现一下GET请求、POST请求、文件上传请求、文件下载请求和支持HTTPS
1、GET请求
2、POST请求
3、文件上传请求
4、文件下载请求
5、支持HTTPS
二、AFNetworking源码分析
1、序列化器模块
2、session和task管理模块
3、支持HTTPS模块
一、我们先自己用NSURLSession实现一下GET请求、POST请求、文件上传、文件下载和支持HTTPS
1、GET请求
#import "GetViewController.h"
@interface GetViewController ()
@end
@implementation GetViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"GET请求";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 第1步:创建一个HTTP请求——即NSURLRequest
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
request.HTTPMethod = @"GET";
/*
设置HTTP请求的URL,注意:
——GET请求的参数是直接放在URL里面的
——使用UTF-8编码对URL编码一下
*/
request.URL = [NSURL URLWithString:[@"http://api.leke.cn/auth/m/parent/homework/getHomeworkSubject.htm?_s=homework&_m=getHomeworkSubject&ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4&studentId=6090647" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
// 第2步:创建一个session
NSURLSession *session = [NSURLSession sharedSession];
// 第3步:创建一个dataTask
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
/*
第5步:HTTP请求结束的回调(注意block回调是在子线程,我们需要主动回到主线程去做事情)
data: 响应体
response: 响应头
error: 请求失败的信息
除了通过block来做HTTP请求结束的回调,我们还可以通过delegate来做HTTP请求结束的回调,这部分代码会在POST请求里做演示
*/
NSLog(@"111---%@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"222---%@", [NSThread currentThread]);
if (error != nil) {
NSLog(@"请求失败了");
} else {
// data就是我们请求到的JSON字符串的二进制格式,我们先把它转换成JSON字符串,打印出来看看(NSData -> NSString)
NSString *jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
NSLog(@"请求成功了,解析前:---%@", jsonString);
// 我们把这个JSON字符串解析成OC字典(NSData -> NSDictionary)
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:(0) error:nil];
NSLog(@"请求成功了,解析后:---%@", dictionary);
}
});
}];
/*
第4步:执行dataTask
执行dataTask后就发起了我们刚才创建的HTTP请求,并且NSURLSession会默认把这个HTTP请求放到子线程里去做
*/
[dataTask resume];
}
@end
2、POST请求
#import "PostViewController.h"
// 因为是普通的GET、POST请求,只需要遵循NSURLSessionDataDelegate协议就可以了
@interface PostViewController () <NSURLSessionDataDelegate>
/// 响应体数据
@property (nonatomic, strong) NSMutableData *responseBodyData;
@end
@implementation PostViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"POST请求";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 第1步:创建一个HTTP请求——即NSURLRequest
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
request.HTTPMethod = @"POST";
/*
设置HTTP请求的URL,注意:
——POST请求的参数不放在URL里,而是放在Body里
——使用UTF-8编码对URL编码一下
*/
request.URL = [NSURL URLWithString:[@"https://homework.leke.cn/auth/m/person/homework/getStudentNewHomeworkData.htm" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
/*
设置HTTP请求的请求头:
——如果我们跟服务端约定采用表单提交,那么我们就得把请求头的Content-Type字段设置为application/x-www-form-urlencoded; charset=utf-8
——如果我们跟服务端约定采用JSON提交,那么我们就得把请求头的Content-Type字段设置为application/json; charset=utf-8
*/
[request setValue:@"application/x-www-form-urlencoded; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
// [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
/*
设置HTTP请求的请求体:
——如果我们跟服务端约定采用表单提交,那么请求体的格式就必须得是"username=123&password=456"这样,同时注意对请求体使用UTF-8编码一下
——如果我们跟服务端约定采用JSON提交,那么请求体的格式就必须得是"{username:123,password=456}"这样,同时注意对请求体使用UTF-8编码一下
*/
request.HTTPBody = [@"ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4&studentId=6090647&curPage=1&pageSize=10&startTime=1639497600000&endTime=1642089599000" dataUsingEncoding:NSUTF8StringEncoding];
// request.HTTPBody = [@"{\"ticket\": \"VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4\", \"studentId\": \"6090647\", \"curPage\": \"1\", \"pageSize\": \"10\", \"startTime\": \"1639497600000\", \"endTime\": \"1642089599000\"}" dataUsingEncoding:NSUTF8StringEncoding];
/*
第2步:创建一个session
configuration: 配置信息
delegate: 代理
delegateQueue: 设置代理方法在哪个线程里调用,mainQueue就代表在主线程里调用
*/
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// 第3步:创建一个dataTask
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
/*
第4步:执行dataTask
执行dataTask后就发起了我们刚才创建的HTTP请求,并且NSURLSession会默认把这个HTTP请求放到子线程里去做
*/
[dataTask resume];
}
// 第5步:HTTP请求结束的回调(注意delegate回调是在主线程,我们不需要再主动回到主线程)
#pragma mark - NSURLSessionDataDelegate
/// 接收到响应头的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSLog(@"111---%@", [NSThread currentThread]);
/*
NSURLSessionResponseCancel = 0, // 通过delegate来做HTTP请求结束的回调默认情况下接收到响应头后会取消后续的请求,即内容会调用[dataTask cancel]
NSURLSessionResponseAllow = 1, // 允许后续的请求
NSURLSessionResponseBecomeDownload = 2, // 把当前请求转变成一个下载请求
NSURLSessionResponseBecomeStream = 3, // 把当前请求转变成一个下载请求(iOS9之后可用)
*/
completionHandler(NSURLSessionResponseAllow);
}
/// 接收到响应体的回调
///
/// data: 指得是本次接收到的数据
/// ——如果数据量很小的话,这个方法只触发一次就接收完了,然后会触发请求结束的回调
/// ——如果数据量很大的话,这个方法会触发多次,一段一段地接收,全部接收完后会触发请求结束的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"222---%@", [NSThread currentThread]);
[self.responseBodyData appendData:data];
}
/// 请求结束的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"333---%@", [NSThread currentThread]);
if (error != nil) {
NSLog(@"请求失败了");
} else {
// data就是我们请求到的JSON字符串的二进制格式,我们先把它转换成JSON字符串,打印出来看看(NSData -> NSString)
NSString *jsonString = [[NSString alloc] initWithData:self.responseBodyData encoding:(NSUTF8StringEncoding)];
NSLog(@"请求成功了,解析前:---%@", jsonString);
// 我们把这个JSON字符串解析成OC字典(NSData -> NSDictionary)
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:self.responseBodyData options:(0) error:nil];
NSLog(@"请求成功了,解析后:---%@", dictionary);
}
}
#pragma mark - setter, getter
- (NSMutableData *)responseBodyData {
if (_responseBodyData == nil) {
_responseBodyData = [NSMutableData data];
}
return _responseBodyData;
}
@end
3、文件上传请求
文件上传请求本质上就是一个POST请求,只不过:
- 1️⃣这个POST请求的请求头里必须设置
Content-Type
字段为multipart/form-data; boundary=xxx
; - 2️⃣请求体也必须得严格按照指定的格式去拼接数据。
请求体的格式:
Wireshark抓包AFNetworking
--分隔符
回车换行
Content-Disposition: form-data; name="file"; filename="123.jpg"
回车换行
Content-Type: image/jpg
回车换行
回车换行
数据
回车换行
--分隔符--
// 随机生成的boundary
#define kBoundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx"
// 回车换行
#define kNewLine [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]
#import "UploadViewController.h"
// 因为uploadTask是继承自dataTask的,只需要遵循NSURLSessionDataDelegate协议就可以了
@interface UploadViewController () <NSURLSessionDataDelegate>
@end
@implementation UploadViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"文件上传请求";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 第1步:创建一个HTTP请求——即NSURLRequest
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
request.HTTPMethod = @"POST";
/*
设置HTTP请求的URL,注意:
——POST请求的参数不放在URL里,而是放在Body里
——使用UTF-8编码对URL编码一下
*/
request.URL = [NSURL URLWithString:[@"https://fs.leke.cn/api/w/upload/image/binary.htm?ticket=VFdwblBRPT07S3lvbEtpc29LaXdsTENvPTsyODI4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
/*
设置HTTP请求的请求头
*/
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBoundary] forHTTPHeaderField:@"Content-Type"];
/*
第2步:创建一个session
configuration: 配置信息
delegate: 代理
delegateQueue: 设置代理方法在哪个线程里调用,mainQueue就代表在主线程里调用
*/
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
/*
第3步:创建一个uploadTask
创建uploadTask的时候,把请求体挂上去,而不是直接设置request.HTTPBody
*/
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:[self getRequestBodyData]];
/*
第4步:执行uploadTask
执行uploadTask后就发起了我们刚才创建的HTTP请求,并且NSURLSession会默认把这个HTTP请求放到子线程里去做
*/
[uploadTask resume];
}
// 第5步:HTTP请求结束的回调(注意delegate回调是在主线程,我们不需要再主动回到主线程)
#pragma mark - NSURLSessionDataDelegate
/// 数据上传中的回调
///
/// bytesSent: 本次上传的数据大小
/// totalBytesSent: 已经上传的数据大小
/// totalBytesExpectedToSend: 数据的总大小
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
NSLog(@"上传进度---%f", 1.0 * totalBytesSent / totalBytesExpectedToSend);
}
/// 请求结束的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error != nil) {
NSLog(@"上传失败了");
} else {
NSLog(@"上传成功了");
}
}
#pragma mark - private method
- (NSData *)getRequestBodyData {
NSMutableData *requestBodyData = [NSMutableData data];
// 开始标识
[requestBodyData appendData:[[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
[requestBodyData appendData:kNewLine];
// name: file,服务器规定的参数
// filename: 123.jpg,文件保存到服务器上面的名称
[requestBodyData appendData:[@"Content-Disposition: form-data; name=\"file\"; filename=\"123.jpg\"" dataUsingEncoding:NSUTF8StringEncoding]];
[requestBodyData appendData:kNewLine];
// Content-Type: 文件的类型
[requestBodyData appendData:[@"Content-Type: image/jpg" dataUsingEncoding:NSUTF8StringEncoding]];
[requestBodyData appendData:kNewLine];
[requestBodyData appendData:kNewLine];
// UIImage ---> NSData
UIImage *image = [UIImage imageNamed:@"123.jpg"];
NSData *imageData = UIImagePNGRepresentation(image);
[requestBodyData appendData:imageData];
[requestBodyData appendData:kNewLine];
// 结束标识
[requestBodyData appendData:[[NSString stringWithFormat:@"--%@--", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
return requestBodyData;
}
@end
4、文件下载请求
文件下载请求本质上来说就是一个GET请求,只不过关于下载我们需要注意五个问题:
- 1️⃣下载进度问题:最好是能监听下载进度,这样方便用户知道自己下载了多少东西。block回调的方式是无法监听下载进度的(因为它是等数据全部下载完一次性回调回来的),delegate回调的方式才能监听下载的进度(因为它是一段数据一段数据接收的),所以我们得用delegate回调的方式(NSURLSession的downloadTask已经给我们提供了一个这样的回调,所以我们就不必像NSURLConnection那样自己记录数据了,直接拿来用就行);
- 2️⃣内存暴涨问题:下载的东西最好是边下载边存储到磁盘里,而非放在应用程序内存中,因为下载的东西可能很大(比如1个G),那如果放在内存中就会导致App内存溢出而崩溃(NSURLSession的downloadTask内部已经帮我们做了边下载边存储到磁盘里,只不过是存储到了tmp文件夹下,App退出后就会清除掉,所以我们只需要在下载任务完成后把文件移动到我们自定义的目录下就可以了,不必像NSURLConnection那样自己往文件里写数据);
- 3️⃣断点下载问题:最好能支持断点下载,因为有可能业务允许用户下载到一半的时候暂停下载,当恢复下载时最好是能实现客户端通过请求头告诉服务器我想要下载哪部分数据,以便从指定的位置处开始下载,而非从头开始下载,这样可以节省用户的流量(NSURLSession的downloadTask已经给我们提供了相应的API,很方便就能实现断点下载,就不必像NSURLConnection那样自己记录下载的位置、设置请求头了);
- 4️⃣离线断点下载问题:但是光支持了断点下载可能还不够,因为支持断点下载仅仅是满足了App在活跃期间用户点击(暂停下载就是通过取消网络请求来实现的)、继续下载(继续下载就是通过重新创建一个网络请求、但是把下载位置设置到取消下载之前的那个位置来实现的)这样的需求或者退出界面时我们批量把下载任务暂停掉这样的需求,但是如果用户强制退出了App,根本不会触发我们暂停下载的回调,我们也无从记录断点的信息,那怎么办呢?这就需要做离线断点下载,NSURLSession的downloadTask无法实现离线断点下载,所以我们就得用dataTask和NSFileHandle或NSOutStream像NSURLConnection那样自己记录下载的位置、设置请求头了;
- 5️⃣多线程断点下载问题:如果一个文件很大很大(比如有1000KB),我们开一个线程下载起来有些慢,那我们就可以考虑多开几个线程来下载这个文件,比如第一个线程通过请求头告诉服务器要下载0 ~ 400KB的数据,第二个线程通过请求头告诉服务器要下载401 ~ 800KB的数据,第三个线程通过请求头告诉服务器要下载剩余的数据,最后等三个线程都下载完了,根据顺序把三个线程的数据给拼接起来就可以了。
4.1 通过downloadTask实现
/*
关于下载我们需要注意五个问题:
✅1、下载进度问题
✅2、内存暴涨问题
✅3、断点下载问题
❌4、离线断点下载问题:不支持
❌5、多线程断点下载问题:这里就不实现了,可参考上面的思路
*/
#import "DownloadViewController.h"
// 因为是个文件下载请求,只需要遵循NSURLSessionDownloadDelegate协议就可以了
@interface DownloadViewController () <NSURLSessionDownloadDelegate>
/// session
@property (nonatomic, strong) NSURLSession *session;
/// downloadTask
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
/// 暂停下载时的一些位置信息(注意并非暂停下载时之前下载过的数据)
@property (nonatomic, strong) NSData *resumeData;
@end
@implementation DownloadViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"通过downloadTask实现";
}
- (void)dealloc {
// 释放session,避免内存泄漏
[self.session invalidateAndCancel];
}
- (IBAction)start:(id)sender {
NSLog(@"开始下载");
// 第1步:创建一个HTTP请求——即NSURLRequest
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
request.HTTPMethod = @"GET";
// 设置HTTP请求的URL
request.URL = [NSURL URLWithString:[@"https://view.2amok.com/2019913/9660e7b5ef2a6fe4ea2328d91069f9eb.mp4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
/*
第2步:创建一个session
configuration: 配置信息
delegate: 代理
delegateQueue: 设置代理方法在哪个线程里调用,mainQueue就代表在主线程里调用
*/
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// 第3步:创建一个downloadTask
self.downloadTask = [self.session downloadTaskWithRequest:request];
/*
第4步:执行downloadTask
执行downloadTask后就发起了我们刚才创建的HTTP请求,并且NSURLSession会默认把这个HTTP请求放到子线程里去做
*/
[self.downloadTask resume];
}
- (IBAction)cancel:(id)sender {
NSLog(@"暂停下载");
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
// 暂停下载时会触发这个回调,记录暂停下载时的一些位置信息
self.resumeData = resumeData;
}];
}
- (IBAction)resume:(id)sender {
NSLog(@"继续下载");
if (self.resumeData != nil) { // 说明之前暂停过下载
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
}
[self.downloadTask resume];
}
// 第5步:HTTP请求结束的回调(注意delegate回调是在主线程,我们不需要再主动回到主线程)
#pragma mark - NSURLSessionDownloadDelegate
/// 数据下载中的回调
///
/// bytesWritten: 本次下载的数据大小
/// totalBytesWritten: 已经下载的数据大小
/// totalBytesExpectedToWrite: 数据的总大小
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
NSLog(@"下载进度---%f", 1.0 * totalBytesWritten / totalBytesExpectedToWrite);
}
/// 数据下载完成的回调
///
/// location: 文件的临时存储路径
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
// 这里我们把文件存储到Library/Cache目录下
NSString *cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:cachePath] error:nil];
NSLog(@"文件路径---%@", cachePath);
}
/// 数据下载结束的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error != nil) {
NSLog(@"请求失败了");
} else {
NSLog(@"请求成功了");
}
}
@end
4.2 通过dataTask实现
/*
关于下载我们需要注意五个问题:
✅1、下载进度问题
✅2、内存暴涨问题
✅3、断点下载问题
✅4、离线断点下载问题
❌5、多线程断点下载问题:这里就不实现了,可参考上面的思路
*/
#import "DownloadViewController1.h"
// 因为是普通的GET、POST请求,只需要遵循NSURLSessionDataDelegate协议就可以了
@interface DownloadViewController1 () <NSURLSessionDataDelegate>
/// request
@property (nonatomic, strong) NSMutableURLRequest *request;
/// session
@property (nonatomic, strong) NSURLSession *session;
/// dataTask
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
/// 数据的总大小
@property (nonatomic, assign) NSInteger totalSize;
/// 已经下载的数据大小
@property (nonatomic, assign) NSInteger currentSize;
/// 要把文件下载到的路径
@property (nonatomic, copy) NSString *cachePath;
/// fileHandle
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end
@implementation DownloadViewController1
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"通过dataTask实现";
}
- (void)dealloc {
// 释放session,避免内存泄漏
[self.session invalidateAndCancel];
}
- (IBAction)start:(id)sender {
NSLog(@"开始下载");
/*
第4步:执行dataTask
执行dataTask后就发起了我们刚才创建的HTTP请求,并且NSURLSession会默认把这个HTTP请求放到子线程里去做
*/
[self.dataTask resume];
}
- (IBAction)cancel:(id)sender {
NSLog(@"暂停下载");
[self.dataTask cancel];
self.request = nil;
self.session = nil;
self.dataTask = nil;
}
- (IBAction)resume:(id)sender {
NSLog(@"继续下载");
[self.dataTask resume];
}
// 第5步:HTTP请求结束的回调(注意delegate回调是在主线程,我们不需要再主动回到主线程)
#pragma mark - NSURLSessionDataDelegate
/// 接收到响应头的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSLog(@"111---%@", [NSThread currentThread]);
/*
NSURLSessionResponseCancel = 0, // 通过delegate来做HTTP请求结束的回调默认情况下接收到响应头后会取消后续的请求,即内容会调用[dataTask cancel]
NSURLSessionResponseAllow = 1, // 允许后续的请求
NSURLSessionResponseBecomeDownload = 2, // 把当前请求转变成一个下载请求
NSURLSessionResponseBecomeStream = 3, // 把当前请求转变成一个下载请求(iOS9之后可用)
*/
completionHandler(NSURLSessionResponseAllow);
// 获取到数据的总大小(注意是获得本次请求数据的总大小,如果我们使用了断点下载,那么这个值很可能就是某一段数据的大小,而非整个数据的大小,所以得加上之前的数据大小)
self.totalSize = response.expectedContentLength + self.currentSize;
if (self.currentSize <= 0) { // 如果之前没创建过再创建
[[NSFileManager defaultManager] createFileAtPath:self.cachePath contents:nil attributes:nil];
}
// 创建fileHandle,以便往文件里写数据
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.cachePath];
// 先把fileHandle的指针移动到末尾
[self.fileHandle seekToEndOfFile];
}
/// 接收到响应体的回调
///
/// data: 指得是本次接收到的数据
/// ——如果数据量很小的话,这个方法只触发一次就接收完了,然后会触发请求结束的回调
/// ——如果数据量很大的话,这个方法会触发多次,一段一段地接收,全部接收完后会触发请求结束的回调
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"222---%@", [NSThread currentThread]);
// 获取到已经下载的数据大小,并计算下载进度
self.currentSize += data.length;
// 然后再通过fileHandle往文件里写数据
[self.fileHandle writeData:data];
NSLog(@"下载进度---%f", 1.0 * self.currentSize / self.totalSize);
}
/// 请求结束的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"333---%@", [NSThread currentThread]);
if (error != nil) {
NSLog(@"请求失败了");
} else {
NSLog(@"请求成功了");
}
// 释放fileHandle
[self.fileHandle closeFile];
self.fileHandle = nil;
// 释放dataTask
if (_dataTask != nil) {
self.currentSize = 0;
self.request = nil;
self.session = nil;
self.dataTask = nil;
}
}
#pragma mark - setter, getter
// 第1步:创建一个HTTP请求——即NSURLRequest
- (NSMutableURLRequest *)request {
if (_request == nil) {
_request = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
_request.HTTPMethod = @"GET";
// 设置HTTP请求的URL
_request.URL = [NSURL URLWithString:[@"https://view.2amok.com/2019913/9660e7b5ef2a6fe4ea2328d91069f9eb.mp4" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
// 先获取一下之前已经下载到磁盘里数据的大小
NSDictionary *fileInfoDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:self.cachePath error:nil];
NSLog(@"fileInfoDictionary---%@", fileInfoDictionary);
self.currentSize = [fileInfoDictionary[@"NSFileSize"] integerValue];
// 设置HTTP请求的请求头,告诉服务器要获取哪一部分的数据
[_request setValue:[NSString stringWithFormat:@"bytes=%ld-", self.currentSize] forHTTPHeaderField:@"Range"];
}
return _request;
}
/*
第2步:创建一个session
configuration: 配置信息
delegate: 代理
delegateQueue: 设置代理方法在哪个线程里调用,mainQueue就代表在主线程里调用
*/
- (NSURLSession *)session {
if (_session == nil) {
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
}
return _session;
}
// 第3步:创建一个dataTask
- (NSURLSessionDataTask *)dataTask {
if (_dataTask == nil) {
_dataTask = [self.session dataTaskWithRequest:self.request];
}
return _dataTask;
}
// 这里我们把文件存储到Library/Cache目录下
- (NSString *)cachePath {
if (_cachePath == nil) {
_cachePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"];
NSLog(@"文件路径---%@", _cachePath);
}
return _cachePath;
}
@end
5、支持HTTPS
- 1️⃣当我们用NSURLSession发起一个HTTPS请求后,其实服务器并不会立马把真正的数据发给我们,而是先发一个证书给我们;(这部分知识可以参考下应用层:HTTPS)
- 2️⃣当我们收到这个证书后,就会触发NSURLSession的一个收到证书的回调,我们要在这个回调里来决定如何对待证书,所以使用NSURLSession支持HTTPS很简单,我们只要实现它的
didReceiveChallenge
这个代理方法就可以了。(当然我们访问一些大型网站时——比如苹果官网,即便是HTTPS请求也不会触发这个回调,这是因为这些大型网站的证书已经被内置安装在系统里了)
#import "HTTPSViewController.h"
@interface HTTPSViewController () <NSURLSessionDataDelegate>
@end
@implementation HTTPSViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"支持HTTPS";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
request.HTTPMethod = @"GET";
request.URL = [NSURL URLWithString:[@"https://www.12306.cn/index/" stringByAddingPercentEscapesUsingEncoding:(NSUTF8StringEncoding)]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
NSLog(@"请求失败了---%@", error);
} else {
NSString *jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
NSLog(@"请求成功了---%@", jsonString);
}
});
}];
[dataTask resume];
}
#pragma mark - NSURLSessionDataDelegate
/// 收到证书的回调
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
/*
第一个参数:如何对待证书
NSURLSessionAuthChallengeUseCredential = 0, // 安装并使用该证书
NSURLSessionAuthChallengePerformDefaultHandling = 1, // 默认,忽略该证书
NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, // 忽略该证书,并取消请求
NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 拒绝该证书
第二个参数:身份验证
*/
completionHandler(NSURLSessionAuthChallengeUseCredential, [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust]);
}
}
@end
二、AFNetworking源码分析
其实AFN的三大核心模块就是我们上面所实现的序列化器模块(创建NSURLRequest + 解析NSURLResponse)、session和task管理模块、支持HTTPS模块,当然除此之外它还提供了非常方便易用的网络状态监听模块和UIKit分类模块(类似于SDWebImage的UIKit分类模块)。
1、序列化器模块
1.1 请求序列化器
请求序列化器主要用来创建一个HTTP请求——即NSURLRequest,这其中就包含了设置HTTP请求的方法、设置HTTP请求的URL + URL编码、设置HTTP请求的请求头、设置HTTP请求的Body + Body编码等内容。AFN提供了多种请求序列化器,不过常用的就两种AFHTTPRequestSerializer
和AFJSONRequestSerializer
:
AFHTTPRequestSerializer
:默认,主要用来创建一个GET请求 || 采用表单提交的POST请求 || 文件上传请求。
/// 单例
+ (instancetype)serializer {
return [[self alloc] init];
}
/// 初始化方法
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 设置默认的URL编码方式和Body编码方式为UTF-8编码
self.stringEncoding = NSUTF8StringEncoding;
// 存储在这个Set里的请求——即GET请求、HEAD请求、DELETE请求这三种请求的入参会被编码后拼接到URL后面
self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];
return self;
}
/// 创建一个GET请求 || 采用表单提交的POST请求
///
/// method: 请求方法,@"GET"、@"POST"等
/// URLString: 请求的URL
/// parameters: 入参
///
/// return 一个HTTP请求——即NSURLRequest
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters {
// 创建一个HTTP请求——即NSURLRequest
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] init];
// 设置HTTP请求的方法
mutableRequest.HTTPMethod = method;
// 设置HTTP请求的URL
mutableRequest.url = [NSURL URLWithString:URLString];
// URL编码 + Body编码 + Body格式组织
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters] mutableCopy];
return mutableRequest;
}
/// URL编码 + Body编码 + Body格式组织
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
/**
实际开发中,我们的入参主要是个字典,因此这里主要考虑字典。
把入参字典转换成特定格式的字符串,并采用UTF-8编码对转换后的字符串进行编码
比如入参字典是这样的:
@{
@"username": @"123",
@"password": @"456",
}
那么query最终会是这样的字符串:
@"username=123&password=456"
*/
NSString *query = AFQueryStringFromParameters(parameters);;
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
// 如果是GET请求(我们主要分析GET请求和POST请求),那就把入参直接拼接到URL后面
if (query) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
// 如果是POST请求(我们主要分析GET请求和POST请求)
// 那就设置请求头的@"Content-Type"字段为@"application/x-www-form-urlencoded",代表默认采用表单提交
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
// 把入参放到POST请求的请求体里,表单提交请求体的格式正好就是@"username=123&password=456"这样的格式
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
回顾一下文件上传请求体的格式:
Wireshark抓包AFNetworking
--分隔符
回车换行
Content-Disposition: form-data; name="file"; filename="123.jpg"
回车换行
Content-Type: image/jpg
回车换行
回车换行
数据
回车换行
--分隔符--
/// 创建一个文件上传请求
///
/// 文件上传请求本质来说就是一个POST请求,只不过:
/// ——这个POST请求的请求头里必须设置Content-Type字段为multipart/form-data; boundary=xxx
/// ——请求体也必须得严格按照指定的格式去拼接数据
- (NSMutableURLRequest *)multipartFormRequestWithURLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
NSMutableURLRequest *mutableRequest = [self requestWithMethod:@"POST" URLString:URLString parameters:nil error:nil];
// 创建一个formData
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
// 把formData回调出去,好让外界往上拼接数据
if (block) {
block(formData);
}
// 把设置好请求头并且组织好请求体格式的request给返回
return [formData requestByFinalizingMultipartFormData];
}
// 随机生成的Boundary
#define kBoundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx"
// 回车换行
#define kNewLine @"\r\n"
/// 我们会调用AFMultipartFormData的这个方法往formData上拼接数据,下面的self就等于上面创建一个上传任务请求时block回调出来的formData
///
/// data: 要上传的数据
/// name: 服务器规定的参数,一般填file,和服务端商量好
/// filename: 文件保存到服务器上面的名称
/// mimeType: 文件的类型,大类型/小类型
///
/// 伪代码,但原理就是这样
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType {
[self appendData:[[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
[self appendData:kNewLine];
[bodyData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"", name, fileName] dataUsingEncoding:NSUTF8StringEncoding]];
[bodyData appendData:kNewLine];
[bodyData appendData:[[NSString stringWithFormat:@"Content-Type: %@", mimeType] dataUsingEncoding:NSUTF8StringEncoding]];
[bodyData appendData:kNewLine];
[bodyData appendData:kNewLine];
[bodyData appendData:data];
[bodyData appendData:kNewLine];
[bodyData appendData:[[NSString stringWithFormat:@"--%@--", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
}
AFJSONRequestSerializer
:主要用来创建一个采用JSON提交的POST请求。
// 单例
+ (instancetype)serializer {
return [[self alloc] init];
}
/// 初始化方法
- (instancetype)init {
// 因为AFJSONRequestSerializer继承自AFHTTPRequestSerializer
// 所以这里会调用父类AFHTTPRequestSerializer的init方法,见上面
self = [super init];
if (!self) {
return nil;
}
return self;
}
/// 创建采用JSON提交的POST请求
///
/// method: 请求方法,@"GET"、@"POST"等
/// URLString: 请求的URL
/// parameters: 入参
///
/// return 一个HTTP请求——即NSURLRequest
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters {
// 直接调用父类AFHTTPRequestSerializer的同名方法,见上面
return [super requestWithMethod:method URLString:URLString parameters:parameters];
}
/// Body编码 + Body格式组织
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
if (parameters) {
// 设置请求头的@"Content-Type"字段为@"application/json",代表采用JSON提交
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
}
/**
把入参字典转换成特定格式的字符串,并采用UTF-8编码对转换后的字符串进行编码
比如入参字典是这样的:
@{
@"username": @"123",
@"password": @"456",
}
那么httpBody最终会是这样的字符串:
@"{\"username\": \"123\", \"password\": \"456\"}"
*/
NSData *httpBody = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:nil];
// 把入参放到POST请求的请求体里
[mutableRequest setHTTPBody:httpBody];
}
return mutableRequest;
}
1.2 响应序列化器
响应序列化器主要用来决定采用什么方式解析响应体。AFN提供了多种响应序列化器,不过常用的就两种AFHTTPResponseSerializer
和AFJSONResponseSerializer
:
AFHTTPResponseSerializer
:不对响应体做解析,直接把服务器返回的二进制数据暴露给我们开发者(NSData )。所以如果我们想以二进制的格式接收响应体就用这个,这样就可以拿着二进制数据做一些自定义的处理。
/// 单例
+ (instancetype)serializer {
return [[self alloc] init];
}
/// 初始化方法
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 设置对响应体的解码方式,默认为UTF-8
self.stringEncoding = NSUTF8StringEncoding;
// 设置可接收的MIMEType,默认为nil
self.acceptableContentTypes = nil;
// 设置可接收的状态码,默认为200~299
self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
return self;
}
/// 验证响应的有效性
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError * __autoreleasing *)error {
// 1、默认响应是有效的
BOOL responseIsValid = YES;
NSError *validationError = nil;
// 2、如果响应存在 && 响应是NSHTTPURLResponse
if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
// 2.1 如果self.acceptableContentTypes != nil && 不包含响应的MIMEType,就判定为响应无效从而报错(这就是为什么我们在开发中会遇到这个报错的原因,设置一下acceptableContentTypes就可以了)
if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]]) {
if ([data length] > 0 && [response URL]) {
// 报@"Request failed: unacceptable content-type: %@"这么个错
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError);
}
// 判定为响应无效
responseIsValid = NO;
}
// 2.2 同上
if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) {
NSMutableDictionary *mutableUserInfo = [@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode],
NSURLErrorFailingURLErrorKey:[response URL],
AFNetworkingOperationFailingURLResponseErrorKey: response,
} mutableCopy];
if (data) {
mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data;
}
validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError);
responseIsValid = NO;
}
}
if (error && !responseIsValid) {
*error = validationError;
}
return responseIsValid;
}
/// data: 响应体,服务器返回的二进制数据
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error {
// 验证响应的有效性
[self validateResponse:(NSHTTPURLResponse *)response data:data error:error];
// 不做解析,直接返回
return data;
}
AFJSONResponseSerializer
:默认,以JSON格式解析响应体,也就是说AFN会把服务器返回的二进制数据当成一个JSON字符串来对待,把它解析成相应的OC字典或OC数组暴露给我们开发者(NSData -> NSDictionary、NSArray)。所以如果我们想以OC字典或OC数组的格式接收响应体就用这个,这样我们就可以直接拿着响应体使用了,就不用自己做(NSData -> NSDictionary、NSArray)的解析了。
/// 单例
+ (instancetype)serializer {
return [[self alloc] init];
}
/// 初始化方法
- (instancetype)init {
// 因为AFJSONResponseSerializer继承自AFHTTPResponseSerializer
// 所以这里会调用父类AFHTTPResponseSerializer的init方法,见上面
self = [super init];
if (!self) {
return nil;
}
// 设置可接收的MIMEType,包括[@"application/json", @"text/json", @"text/javascript"]
self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
return self;
}
/// 验证响应的有效性
- (BOOL)validateResponse:(NSHTTPURLResponse *)response
data:(NSData *)data
error:(NSError * __autoreleasing *)error {
// 直接调用父类AFHTTPResponseSerializer的同名方法,见上面
return [super validateResponse:response data:data error:error];
}
/// data: 响应体,服务器返回的二进制数据
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error {
// 验证响应的有效性,如果验证失败,不做解析,直接返回
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
return nil;
}
// 对响应体的解码方式为UTF-8
NSStringEncoding stringEncoding = self.stringEncoding;
id responseObject = nil;
NSError *serializationError = nil;
// 把服务器返回的二进制数据当成JSON字符串来对待
// 把它解析成相应的OC字典或OC数组暴露给我们开发者(NSData -> NSDictionary、NSArray)
if ([data length] > 0) {
responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
} else {
return nil;
}
// 删除响应体中的NULL
if (self.removesKeysWithNullValues && responseObject) {
responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}
if (error) {
*error = AFErrorWithUnderlyingError(serializationError, *error);
}
return responseObject;
}
2、session和task管理模块
我们都知道AFN 3.0是基于NSURLSession实现的,AFURLSessionManager和AFHTTPSessionManager这两个类就创建并管理着session和不同类型的task。其中AFHTTPSessionManager继承自AFURLSessionManager,是专门为HTTP请求设计的,它内部封装了session和task的调用过程,给我们提供了一堆非常方便易用的API,实际开发中我们就是用它来发起一个来自序列化器模块创建好的网络请求,接下来我们就看看AFHTTPSessionManager的内部实现。
- 创建
/// 单例
+ (instancetype)manager {
return [[[self class] alloc] initWithBaseURL:nil];
}
/// 初始化方法
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
// 可见AFN默认的请求序列化器为AFHTTPRequestSerializer
self.requestSerializer = [AFHTTPRequestSerializer serializer];
// 可见AFN默认的响应序列化器为AFJSONResponseSerializer
self.responseSerializer = [AFJSONResponseSerializer serializer];
return self;
}
- GET请求
可见AFN的GET请求就是通过NSURLSession的dataTask发起的,然后把各种delegate回调封装成了block暴露给我们使用;至于session的创建代码比较深,这里就不贴了。
// GET请求不带进度
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
return [self GET:URLString parameters:parameters progress:nil success:success failure:failure];
}
// GET请求带进度
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))downloadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET"
URLString:URLString
parameters:parameters
uploadProgress:nil
downloadProgress:downloadProgress
success:success
failure:failure];
[dataTask resume];
return dataTask;
}
- POST请求
可见AFN的POST请求也是通过NSURLSession的dataTask发起的,然后把各种delegate回调封装成了block暴露给我们使用;至于session的创建代码比较深,这里就不贴了。
// POST请求不带进度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
return [self POST:URLString parameters:parameters progress:nil success:success failure:failure];
}
// POST请求带进度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST"
URLString:URLString
parameters:parameters
uploadProgress:uploadProgress
downloadProgress:nil
success:success
failure:failure];
[dataTask resume];
return dataTask;
}
- 文件上传请求
可见AFN的文件上传请求就是通过NSURLSession的uploadTask发起的,然后把各种delegate回调封装成了block暴露给我们使用;至于session的创建代码比较深,这里就不贴了。
跟我们自己写的不同的地方在于,我们是把请求体放进了uploadTask里,而AFN是暴露了一个block出来,让我们把请求体拼接到formData上,然后它把请求体通过流的方式放进了request里。
// 文件上传请求不带进度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id<AFMultipartFormData> _Nonnull))block
success:(nullable void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure {
return [self POST:URLString parameters:parameters constructingBodyWithBlock:block progress:nil success:success failure:failure];
}
// 文件上传请求带进度
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
if (serializationError) {
if (failure) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
#pragma clang diagnostic pop
}
return nil;
}
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
return task;
}
- 文件下载请求
可见AFN的文件下载请求就是通过NSURLSession的downloadTask发起的,然后把各种delegate回调封装成了block暴露给我们使用;至于session的创建代码比较深,这里就不贴了。
使用AFN做文件下载,我们得自己创建NSURLRequest,再调用它提供的API。同时针对下载需要注意的几个问题,AFN提供了下载进度的回调,AFN提供了边下载边存储到磁盘里、我们只需要告诉它下载到哪里即可,AFN支持断点下载但不支持离线断点下载。
// 不搞断点下载就用这个
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
__block NSURLSessionDownloadTask *downloadTask = nil;
url_session_manager_create_task_safely(^{
downloadTask = [self.session downloadTaskWithRequest:request];
});
[self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];
return downloadTask;
}
// 想搞断点下载就用这个
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {
__block NSURLSessionDownloadTask *downloadTask = nil;
url_session_manager_create_task_safely(^{
downloadTask = [self.session downloadTaskWithResumeData:resumeData];
});
[self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];
return downloadTask;
}
3、支持HTTPS模块
/// 伪代码
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 如果证书有效,则安装并使用该证书
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
// 如果证书无效,则拒绝该证书
disposition = NSURLSessionAuthChallengeRejectProtectionSpace;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
可见AFN支持HTTPS也是通过NSURLSession收到证书的回调来实现的,只不过它比我们自己实现的多了一步验证证书有效性的操作(AFSecurityPolicy这个类就是用来验证证书有效性的),接下来我们就看看它是怎么验证的。
/**
是否允许无效证书(即自建证书,大多数网站的证书都是权威机构颁发的,但是12306以前的证书就是自建的)或过期证书
默认为NO
*/
@property (nonatomic, assign) BOOL allowInvalidCertificates;
/**
是否验证证书中的域名
默认为YES
假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议保持为YES
置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的
如置为NO,建议自己添加对应域名的校验逻辑
*/
@property (nonatomic, assign) BOOL validatesDomainName;
// 对服务器发过来的证书采取什么验证策略
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone, // 默认,不验证证书
AFSSLPinningModePublicKey, // 验证证书中的公钥,通过就代表证书有效,不通过就代表证书无效
AFSSLPinningModeCertificate, // 把服务器发过来的证书和本地证书做完全对比,通过就代表证书有效,不通过就代表证书无效
};
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain {
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
return NO;
}
// 验证证书中的域名处理
NSMutableArray *policies = [NSMutableArray array];
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
// 无效证书或自建证书处理
if (self.SSLPinningMode == AFSSLPinningModeNone) {
if (self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust)) {
return YES;
} else {
return NO;
}
} else if (!self.allowInvalidCertificates && !AFServerTrustIsValid(serverTrust)) {
return NO;
}
// 对服务器发过来的证书采取什么验证策略处理
switch (self.SSLPinningMode) {
case AFSSLPinningModeCertificate: { // 把服务器发过来的证书和本地证书做完全对比
/*
self.pinnedCertificates:这个属性保存着所有可用于跟服务器证书做完全对比的本地证书集合
AFNetworking会自动搜索工程中所有.cer的证书文件,并把它们添加到这个属性中
所以我们在使用AFN的做HTTPS的时候就可能需要把服务器的证书转换成cer文件放到项目里,但很多时候我们都采用了默认验证策略——不验证证书,所以不放进来也没事
*/
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
// 判断本地证书和服务器证书是否相同
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: { // 验证证书中的公钥
NSUInteger trustedPublicKeyCount = 0;
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
// 如果包含这个公钥就通过
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
default:
return NO;
}
return NO;
}
可见证书的验证主要分三步:是否允许无效证书或自建证书、是否验证证书中的域名和对服务器发过来的证书采取什么验证策略。
网友评论