[译]URL Session编程指南

本文翻译至苹果开发者文档:URL Session Programming Guide
部分翻译有参考。

关于URL加载系统

本指南描述了使用标准Internet协议来处理URLs、与服务器通信的相关类。
URL Loading System包含了一系列类和协议,来支持app访问URL上的内容。核心类是NSURL,使得app可以操作URL以及该URL指向的内容。
为了支持NSURL,Foundation framework提供了丰富的类来帮助你加载URL的内容、上传数据到服务器、管理cookie存储,控制缓存,在app上处理证书和认证方式,以及扩展用户协议。

URL Loading System支持通过以下协议来访问资源:

  • FTP协议(ftp://)
  • 超文本传输协议(http://)
  • 加密超文本传输协议(https://)
  • 本地资源(file://)
  • 数据URLs(data://)

它也透明的支持用户系统设置的代理服务器和SOCKS网关。

提示:
获得更多关于在OS X中加载服务的,阅读Launch Services Programming Guide;
获得更多关于在OS X中NSWorkSpace使用“openURL:”方法,阅读NSWorkspace Class Reference;
获得更多关于在iOS中UIApplication使用“openURL:”方法,阅读 UIApplication Class Reference ;

概述

URL loading system包含了一系列类,主要是五个部分:协议支持、认证与证书、cookie存储、配置管理、缓存管理。如下图:

URL 加载

获取URL内容,你可以使用NSURLSession,至于你需要使用的方法,很大程度上决定于你希望把获取到的数据缓存到内存还是磁盘。

数据获取到内存

在高级API层面,有两种最基本的方法来获取数据:

  • 简单的,直接从NSURL使用NSURLSession API来获取内容,不管是NSData对象还是磁盘上的文件;
  • 复杂的,比如上传数据的网络请求,需要提供NSURLRequest(或其可变对象NSMutableURLRequest)给NSURLSession。

不管你选择哪种方式,你的APP可以以两种方式从响应中的获得数据:

  • 提供一个完成的block。当接受完数据,即执行该block;
  • 提供一个自定义的代理,可以定期的从代理方法中获取到源数据,如何处理,自己决定。

除了数据本身,URL loading class还给你的代理,或者完成后的block提供了一个响应对象,该对象包含了关于该网络请求的元数据,比如MIME类型、content length。

下载数据,并存储为文件

同样,有两种方式来下载数据。同上。

注意:由NSURLSession下载的数据,并不会缓存,如果你需要缓存结果,APP需要利用NSURLSession,或写到磁盘。

辅助类

URL loading classes提供了两个辅助类,来提供附加的数据,一个是请求类(NSURLRequest),一个是响应类(NSURLResponse)。

重定向和其他请求变更

一些协议,比如HTTP,提供了一种服务来告诉APP说要访问的资源已经迁移到新的地址了。URL loading classes可以告知app中的代理。可以选择继续跳转到新地址或直接报错。

证书与认证

某些服务器对访问特定内容有限制,需要用户提供证明——比如,客户端证书、用户名密码等——来进行认证,获得访问权限。如果是服务器,受限内容会按范围分组,并需要单个或者一系列证书。当然,证书也可以用在客户端来认证服务器是否可信。

缓存管理

URL loading system 提供了基于磁盘和内存缓存组件来减少对网络连接的依赖,以及提供更快的访问之前缓存的响应数据。缓存是基于APP维度的,缓存由NSURLSession通过NSURLRequest 和 NSURLSessionConfiguration在初始化对象指定的cache policy来查询。

Cookie存储

由于HTTP协议的无状态特性,客户端常使用cookies来提供URL请求数据的持久化。URL loading system 提供了创建、管理cookies、发送作为请求一部分的cookies、接受请求响应中的cookies等操作的接口。

自定义协议

URL loading system 原生只支持http、https、file、ftp和data协议。然而,也允许你的APP注册自己的类,来实现应用层的网络协议。同样,你也可以在URL请求和响应中添加自定义的属性字段。

使用NSURLSession

NSURLSession及其相关的类提供了通过HTTP协议下载内容的API。这些API提供了丰富的代理方法支持认证,并且让你的应用程序在没有运行(在iOS中即是挂起时)时具有后台执行下载任务的能力。

为了使用NSURLSession的API,你的应用程序创建了一系列的会话,每一个会话都关联了一组数据传输相关的任务。比如,如果你在开发一个web浏览器,你的应用程序可能会每个标签或者窗口创建一个会话。对于每个会话,你的应用程序可能会添加一系列的任务到会话中,每个任务表示了一个特定URL的请求(以及任何返回的HTTP重定向)。

类似于大多数网络请求API,NSURLSession的API是高度异步的。如果你使用默认的NSURLSession,系统会提供代理,你必须提供一个处理完成的Block,用来在传输成功完成或者发生了错误时给你的应用程序返回数据。另外,如果你提供了你自己自定义的代理对象,任务对象在接收来自服务器的数据后调用这些代理的方法。

注意:完成回调主要用来当作一种可替代的自定义代理。如果你使用接收完成回调的方法创建了一个任务,那么用来传输响应和数据的代理方法就不会被调用。

NSURLSession的API提供了状态和进度属性,除了将这些信息传递给代理。它也支持取消,重新开始(恢复),挂起任务,也提供了恢复挂起,取消,或者下载失败的任务的能力。

理解URL Session概念

在一个会话中,任务的行为依赖于三件事情:会话的类型(取决于创建会话时使用的配置对象),任务的类型,以及任务创建时应用程序是否在前台。

会话类型

NSURLSession的API支持三种类型的会话,由创建会话时配置对象的类型决定:

  • Default会话:行为类似于其它用来下载URL内容的Foundation方法。使用持久化的基于磁盘的缓存和存储在用户钥匙串里的凭证。

  • Ephemeral会话:不会存储任何数据到磁盘;所有的缓存,凭证等等都与会话绑定存储在内存中。因此,当你的应用中会话无效时,它们会被自动清理。

  • Background会话:类似于Default类型,除了一个独立的进程处理所有的数据传输。Background会话有些额外的限制,在 Background Transfer Considerations 中描述了。

任务类型

对于会话,NSURLSession类支持三种类型的任务:data tasks,download tasks,and upload tasks。

  • Data tasks:使用NSData对象发送和接收数据。Data tasks主要针对你应用程序中那些与服务器简短的、经常交互的请求。Data tasks能够一次返回一小段数据,或者通过完成处理句炳一次返回。

  • Download tasks:在文件形式中检索数据,支持在应用程序没有运行时进行后台下载任务。

  • Upload tasks:以文件形式发送数据,支持在应用程序没有运行时进行后台上传任务。

后台传输的注意事项

NSURLSession类支持应用程序被挂起时后台传输。只有使用后台会话的配置对象创建的NSURLSession对象才支持后台传输(使用 backgroundSessionConfiguration: 创建后台会话的配置对象)。

因为后台会话在实际传输中,是在独立的进程中处理,以及重启你应用程序的进程是代价高昂,所以一些功能无法实现,导致了如下所示的限制:

  • 会话必须给每一个传输提供一个代理用于事件分发。(对于上传和下载,代理的行为类似于处理传输。)
  • 只有HTTP和HTTPS协议支持(自定义协议不支持)。
  • 始终允许重定向。
  • 只支持基于文件的上传任务(基于数据对象和流的上传任务在程序退出时就会失败)。
  • 如果后台传输是应用程序在后台时初始化的,那么配置对象的 discretionary 属性应该设置为true。

注意:在iOS8和OS X 10.10之前,Data tasks不支持后台会话。

iOS和OS X重新启动应用程序的行为有一些不一样。

在iOS中,当一个后台传输完成或者需要凭证,如果你的应用程序没有在运行,iOS在后台自动重启你的应用程序然后在应用程序的UIApplicationDelegate对象上调用application:handleEventsForBackgroundURLSession:completionHandler:方法。这个方法提供了导致你应用程序重启的会话的identifier。你的应用程序应该存储完成处理句柄,使用这个相同的identifier创建一个后台配置对象,然后使用这个后台配置对象创建一个会话。新的会话会自动与后台活动的关联。之后,当会话完成了最后一个后台下载任务,它会给会话的代理发送一个URLSessionDidFinishEventsForBackgroundURLSession:消息。你的会话代理应该在主线程调用之前存储的完成句柄,以便让系统知道可以再次安全的挂起你的应用。

在iOS和OS X中,当用户重启了你的应用程序,你的应用程序应该立即使用相同的identifier为应用程序最后运行的未完成的任务创建后台配置的对象,然后为每一个配置对象创建一个会话。这些新的会话会自动与后台活动的关联。

注意:每个identifier只能创建一个会话(创建配置对象的时候指定)。多个会话共享同一个identifier的行为是不确定。

当应用程序挂起的时候,有任务完成,代理的URLSession:downloadTask:didFinishDownloadingToURL:方法会被调用,以及与新的下载文件相关的任务和URL。
类似的,如果任务需要凭证,NSURLSession对象调用代理的URLSession:task:didReceiveChallenge:completionHandler:方法或者URLSession:didReceiveChallenge:completionHandler:方法。

后台会话中的上传和下载的任务在网络错误后,URL加载系统会自动重试。所以没有必要使用reachability的API来侦测网络,来确定何时重试失败任务。

NSURLSession后台传输的例子,参见Simple Background Transfer

生命周期和代理交互

取决于你使用NSURLSession类做什么,完整了解会话的生命周期是有帮助的,包括会话是如何与代理进行交互的,以及代理调用的顺序,当服务器返回重定向的时候会发生什么,以及当你的应用程序恢复一个下载失败的任务时发生了什么,等等。

关于会话生命周期的完整描述,参见Life Cycle of a URL Session

NSCopying 行为

会话和任务对象按照如下适配NSCopying协议:

  • 当你的应用程序复制会话或者任务对象时,你会得到同一个对象。
  • 当你的应用程序复制一个配置对象,你会得到一个新的拷贝的对象,便于你可以独立的修改。

代理类接口实例

Listing 1-1 Sample delegate class interface

import <Foundation/Foundation.h>

typedef void (^CompletionHandlerType)();

@interface MySessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>

@property NSURLSession *backgroundSession;
@property NSURLSession *defaultSession;
@property NSURLSession *ephemeralSession;

if TARGET_OS_IPHONE
@property NSMutableDictionary *completionHandlerDictionary;
endif

-(void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier;
-(void) callCompletionHandlerForSession: (NSString *)identifier;

@end

创建和配置会话

NSURLSession的API提供了广泛的配置选项:

  • 支持给单个的会话指定私有的缓存,Cookie,凭证,和协议。
  • 依赖于特定请求(task)或者一组请求(session)的认证。
  • 通过URL上传和下载文件,支持数据(文件内容)和元数据(URL和设置)分离。
  • 配置每个主机的最大连接数。
  • 如果一整个资源在一定的时间下不能下载就会触发每个资源超时。
  • 最低和最高的TLS版本支持。
  • 自定义的代理字典。
  • 控制Cookie策略。
  • 控制HTTP管道行为。

因为大部分设置都包含在一个分开的配置对象里,你可以重用过去常用的设置。当你初始化一个会话对象时,按照如下:

  • 一个配置对象,管理会话和任务的行为。
  • 一个可选的代理对象,用来处理接收的数据以及处理指定给会话和任务的其它事件,比如服务器认证,确定资源请求是否应该被转换成下载等。

如果你没有提供代理对象,NSURLSession对象会使用系统提供的代理。通过这种方式,你可以很容易的使用NSURLSession来替换已经在使用 sendAsynchronousRequest:queue:completionHandler: 方法的代码。

注意:如果的应用程序需要执行后台传输,你必须提供一个自定义的代理。

在你初始化一个会话对象后,你不能改变配置或者代理,除非重新创建一个新的会话。

Listing 1-2展示了如何创建一个normal, ephemeral, 和 background的会话的例子。
Listing 1-2 Creating and configuring sessions

if TARGET_OS_IPHONE
self.completionHandlerDictionary = [NSMutableDictionary dictionaryWithCapacity:0];
endif

/* Create some configuration objects. */

NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: @"myBackgroundSessionIdentifier"];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSessionConfiguration *ephemeralConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];


/* Configure caching behavior for the default session.
 Note that iOS requires the cache path to be a path relative
 to the ~/Library/Caches directory, but OS X expects an
 absolute path.
 */
if TARGET_OS_IPHONE
NSString *cachePath = @"/MyCacheDirectory";

NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *myPath    = [myPathList  objectAtIndex:0];

NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];

NSString *fullCachePath = [[myPath stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:cachePath];
NSLog(@"Cache path: %@\n", fullCachePath);
else
NSString *cachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"/nsurlsessiondemo.cache"];

NSLog(@"Cache path: %@\n", cachePath);
endif

NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384 diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;

/* Create a session for each configurations. */
self.defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.ephemeralSession = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

除了后台配置,你可以重用其他的配置对象创建额外的会话。(你不能重用后台会话配置,因为两个后台会话对象共享相同的identifier的行为不确定的。)

任何时候修改配置对象都是安全的。当你创建会话的时候,会话会对配置对象执行深拷贝,所以修改只会影响新创建的会话,不会影响已经存在的。比如,你可能创建了第二个会话,只在Wi-Fi连接下才获取内容,如Listing 1-3所示。

Listing 1-3 Creating a second session with the same configuration object

ephemeralConfigObject.allowsCellularAccess = NO;
// ...

NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

使用系统代理获取资源

使用NSURLSession的最直接的方法就是使用NSURLSession上的方法替换sendAsynchronousRequest:queue:completionHandler: 方法。使用这种方法,你只需要完成两部分代码:

  • 创建一个配置对象,然后使用配置对象创建一个会话。
  • 当数据完全接收后的处理句柄。

使用系统提供的代理,你可以只使用一行代码就能发起获取特定URL的请求。Listing 1-4演示了这个简单的例子。

注意:系统提供的代理只在自定义网络行为的时候有限制。如果你的应用程序需要除了基本请求之外的特别行为,比如自定义的认证或者后台下载,这种技术就不太适合。对于你必须实现完整代理的情形,参见Life Cycle of a URL Session。

Listing 1-4 Requesting a resource using system-provided delegates

NSURLSession *sessionWithoutADelegate = [NSURLSession sessionWithConfiguration:defaultConfiguration];
NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];

[[sessionWithoutADelegate dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"Got response %@ with error %@.\n", response, error);
NSLog(@"DATA:\n%@\nEND DATA\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}] resume];

let sessionWithoutADelegate = URLSession(configuration: defaultConfiguration)
if let url = URL(string: "https://www.example.com/") {
  (sessionWithoutADelegate.dataTask(with: url) { (data, response, error) in
  if let error = error {
      print("Error: \(error)")
  } else if let response = response,
  let data = data,
  let string = String(data: data, encoding: .utf8) {
  print("Response: \(response)")
  print("DATA:\n\(string)\nEND DATA\n")
  }
  }).resume()
}

使用自定义代理获取数据

如果使用自定义的代理方法,至少要实现下面两个代理方法:

  • URLSession:dataTask:didReceiveData::提供任务请求返回的数据,一次一小块数据。
  • URLSession:task:didCompleteWithError::检测任务是否完整接受数据。

如果我们的应用需要在URLSession:dataTask:didReceiveData:方法之后使用数据,我们自己需要负责存储所有返回的数据.

Listing 1-5 显示如何创建和启动任务

NSURL *url = [NSURL URLWithString: @"https://www.example.com/"];
NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithURL:url];
[dataTask resume];


if let url = URL(string: "https://www.example.com/") {
let dataTask = defaultSession.dataTask(with: url)
    dataTask.resume()
}

下载文件

在高级API中,下载文件和检索数据相似。应用需要实现下面的代理方法:

  • URLSession:downloadTask:didFinishDownloadingToURL:提供了下载文件内容暂存的URL地址

    重要:在该方法返回前,必须将下载的文件移动到新位置,或者另外打开一个文件进行读写。否则,在返回之后,该暂存位置的文件将被删除。

  • URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:提供了下载进度
  • URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:通知应用重启之前下载失败的任务
  • URLSession:task:didCompleteWithError:下载失败

当我们计划下载一个后台任务,即使应用没启动,后台也会下载。但是使用标准和短暂的会话,下载任务必须在应用重启后开启新的会话。

在下载过程中,用户可以通过cancelByProducingResumeData:方法暂停正在执行的任务。如果后续要继续下载,我们将从这个方法中获取的数据存储起来,然后使用downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:来创建新的任务继续下载。

如果传输失败了,代理方法URLSession:task:didCompleteWithError:会被调用,如果任务可以继续下载,会在userInfo字典中存储key为NSURLSessionDownloadTaskResumeData的值,取到未下载完的数据可以继续创建新的会话下载.

Listing 1-6 provides an example of downloading a moderately large file. Listing 1-7 provides an example of download task delegate methods

Listing 1-6  Download task example

NSURL *url = [NSURL URLWithString:@"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/ObjC_classic/FoundationObjC.pdf"];
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:url];
[downloadTask resume];


if let url = URL(string: "https://www.example.com/") {
    let dataTask = defaultSession.dataTask(with: url)
    dataTask.resume()
}


Listing 1-7  Delegate methods for download tasks

- (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);
}

-(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);
}

-(void)URLSession:(NSURLSession *)session
    downloadTask:(NSURLSessionDownloadTask *)downloadTask
    didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"Session %@ download task %@ finished downloading to URL %@\n", session, downloadTask, location);

    // Perform the completion handler for the current session
    self.completionHandlers[session.configuration.identifier]();

    // Open the downloaded file for reading
    NSError *readError = nil;
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:location error:readError];
    // ...

    // Move the file to a new URL
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *cacheDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
NSError *moveError = nil;
    if ([fileManager moveItemAtURL:location toURL:cacheDirectory error:moveError]) {
        // ...
    }
}


func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    print("Session \(session) download task \(downloadTask) wrote an additional \(bytesWritten) bytes (total \(totalBytesWritten) bytes) out of an expected \(totalBytesExpectedToWrite) bytes.\n")
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("Session \(session) download task \(downloadTask) resumed at offset \(fileOffset) bytes out of an expected \(expectedTotalBytes) bytes.\n")
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    print("Session \(session) download task \(downloadTask) finished downloading to URL \(location)\n")

    // Perform the completion handler for the current session
    self.completionHandlers[session.configuration.identifier]()

    // Open the downloaded file for reading
    if let fileHandle = try? FileHandle(forReadingFrom: location) {
    // ...
    }

    // Move the file to a new URL
    let fileManager = FileManager.default()
    if let cacheDirectory = fileManager.urlsForDirectory(.cachesDirectory, inDomains: .userDomainMask).first {
        do {
            try fileManager.moveItem(at: location, to: cacheDirectory)
        } catch {
            // ...
        }
    }
}

上传Body内容

应用发送POST请求会携带Body内容有三种形式: NSData对象,文件和流对象.
• 如果应用内存中已经有上传的数据,使用NSData对象上传,而且没有理由重新部署数据。
• 如果上传的内容在磁盘中的文件中,或者执行后台任务,或者为了释放对应数据的内存而将数据写入文件,都可以使用文件方式.
• 在通过网络收到的数据源或者转换现有的NSURLConnection对象时,使用流对象。

不论选择哪种方式,如果提供了自己的代理,代理方法应该实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:来获取上传进度。

另外,如果使用流对象上传,必须提供一个自定义的会话代理方法实现uploadTaskWithRequest:fromData:completionHandler:,该方法在 Uploading Body Content Using a Stream.

使用NSData对象上传

通过NSData对象上传,应用会调用uploadTaskWithRequest:fromData:或者uploadTaskWithRequest:fromData:completionHandler:方法创建任务,并且通过fromData参数获取到提供的Body数据。

会话基于数据对象的大小计算出Content-Length中。
应用也需要提供任何其他服务器可选的附加字段——Content-Type。

Listing 1-8  Uploading task from data example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];
NSData *data = [NSData dataWithContentsOfURL:textFileURL];

NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Type"];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];

NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromData:data];
[uploadTask resume];


let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let data = try? Data(contentsOf: textFileURL) {
    if let url = URL(string: "https://www.example.com/") {
        var mutableRequest = MutableURLRequest(url: url)
        mutableRequest.httpMethod = "POST"
        mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
        mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")

        let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: data)
        uploadTask.resume()
    }
}

使用文件上传

通过文件上传,会调用uploadTaskWithRequest:fromFile:或uploadTaskWithRequest:fromFile:completionHandler:。且需要提供一个URL指定文件位置.

Listing 1-9  Uploading task from streamed request example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];

NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";

NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithRequest:mutableRequest fromFile:textFileURL];
[uploadTask resume];


let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
    var mutableRequest = MutableURLRequest(url: url)
    mutableRequest.httpMethod = "POST"
    mutableRequest.httpBodyStream = InputStream(fileAtPath: textFileURL.path!)
    mutableRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
    mutableRequest.setValue("text/plain", forHTTPHeaderField: "Content-Type")

    let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
    uploadTask.resume()
}

使用流对象上传

使用方法uploadTaskWithStreamedRequest:创建任务。应用需提供一个流对象关联的请求,请求会从流对象读取内容。

另外,由于会话不能重读流中的信息,所以在任务重试(比如,认证失败后)的时候需要提供一个新的流对象——使用方法URLSession:task:needNewBodyStream:方法。当方法调用时,应用可以为获取或者创建新的流对象做任何操作,然后调用提供的完成句柄来处理新的流对象。

Listing 1-10  Uploading task from stream example

NSURL *textFileURL = [NSURL fileURLWithPath:@"/path/to/file.txt"];

NSURL *url = [NSURL URLWithString:@"https://www.example.com/"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url];
mutableRequest.HTTPMethod = @"POST";
mutableRequest.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:textFileURL.path];
[mutableRequest setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
[mutableRequest setValue:[NSString stringWithFormat:@"%lld", data.length] forHTTPHeaderField:@"Content-Type"];

NSURLSessionUploadTask *uploadTask = [defaultSession uploadTaskWithStreamedRequest:mutableRequest];
[uploadTask resume];


let textFileURL = URL(fileURLWithPath: "/path/to/file.txt")
if let url = URL(string: "https://www.example.com/") {
var mutableRequest = MutableURLRequest(url: url)
mutableRequest.httpMethod = "POST"

let uploadTask = defaultSession.uploadTask(with: mutableRequest, from: textFileURL)
uploadTask.resume()
}

使用下载任务上传文件

当使用下载任务上传文件的时,在创建下载请求时必须提供NSData对象或者流对象作为NSURLRequest对象的一部分。

如果使用了流对象,必须实现代理方法URLSession:task:needNewBodyStream:,用于在认证失败的时候回调处理。

下载任务的行为除了在返回数据的处理方式上不同,其他和普通的数据任务一致。

身份认证和自定义TLS链验证

如果远端服务器返回状态码标识需要鉴权或者需要在连接的时候需要认证,NSURLSession会回到认证相关的代理方法。

  • 会话层的认证:针对NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, or NSURLAuthenticationMethodServerTrust,NSURLSession对象会调用代理方法URLSession:didReceiveChallenge:completionHandler:,若未该代理方法,会话会回调任务的代理方法URLSession:task:didReceiveChallenge:completionHandler:去处理。
  • 非会话层的认证:NSURLSession类会调用代理方法URLSession:task:didReceiveChallenge:completionHandler:.如果应用提供了会话的代理,且你需要处理认证过程,此时必须在任务层做认证,或者提供一个任务层的处理方法,明确调用每个代理的处理方法。对于非会话层的认证,URLSession:didReceiveChallenge:completionHandler:不会被调用.

当以流形式上传内容体的时候,任务不能安全地重新绑定或重用流对象,而是NSURLSession对象会调用代理方法URLSession:task:needNewBodyStream: 来获取新的NSInputStream作为新请求的内容体。

处理iOS后台活动

如果使用NSURLSession,当后台下载任务完成的时候会在后台重启app,代理方法application:handleEventsForBackgroundURLSession:completionHandler:负责重新创建合适的会话和保存完成句柄。然后在URLSessionDidFinishEventsForBackgroundURLSession: 方法中调用完成句柄。

URL数据编码

使用Core Foundation方法CFURLCreateStringByAddingPercentEscapesCFURLCreateStringByReplacingPercentEscapesUsingEncoding来编码URL字符串。这些方法允许你指定需要编码的字符列表,另外以高ASCII(0x80-0xff),和不可见字符来编码。

根据 RFC 3986,URL的保留字符如下:

reserved    = gen-delims / sub-delims
gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="

因此,需要正确对一个包含UTF-8字符串的URL编码,你需要和下面这样做:

CFStringRef originalString = ...

CFStringRef encodedString = CFURLCreateStringByAddingPercentEscapes(
kCFAllocatorDefault,
originalString,
NULL,
CFSTR(":/?#[]@!$&'()*+,;="),
kCFStringEncodingUTF8);

如果你想要解码URL片段,首先,你必须将URL字符串分为域名和路径。如果你未解码,你将无法分辨出域名中一个经过编码后的字符和未编码时域名中的结束符它们两者之间的区别。

在你已经将URL分片后,你可以像下面这样解码:

CFStringRef decodedString = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(
kCFAllocatorDefault,
encodedString,
CFSTR(""),
kCFStringEncodingUTF8);

处理重定向以及其他请求变更

重定向发生在服务器响应请求后,又发现客户端需要重新创建一个指向不同URL地址的请求时。此时,NSURLSession类会通知其代理。

处理重定向,你必须实现URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:代理方法。

在该方法中,你可以做:

  • 通过返回的重定向后的请求,允许进行重定向;
  • 创建一个新的请求,指向新URL地址,并返回该请求;
  • 拒绝重定向(此时该连接返回的数据为nil);

另外,代理也可以NSURLSession类,对任务对象发送“cancel”消息,来同时取消重定向以及连接。

同时,假如NSURLProtocol子类为了格式标准化而改变了NSURLRequest,代理也会收到URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:消息。比如,将http://www.apple.com 改变为 http://www.apple.com/。这种改变用于标准化或规范化,以便用于缓存管理。在这种特殊的情况下,response会返回nil,并且代理应该直接跳转到提供的请求地址。

Listing 3-1  Example of an implementation ofURLSession:task:willPerformHTTPRedirection:newRequest:completionHandler

(void)URLSession:(NSURLSession *)session
    task:(NSURLSessionTask *)task
    willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse
    newRequest:(NSURLRequest *)request
    completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSURLRequest *newRequest = request;
if (redirectResponse) {
newRequest = nil;
}
completionHandler(newRequest);

}

假如代理没有实现重定向代理方法,那么,所有的请求格式标准化和服务器重定向都是可以的。

身份认证与TLS链认证

NSURLRequest对象经常遇到身份认证,或者从连接的服务器获取证书。当遇到身份认证,NSURLSession就会通知其代理对象。

重要
URL loading classes除非在服务器响应头部包含“WWW-Authenticate”,否则不会通知对应的代理来处理请求认证。其他认证类型,比如代理(proxy)或者TLS信任不需要该头部。

如何处理身份认证

如果一个会话任务需要认证,并且没有有效凭证,无论是作为请求的URL的一部分或在共享nsurlcredentialstorage。首先它会给任务的代理发送 URLSession:task:didReceiveChallenge:completionHandler: 消息来处理,如果任务代理未响应处理,那么任务就会转发送给会话代理。

代理可以有三种处理选择:

  • 提供认证凭证;
  • 尝试不带凭证继续请求;
  • 取消认证;

为了正确的处理, NSURLAuthenticationChallenge实例会提供代理方法:什么触发了认证、可以尝试多少次认证、之前尝试过的认证凭据、请求凭据的NSURLProtectionSpace以及谁发起认证。

如果身份认证在之前认证失败,你可以通过调用proposedCredential获得尝试过的认证凭据,在代理中利用这些凭据来弹出对话框告知用户。

通过调用previousFailureCount,可以获取到之前各种不同的认证协议的认证的总次数。代理方法可以将这部分信息通知用户,来决定是否之前提供的凭据是失败的,或者限制尝试认证的次数。

响应认证

下面三种方式可以在URLSession:didReceiveChallenge:completionHandler: 或URLSession:task:didReceiveChallenge:completionHandler: 代理方法中响应。

提供凭据

认证需要创建一个服务器期望的NSURLCredential对象,该对象可以通过 authenticationMethod 来制定认证方式。下面是几种认证方式:

  • HTTP基本认证(NSURLAuthenticationMethodHTTPBasic)需要用户名和密码。通过credentialWithUser: password:persistence:.创建NSURLCredential对象;
  • HTTP摘要认证(NSURLAuthenticationMethodHTTPDigest),类似于基本认证,不过在提供用户名和秘密后,摘要会自动生成。通过credentialWithUser: password:persistence:.创建NSURLCredential对象;
  • 客户端证书认证(NSURLAuthenticationMethodClientCertificate)需要系统识别出所有服务器需要的证书。通过 credentialWithIdentity:certificates:persistence:. 来创建NSURLCredential对象;
  • 服务器信任认证(NSURLAuthenticationMethodServerTrust)需要一个信任对象,通过 credentialForTrust:创建NSURLCredential对象。

在创建完NSURLCredential对象后,将该对象传递给对应的完成处理句柄。

继续无凭据

如果代理方法不能提供有效凭据,可以尝试继续无凭据访问。可将下面两个值之一传给完成句柄:

  • NSURLSessionAuthChallengePerformDefaultHandling 处理请求就像代理没有提供处理认证的代理方法一样。
  • NSURLSessionAuthChallengeRejectProtectionSpace 拒绝认证。根据服务器的响应,URL loading class可能会在多个受保护的空间进行多次调用该代理方法。

取消认证

将值NSURLSessionAuthChallengeCancelAuthenticationChallenge 传递给完成句柄即可。

一个认证实例

Listing 4-1 An example of using the

-(void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if ([challenge previousFailureCount] == 0) {
        NSURLCredential *newCredential = [NSURLCredential credentialWithUser:[self preferencesName]
        password:[self preferencesPassword]
        persistence:NSURLCredentialPersistenceNone];
        completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
    } else {
    // Inform the user that the user name and password are incorrect
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    guard challenge.previousFailureCount == 0 else {
        challenge.sender?.cancel(challenge)
        // Inform the user that the user name and password are incorrect
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }
    let proposedCredential = URLCredential(user: self.preferencesName, password:                self.preferencesPassword, persistence: .none)
    completionHandler(.useCredential, proposedCredential)
}

如果身份认证未被会话或者任务代理方法处理,而且凭据不可用,或者认证失败,那么会发送一个continueWithoutCredentialForAuthenticationChallenge:消息给底层实现。

执行自定义验证TLS链

在NSURL家族的API中,TLS链的验证交由应用认证代理方法处理,不同的是,并不提供凭据给服务器来认证用户(或者应用),你的应用会检查服务器在TLS握手时返回的凭据。然后告诉URL加载系统是否应该接受或拒绝该凭据。

如果你需要以一个非标准的方式进行链验证(如接受特定的自签名证书用于测试),你的应用程序必须实现的uURLSession:didReceiveChallenge:completionHandler: orURLSession:task:didReceiveChallenge:completionHandler: 。如果你都实现了,会话层负责处理认证方法。

你的认证处理委托方法中,你应该检查认证保护空间是否有一个认证类型nsurlauthenticationmethodservertrust,如果有,从保护空间信息获得serverTrust信息。

理解缓存是如何获取的

Cookie与自定义协议

附录 A:会话的生命周期

你可以通过两种方式使用NSURLSession API:系统代理或者自定义代理方法。一般来说,假如有以下情况,你就需要自己实现代理:

  • 在APP未运行时,利用background session执行在后台下载或者上传任务;

  • 执行自定义认证;

  • 基于服务器返回的MIME类型或者其他标准,决定暂存对象是否被下载到磁盘还是直接展示;

  • 基于流上传数据;

  • 手动管理缓存限制;

  • 手动管理HTTP重定向;

如果你的应用不会使用上面任何一条,就可以使用系统代理。基于你选择的代理,你需要阅读:

基于系统代理的URL Session的生命周期

  1. 创建一个session configuration 对象。对于后台任务,该配置必须包含一个唯一标识符。存储该标识符,并在应用崩溃、终止或挂起后用于重新关联到session;
  2. 创建一个session,指定configuration,并将delegate置为nil;
  3. 使用会话创建一个task对象,每个task代表一个资源请求;

    每个task的初态是挂起态,当你调用resume后,task才开始下载指定的资源。

    task 对象是NSURLSessionTask,NSURLSessionDataTask,NSURLSessionUploadTask, 或NSURLSessionDownloadTask之一的之类。

    虽然你的应用可以(常规做法)在一个session中添加多个任务,但是为了简单,下面的步骤,以单任务来说明

    重要

    如果你在使用NSURLSession时没有指定代理,那么就必须提供completionHandler参数给任务对象,否则无法获取到数据。

  4. 对于下载任务,假如在网络传输过程中,用户告诉用户暂停下载、发送cancelByProducingResumeData:消息取消下载任务。接下来,将会把需要恢复的数据传递给方法downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:,以便创建一个新的下载任务来继续下载。

  5. 当一个任务完成后,NSURLSession会调用任务完成句柄。

    提示
    NSURLSession不会通过error参数报告服务器的错误。只有在客户端的错误才会在APP端接收到,比如不能解析主机名或者不能连接到主机。这些错误编码在URL Loading System Error Codes中描述。
    服务端错误通过NSHTTPURLResponse 对象在HTTP 状态码返回。可以参考[NSHTTPURLResponseNSURLResponse

  6. 当你的应用不在需要会话,通过调用invalidateAndCancel(取消未完成的任务)或者finishTasksAndInvalidate(等待完成后废弃该会话)。

自定义代理的URL Session周期

在比如后台下载任务时,必须使用自定义的代理。代理作用于:

  • 当使用下载任务,NSURLSession对象会使用代理给你的应用提供一个可以获取到下载数据的文件URL。代理对于后台上传和下载都是必须的,这些代理必须实现NSURLSessionDownloadDelegate协议的所有方法。
  • 代理可处理某种身份认证;
  • 代理提供流内容;
  • 代理决定是否进行重定向;
  • NSURLSession对象利用代理向你的应用提供每个数据传输的状态。数据任务接受初始化回调,以及随后进行的回调,前者允许你将请求转换为下载,后者则向你提供了服务器传输来的片段化的数据。
  • NSURLSession对象可以告知你的应用传输完成。

自定义代理之后,下面是调用的方法基本顺序:

  1. 创建一个session configuration。对于background session,configuration必须包含一个唯一标识符。存储该标识符,并在应用崩溃、终止或挂起后用于重新关联到session;
  2. 创建一个session,指定configuration,代理对象是可选的;
  3. 创建一个session task对象,每个对象代表着一个资源请求;
  4. 如果远程服务器返回的状态码表明需要认证,假如认证是针对连接层的(比如SSL客户端证书),NSURLSession对象就会调用身份认证的代理方法。
  • 会话层的认证:针对NSURLAuthenticationMethodNTLM, NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate, or NSURLAuthenticationMethodServerTrust,NSURLSession对象会调用session 代理实现的方法URLSession:didReceiveChallenge:completionHandler:,若未提供该代理方法,会话会回调task的代理方法URLSession:task:didReceiveChallenge:completionHandler:去处理。
  • 非会话层的认证:NSURLSession类会调用代理方法URLSession:task:didReceiveChallenge:completionHandler:.如果应用提供了会话的代理,且你需要处理认证过程,此时必须在任务层做认证,或者提供一个任务层的处理方法,明确调用每个代理的处理方法。对于非会话层的认证,URLSession:didReceiveChallenge:completionHandler:不会被调用.

当以流形式上传内容体的时候,任务不能安全地重新绑定或重用流对象,而是NSURLSession对象会调用代理方法URLSession:task:needNewBodyStream: 来获取新的NSInputStream作为新请求的内容体。

  1. 在收到HTTP响应的重定向后,NSURLSession对象会调用代理的URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法。在提供的完成句柄中处理NSURLRequest对象(进行重定向)、新的NSURLRequest对象(重定向到不同的URL)、或者nil(将重定向的响应体左右有效的响应并返回)。

    • 如果进行重定向,就返回步骤4;
    • 如果代理没有实现该方法,重定向就会最大次数的尝试进行重定向。
  2. 通过调用downloadTaskWithResumeData: 或者downloadTaskWithResumeData:completionHandler:创建下载(重新下载)任务。NSURLSession对于新任务对象会调用代理的URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

  3. 对于数据任务,NSURLSession对象会调用URLSession:dataTask:didReceiveResponse:completionHandler:来决定是否将数据任务转换为下载任务,然后调用完成回调来继续接受数据或者下载数据;
    如果你的应用选择将数据任务转换为下载任务,NSURLSession会调用代理URLSession:dataTask:didBecomeDownloadTask:方法,并把新的下载任务作为该方法的参数。调用后,代理不在调用数据任务的相关回调,而是开始接受下载任务的回调;

  4. 如果通过uploadTaskWithStreamedRequest:,创建的任务,NSURLSession会调用回调方法URLSession:task:needNewBodyStream: 来提供内容数据;

  5. 在初始化上传内容到服务器的过程中,代理会定期回调方法URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend: 来报告上传的进度;

  6. 在从服务器到客户端的数据传输过程中,任务代理会定期的回调来报告传输的过程。对于下载任务,会话会调用代理URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:来显示多少字节已成功写到磁盘。对于数据任务,会话会调用URLSession:dataTask:didReceiveData:来表明实际接收到数据片段。

    对于下载任务,在传输过程中,如果用户告诉用户可通过调用cancelByProducingResumeData: 暂停下载、取消任务。

    之后,若要重启下载,在 downloadTaskWithResumeData: downloadTaskWithResumeData:completionHandler: 方法中获取需要回复的数据来创建新的下载任务。并重新回到步骤3;

  7. 对于数据任务,NSURLSession对象会调用方法 URLSession:dataTask:willCacheResponse:completionHandler: 来决定是否允许缓存。如果未实现该方法,那么默认表现就是利用在配置对象中指定的缓存策略;

  8. 如果下载任务完成后,NSURLSession对象会调用URLSession:downloadTask:didFinishDownloadingToURL: 并返回暂存文件的路径。你的应用必须在该方法return前,从文件中读取响应数据或者将该文件移动到应用沙盒目录中的永久路径;

  9. 当任何任务完成时,都会调用方法 URLSession:task:didCompleteWithError: ,并将传递一个错误对象。

    如果任务失败了,多数应用会重试请求,除非用户取消下载或者服务器返回的错误表明该请求永远不会成功。你的应用不应该立马进行重试,而是应该在使用reachability的API检测网络来决定服务器是否可达,然后在接受到网络变成可达的通知后创建一个新的请求。

    如果下载任务可以重新恢复,NSError对象的userInfo字典包含了 NSURLSessionDownloadTaskResumeData 键的值应该传递给downloadTaskWithResumeData: downloadTaskWithResumeData:completionHandler: ,以创建一个I下载任务来继续已存在的下载。

    如果任务不能恢复,你的应用必须重新创建一个新的下载任务,重现开始下载。

    除了服务器错误,如果传输失败在任何情况下,回到步骤3.。

    提示:NSURLSession不会通过error参数报告服务端的错误,从error参数只能获取到客户端的错误。比如不能解析域名或者不能连接到地址。错误编码在URL Loading System Error Codes描述。

    服务端错误通过NSHTTPURLResponse对象的HTTP 返回的状态码可以看出,这部分可以参考NSHTTPURLResponseNSURLResponse对象。

  10. 如果响应是多部分编码的,会话可能会多次回调didReceiveResponse,不调用或者多次调用didReceiveData。假如发生这种情况,回到步骤7;

  11. 当你的应用不再需要会话,通过调用invalidateAndCancel(取消未完成的任务)或者finishTasksAndInvalidate(等待完成后废弃该会话)。

    在废弃会话后,所有未完成的任务要么被取消,要么是已经完成。会话发送 URLSession:didBecomeInvalidWithError: 消息。当该方法return时,会话对象持有代理的强引用。

    重要:
    会话对象使用持有代理的强引用,直到该会话明确被废弃。如果你未废弃该会话,你的应用可能会发送内存泄漏。

如果应用在下载过程中取消,NSURLSession对象会看做发生错误,而调用代理URLSession:task:didCompleteWithError: