[iOS开发]网络请求合集
NSURLSessionTaskTransactionMetrics
经常用到网络请求,想着总结一下iOS
网络请求的部分
iOS
开发中的网络下载方式包括NSData
(最原始,实际开发基本不会用),NSURLConnection
(古老又过气的苹果原生网络框架),NSURLSession
(现在流行的苹果网络框架),AFNetworking
,SDWebImage
以及基于AFNetworking
的二次封装框架例如XMNetworking
,HYBNetworking
等等。
NSData + NSURL
方式NSString -> NSURL -> NSData -> UIImage
URLWithString dataWithContentsOfURL:url imageWithData:data
// 在子线程中发送下载文件请求
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 创建下载路径
NSURL *url = [NSURL URLWithString:@"......"];
// NSData的dataWithContentsOfURL:方法下载
NSData *data = [NSData dataWithContentsOfURL:url];
// 回到主线程,刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = [UIImage imageWithData:data];
});
});
iOS9.0
之后,以前使用的NSURLConnection
过期,苹果推荐使用NSURLSession
来替换NSURLConnection
来完成网络请求相关操作NSURLSession
的优势:
NSURLSession
支持http2.0
协议session
发送多次请求,只需要建立一次连接(复用了TCP
)session
并且可以统一配置,使用更加方便NSURLSessionTask
及其子类
NSURLSessionTask
本身是一个抽象类,在使用时,通常是根据具体的需求使用它的几个子类:NSURLSessionDataTask
可以用来发送常见的Get
,Post
请求,既可以用来上传也可以用来下载NSURLSessionDownloadTask
可以用来发送下载请求,专门用来下载数据NSURLSessionUploadTask
可以用来发送上传请求,专门用来上传数据下面讲解一下GET和POST两种请求的用法,由于下方的讲解可能比较局限,想要参考来实际使用的话请参考该博客:[iOS]-POST和GET网络请求
过程如下:
GET
请求参数直接跟在URL
后面GET
】)NSURLSession
)NSURLSessionDataTask
)Task
API
,通过request
来提供参数(当然requese
是基于URL
的) //1. 确定请求路径
NSURL *url = [NSURL URLWithString:@"http://news-at.zhihu.com/api/4/news/latest"];
//2. 创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3. 创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//4. 根据会话对象创建请求任务
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
//6.解析服务器返回的数据
//说明:(此处返回的数据是JSON格式的,因此使用NSJSONSerialization进行反序列化处理)
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"%@",dict);
}
}];
[task resume];
API
,直接通过URL
来提供参数dataTaskWithURL:completionHandler:
这两个API
就使用上来说无非就是第一个使用request
将URL
进行了封装,但是要注意直接通过URL
来提供请求对象的方法在POST
请求种不能使用,因为POST
请求需要设置请求头
和请求体
这些额外的东西。
代理形式无非就是将下载的任务在代理函数中去执行:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//1. 确定请求路径
NSURL *url = [NSURL URLWithString:@"http://news-at.zhihu.com/api/4/news/latest"];
//2. 创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3. 创建会话对象
// 第一个参数:会话对象的配置信息defaultSessionConfiguration 表示默认配置
// 第二个参数:谁成为代理,此处为控制器本身即self
// 第三个参数:队列,该队列决定代理方法在哪个线程中调用
// 可以传主队列、非主队列 如果不指定线程,则completionHandler和delegate的回调方法,都会在子线程中执行。
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//4. 根据会话对象创建请求任务
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
[task resume];
}
//1.接收到服务器响应的时候调用该方法
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSLog(@"%@",response);
completionHandler(NSURLSessionResponseAllow);
//注意:需要使用completionHandler回调告诉系统应该如何处理服务器返回的数据
//默认是取消的
/*
NSURLSessionResponseCancel = 0, 默认的处理方式,取消
NSURLSessionResponseAllow = 1, 接收服务器返回的数据
NSURLSessionResponseBecomeDownload = 2,变成一个下载请求
NSURLSessionResponseBecomeStream 变成一个流
*/
}
//2.接收到服务器返回数据的时候会调用该方法,如果数据较大那么该方法可能会调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"%@",data);
}
//3.当请求完成(成功|失败)的时候会调用该方法,如果请求失败,则error有值
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"%@",error);
}
简单来说 POST
请求需要另外单独设置request.HTTPMethod
属性
//1.创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//2.根据会话对象创建task
NSURL *url = [NSURL URLWithString:@"http://116.62.21.180:8088/user/get_detail"];
//3.创建可变的请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//4.修改请求方法为POST
request.HTTPMethod = @"POST";
//5.设置请求体
request.HTTPBody = [@"phone=xxxxxxxxxxxx" dataUsingEncoding:NSUTF8StringEncoding];
//设置请求头,这个很重要,一定要按照服务器接口的需求设置对应的请求头
[requestTest addValue:@"application/json;UTF-8" forHTTPHeaderField:@"Content-Type"];
//6.根据会话对象创建一个Task(发送请求)
/*
第一个参数:请求对象
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
*/
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (!error) {
//8.解析数据
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"%@",dict);
} else {
NSLog(@"%@",error);
}
}];
//7.执行任务
[dataTask resume];
POST
传输的数据都有以下格式(都是设置Content-Type
时的几种类型):
application/json
:现在非常常用的一种,用来告诉服务器端消息主体是序列化后的JSON
字符串application/x-www-form-urlencoded
:提交的数据按照key1=val1&key2=val2
的方式进行编码,key
和val
都进行了URL
转码multipart/form-data
:本次 请求的boundary
是什么内容,消息主体例就按照字段个数又分为多个结构类似的部分,被部分都是以-- boundary
开始,紧接着是内容描述消息,然后是回车,最后是字段具体内容(文本或二进制)。text/xml
: 这种直接传的xml
格式区别:
GET
向服务器获取数据,POST
向服务器发送请求GET
会把查询字符串的参数追加到URL
的末尾,POST
请求则把数据作为请求的主体来提交,可以包含非常多的数据,因此客户可以看到GET
提交的参数,POST
则不可以GET
请求提交的数据直接加到URL
的末尾,所以大小有限制,而POST
则没有POST
安全性比GET
高get
方式,服务器端有Request.QueryString
来获取变量的值,对于post
方式,服务器用Request.Form
来获取提交的数据get
形式的URL
对搜索引擎更加友好,可以提高搜索引擎排名。而POST
甚至会阻止爬虫和搜索引擎的访问HTML
表单内的输入字段名称的总长不超过1024
个字节iOS9
为了增强数据访问安全,将所有的HTTP
请求都改为了 HTTPS
,现在苹果默认支持请求的URL
格式是HTTPS
,使用HTTP
则会被认定为不安全从而请求失败
若一定要使用HTTP
,就需要对Info
文件进行一些配置,如下:
我们需要在App Transport Security Settings
下创建Allow Aebitrary Loads
,并将其Type
为Boolean
类型的Value
设置为YES
;如本身就没有App Transport Security Settings
的话则需要创建一个
那么HTTP
与HTTPS
有什么区别呢?
HTTP
协议运行在TCP
之上,明文运输,客户端与服务器端都无法验证对方的身份;HTTPS
是身披SSL
(Secure Socket Layer
)外壳的Http
,运行于SSL
上,SSL
运行在TCP
上,是添加了加密和认证机制的HTTP
。Https
的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制
Http
和Https
使用不同的连接方式,http
是80
,https
是443
Http
通信相比,Https
通信会由于加减密处理消耗更多的CPU
和内存资源Https
通信需要证书,而证书一般需要向认证机构购买NSURLSession
创建的Task
任务,只能在任务结束的completionHandler
的block
中获取到结束后的数据,想要使用这些数据的话也需要等到下载完成了,才能拿来使用,至于下载的过程中,想要使用数据不可能。而NSURlSessionConfiguration
就是一个代理,是为了监控下载过程
的。
所以除了上面的两种session
的创建方式sharedSession
、sessionWithConfiguration:delegate:delegateQueue:
,还有sessionWithConfiguration:
可以帮助我们监控下载过程
NSURLSessionConfiguration
可以设置请求的Cookie
、密钥
、缓存
、请求头
等参数,将网络请求的一些配置参数从NSURLSession
中分离出来
NSURLSessionConfiguration
提供defaultSessionConfiguration
的方式创建,但这并不是单例方法,而是类方法,创建的是不同对象。通过这种方式创建的configuration
,并不会共享cookie
、cache
、密钥
等,而是不同configuration
都需要单独设置
这里URL
为http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4
一个在线的视频流
简单,通过Session
创建一个downloadTask
,并调用resume
即可开启一个下载任务(此处例子用的是delegate
代理的请求方式)
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURL *url = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
// 从服务器接收数据,下载进度回调
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
}
// 下载完成后回调
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
}
当然我们也可以调用suspend
将下载任务挂起
,随后调用resume
方法继续下载
任务,suspend
和resume
需要是成对的。但是suspend
挂起任务是有超市设置的,默认是60s
,如果超时系统会将TCP连接断开
,我们再调用resume
是失效的。可以通过NSURLSessionConfiguration
的timeoutIntervalForResource
来设置上传和下载的资源耗时。suspend
只针对于下载任务,其他任务挂起后将会重新开始
断点续传就是从文件上次中断的地方重新开始下载或者上传数据,而不是从头开始
如果使用断点续传,不仅仅是客户端的工作,还需要服务器支持断点续传功能,否则无法生成正确的resumeData
如何验证服务器是否支持断点续传呢?
我们可以通过下载文件的时候的响应头来查看:
根据苹果官方文档的描述:
只有满足以下条件,才能恢复下载:
- 自您第一次请求资源以来,
资源没有变化
- 该任务是
HTTP
或HTTPS
GET
请求必须是get请求
- 服务器在其响应中提供
ETag
或者Last-Modified
标题(或两者) 和第一条基本上是相同- 服务器支持字节范围请求
如:服务器响应头包含accept-range:Btyes
- 系统尚未删除临时文件以响应磁盘空间压力
本地缓存文件存在
满足上述条件的,才能使用断点续传功能
其次,使用断点续传的时候,客户端请求的range
是比如说19799+
,服务器此时返回的文件的部分数据,服务器响应码
必须是206
,不可以是200
或者其他状态码
,否则客户端会从头下载
。
HTTP
协议支持断点续传操作,在开始下载请求时通过请求头设置Range
字段,标示从什么位置开始下载
Range:bytes=512000-
服务端收到客户端请求后,开始从512kb
的位置开始传输数据,并通过Content-Range
字段告知客户端传输数据的开始位置
Content-Range:bytes 512000-/1024000
downloadTask
任务开始请求后,可以调用cancelByProducingResumeData:
方法可以取消下载,并且可以获得一个resumeData
,resumeData
中存放一些断点下载的信息。可以将resumedata
写到本地,后面通过这个文件可以进行断点续传。
NSString *library = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];//以URL为值,创建一个path
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
[resumeData writeToFile:resumePath atomically:YES];//将得到的resumeData传入url的路径中
}];
在创建下载任务前,可以判断当前任务有没有之前待恢复的任务,如果有的话调用downloadTaskWithResumeData:
方法并传入一个resumeData
,可以恢复之前的下载,并重新创建一个downloadTask
任务:
NSString *library = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];
NSData *resumeData = [[NSData alloc] initWithContentsOfFile:resumePath];
self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];
[self.downloadTask resume];
通过suspend
和resume
这种方式挂起的任务,downloadTask
是同一个对象,而通过cancel
然后resumeData
恢复的任务,会创建一个新的downloadTask
任务
当调用downloadTaskWithResumeData
方法恢复下载之后,会产生回调下面的方法:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
fileOffset
是上次文件的下载大小
expectedTotalBytes
是预估的文件总大小
通过这两个参数,我们也能给出某些比例之类的。比如下载了多少等等
NSURLSessionTaskTransactionMetrics
是封装在执行会话任务期间收集的性能指标的对象,其中的属性都是用来做统计的,功能都是记录某个值,并没有逻辑上的意义
每个NSURLSessionTaskTransactionMetrics
对象由一个请求
和响应
属性组成,对应于相应任务的请求和响应
它还包含时间度量,从fetchStartDate
开始,以responseEndDate
结束,以及其他特征,如networkProtocolName
和resourceFetchType
该类定义在任务执行期间为请求/响应事务收集的性能指标:
typedef NS_ENUM(NSInteger, NSURLSessionTaskMetricsResourceFetchType) {
NSURLSessionTaskMetricsResourceFetchTypeUnknown, 无法确定获取资源的方式。
NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad, 资源是通过网络加载的。
NSURLSessionTaskMetricsResourceFetchTypeServerPush, 资源由服务器推送到客户机。
NSURLSessionTaskMetricsResourceFetchTypeLocalCache, 从本地存储中检索资源。
} API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
下面我们就简单展示一下其收集性能指标的顺序逻辑,具体各个属性的含义大家可以详见该博客:NSURLSessionTaskTransactionMetrics
苹果官方文档对其的讲解如下:
翻译版如下:
图里面的这个流程图解释的非常详细,还有如果任务由于什么原因失败的话,domainLookupEndDate
属性和随后要记录的属性值都会为nil
。
可以看到加密的安全连接部分是TLS
的连接部分,我们在这里浅浅介绍一下TLS
:
安全传输层协议(TLS
)用于在两个通信应用程序之间提供保密性和数据完整性
该协议由两层组成: TLS
记录协议(TLS Record
)和 TLS
握手协议(TLS Handshake
)
@interface NSURLSessionTaskTransactionMetrics : NSObject
// 请求对象
@property (copy, readonly) NSURLRequest *request;
// 响应对象,请求失败可能会为nil
@property (nullable, copy, readonly) NSURLResponse *response;
// 请求开始时间
@property (nullable, copy, readonly) NSDate *fetchStartDate;
// DNS解析开始时间
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;
// DNS解析结束时间,如果解析失败可能为nil
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;
// 开始建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectStartDate;
// 结束建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectEndDate;
// 开始TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
// 结束TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
// 开始传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestStartDate;
// 结束传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestEndDate;
// 接收到服务端响应数据时间
@property (nullable, copy, readonly) NSDate *responseStartDate;
// 服务端响应数据传输完成时间
@property (nullable, copy, readonly) NSDate *responseEndDate;
// 网络协议,例如http/1.1
@property (nullable, copy, readonly) NSString *networkProtocolName;
// 请求是否使用代理
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
// 是否复用已有连接
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
// 资源标识符,表示请求是从Cache、Push、Network哪种类型加载的
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
// 本地IP
@property (nullable, copy, readonly) NSString *localAddress;
// 本地端口号
@property (nullable, copy, readonly) NSNumber *localPort;
// 远端IP
@property (nullable, copy, readonly) NSString *remoteAddress;
// 远端端口号
@property (nullable, copy, readonly) NSNumber *remotePort;
// TLS协议版本,如果是http则是0x0000
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;
// 是否使用蜂窝数据
@property (readonly, getter=isCellular) BOOL cellular;
//两个初始化方法
- (instancetype)init API_DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));
+ (instancetype)new API_DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));
@end
可以看到里面有一堆属性,然后还有一个init
方法和一个new
方法。