OC之NSURLSessionTask

2020-06-13  本文已影响0人  苏沫离

NSURLSessionTask 是对网络会话的管理者 NSURLSession 创建任务的封装,它仅仅是个抽象的基类,实际使用中可以划分为不同的任务:

NSURLSessionTask.png

通过NSURLSession 创建任务,有两种响应方式:

如果设置了delegate,根据不通的任务,由不同的 NSURLSessionDelegate 方法来处理:

NSURLSessionDelegate.png

可以重复使用一个NSURLSession来创建多个任务,创建的 NSURLSessionTask 对象总是处于挂起状态,在它们执行之前必须调用 -resume 方法。启动任务之后NSURLSession对该任务持有强引用,直到任务完成或者失败!

1、NSURLSessionTask API

NSURLSessionTask 只能通过 NSURLSession的实例方法获取,不能使用-init+new 创建!

所有任务属性都支持键值观察!

1.1、获取任务信息
/** 当前任务的请求对象
 * @note 通常与 originalRequest 相同,除非服务器以重定向到不同URL的方式响应初始请求
*/
@property (nullable, readonly, copy) NSURLRequest  *currentRequest;

/** 创建任务时传递的原始请求对象;
 * 如果是一个streamTask,则可能为 nil
 * @note 通常与 currentRequest 相同,除非服务器以重定向到不同URL的方式响应初始请求
*/
@property (nullable, readonly, copy) NSURLRequest  *originalRequest;

/** 服务器对 currentRequest 的响应信息
 * 包含关于服务器提供的请求信息,此信息始终包含originalRequest;它还可能包含预期的数据长度、MIME类型信息、编码信息、建议的文件名等;
 * @note 如果没有收到服务器的回应,可能为 nil
 */
@property (nullable, readonly, copy) NSURLResponse *response;
 
/** 该任务的标识符,由所属的 NSURLSession 实例分配,且具有唯一性
 * @note 在不同 NSURLSession 中的任务,可能具有相同的 taskIdentifier ;
 */
@property (readonly) NSUInteger taskIdentifier;

/** 开发人员为任务提供的描述,可用于调试程序、追踪任务、或直接显示给用户!
 * @note 系统不会设置该值,可能是 nil;
 */
@property (nullable, copy) NSString *taskDescription;

/** 任务失败的错误信息
 * 如果有错误,由代理方法 -URLSession:task:didCompleteWithError 返回
 * 如果没有错误,该值为 nil
*/
@property (nullable, readonly, copy) NSError *error;
1.2、任务进展
/** 任务进度 */
@property (readonly, strong) NSProgress *progress API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/** 响应数据的预期长度,通常在 HTTP 响应头的 Content-Length 字段;
 * 如果响应头没有该字段,则此值为 NSURLSessionTransferSizeUnknown
 */
@property (readonly) int64_t countOfBytesExpectedToReceive;

/** 从服务器已接收到的body字节数
 * 没有body预期,字节数可能是零;
 * 如果不知道字节数,值为 NSURLSessionTransferSizeUnknown;
 * @note 如果需要监测该值的变化,需要实现某个代理方法:
 *  <li> 用于上传任务,实现 -URLSession:dataTask:didReceiveData: 方法
 *  <li> 用于下载任务,实现 -URLSession:downloadTask:didWriteData:totalBytesWritten:totalbytestowrite: 方法
 */
@property (readonly) int64_t countOfBytesReceived;

/** 请求体 requestBody 的预计长度,在 HTTP 请求头的 Content-Length 字段设置
 * 可以通过三种方式预估请求体的长度:
 *  <1>数据对象 upload body 的长度
 *  <2>上传任务(而不是下载任务) :从磁盘上获取上传文件主体的长度
 *  <3>从 NSURLRequest 对象中获取 Content-Length
 *
 * @note 如果提供 stream 或 body data,则值为NSURLSessionTransferSizeUnknown;
 *      如果不提供 stream 或 body data,则值为0
 */
@property (readonly) int64_t countOfBytesExpectedToSend;

/** 已发送到服务器的 body 的字节数
 * 监听该值的改变,需要实现代理方法:
 *    -URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
 * @note 只统计请求体的字节数,不包括请求头
 */
@property (readonly) int64_t countOfBytesSent;
1.3、调度任务
/** 客户端通过该任务预计将发送或者接收的最大字节数
 * 设置该值应考虑 HTTP headers 和body 或者 body stream 的大小;
 * 如果未设定该值,系统使用默认值 NSURLSessionTransferSizeUnknown;
 * 系统使用该值来优化 UNSURLSessionTask 的调度,因此建议提供一个近似值,而不是接受默认值。
*/
@property int64_t countOfBytesClientExpectsToSend API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));
@property int64_t countOfBytesClientExpectsToReceive API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/** 设置网络加载开始的最早时间
 * 该值并不能保证网络加载将在指定时间开始,仅保证加载不会提前开始;如果未设置,则不使用启动延迟。
 * 该值仅适用于从后台NSURLSession创建的任务;其它类型会话创建的任务无效;
 */
@property (nullable, copy) NSDate *earliestBeginDate API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));
1.4、控制任务状态
/** 取消任务:正在执行的任务、已经挂起的任务;
 *  该方法立即返回,将任务标记为被取消。
 *  当任务被标记为被取消时,代理方法 -URLSession:task:didCompleteWithError: 收到一个 NSURLErrorCancelled 的错误
*/
- (void)cancel;

/** 临时挂起任务:会阻止 NSURLSession 继续加载数据
 * 任务在被挂起时不会产生网络流量,与任务关联的超时计时器将被禁用 -> 不受超时限制;
 * 下载任务可以在稍后继续传输数据,但其它任务必须重新开始  -resume ;
 * 可能仍有此任务的代理方法被调用,例如在挂起时收到的数据,但在 -resume 发送之前,不会再使用代理方法进行数据传输;
*/
- (void)suspend;

/** 恢复被挂起的任务
 * 刚初始化的任务处于挂起状态,需要调用此方法来启动任务
*/
- (void)resume;

/** 任务的当前状态(活动的、挂起的、正在取消、完成)
 * <ul> NSURLSessionTaskState 的枚举值
 *     <li> NSURLSessionTaskStateRunning 任务正在进行,此时任务受限于 Configuration 中指定的请求和资源超时时间
 *     <li> NSURLSessionTaskStateSuspended 任务暂停,在任务恢复之前,不会进行进一步的处理;此时任务不受超时限制;
 *     <li> NSURLSessionTaskStateCanceling 任务取消;会话将收到代理方法 -URLSession:task:didCompleteWithError:
 *     <li>  NSURLSessionTaskStateCompleted 任务已经完成(没有被取消),会话将不再收到代理方法;
 *           如果成功完成,则属性error为nil;否则提供错误信息。
 *  </ul>
 */
@property (readonly) NSURLSessionTaskState state;

/** 主机处理该任务的相对优先级,指定为0.0(最低优先级)和1.0(最高优先级)之间的浮点值
 * @discussion 优先级只提供一个提示,而不是任务执行的硬性要求,并不保证性能;
 *      任务的优先级可以在任何时候改变,但不是所有的网络协议都支持这个;在这些情况下,将使用最后一个生效的优先级。
 * <ul> 系统提供的优先级
 *     <li> NSURLSessionTaskPriorityDefault  默认优先级,值为0.5,用于任何未指定优先级的任务;
 *     <li> NSURLSessionTaskPriorityLow 低优先级,值高于最小值0,低于默认值
 *     <li> NSURLSessionTaskPriorityHigh 高优先级,其值高于默认值,低于最大值 1.0
 *  </ul>
 * 除了系统提供的这个三优先级,还可以使用 0 ~ 1.0 之间的任何浮点值
*/
@property float priority API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

2、数据任务NSURLSessionDataTask

NSURLSessionDataTask 数据任务:直接将下载的数据作为一个或多个NSData返回到应用程序的内存中;是NSURLSessionTask的一个子类,在 NSURLSessionTask 基础上不提供任何额外的功能,仅仅是词义的区别。当使用数据任务:

3、上传任务NSURLSessionUploadTask

NSURLSessionUploadTask 上传任务:用于需要 request body 的 HTTP 请求,如 POST 或 PUT ;它是数据任务 NSURLSessionDataTask 的一个子类,在 NSURLSessionDataTask 基础上不提供任何额外的功能,与数据任务对比:

创建上传任务时,需要提供一个URLRequest实例,该实例包含请求头的某些字段,如 content-Typecontent-Disposition 等。

4、下载任务NSURLSessionDownloadTask

NSURLSessionDownloadTask 下载任务:将远程文件下载到本地;是NSURLSessionTask的一个子类,在 NSURLSessionTask 基础上提供新的功能;

使用下载任务时,delegate 会收到几个对下载场景唯一的回调

@interface NSURLSessionDownloadTask : NSURLSessionTask

/** 可以恢复下载的取消任务方法
 * @param completionHandler 任务取消后的回调
 *        如果允许取消的任务恢复下载,在该回调中需要提供一个 resumeData;在恢复下载时,系统将该 resumeData 传递给 NSURLSession 的 -downloadTaskWithResumeData: 或 -downloadTaskWithResumeData:completionHandler: 方法来创建一个新任务,继续它停止的地方的下载。
 *       需要指定一个分派队列,以便在其中执行Handler
 *
 * 什么情况下才能继续下载该任务呢?
 *  <li> 自第一次 request,资源一直没有更改
 *  <li> 任务是 HTTP 或 HTTPS GET 请求
 *  <li> 服务器在响应中提供 ETag 或 Last-Modified 头(或两者都提供)
 *  <li> 服务器支持 byte-range 请求
 *  <li> 系统没有因为磁盘空间而删除临时文件
 * 只有满足上述条件,才能继续下载该任务
*/
- (void)cancelByProducingResumeData:(void (^)(NSData * _Nullable resumeData))completionHandler;

@end

5、StreamTask

从 iOS9 开始支持 NSURLSessionStreamTask 任务,这允许TCP/IP连接到指定的主机和可选的安全握手和代理导航的端口;使用 NSURLSession 显示创建该任务!
NSURLSessionStreamTaskNSURLSessionTask 的一个子类,在 NSURLSessionTask 基础上增加了额外的功能。
NSURLSessionStreamTask 在串行队列中执行异步读写操作,读写操作完后或者失败时在delegateQueue 上调用 completionHandler。 调用 -captureStreams 方法创建 NSInputStreamNSOutputStream 实例;所有未完成的读写在 streams 创建之前完成。一旦 streams 被传递到 NSURLSessionStreamDelegate 的代理方法,任务就被认为完成,不再接收消息;这些 streamsNSURLSession 分离。

5.1、读写数据
/** 异步地从 stream 中读取字节,并在完成时调用 completionHandler
 * @param minBytes 要读取的最小字节数
 * @param maxBytes 要读取的最大字节数
 * @param timeout 读取字节的超时时间:如果在指定的时间间隔内未完成读取,则取消读取,并调用 completionHandler 返回错误;
 *                可以设置为 0 防止读取超时;
 * @param completionHandler 读取完成或失败时在 delegateQueue 上调用
 *              param data 从 stream 读取的数据
 *              param atEOF 是否到达文件结束(EOF),以便不能读取更多数据
 *              param error 失败的原因;如果读取成功则为nil
 * @note 如果发生错误,任何未完成的读取也将失败,新的读取请求将立即出错。
*/
- (void)readDataOfMinLength:(NSUInteger)minBytes maxLength:(NSUInteger)maxBytes timeout:(NSTimeInterval)timeout completionHandler:(void (^) (NSData * _Nullable data, BOOL atEOF, NSError * _Nullable error))completionHandler;

/** 异步地指定的数据底层 Socket,并在完成时调用 completionHandler
 * @param data 待写入的数据
 * @param timeout 写入字节的超时时间:如果在指定的时间间隔内未完成写入,则取消写入,并调用 completionHandler 返回错误;
 *                可以设置为 0 防止写入超时;
 * @param completionHandler 写入完成或失败时在 delegateQueue 上调用
 *
 * @note completionHandler 的调用并不保证已经接收到所有字节,只能保证所有的数据都已写入到内核
*/
- (void)writeData:(NSData *)data timeout:(NSTimeInterval)timeout completionHandler:(void (^) (NSError * _Nullable error))completionHandler;
5.2、捕捉 StreamData
/** 完成在串行队列中等待的读写操作,然后调用 -URLSession:streamTask:didBecomeInputStream:outputStream: 代理方法。
 * 在该代理方法中,将认为任务已完成,不再接收任何代理消息。
*/
- (void)captureStreams;
5.3、关闭读Socket、写Socket
/** 完成队列中的读写操作,然后关闭底层 Socket 的写端
 * 在调用此方法后,可以继续读取数据 -readDataOfMinLength:maxLength:timeout:completionHandler: 直到流到达文件末尾(EOF)为止
 * 但不能调用 -writeData:timeout:completionHandler: 否则发生错误。
*/
- (void)closeWrite;

/** 完成队列中的读写操作,然后关闭底层 Socket 的读端
 * 调用此方法后,可以继续写入数据 -writeData:timeout:completionHandler:
 * 但不能调用 -readDataOfMinLength:maxLength:timeout:completionHandler: 否则发生错误。
*/
- (void)closeRead;
5.4、启动和停止安全连接
/** 完成队列中的的读写操作,然后建立安全连接,加密 handshake
 * TLS认证回调通过 -URLSession:task:didReceiveChallenge:completionHandler:
 */
- (void)startSecureConnection;

/** 队列中的读写操作完成后,干净地关闭安全连接。
 * @warning 该方法已被废弃
 */
- (void)stopSecureConnection API_DEPRECATED("TLS cannot be disabled once it is enabled", macos(10.9,10.15), ios(7.0,13.0), watchos(2.0,6.0), tvos(9.0,13.0));

6、WebSocket任务

NSURLSessionWebSocketTaskNSURLSessionTask的子类,以 WebSocket 框架的形式在TCP和TLS上提供了面向消息的传输协议,它遵循RFC 6455中定义的WebSocket协议。

一个 WebSocket 任务,客户端也可以提供一个协议列表,公布在WebSocket握手阶段;旦握手成功完成,通过一个可选的 delegate 被通知。在握手完成之前,客户端可以被调用来处理 HTTP 重定向或身份认证。
WebSocket任务也支持 Cookie,将cookie存储到 NSURLSession 的cookie中,并将cookie附加到传出的 HTTP handshake 请求中。

6.1、封装的消息体

封装的一个 WebSocket 消息,该对象将被传递给 -send 调用,并将从 -receive 调用中传递

/** 创建的消息类型:Data 或 String
*/
typedef NS_ENUM(NSInteger, NSURLSessionWebSocketMessageType) {
    NSURLSessionWebSocketMessageTypeData = 0,
    NSURLSessionWebSocketMessageTypeString = 1,
} API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0))
@interface NSURLSessionWebSocketMessage : NSObject

/** 使用 data 创建消息
 * 此时属性 string = nil
*/
- (instancetype)initWithData:(NSData *)data NS_DESIGNATED_INITIALIZER;

/** 使用 string 创建消息
 * 此时属性 data = nil
*/
- (instancetype)initWithString:(NSString *)string NS_DESIGNATED_INITIALIZER;

@property (readonly) NSURLSessionWebSocketMessageType type;
@property (nullable, readonly, copy) NSData *data;
@property (nullable, readonly, copy) NSString *string;


- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

@end
6.2、发送和接收数据

异步执行读写操作,并允许发送和接收包含二进制帧和UTF-8编码文本帧的消息;该任务会将握手完成前队列中的读写操作将在握手成功后按顺序执行;

/** 发送一个WebSocket消息
 * @param message 待发送的WebSocket消息
 * @param completionHandler 接收发送结果,发送失败返回 error ;发送成功则error=nil
 *        如果出现错误,任何未完成的工作也将失败;
 * @note completionHandler的调用并不保证远程端已经接收到所有字节,只是它们已经被写入到内核。
*/
- (void)sendMessage:(NSURLSessionWebSocketMessage *)message completionHandler:(void (^)(NSError * _Nullable error))completionHandler;

/** 一旦消息的所有frames可用,读取一个WebSocket消息
 * @param completionHandler 读取结果,成功获取message,失败返回 error;
 * @note 在缓冲frames时,如果达到了 maximumMessage ,则receiveMessage调用将出错,所有未完成的工作也将失败,导致任务结束。
*/
- (void)receiveMessageWithCompletionHandler:(void (^)(NSURLSessionWebSocketMessage* _Nullable message, NSError * _Nullable error))completionHandler;

/** 在接收消息失败之前缓冲的最大字节数
 * 该值包括连续帧中所有字节的总和,一旦任务达到这个限制,receive 就会失败。
*/
@property NSInteger maximumMessageSize;

/** 从客户端发送一个ping帧,带有一个从服务器端点接收pong的闭包。
 * @param pongReceiveHandler 当客户端收到来自服务器端点的pong时,该Block被调用;
 *          如果在接收服务器端点pong之前连接丢失或发生错误,该Block被调用返回错误信息
 *      当发送多个ping时 ,pongReceiveHandler 将总是按照客户端发送 ping 的顺序被调用;
 *
 * @discussion Ping-pong  是一种数据传输技术,本质是一种数据缓冲的手段;同时利用客户端、服务器两个数据缓冲区达到数据连续传输的目的:
 *   客户端不必等待服务器的处理结果,继续执行并将结果保存在ping路的缓存中;客户端执行到一定时刻,服务端处理完成将结果 保存在pong路中;
 *  这样服务端模块无需等待继续执行,客户端也无需等待继续执行,转而将结果存储在ping路;提高了处理效率。
 *
 * 单个缓冲区得到的数据在传输和处理中很容易被覆盖,而 Ping-pong  缓冲区的方式能够总是保持一个缓冲区的数据被利用,另一个缓冲去用于存储数据;即两个相同的对象作为缓冲区交替地被读和 被写。
*/
- (void)sendPingWithPongReceiveHandler:(void (^)(NSError* _Nullable error))pongReceiveHandler;
6.3、关闭连接
typedef NS_ENUM(NSInteger, NSURLSessionWebSocketCloseCode)
{
    NSURLSessionWebSocketCloseCodeInvalid =                             0,
    NSURLSessionWebSocketCloseCodeNormalClosure =                    1000,
    NSURLSessionWebSocketCloseCodeGoingAway =                        1001,
    NSURLSessionWebSocketCloseCodeProtocolError =                    1002,
    NSURLSessionWebSocketCloseCodeUnsupportedData =                  1003,
    NSURLSessionWebSocketCloseCodeNoStatusReceived =                 1005,
    NSURLSessionWebSocketCloseCodeAbnormalClosure =                  1006,
    NSURLSessionWebSocketCloseCodeInvalidFramePayloadData =          1007,
    NSURLSessionWebSocketCloseCodePolicyViolation =                  1008,
    NSURLSessionWebSocketCloseCodeMessageTooBig =                    1009,
    NSURLSessionWebSocketCloseCodeMandatoryExtensionMissing =        1010,
    NSURLSessionWebSocketCloseCodeInternalServerError =              1011,
    NSURLSessionWebSocketCloseCodeTLSHandshakeFailure =              1015,
} API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/** 使用指定的 closeCode 发送一个关闭帧,同时可以提供关闭的原因
 * @param closeCode 关闭连接的原因
 * @param reason 进一步解释为何关闭,由客户端自定义,可传 nil;
 * @note 如果调用 [NSURLSessionTask cancel] 而非该方法,它会发送一个没有closeCode或reason的关闭帧;
*/
- (void)cancelWithCloseCode:(NSURLSessionWebSocketCloseCode)closeCode reason:(NSData * _Nullable)reason;

/** 连接关闭原因,可以在任何时候查看
 * 当连接尚未关闭时,此值为 NSURLSessionWebSocketCloseCodeInvalid
 *
 *  <ul> NSURLSessionWebSocketCloseCode : 说明 WebSocket 连接关闭的原因
 *     <li> NSURLSessionWebSocketCloseCodeInvalid   表示连接仍处于打开状态
 *     <li> NSURLSessionWebSocketCloseCodeNormalClosure 表示连接正常关闭
 *     <li> NSURLSessionWebSocketCloseCodeGoingAway 表示服务器宕机或浏览器从页面导航离开,导致端点即将消失
 *     <li> NSURLSessionWebSocketCloseCodeProtocolError 表示端点由于协议错误而终止连接
 *     <li> NSURLSessionWebSocketCloseCodeUnsupportedData 表示端点接收到不能处理的数据类型后终止连接:如只接收文本的端点接收到二进制消息
 *     <li> NSURLSessionWebSocketCloseCodeNoStatusReceived 表示端点需要状态码而没有接收到状态码
 *     <li> NSURLSessionWebSocketCloseCodeAbnormalClosure  表示连接在没有关闭帧的情况下关闭
 *     <li> NSURLSessionWebSocketCloseCodeInvalidFramePayloadData 表示服务器接收到与消息类型不一致的数据而终止连接
 *     <li> NSURLSessionWebSocketCloseCodePolicyViolation 表示端点终止连接,因为它收到了违反其策略的消息
 *     <li> NSURLSessionWebSocketCloseCodeMessageTooBig 表示端点因接收到无法处理的大消息而终止连接
 *     <li> NSURLSessionWebSocketCloseCodeMandatoryExtensionMissing 表示客户端终止连接,因为服务器没有协商所需的扩展;
 *                                      RFC 6455指示客户端应该提供一个包含所需扩展的列表
 *     <li> NSURLSessionWebSocketCloseCodeInternalServerError 表示服务器因意外情况而终止连接
 *     <li> NSURLSessionWebSocketCloseCodeTLSHandshakeFailure 表示由于未能执行TLS握手而关闭连接
 *  </ul>
 */
@property (readonly) NSURLSessionWebSocketCloseCode closeCode;

/** 连接关闭的详细原因,可以在任何时候查看
 * nil 表示没有closeReason,也或许 SocketTask 仍在运行
 */
@property (nullable, readonly, copy) NSData *closeReason;
上一篇下一篇

猜你喜欢

热点阅读