iOS9.0之前,网络请求都是通过NSURLConnection来获取远程服务器数据,iOS9.0之后苹果已弃用了NSURLConnection,使用了NSURLSession来替代之,功能强大的NSURLSession让我们获取数据更加得心应手!
NSURLConnection
- 发送请求方式
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://pic1.nipic.com/2008-08-14/2008814183939909_2.jpg"]];
// 异步请求方式
// 第一种
[NSURLConnection connectionWithRequest:request delegate:self];
// 第二种
NSURLConnection* connect = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connect start];
// 第三种 startImmediately设置为YES则不用手动设置start方法,会自动执行请求
NSURLConnection* connect2 = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connect2 start];
// 第四种
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
}];
// 同步请求方式
NSURLResponse* response = nil;
NSError* error = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
- GET请求
通过将请求地址URL生成NSURLRequest对象,进而通过NSURLConnection方法实现请求!NSURLRequest不允许设置额外的参数!!
- POST请求
不仅可以通过将请求地址URL生成NSMutableURLRequest对象实现网络请求!还可以设置一些额外的参数!!
// 设置请求方法、请求体、请求超时时间、请求首部参数等...
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://pic1.nipic.com/2008-08-14/2008814183939909_2.jpg"]];
request.HTTPMethod = @"POST";
request.timeoutInterval = 60.0;
NSString* bodyString = [NSString stringWithFormat:@"name=%@&age=%@", @"mjz", @"100"];
request.HTTPBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
[request setValue:@"iOS 11.0" forHTTPHeaderField:@"User-Agent"];
- Delegate
// 重定向URL
- (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response;
// 接收请求返回数据,可获取到请求数据的一些信息,例如:response.expectedContentLength
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
// 服务器响应的数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
// 设置加载进度
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;
// 缓存响应
- (nullable NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse;
// 获取数据成功
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
注意细节:
- 苹果要求我们更新UI在主线程上执行,所以在请求数据成功后涉及到更新UI务必在主线程上;虽然在主线程上请求,代理回调也是在主线程上,但是可能用户是在子线程上开启的请求,此时就不一定在主线程上了;
- 在子线程上开启请求时,会将NSURLConnection对象加入到当前对应的RunLoop中,当我们在子线程中进行网络请求,默认子线程的RunLoop不会自动创建,NSURLConnection对象会被释放,因此我们需要开启子线程中的RunLoop,保证NSURLConnection对象不会被释放;
NSURLSession
- NSURLSession
NSURLSession是用于网络数据请求的应用会话,可通过会话创建下载、上传、获取数据等任务形式,向服务器端请求数据!
NSURLSession分为几种:
- 单例sharedSession : 有一定的局限性,可快速生成NSURLSession对象开启task任务;
- 自定义session : 通过自定义配置文件NSURLSessionConfiguration,并设置代理,大多数情况下被使用;
- 后台session : 也是通过自定义session,只不过主要用于后台上传或下载任务;
- NSURLSessionConfiguration
NSURLSessionConfiguration用于对会话设置数据是否进行缓存或缓存策略,每端口的最大并发HTTP请求数目,以及是否允许蜂窝网络, 请求超时时间,cookies或证书存储策略等...
NSURLSessionConfiguration分为几种:
- defaultSessionConfiguration :默认标准配置;
- ephemeralSessionConfiguration : 返回一个预设配置,没有磁盘存储的缓存,Cookie或证书。可以用来实现像"无痕浏览"功能的功能;
- backgroundSessionConfiguration : 创建一个后台会话,甚至可以在应用程序挂起,退出,崩溃的情况下运行上传和下载任务;
- 任务类型
1、NSURLSessionDataTask
// 第一种:没有设置代理,通过回调获取数据
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://pic1.nipic.com/2008-08-14/2008814183939909_2.jpg"]];
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
}];
[task resume];
// 第二种:设置代理请求数据
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://pic1.nipic.com/2008-08-14/2008814183939909_2.jpg"]];
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionDataTask* task = [urlSession dataTaskWithRequest:request];
[task resume];
// 请求完成时都会调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// 将数据Data转为NSDictionary
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:self.mu_data options:NSJSONReadingMutableLeaves error:nil];
if (self.mu_data) { // 在主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = [UIImage imageWithData:self.mu_data];
});
}
}
// 允许服务器请求并接收服务器数据:NSURLSessionResponseAllow
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
// 服务器返回数据拼接
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
// 通过mu_data进行数据拼接
if (!self.mu_data) {
self.mu_data = [NSMutableData data];
}
[self.mu_data appendData:data];
}
- 当开启一个回调的data task,即使设置了代理也不会执行代理方法;若没有设置回调而是设置了代理,则请求过程会调用代理方法;
- didCompleteWithError这个方法会在获取数据、下载、上传等任务请求完成时都会进行回调;
- 在服务器传输数据给客户端期间, 代理会周期性地收到
URLSession:dataTask:didReceiveData:数据
回调; - 对于一个data task来说, session会调用代理的
URLSession:dataTask:didReceiveResponse:completionHandler:
方法,决定是否允许继续接收服务器数据(NSURLSessionResponseAllow),还是要将一个data dask转换成download task(NSURLSessionResponseBecomeDownload),亦或者是取消(NSURLSessionResponseCancel);
2、NSURLSessionUploadTask
Content-type:multipart/formdata, boundary=boundary
--boundary
Content-disposition: form-data; name="name"
mjz
--boundary
Content-disposition: form-data; name: "pic", filename: "mjz.jpg"
Content-type: image/jpg
<mjz.jpg>
--boundary--
/**************************以上是上传文件时需遵守的格式****************************/
// 上传视频内容
- (void)uploadVideo {
NSMutableDictionary* param = [NSMutableDictionary dictionary];
param[@"hphm"] = @"粤B12345";
param[@"channelid"] = @"4";
NSMutableString* string = [NSMutableString string];
[string appendString:@"{\"file1\":{\"time\":\""];
[string appendString:[NSString stringWithFormat:@"%@", @"2019-01-01 00:00:00"]];
[string appendString:@"\",\"address\":\""];
[string appendString:[NSString stringWithFormat:@"%@", @"广东省广州市白云区"]];
[string appendString:@"\",\"lng\":\""];
[string appendString:[NSString stringWithFormat:@"%f", 113.3736180000]];
[string appendString:@"\",\"lat\":\""];
[string appendString:[NSString stringWithFormat:@"%f", 23.0982070000]];
[string appendString:@"\",\"seq\":\"1\"}}"];
param[@"mediainfo"] = string;
NSURL* url = [NSURL URLWithString:@"http://xxxxxxxxx/upload/uploadMediaFile"];
// test.mp4是放在工程下的视频
NSString* filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"mp4"];
NSMutableURLRequest* mu_req = [self requestWithURL:url andFilenName:@"file1" andLocalFilePath:filePath params:param];
NSURLSessionDataTask* task = [[NSURLSession sharedSession] uploadTaskWithStreamedRequest:mu_req];
[task resume];
}
- (NSMutableURLRequest *)requestWithURL:(NSURL *)url andFilenName:(NSString *)fileName andLocalFilePath:(NSString *)localFilePath params:(NSDictionary *)param{
//post请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:2.0f];
request.HTTPMethod = @"POST";//设置请求方法是POST,不写默认为GET
//拼接请求体数据(1-7步)
NSMutableData *requestMutableData = [NSMutableData data];
NSString* boundary = [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
//1.\r\n--Boundary+72D4CD655314C423\r\n // 分割符,以“--”开头,后面的字随便写,只要不写中文即可
NSMutableString *myString = [NSMutableString string];
// 拼接字段参数
NSArray *keys = [param allKeys];
for (NSString *key in keys) {
[myString appendString:[NSString stringWithFormat:@"\r\n--%@\r\n",boundary]];
[myString appendFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key];
[myString appendFormat:@"%@\r\n",[param objectForKey:key]];
}
[myString appendString:[NSString stringWithFormat:@"\r\n--%@\r\n",boundary]];
//2. Content-Disposition: form-data; name="file1"; filename="test.mp4"\r\n
[myString appendString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file1\"; filename=\"%@\"\r\n",fileName]];
//3. Content-Type:video/mp4 \r\n // 视频为mp4
[myString appendString:[NSString stringWithFormat:@"Content-Type:video/mp4\r\n"]];
//4. Content-Transfer-Encoding: binary\r\n\r\n // 编码方式
[myString appendString:@"Content-Transfer-Encoding: binary\r\n\r\n"];
//5. 转换成为NSData类型后再拼接视频数据
[requestMutableData appendData:[myString dataUsingEncoding:NSUTF8StringEncoding]];
//6.文件数据部分
NSURL *filePathUrl = [NSURL fileURLWithPath:localFilePath];
[requestMutableData appendData:[NSData dataWithContentsOfURL:filePathUrl]];
//7. \r\n--Boundary+72D4CD655314C423--\r\n // 分隔符后面以"--"结尾,表明结束
[requestMutableData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundary] dataUsingEncoding:NSUTF8StringEncoding]];
//设置请求体
request.HTTPBody = requestMutableData;
//设置请求头
NSString *headString = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
[request setValue:headString forHTTPHeaderField:@"Content-Type"];
return request;
}
- (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);
}
- 上传数据需遵循上面的格式才能上传成功;
- 上传数据去服务器期间, 代理会周期性收到
URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
回调,可获取当前上传进度;
3、NSURLSessionDownloadTask
- (void)viewDidLoad {
[super viewDidLoad];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://pic1.nipic.com/2008-08-14/2008814183939909_2.jpg"]];
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithRequest:request];
[task resume];
}
// 下载完成时调用
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
// 将数据进行存储
NSString* filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"mjz.jpg"];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
}
// 下载过程中回调,获取下载进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
NSLog(@"下载进度:%f", progress);
}
- 通过didWriteData:方法获取请求数据下载过程;
- didFinishDownloadingToURL:下载完成时,会生成一个临时文件路径location,我们需要对临时文件进行持久性存储;
文件断点下载
1、NSURLSessionDownloadTask实现
@interface ViewController ()<NSURLSessionDelegate, NSURLSessionDownloadDelegate>
@property (strong, nonatomic) NSURLSession* session;
@property (strong, nonatomic) NSURLSessionDownloadTask* downloadTask;
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (assign, nonatomic) BOOL isResume;
@property (strong, nonatomic) NSData* resumeData;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://XXXXX/02.qsv"]];
[self.downloadTask resume];
}
- (void)downloadData {
if (self.isResume) { // 继续下载
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
} else { // 暂停下载
self.downloadTask = [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://XXXXXX/02.qsv"]];
}
[self.downloadTask resume];
}
- (IBAction)suspendTask:(id)sender {
self.isResume = NO;
__block typeof(self) weakSelf = self;
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
weakSelf.resumeData = resumeData;
weakSelf.downloadTask = nil;
}];
}
- (IBAction)resumeTask:(id)sender {
self.isResume = YES;
[self downloadData];
}
- (IBAction)cancelTask:(id)sender {
self.isResume = NO;
[self.downloadTask cancel];
self.progressView.progress = 0.0;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
NSLog(@"下载进度:%f", progress);
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
NSLog(@"继续下载调用方法");
}
原理:
- 当用户暂停下载时, 调用
cancelByProducingResumeData: 将当前已经下载好的resumeData数据保存;
- 如果用户想要恢复下载, 把刚刚的resumeData以参数的形式传给
downloadTaskWithResumeData:
方法创建新的task继续下载; - 当用户退出程序时,由于下载的数据存储于临时文件夹中,可能会被销毁,所以实现断点下载有时候这种方式不太靠谱;
2、NSURLSessionDataTask实现
#import "ViewController.h"
#define saveFileString [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"mjz.mov"]
#define kDownLoadLength @"kDownLoadLength"
#define kCurrentLength @"kCurrentLength"
@interface ViewController ()<NSURLSessionDelegate, NSURLSessionDataDelegate>
@property (strong, nonatomic) NSURLSession* session;
@property (strong, nonatomic) NSURLSessionDataTask* dataTask;
@property (strong, nonatomic) NSOutputStream* outputStream;
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (assign, nonatomic) BOOL isResume; // 标记是否恢复下载
@property (assign, nonatomic) NSInteger currentLength; // 当前已下载大小
@property (assign, nonatomic) NSInteger totalSizeLength; // 总共需下载长度
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
// 是否已经下载过数据
NSNumber* currentLengthNum = [[NSUserDefaults standardUserDefaults] objectForKey:kCurrentLength];
if ([currentLengthNum integerValue] == 0) {
self.dataTask = [self.session dataTaskWithURL:[NSURL URLWithString:@"http://10.1.2.9/CxyAPIServerWithAuth/02.qsv"]];
} else {
self.currentLength = [currentLengthNum integerValue];
NSMutableURLRequest* mu_req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://10.1.2.9/CxyAPIServerWithAuth/02.qsv"]];
// 重新下载时主要设置的重点代码
NSString *range =[NSString stringWithFormat:@"bytes=%zd-",self.currentLength];
[mu_req setValue:range forHTTPHeaderField:@"Range"];
self.dataTask = [self.session dataTaskWithRequest:mu_req];
}
[self.dataTask resume];
}
- (IBAction)suspendTask:(id)sender {
self.isResume = NO;
[self.dataTask suspend];
}
- (IBAction)resumeTask:(id)sender {
self.isResume = YES;
[self.dataTask resume];
}
- (IBAction)cancelTask:(id)sender {
self.isResume = NO;
[self.dataTask cancel];
self.dataTask = nil;
[self.outputStream close];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// 总大小,当前response.expectedContentLength是剩余还未下载的数据
NSInteger totalLength = response.expectedContentLength + self.currentLength;
self.totalSizeLength = totalLength;
if (self.currentLength==0) {
[[NSUserDefaults standardUserDefaults] setObject:@(totalLength) forKey:kDownLoadLength];
[[NSUserDefaults standardUserDefaults] synchronize];
}
self.outputStream = [NSOutputStream outputStreamToFileAtPath:saveFileString append:YES];
[self.outputStream open];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
CGFloat progress = 1.0 * self.currentLength / self.totalSizeLength;
NSLog(@"下载进度:%f", progress);
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
self.currentLength += data.length;
[self.outputStream write:data.bytes maxLength:data.length];
// 设置当前已下载的大小
[[NSUserDefaults standardUserDefaults] setObject:@(self.currentLength) forKey:kCurrentLength];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@end
原理:(代码很长,原理很简单)
- 通过didReceiveData:代理方法保存当前文件下载的进度currentLength;
- 恢复下载时,通过设置请求首部的@"Range"字段,假设当前已下载currentLength为100,那么请求首部将从100开始获取还未下载的数据,我们只需将获取的新数据拼接在之前已下载的数据后面即可;
总结
- NSURLSession请求回调的时候在子线程执行,若涉及到更新UI,我们需要切换到主线程执行;
- NSURLConnection在子线程不会开启请求任务,因为子线程的Runloop默认不执行;
- NSURLSession还有很多强大的地方本文还未实现,以后还会进行更多的补充;