NSURLSession在2013年随着iOS7的发布一起面世,苹果对它的定位是作为NSURLConnection的替代者,然后逐步将NSURLConnection退出历史舞台。现在使用最广泛的第三方网络框架:AFNetworking、SDWebImage等等都使用了NSURLSession。作为iOS开发人员,应该紧随苹果的步伐,不断的学习,无论是软件的更新、系统的更新、API的更新,而不能墨守成规。
Session翻译为中文意思是会话,我们知道,在七层网络协议中有物理层->数据链路层->网络层->传输层->会话层->表示层->应用层,那我们可以将NSURLSession类理解为会话层,用于管理网络接口的创建、维护、删除等等工作,我们要做的工作也只是会话层之后的层即可,底层的工作NSURLSession已经帮我们封装好了。
另外还有一些Session,比如AVAudioSession用于音视频访问,WCSession用于WatchOS通讯,它们都是建立一个会话,并管理会话,封装一些底层,方便我们使用。举一反三。
其核心就是对网络任务进行封装,实现多线程。比如将一个网络请求交给NSURLSession,最后NSURLSession将访问结果通过block回调返回,期间自动实现多线程,而且可以通过代理实现监听(是否成功,当前的进度等等); 大致分为3个步骤:
NSURL *url = [NSURL URLWithString:@"协议://主机地址/路径?参数&参数"];
解释如下:
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];
参数解释如下:
另外,还可以设置其它一些信息,比如请求头,请求体等等,如下:
注意,下面的request应为NSMutableURLRequest,即可变类型
// 告诉服务器数据为json类型 [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; // 设置请求体(json类型) NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"userid":@"123456"} options:NSJSONWritingPrettyPrinted error:nil]; request.HTTPBody = jsonData;
下面以苹果提供的全局NSURLSession单例为例,代码如下:
/// 向网络请求数据 - (void)NSURLSessionTest { // 1.创建url // 请求一个网页 NSString *urlString = @"http://www.baidu.com"; // 一些特殊字符编码 urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; NSURL *url = [NSURL URLWithString:urlString]; // 2.创建请求 并:设置缓存策略为每次都从网络加载 超时时间30秒 NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30]; // 3.采用苹果提供的共享session NSURLSession *sharedSession = [NSURLSession sharedSession]; // 4.由系统直接返回一个dataTask任务 NSURLSessionDataTask *dataTask = [sharedSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // 网络请求完成之后就会执行,NSURLSession自动实现多线程 NSLog(@"%@",[NSThread currentThread]); if (data && (error == nil)) { // 网络访问成功 NSLog(@"data=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } else { // 网络访问失败 NSLog(@"error=%@",error); } }]; // 5.每一个任务默认都是挂起的,需要调用 resume 方法 [dataTask resume]; }
/// 文件下载 - (void)NSURLSessionDownloadTaskTest { // 1.创建url NSString *urlString = [NSString stringWithFormat:@"http://localhost/周杰伦 - 枫.mp3"]; // 一些特殊字符编码 urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; NSURL *url = [NSURL URLWithString:urlString]; // 2.创建请求 NSURLRequest *request = [NSURLRequest requestWithURL:url]; // 3.创建会话,采用苹果提供全局的共享session NSURLSession *sharedSession = [NSURLSession sharedSession]; // 4.创建任务 NSURLSessionDownloadTask *downloadTask = [sharedSession downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil) { // location:下载任务完成之后,文件存储的位置,这个路径默认是在tmp文件夹下! // 只会临时保存,因此需要将其另存 NSLog(@"location:%@",location.path); // 采用模拟器测试,为了方便将其下载到Mac桌面 // NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *filePath = @"/Users/userName/Desktop/周杰伦 - 枫.mp3"; NSError *fileError; [[NSFileManager defaultManager] copyItemAtPath:location.path toPath:filePath error:&fileError]; if (fileError == nil) { NSLog(@"file save success"); } else { NSLog(@"file save error: %@",fileError); } } else { NSLog(@"download error:%@",error); } }]; // 5.开启任务 [downloadTask resume]; }
3.1 采用uploadTask任务,以数据流的方式进行上传
这种方式好处就是大小不受限制,上传需要服务器端脚本支持,脚本源代码见本文档最后的附录,客户端示例代码如下:
/// 以流的方式上传,大小理论上不受限制,但应注意时间 - (void) NSURLSessionBinaryUploadTaskTest { // 1.创建url 采用Apache本地服务器 NSString *urlString = @"http://localhost/upload.php"; urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]; NSURL *url = [NSURL URLWithString:urlString]; // 2.创建请求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; // 文件上传使用post request.HTTPMethod = @"POST"; // 3.开始上传 request的body data将被忽略,而由fromData提供 [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:[NSData dataWithContentsOfFile:@"/Users/userName/Desktop/IMG_0359.jpg"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil) { NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } else { NSLog(@"upload error:%@",error); } }] resume]; }
3.2 采用dataTask任务,拼接表单的方式进行上传
上传的关键是请求体部分的表单拼接,获取本地上传文件的类型(MIME Types),至于具体的网络上传则很简单。 另外拼接表单的方式会有大小限制,即HTML的MAX_FILE_SIZE
限制(可以自己设定,一般2MB)。
根据上面的继承关系图,我们知道uploadTask是dataTask的子类,也可以使用uploadTask来代替dataTask。在代码示例中4.2步骤完全可以替换4.1步骤。这时,uploadTaskWithRequest函数的fromData可有可无,文件已在request里面包含。
注意:然而在苹果官方对uploadTaskWithRequest函数的介绍:
request的body data in this request object are ignored
,会被忽略,而测试时发现没有被忽略,且request必须包含HTTPBody,反而fromData被忽略。那么暂时理解为苹果对uploadTaskWithRequest函数的使用时没有考虑拼接表单的方式,那么当我们使用拼接表单时,建议不要使用uploadTask,虽然这样也能成功
表单拼接格式如下,boundary作为分界线:
--boundary Content-Disposition:form-data;name=”表单控件名称”;filename=”上传文件名称” Content-Type:要上传文件MIME Types 要上传文件二进制数据; --boundary--
拼接表单示例代码:
/// 文件上传 - (void)NSURLSessionUploadTaskTest { // 1.创建url 采用Apache本地服务器 NSString *urlString = @"http://localhost/upload/upload.php"; urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]; NSURL *url = [NSURL URLWithString:urlString]; // 2.创建请求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; // 文件上传使用post request.HTTPMethod = @"POST"; NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"boundary"]; [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; // 3.拼接表单,大小受MAX_FILE_SIZE限制(2MB) FilePath:要上传的本地文件路径 formName:表单控件名称,应于服务器一致 NSData* data = [self getHttpBodyWithFilePath:@"/Users/userName/Desktop/IMG_0359.jpg" formName:@"file" reName:@"newName.png"]; request.HTTPBody = data; // 根据需要是否提供,非必须,如果不提供,session会自动计算 [request setValue:[NSString stringWithFormat:@"%lu",data.length] forHTTPHeaderField:@"Content-Length"]; // 4.1 使用dataTask [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil) { NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } else { NSLog(@"upload error:%@",error); } }] resume]; #if 0 // 4.2 开始上传 使用uploadTask fromData:可有可无,会被忽略 [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:nil completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil) { NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } else { NSLog(@"upload error:%@",error); } }] resume]; #endif } /// filePath:要上传的文件路径 formName:表单控件名称 reName:上传后文件名 - (NSData *)getHttpBodyWithFilePath:(NSString *)filePath formName:(NSString *)formName reName:(NSString *)reName { NSMutableData *data = [NSMutableData data]; NSURLResponse *response = [self getLocalFileResponse:filePath]; // 文件类型:MIMEType 文件的大小:expectedContentLength 文件名字:suggestedFilename NSString *fileType = response.MIMEType; // 如果没有传入上传后文件名称,采用本地文件名! if (reName == nil) { reName = response.suggestedFilename; } // 表单拼接 NSMutableString *headerStrM =[NSMutableString string]; [headerStrM appendFormat:@"--%@\r\n",@"boundary"]; // name:表单控件名称 filename:上传文件名 [headerStrM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",formName,reName]; [headerStrM appendFormat:@"Content-Type: %@\r\n\r\n",fileType]; [data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]]; // 文件内容 NSData *fileData = [NSData dataWithContentsOfFile:filePath]; [data appendData:fileData]; NSMutableString *footerStrM = [NSMutableString stringWithFormat:@"\r\n--%@--\r\n",@"boundary"]; [data appendData:[footerStrM dataUsingEncoding:NSUTF8StringEncoding]]; // NSLog(@"dataStr=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); return data; } /// 获取响应,主要是文件类型和文件名 - (NSURLResponse *)getLocalFileResponse:(NSString *)urlString { urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]]; // 本地文件请求 NSURL *url = [NSURL fileURLWithPath:urlString]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; __block NSURLResponse *localResponse = nil; // 使用信号量实现NSURLSession同步请求 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { localResponse = response; dispatch_semaphore_signal(semaphore); }] resume]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); return localResponse; }
NSURLConnection是全局性的,即它的配置对全局有效,如果有两个链接需要不同的cookies、证书这些公共资源,则NSURLConnection无法满足要求,这时NSURLSession的优势则体现出来,NSURLSession可以同过NSURLSessionConfiguration可以设置全局的网络访问属性。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; // delegateQueue:请求完成回调函数和代理函数的运行线程,如果为nil则系统自动创建一个串行队列,不影响sessionTask的运行线程 NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
三种会话方式:
设置一些网络属性:
configuration.HTTPAdditionalHeaders = @{ @"Accept": @"application/json", @"Accept-Language": @"en", @"Authorization": authString, @"User-Agent": userAgentString };
注意事项:如果是自定义会话并指定了代理,会话会对代理进行强引用,在视图控制器销毁之前,需要取消网络会话,否则会造成内存泄漏
<?php header("Content-type: application/json; charset=utf-8"); // 配置文件需要上传到服务器的路径,需要允许所有用户有可写权限,否则无法上传! $uploaddir = 'images/'; // file表单名称,应与客户端一致 $uploadfile = $uploaddir . basename($_FILES['file']['name']); move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile); echo json_encode($_FILES); ?>
<?php /** 二进制流生成文件 * $_POST 无法解释二进制流,需要用到 $GLOBALS['HTTP_RAW_POST_DATA'] 或 php://input * $GLOBALS['HTTP_RAW_POST_DATA'] 和 php://input 都不能用于 enctype=multipart/form-data * @param String $file 要生成的文件路径 * @return boolean */ function binary_to_file($file){ $content = $GLOBALS['HTTP_RAW_POST_DATA']; // 需要php.ini设置 if(empty($content)){ $content = file_get_contents('php://input'); // 不需要php.ini设置,内存压力小 } $ret = file_put_contents($file, $content, true); return $ret; } $file_dir="images/image.png"; // 固定的文件名,注意设置images文件夹权限为所有用户可读写!!! binary_to_file($file_dir); ?>
代码下载
关于NSURLSession还有很多,后续会再贴一些例子 未完待续。。。