HTTP网络编程

HTTP网络应用协议是互联网上最伟大的发明之一,是信息交换传输的最基本的协议。为了增强数据传输的安全性,防止数据被窃听篡改,又衍生出了基于加密通道的HTTP安全传输协议,称之为HTTPS。

HTTP是一种Clinet-Server间请求响应的无状态的数据交换,多个请求之间互相是没有关联的,而Cookie技术允许Client携带客户端状态信息给服务端,这样就可以在复杂多次交互的有关联的多个请求之间共享数据,实现HTTP请求之间的状态管理。HTTP的Cache缓存技术在一些场景下能提升系统的响应速度和性能。

Cocoa中网络编程经过了不同时期2个阶段的演进,第一个阶段(iOS7和OSX10.9之前)是围绕NSURLConnection提供了一组相关辅助类和代理协议接口;第二个阶段(iOS7及之后和OSX10.9及之后)提供了以NSURLSessionTask任务为中心的一组相关辅助类和代理协议接口,支持后台的网络请求处理,功能更为强大。

URL Connection网络请求处理过程可以简单概括如下:

  1. 构造NSURLRequest对象,设置HTTP请求相关的各种参数,包括设置缓存数据的规则
  2. 如果需要Cookie设置,可以通过NSURLHTTPCookieStorage设置,设置完成后对当前应用中后续所有的请求都有效,是全局的。
  3. 如果是同步请求,则等待数据返回后结束任务
  4. 如果是异步请求,创建NSURLConnection对象,设置它的代理,发起网络请求。
  5. 根据需要实现相关的代理方法

    1)处理HTTP 的请求响应NSHTTPURLResponse

    2)对接收到的数据可以在前面第一步中的缓存设置规则上做额外的处理

    3)如果是收控的资源的访问,则提供验证的方式

    4)接收数据

下图是NSURLConnection为核心的相关类

URLConnection

上图中蓝色NSURLDownload和NSURLDownloadDelegate仅在OS X系统支持,iOS不支持。

NSURLSessionTask具体分为四种简单的数据请求任务,上传任务,下载任务,流处理任务。
每个任务都是通过NSURLSession创建的,Session有3种配置类型:

  1. 默认的sessions:类似于NSURLConnection的行为,将缓存和Cookie存储到硬盘,证书存储到钥匙串keyChain中。
  2. 临时sessions:缓存数据,Cookie,证书等信息全部存储在内存中,当session失效后自动清除。
  3. 后台sessions:类似于默认的sessions,不同的是当应用挂起在后台时能继续运行任务 这个配置同时可以对Cookie,Cache,认证方式做统一的配置。

这样我们可以简单的理解NSURLSession为一组相同配置类型的任务队列,Session中可以创建多个任务去并发异步的执行。

对每个Task任务我们可以使用系统默认的代理方法,仅提供一个任务执行完的Block回调;或者根据不同的任务类型实现代理协议方法来对任务过程的响应进行处理。

下图是NSURLSessionTask为核心的相关类
URLSession

我们推荐使用最新的基于Task的相关类去完成网络任务处理,因此本章会重点介绍NSURLSession,
NSURLSessionTask的相关使用。

简单的数据请求

对于应用中常见的API数据请求接口,可以非常方便的使用系统默认的代理,通过任务完成的回调方式处理接收的数据,也可以自定义实现代理方法进行数据接收处理。

使用系统默认的代理

  1. 创建一个默认类型的Session配置
  2. 基于Session配置创建NSURLSession,对于使用系统默认的代理进行收到数据处理的情况下,设置NSURLSession的delegate参数为nil,同时设置代理执行的队列为主线程队列,在此你也可以创建自己的私有队列来处理。
  3. 构造NSMutableURLRequest对象,第一个参数为接口的url路径;第二个参数为缓存规则,设置为协议约定的策略(缓存规则类型我们在后面缓存章节详细说明);第三个参数为超时时间单位为秒。
    同时设置HTTP请求为POST方式,设置消息头中的content-type类型为form编码形式。最后将post参数从NSString类型转换为NSData类型,做为NSMutableURLRequest的HTTPBody数据。

  4. 通过Session创建任务,completionHandler定义了任务完成的回调。

#define   kServerBaseUrl          @"http://127.0.0.1/iosxhelper/SAPI"

- (void)urlSessionNoDelegateTest {
    
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                      defaultSessionConfiguration];
    NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                                                                 defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
    
    NSURL *url = [NSURL
                  URLWithString:[NSString stringWithFormat:@"%@%@",kServerBaseUrl,@"/VersionCheck"]];
    NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url
                                                         cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                     timeoutInterval:60.0];
    request.HTTPMethod = @"POST";
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"content-type"];
    
    NSString *post  = [[NSString alloc] initWithFormat:@"versionNo=%@&platform=%@&channel=%@&appName=%@",@"1.0",@"Mac",@"appstore",@"DBAppX"];
    NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding];
    request.HTTPBody = postData;
    
    NSURLSessionTask *task =  [session dataTaskWithRequest:request
          completionHandler:^(NSData *data, NSURLResponse *response,
                    NSError *error) {
              NSString *responseStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
              NSLog(@"data =%@",responseStr);
          }
    ];
    [task resume];
}

使用自定义的代理方法

创建session时设置代理参数,task的创建也变得简单,传入request参数即可。
下面的代码仅写出了变化的部分,其他代码跟上面系统默认代理的中的完全一样。

有一个特别需要注意的问题,由于session对delegate是retain强引用,如果是任务完成后session不在被使用,需要调用session的finishTasksAndInvalidate方法完成对delegate的释放,避免发生内存循环引用。

NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                              defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

.....


NSURLSessionTask *task =  [session dataTaskWithRequest:request];
[task resume];
 

实现代理协议方法

//申明一个data的属性变量用来存储返回的数据
@property (nonatomic,strong ) NSMutableData              *data;

#pragma mark-  NSURLSessionDataDelegate
//接收数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    if(!self.data){
        self.data = [NSMutableData data];
    }
    [self.data appendData:data];
}

//此代理方法为可选,用来控制请求,有4种参数可以设置(取消任务,继续进行,转为下载任务,转为流式任务)
//不实现此代理方法时默认为允许继续进行任务。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    completionHandler(NSURLSessionResponseAllow);
}
//任务完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    NSString *responseStr = [[NSString alloc]initWithData:self.data encoding:NSUTF8StringEncoding];
    NSLog(@"data =%@ taskIdentifier=%ld",responseStr,task.taskIdentifier);
   //释放session资源
    [session finishTasksAndInvalidate];
}

使用自定义的代理的方式可以实现更加灵活的控制,包括是否允许缓存,对接收到的数据可以灵活处理。

POST方式传递数据

除了前面使用NSURLSessionTask通过在NSMutableURLRequest请求的Body中设置POST的Data数据外,还可以使用NSURLSessionUploadTask的Data参数接口来进行POST数据上传

NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                  defaultSessionConfiguration];
NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                          defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
NSURL *url = [NSURL
              URLWithString:[NSString stringWithFormat:@"%@%@",kServerBaseUrl,@"/VersionCheck"]];

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"content-type"];
request.HTTPMethod = @"POST";

NSError *error;
NSString *post  = [[NSString alloc] initWithFormat:@"versionNo=%@&platform=%@&channel=%@&appName=%@",@"1.0",@"Mac",@"appstore",@"DBAppX"];
NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding];
if (!error) {
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request
                                                               fromData:postData completionHandler:^(NSData *data,NSURLResponse *response,NSError *error) {
                                                                   
                                                                   NSString *responseStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
                                                                   NSLog(@"data =%@",responseStr);
                                                                   
                                                                   [session finishTasksAndInvalidate];
                                                                   
                                                               }];
    [uploadTask resume];
}

文件下载

使用NSURLSessionDownloadTask类来完成文件下载,只要指定下载的文件的URL创建下载任务,实现相关的代理协议方法即可。文件下载过程中会写入临时文件,下载完成后需要copy到指定的地方。

NSURLSessionDownloadTask是边下载边写入临时文件,因此在下载过程中对内存的占用不会随着文件增大而无限增大,因此使用它可以来进行大文件的下载。

1.创建下载任务

NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                  defaultSessionConfiguration];
//创建session 指定代理和任务队列
NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                          defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
//指定需要下载的文件URL路径
NSURL *url = [NSURL URLWithString:
              @"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/"
              "Foundation/ObjC_classic/FoundationObjC.pdf"];
//创建Download任务
NSURLSessionDownloadTask *task =  [session downloadTaskWithURL:url];
[task resume];
    

2.实现下载代理协议

主要是下载完成和下载进度回调的处理

1)文件下载完成回调

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
    
    NSError *err = nil;
    //获取原始的文件名
    NSString *fileName = [[downloadTask.originalRequest.URL absoluteString]lastPathComponent];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *downloadDir = [[NSHomeDirectory()
                              stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:fileName];
    //要保存的路径
    NSURL *downloadURL = [NSURL fileURLWithPath:downloadDir];
    //从下载的临时路径移动到期望的路径
    if ([fileManager moveItemAtURL:location
                             toURL:downloadURL
                             error: &err]) {
       
    } else {
        NSLog(@"err %@ ",err);
    }
}

2)下载过程中进度回调: bytesWritten参数为本次回调通知时下载完成的数据长度,totalBytesWritten为目前为止总下载的数据长度,totalBytesExpectedToWrite为文件总的长度

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    
    NSLog(@"Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.\n",
          session, downloadTask, bytesWritten, totalBytesWritten,
          totalBytesExpectedToWrite);
}

文件上传

流式文件上传

其中设置Content-Length和Content-Type的代码可以省略,系统会默认设置。

// Define the Paths
NSURL *uploadURL = [NSURL URLWithString:@"http://127.0.0.1/fileUpload.php"];

// Create the Request
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:uploadURL];
[request setHTTPMethod:@"POST"];

NSString *downloadDir = [[NSHomeDirectory()
                          stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"openMacIcon.png"];


//要上传的路径
NSURL *fileURL = [NSURL fileURLWithPath:downloadDir];

uint64_t bytesTotalForThisFile = [[[NSFileManager defaultManager] attributesOfItemAtPath:fileURL.path error:NULL] fileSize];

[request setValue:[NSString stringWithFormat:@"%llu", bytesTotalForThisFile] forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];


NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                  defaultSessionConfiguration];


NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                          defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];


NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:fileURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if(!error) {
        // handle success
    } else {
        // handle error
        NSLog(@"error %@",error);
    }
}];

[uploadTask resume];
    

上传进度代理方法

-(void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
   didSendBodyData:(int64_t)bytesSent
    totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
  
 NSLog(@"Session %@ upload task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.\n",
          session, task, bytesSent, totalBytesSent,
          totalBytesExpectedToSend);
    
}

PHP上传处理代码(fileUpload.php)

<?php
    // File Path to save file
    $file = 'upload/recording.png';
    
    // Get the Request body
    $request_body = @file_get_contents('php://input');
    
    // Get some information on the file
    $file_info = new finfo(FILEINFO_MIME);
    
    // Extract the mime type
    $mime_type = $file_info->buffer($request_body);

    // Logic to deal with the type returned
    switch($mime_type) 
    {
        case "image/png; charset=binary":
            
            // Write the request body to file
            file_put_contents($file, $request_body);
            
            break;
            
        default:
            // Handle wrong file type here
    }
?>

Form表单文件上传

将文件数据按form表单格式进行组装,最后使用NSURLSessionUploadTask的fromData形式的方法上传数据。其中Request 的URL参数为HTML form表单中action对应的url path,文件内容以Data 二进制流形式进行上传。

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];

NSString *urlString = @"http://127.0.0.1/upload_file.php";
NSURL *url = [NSURL URLWithString:urlString];

NSString *filePath = [[NSHomeDirectory()
                       stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"openMacIcon.png"];

NSString *fileName = [filePath lastPathComponent];
NSData   *fileData      = [NSData dataWithContentsOfFile:filePath];

NSString *boundary = @"-boundary";
NSMutableData *dataSend = [[NSMutableData alloc] init];

[dataSend appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[dataSend appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@\"\r\n", fileName] dataUsingEncoding:NSUTF8StringEncoding]];
[dataSend appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[dataSend appendData:fileData];

[dataSend appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[dataSend appendData:[[NSString stringWithFormat:@"--%@--\r\n\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:dataSend];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];

NSURLSessionUploadTask *sessionUploadTask = [session uploadTaskWithRequest:request fromData:dataSend];
[sessionUploadTask resume];
    

PHP上传处理代码(upload_file.php)

<?php

  if ($_FILES["file"]["error"] > 0)
    {
    echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
    }
  else
    {
    echo "Upload: " . $_FILES["file"]["name"] . "<br />";
    echo "Type: " . $_FILES["file"]["type"] . "<br />";
    echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
    echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";

    if (file_exists("upload/" . $_FILES["file"]["name"]))
      {
      echo $_FILES["file"]["name"] . " already exists. ";
      }
    else
      {
      move_uploaded_file($_FILES["file"]["tmp_name"],
      "upload/" . $_FILES["file"]["name"]);
      echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
      }
    }
 
?>

HTML测试文件代码(upload.html)

<html>
<body>

<form action="upload_file.php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" /> 
<br />
<input type="submit" name="submit" value="Submit" />
</form>

</body>
</html>

缓存

1.缓存策略
NSURLRequestCachePolicy定义了4种缓存策略

NSURLRequestUseProtocolCachePolicy:协议规定的缓存策略

NSURLRequestReloadIgnoringLocalCacheData:不使用缓存,永远使用网络数据

NSURLRequestReturnCacheDataElseLoad:使用缓存的数据,不管数据事否过期;如果缓存没有数据才请求网络数据

NSURLRequestReturnCacheDataDontLoad:仅仅使用缓存数据

2.使用缓存策略

request中指定缓存策略,如果创建request时没有使用cachePolicy参数则默认为第一种协议规定的策略

NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url
                                                         cachePolicy:NSURLRequestUseProtocolCachePolicy
                                                     timeoutInterval:60.0];
                                                     

3.缓存控制编程

1)NSURLSession中使用缓存

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler {
    
    NSCachedURLResponse *returnCachedResponse = proposedResponse;
    NSDictionary *newUserInfo;
    newUserInfo = [NSDictionary dictionaryWithObject:[NSDate date]
                                              forKey:@"Cached Date"];
 #if ALLOW_CACHING
    returnCachedResponse = [[NSCachedURLResponse alloc]
                            initWithResponse:[proposedResponse response]
                            data:[proposedResponse data]
                            userInfo:newUserInfo
                            storagePolicy:[proposedResponse storagePolicy]];
    
 #else
    returnCachedResponse = nil;
 #endif
    
    if(completionHandler){
         completionHandler(returnCachedResponse);
    }
    
}

2)NSURLConnection 中使用缓存

如果不允许缓存 可以之间返回空

-(NSCachedURLResponse *)connection:(NSURLConnection *)connection
                 willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    
    NSCachedURLResponse *newCachedResponse = cachedResponse;
    NSDictionary *newUserInfo;
    newUserInfo = [NSDictionary dictionaryWithObject:[NSDate date]
                                              forKey:@"Cached Date"];
    #if ALLOW_CACHING
    newCachedResponse = [[NSCachedURLResponse alloc]
                         initWithResponse:[cachedResponse response]
                         data:[cachedResponse data]
                         userInfo:newUserInfo
                         storagePolicy:[cachedResponse storagePolicy]];
    
    #else
    newCachedResponse = nil;
    #endif
    return newCachedResponse;
}

4.缓存大小控制

//设置内存缓存2M,硬盘缓存10M,路径为系统默认
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:2 * 1024 * 1024 diskCapacity:10 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
    

URL缓存的一定要在应用applicationDidFinishLaunching的初始化代码中设置,发起正式请求之前完成。

使用上面方法设置的缓存是全局的,对NSURLConnection和NSURLSession均有效,但是NSURLSession可以单独配置缓存。

NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                      defaultSessionConfiguration];
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
defaultConfigObject.URLCache = cache;
NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                                                                 defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
    

Cookie管理

Cookie是Web 服务器和浏览器之间传递的key-value形式的文本数据信息,基本流程是客户端请求url访问Web 服务器时,Web 服务器会在响应的消息头中Set-Cookie字段中附带Cookie信息。客户端会保留这个Cookie信息,后续客户端请求Web 服务器时会在请求头Header中附加保留的Cookie信息。Cookie最大字节长度不能超过4096k。

下面是使用FireFox浏览器首次访问www.reddit.com网站时,通过FireFox的菜单Tools->Web Developer->Inspector观察Cookie的情况。可以看到Response Header中Set-Cookie字段中具体的Cookie参数,每个Cookie有多个属性,其中最主要的几个属性为名称name,值value,超期时间expires,域domain,路径path,浏览器自动保存这些Cookie信息。

redditResponseCookie

当从浏览器中访问任何资源(页面,图片,文件等)时,浏览器首先从所有存储的Cookie中通过Domain,Path去匹配,如果匹配成功并且Cookie在有限期内没有过期,就会添加到请求头Header中去。

因此可以看到后续访问www.reddit.com域名下的request_promo时, Request Header中附加了Cookie信息,name为__cfduid,值为d9bd1fa536e04314a996b04554849dc811455934534。

redditRequestCookie

Cookie 编程

NSHTTPCookie用来描述定义Cookie的属性,NSHTTPCookieStorage是一个单例对象,提供增加删除Cookie 的接口,用来对NSHTTPCookie实例进行管理。

1.Cookie增加

NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];

NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties: @{NSHTTPCookieDomain : @".reddit.com", NSHTTPCookiePath : @"/", NSHTTPCookieName : @"__cfduid", NSHTTPCookieValue : @"d9bd1fa536e04314a996b04554849dc811455934534"}];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
   

2.Cookie删除

NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [storage cookies])
{
    [storage deleteCookie:cookie];
}

Cookie的一些使用场景

1.简化API接口参数设计

多个接口有公共参数时,发起HTTP请求前可以将固定不变的参数通过NSHTTPCookieStorage存储到Cookie中,这样每个请求中Header中会自动填充Cookie字段,服务器可以从Cookie中获取这些信息。

2.与H5 Web页面无缝集成

客户端和H5页面都需要登录访问的页面,如果使用Native完成了登录,可以将登录的用户Session或token认证信息通过Cookie存储,Web 服务器检查如果Cookie中有登录信息,后续访问Web页面就可以实现自动登录了(无需再次弹出Web登录页面验证)。

断点续传

下载任务被用户取消或者由于网络中断停止下载,当后续需要重新恢复下载时,可以使用断点续传而避免完全从头开始下载加快下载速度。

开始下载任务后,可以使用NSURLSessionDownloadTask的cancelByProducingResumeData方法来停止下载任务。同时其回调的函数会返回resumeData,将这个resumeData保存,甚至可以将resumeData持久化存储,后续恢复下载时可以重新使用resumeData创建新的下载任务。

@property (nonatomic,strong ) NSData                                            *resumeData;
@property (nonatomic,strong ) NSURLSessionDownloadTask         *downTask;

//开始下载
- (IBAction)startDown:(id)sender {
    
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                      defaultSessionConfiguration];
    NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                              defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
    
    NSURL *url = [NSURL URLWithString:
                  @"http://devstreaming.apple.com/videos/wwdc/2015/105ncyldc6ofunvsgtan/105/105_hd_introducing_watchkit_for_watchos_2.mp4?dl=1"];
    
    NSURLSessionDownloadTask *task =  [session downloadTaskWithURL:url];
    self.downTask = task;
    [task resume];
    
}

//停止下载
- (IBAction)stopDown:(id)sender {
    [self.downTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        if(resumeData){
            self.resumeData = resumeData;
        }
    }];
}

//恢复下载
- (IBAction)resumeDown:(id)sender {
    
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                      defaultSessionConfiguration];
    NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                              defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
    self.downTask= [session downloadTaskWithResumeData:self.resumeData]
    [self.downTask resume];
    
}

重新下载开始时回调的代理方法,表示从fileOffset字节处开始下载

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
    
    NSLog(@"Session %@ download task %@ resumed at offset %lld bytes out of an expected %lld bytes.\n",
          session, downloadTask, fileOffset, expectedTotalBytes);
}

当传输失败网络异常会接收到下面的回调函数,可以从error中获取resumeData

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    if (error.userInfo && [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
            self.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
    }
}

当应用被关闭终止时,可以判断当前如果存在下载任务,可以取消任务并保存resumeData

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    if(!self.downTask){
        return;
    }
    [self.downTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        if(resumeData){
            self.resumeData = resumeData;
            //可以进一步存储到NSUserDefault或持久化文件中,等下次应用启动后决定是否需要断点续传重新下载
        }
    }];
    
}

后台任务处理

在iOS中可以创建后台下载任务,当应用切换到后台时下载任务可以继续进行。可能同时存在多个后台任务,因此每个后台任务创建时有独立的标识identifier。

应用进入后台运行时会执行application:handleEventsForBackgroundURLSession:completionHandler: 代理方法,在此方法中保存任务的处理handler

后台任务完成时执行NSURLSessionDelegate中代理方法 URLSessionDidFinishEventsForBackgroundURLSession:,在此方法中执行后台任务对应的handler方法

@interface AppDelegate ()
@property(nonatomic,strong)NSMutableDictionary   *completionHandlerDictionary;
@end
@implementation AppDelegate

//应用启动创建后台下载任务
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self createBackgroundURLSession];
    return YES;
}

//任务进入后台下载的代理方法回调,根据Session的identifier 保存handler
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)())completionHandler {
    if(!self.completionHandlerDictionary){
        self.completionHandlerDictionary = [NSMutableDictionary dictionary];
    }
    self.completionHandlerDictionary[identifier] = completionHandler;
}

#pragma mark - NSURLSessionDelegate
//后台下载完成的回调
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"Background URL session %@ finished events.\n", session);
    NSString *identifier = session.configuration.identifier;
    void (^handler)()   = [self.completionHandlerDictionary objectForKey: identifier];
    if (handler) {
        [self.completionHandlerDictionary removeObjectForKey: identifier];
        NSLog(@"Calling completion handler.\n");
        handler();
    }
}

- (void)createBackgroundURLSession {
    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration
                                                      backgroundSessionConfigurationWithIdentifier:@"backgroundDownID"];
    __weak id safeSelf = self;
    NSURLSession *session  = [NSURLSession sessionWithConfiguration:
                              defaultConfigObject delegate: safeSelf delegateQueue: [NSOperationQueue mainQueue]];
    NSURL *url = [NSURL URLWithString:
                  @"https://www.gitbook.com/download/pdf/book/frontendmasters/front-end-handbook"];
    NSURLSessionDownloadTask *task =  [session downloadTaskWithURL:url];
    [task resume];
}

NSURLSessionDataTask封装的工具类

NSURLSessionDataTask 使用系统默认的代理,可以实现基本的网络请求处理;如果有特殊流程需要定制处理,就必须实现代理协议方法。

如果一个类Controller或页面中只有一个由NSURLSessionDataTask发起的网络请求,直接使用Controller做为代理就可以方便的处理。

SessionDataTaskControlle

如果一个类,Controller或页面中有多个网络请求由NSURLSessionDataTask发起,使用代理怎么去实现 ?
多个NSURLSessionDataTask使用了同一个Controller做为代理,在代理方法中必然要区分不同的NSURLSessionDataTask来处理。

1) 每个NSURLSessionDataTask有不同的identifier

2) 将代理协议方法封装到一个独立的代理协议处理类中,每个任务的代理处理类可以有不同的实现

基于上面2点 我们在创建NSURLSessionDataTask 任务实例时,以任务identifier 做为key,任务实例做为value,将key-value映射存储到字典缓存中。

这样我们可以把这个Controller看成集中的代理中心,虽然 NSURLSessionDataTask的代理仍热是Controller, 但是它不执行具体的逻辑处理,它仅仅起一个路由功能,根据每个Task的identifer 来分发到具体的代理类去执行代理方法。

逻辑关系图如下:

SessionDataTaskControlle

另外NSURLSessionDataTask 发起的请求都会由系统创建NSOperation,加入NSOperation线程队列调度处理,因此对NSURLSessionDataTask的进一步的封装类不需要在继承NSOperation来实现。

HTTPClient 工具类

我们实现一个简单的HTTP GET/POST网络请求接口工具类,来说明上面NSURLSessionDataTask协议封装的思路。

1.HTTPClientSessionDelegate 代理协议处理类:主要实现协议定义的方法

@interface HTTPClientSessionDelegate : NSObject<NSURLSessionTaskDelegate,NSURLSessionDataDelegate>
@property(nonatomic,copy)HTTPSessionDataTaskCompletionBlock  taskCompletionHandler;
@property (nonatomic,strong) NSMutableData *mutableData;
@end

@implementation HTTPClientSessionDelegate

#pragma mark-- NSURLSessionTaskDelegate

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    NSData *data = nil;
    if (self.mutableData) {
        data = [self.mutableData copy];
        self.mutableData = nil;
    }
    if (self.taskCompletionHandler) {
        self.taskCompletionHandler(task.response, data, error);
    }
}
#pragma mark-- NSURLSessionDataDelegate

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    completionHandler(NSURLSessionResponseAllow);
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    if(!self.mutableData) {
        self.mutableData = [NSMutableData data];
    }
    [self.mutableData appendData:data];
}

-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler {
    
    if(completionHandler){
        completionHandler(proposedResponse);
    }
}
@end

2.HTTPClient 类:定义外部访问的GET/POST接口,创建NSURLSessionDataTask任务实例,实现代理功能路由到具体的代理协议处理类。

@interface HTTPClient : NSObject
-(void)GET:(NSString *)URLString
    parameters:(id)parameters
    success:(void (^)(id responseData))success
    failure:(void (^)(NSError * error))failure;
-(void)POST:(NSString *)URLString
    parameters:(id)parameters
    success:(void (^)(id responseData))success
    failure:(void (^)(NSError * error))failure;
@end

#import "HTTPClient.h"

typedef void (^HTTPSessionDataTaskCompletionBlock)(NSURLResponse * __unused response, id responseObject, NSError *error);
@interface HTTPClient ()<NSURLSessionTaskDelegate,NSURLSessionDataDelegate>
@property ( nonatomic, strong) NSURLSessionConfiguration *sessionConfiguration;
@property ( nonatomic, strong) NSOperationQueue *operationQueue;
@property ( nonatomic, strong) NSURLSession *session;
@property ( nonatomic, strong) NSMutableDictionary *taskDelegates;
@property ( nonatomic, strong) NSLock *lock;
@end

@implementation HTTPClient
- (instancetype)init {
    self = [super init];
    if(self) {
        _sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        _operationQueue = [[NSOperationQueue alloc] init];
        _operationQueue.maxConcurrentOperationCount = 1;
        _session = [NSURLSession sessionWithConfiguration:_sessionConfiguration delegate:self delegateQueue:_operationQueue];
        _taskDelegates = [NSMutableDictionary dictionary];
        _lock = [[NSLock alloc]init];
    }
    return self;
}

- (void)GET:(NSString *)URLString
 parameters:(id)parameters
    success:(void (^)(id responseData))success
    failure:(void (^)(NSError * error))failure {
    
    NSMutableURLRequest  *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URLString]];
    NSURLSessionDataTask *task = [self dataTaskWithRequest:request success:success failure:failure];
    [task resume];
}

- (void)POST:(NSString *)URLString
  parameters:(id)parameters
     success:(void (^)(id responseData))success
     failure:(void (^)(NSError * error))failure {
    
    NSMutableURLRequest  *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URLString]];
    request.HTTPMethod = @"POST";
    NSString *postStr;
    NSData *postData;
    if([parameters isKindOfClass:[NSString class]]){
        postStr = parameters;
    }
    if([parameters isKindOfClass:[NSDictionary class]]){
        
        NSDictionary *keyValues = parameters;
        NSMutableString *tempStr = [NSMutableString string];
        __block int index = 0;
        [keyValues enumerateKeysAndObjectsUsingBlock:^(NSString* key, id obj, BOOL *stop){
            if(index>0){
                [tempStr appendString:@"&"];
            }
            NSString *kv = [NSString stringWithFormat:@"%@=%@",key,obj];
            [tempStr appendString:kv];
            index++;
        }];
        postStr = tempStr;
    }
    
    postData = [postStr dataUsingEncoding:NSUTF8StringEncoding];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"content-type"];
    request.HTTPBody = postData;
    NSURLSessionDataTask *task = [self dataTaskWithRequest:request success:success failure:failure];
    [task resume];
}

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSMutableURLRequest *)request
                                      success:(void (^)(id responseData))success
                                      failure:(void (^)(NSError * error))failure {
    
    __block NSURLSessionDataTask *dataTask  = [self.session dataTaskWithRequest:request];
    HTTPSessionDataTaskCompletionBlock  completionHandler = ^(NSURLResponse * __unused response, id responseObject, NSError *error) {
        if (error) {
            if (failure) {
                failure(error);
            }
        } else {
            if (success) {
                success(responseObject);
            }
        }
    };
    [self addDataTaskCompletionBlock:completionHandler forTask:dataTask];
    return dataTask;
}

- (void)addDataTaskCompletionBlock:(HTTPSessionDataTaskCompletionBlock)completionHandler forTask:(NSURLSessionDataTask*)task {
    
    HTTPClientSessionDelegate *sessionDelegate = [[HTTPClientSessionDelegate alloc] init ];
    sessionDelegate.taskCompletionHandler = completionHandler;
    
    NSUInteger identifier = task.taskIdentifier;
    [self.lock lock];
    self.taskDelegates[@(identifier)] = sessionDelegate;
    [self.lock unlock];
}

#pragma mark-- NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    
    HTTPClientSessionDelegate *delegate  = self.taskDelegates[@(task.taskIdentifier)];
    if(delegate) {
        [delegate URLSession:session task:task didCompleteWithError:error];
    }
}

#pragma mark-- NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    HTTPClientSessionDelegate *delegate  = self.taskDelegates[@(dataTask.taskIdentifier)];
    if(delegate) {
        [delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    
    HTTPClientSessionDelegate *delegate  = self.taskDelegates[@(dataTask.taskIdentifier)];
    if(delegate) {
        [delegate URLSession:session dataTask:dataTask didReceiveData:data ];
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * __nullable cachedResponse))completionHandler {
    
    HTTPClientSessionDelegate *delegate  = self.taskDelegates[@(dataTask.taskIdentifier)];
    if(delegate) {
        [delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
    }
}
@end

3.使用示例

HTTPClient *httpClient = [[HTTPClient alloc]init];
NSString *urlString = [NSString stringWithFormat:@"%@%@",kServerBaseUrl,@"/VersionCheck"];
[httpClient GET:urlString parameters:nil
        success:^(id responseObject){
            
            NSString *responseStr = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
            NSLog(@"data =%@ ",responseStr);
            
        }
        failure:^(NSError * error){
        }
 ];

NSDictionary *parameters = @{
                             
                             @"versionNo":@"1.0",
                             @"platform":@"Mac",
                             @"channel":@"appstore",
                             @"appName":@"DBAppX"
                             
                             };
[httpClient POST: urlString parameters:parameters
         success:^(id responseObject){
             
             NSString *responseStr = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
             NSLog(@"data =%@ ",responseStr);
             
         }
         failure:^(NSError * error){
             
         }
 ];

NSURLConnection

目前NSURLConnection在新项目开发中已经不推荐使用,但是在一些老的代码中大量使用,因此了解它的使用方法对阅读老代码和重构是非常有必要的。

同步调用

调用sendSynchronousRequest方法发起同步调用,如果调用超时或网络异常会返回error错误

NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://127.0.0.1/iosxhelper/SAPI/VersionCheck"]
                                       cachePolicy:NSURLRequestUseProtocolCachePolicy
                                   timeoutInterval:60.0];

NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];
if (error) {
    NSLog(@"send request failed: %@", error);
    return ;
}
    

异步调用

[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue new]  completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    
    if (connectionError) {
        NSLog(@"request failed: %@", error);
        return ;
    }
    
    NSString *asynResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"asynResponse: %@", asynResponse);
    
}];
    

使用代理的方式

初始化创建connection,设置代理

 NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request  delegate:self];
 

代理协议

#pragma mark-- NSURLConnectionDelegate
//接收数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if(!self.data){
        self.data = [NSMutableData data];
    }
    [self.data appendData:data];
}

//接收数据完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
   NSString *response = [[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding];
   NSLog(@"connectionDidFinishLoading recieve data  %@",response);
}

//收到请求后响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"connectionDidFinishLoading recieve response  %@",response);
}

//连接过程中发生错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    if(error){
        NSLog(@"connectionFailWithError %@",error);
    }
}

大文件下载

使用NSURLConnection请求下载文件时,一般对接收到的数据都需要存储到内存变量中。如果文件很大必然对内存要求很高甚至无法满足,因此需要一种能及时把数据写入文件而无需占用大内存的技术。

NSFileHandle可以将数据追加写入文件末尾,很适合在文件下载场景中使用去减少大量的内存使用。

下面是一个27M的文件下载 没有使用NSFileHandle和使用后内存使用对比,没有使用的情况下内存占用43M 而使用后仅为17.9M。

NSFileHandle使用很简单,首先需要在硬盘创建一个文件,然后以文件路径为参数调用NSFileHandle的类方法fileHandleForWritingAtPath方法创建NSFileHandle实例,每次有网络数据需要写入前先定位到文件末尾写入即可。

UseNSFileHandle

下面是使用NSFileHandle来下载文件的一个示例代码

@interface AppDelegate ()<NSURLConnectionDataDelegate>

@property (weak) IBOutlet NSWindow *window;

@property(nonatomic,strong)  NSFileHandle *fileHandle;//文件句柄
@property(nonatomic,strong)  NSString *filePath;//文件路径
@property (nonatomic,assign) NSInteger totalLength;//文件的总大小
@property (nonatomic,assign) NSInteger currentLength;//已接收文件数据的大小

@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    [self startDownload];
}

- (void)startDownload {
    NSURL *url = [NSURL URLWithString:@"http://devstreaming.apple.com/videos/wwdc/2015/1026npwuy2crj2xyuq11/102/102_platforms_state_of_the_union.pdf?dl=1"];
    NSURLRequest *request=[NSURLRequest requestWithURL:url];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request  delegate:self];
}

#pragma mark-- NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if(!self.fileHandle){
        self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.filePath];
    }
    //1.定位到文件末尾
    [self.fileHandle seekToEndOfFile];
    //2.往文件中写入数据
    [self.fileHandle writeData:data];
    self.currentLength += data.length;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"download File Finish !");
    [self.fileHandle closeFile];
    self.fileHandle = nil;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"connection recieve response  %@",response);
    self.totalLength = response.expectedContentLength;
    NSString *fileName = [[ response.URL absoluteString]lastPathComponent];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    self.filePath = [[NSHomeDirectory()
                              stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:fileName];
    //创建文件
    [fileManager createFileAtPath: self.filePath contents:nil attributes:nil];
    
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    if(error){
        NSLog(@"connection error %@",error);
    }
}

断点续传

对于没有完全下载完成的文件,可以在request请求头中设置Range字段,表示从当前已经下载字节数之后开始下载。

NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];    

NSURLDownload

NSURLDownload类不支持iOS,仅支持OSX 系统中使用。使用它进行文件下载,使用Request指定下载源路径,配置本地下载路径,实现NSURLDownloadDelegate代理协议中下载进度,下载完成相关方法即可。

- (void)startURLDownloadTest {
    NSURL *url = [NSURL URLWithString:@"http://devstreaming.apple.com/videos/wwdc/2015/105ncyldc6ofunvsgtan/105/105_introducing_watchkit_for_watchos_2.pdf?dl=1"];
    NSURLRequest *request=[NSURLRequest requestWithURL:url];
    NSURLDownload *down = [[NSURLDownload alloc]initWithRequest:request delegate:self];
    NSString *filePath = [[NSHomeDirectory()
                              stringByAppendingPathComponent:@"Downloads"] stringByAppendingPathComponent:@"105_introducing_watchkit_for_watchos_2.pdf"];
    //设置文件本地下载后的保存地址
    [down setDestination:filePath allowOverwrite:YES];
}

#pragma mark -- NSURLDownloadDelegate
- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response {
    NSLog(@"download didReceiveResponse %@ ",response);
    //获取文件总大小
    self.totalLength = response.expectedContentLength;
}

- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(NSUInteger)length {
    //计算已经下载的大小
    self.currentLength += length;
    NSLog(@"download didReceiveDataOfLength %ld currentLength %ld ",length,self.currentLength);
}

- (void)downloadDidFinish:(NSURLDownload *)download {
    NSLog(@"download Did Finish !");
}

- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error {
    if(error){
        NSLog(@"didFailWithError %@",error);
    }
}