HTTP简介
HTTP根底结构
HTTP恳求体
HTTP 加载恳求
HTTP 模拟测试
HTTP 链式加载器
HTTP 动态修改恳求
HTTP 恳求选项
HTTP 重置
HTTP 撤销
HTTP 限流
HTTP 重试
HTTP 根底鉴权
HTTP 主动鉴权设置
HTTP 主动鉴权
HTTP 复合加载器
HTTP 头脑风暴
HTTP 总结
撤销正在进行的恳求是任何网络库的重要功能,也是咱们期望在此结构中支撑的功能。
配置 Setup
为了支撑撤销,咱们需求对迄今为止构建的 API
进行最终一次严重更改,如下所示:
open class HTTPLoader {
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
func reset(with group: DispatchGroup)
}
咱们看到的局限性是,一旦咱们开端加载恳求,咱们就无法引证该恳求的“履行”; 回想一下 HTTPRequest
是一种值类型,因而它或许被仿制和仿制无数次。
因而,咱们需求引入一些状态来跟踪加载和完结 HTTPRequest
的使命。 从 URLSession
中得到启发,我将其称为 HTTPTask
:
public class HTTPTask {
public var id: UUID { request.id }
private var request: HTTPRequest
private let completion: (HTTPResult) -> Void
public init(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
self.request = request
self.completion = completion
}
public func cancel() {
// TODO
}
public func complete(with result: HTTPResult) {
completion(result)
}
}
果然如此,咱们需求更改 HTTPLoader
才干使用它:
open class HTTPLoader {
...
open func load(task: HTTPTask) {
if let next = nextLoader {
next.load(task: task)
} else {
// a convenience method to construct an HTTPError
// and then call .complete with the error in an HTTPResult
task.fail(.cannotConnect)
}
}
...
}
结构一个使命关于客户来说或许有点冗长,所以为了便利起见,咱们将保存原始办法:
extension HTTPLoader {
...
public func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) -> HTTPTask {
let task = HTTPTask(request: request, completion: completion)
load(task: task)
return task
}
...
}
这是根本的根底设施。 现在让咱们谈谈撤销。
忌讳
撤销是一个极其杂乱的论题。 从表面上看,它似乎很简略,但即使是快速浏览下面也会很快变得紊乱。 首要,撤销实际上意味着什么? 假如我有某种恳求而且我“撤销”了它,预期的行为是什么?
假如我在将恳求传递给加载程序之前撤销恳求,完结处理程序是否应该触发? 为什么或许为什么不? 假如我将撤销的恳求传递给加载程序,加载程序是否应该测验加载它? 为什么或许为什么不?
假如我在开端加载恳求之后但在它抵达终端加载程序之前撤销恳求,当时加载程序是否应该辨认它? 是否应将已撤销的恳求进一步传递到链下? 假如不是,谁负责调用完结处理程序,假如它不是最终一个加载程序?
假如我在恳求抵达终端加载程序后撤销恳求,它是否应该中止传出网络连接? 假如我现已开端收到回复怎么办? 假如我现已收到呼应但还没有开端履行完结处理程序怎么办?
假如我在完结处理程序履行后撤销恳求,会产生什么事吗? 为什么或许为什么不?
我如安在依然允许线程安全的状况下完结一切这些作业?
这些都是杂乱的问题,答案更杂乱,我绝不宣称具有一切答案,我甚至不宣称具有好的代码来测验和完结这些答案。 实施正确的撤销计划是出了名的困难; 问询任何企图完结自己的 NSOperation 子类的开发人员。
当我在咱们的网络库中解说有关撤销的概念时,请理解代码和概念是不完整的。 我在榜首篇文章中正告过你。 因而,代码中会有许多 // TODO:
注释。
对撤销做出反应
所以咱们现在在咱们的 HTTPTask
上有这个 cancel()
办法,但是咱们需求一种办法让各种加载器对它的调用做出反应。 根本上,咱们需求一个闭包列表来在使命被撤销时运转。 为此,让咱们向使命增加一个“撤销回调”数组:
public class HTTPTask {
...
private var cancellationHandlers = Array<() -> Void>()
public func addCancellationHandler(_ handler: @escaping () -> Void>) {
// TODO: make this thread-safe
// TODO: what if this was already cancelled?
// TODO: what if this is already finished but was not cancelled before finishing?
cancellationHandlers.append(handler)
}
public func cancel() {
// TODO: toggle some state to indicate that "isCancelled == true"
// TODO: make this thread-safe
let handlers = cancellationHandlers
cancellationHandlers = []
// invoke each handler in reverse order
handlers.reversed().forEach { $0() }
}
}
在咱们用于与 URLSession
交互的加载器中,假如在 HTTPTask
上调用 cancel()
,咱们现在能够撤销咱们的 URLSessionDataTask
:
public class URLSessionLoader: HTTPLoader {
...
open func load(task: HTTPTask) {
... // constructing the URLRequest from the HTTPRequest
let dataTask = self.session.dataTask(with: urlRequest) { ... }
// if the HTTPTask is cancelled, also cancel the dataTask
task.addCancellationHandler { dataTask.cancel() }
dataTask.resume()
}
}
这为咱们供给了撤销的根底知识。 假如咱们在使命抵达终端加载程序后撤销,它将撤销底层的 URLSessionDataTask
并允许 URLSession
呼应机制指示后续行为:咱们将经过 .cancelled 代码返回 URLError。
依照现在的状况,假如咱们在恳求抵达终端加载程序之前撤销恳求,则什么也不会产生。 假如咱们在完结加载后撤销恳求,相同什么也不会产生。
“正确”的行为是您的需求与合理实施相结合的杂乱相互作用。 “100%”正确的解决计划将需求一些十分细心的作业,触及同步原语(例如 NSRecursiveLock
)和十分细心的状态管理。
显而易见,没有任何正确撤销的解决计划是正确的,除非它还伴随着很多的单元测试。 恭喜! 你现已从地图上掉下来了。
主动撤销加载器
咱们会在这一点上挥手,并假设咱们的撤销逻辑“足够好”。 老实说,一个简略的解决计划关于大多数状况来说或许现已“足够好”,所以即使是这个简略的“撤销处理程序”数组也能用一段时间。 因而,让咱们继续前进,构建一个基于撤销的加载器。
咱们之前现已确认咱们需求能够“重置”加载程序链以供给“从头开端”的语义。 “重新开端”的一部分是撤销咱们一切的飞行恳求; 咱们不能“重新开端”而且依然保存咱们之前仓库的残余。
因而,咱们构建的加载器会将“撤销”与“重置”的概念联系起来:当加载器收到对 reset()
的调用时,它会当即cancel()
任何正在进行的恳求,而且只允许重置完结一次 其中的恳求现已完结。
这意味着咱们需求跟踪经过咱们的任何恳求,并在它们完结时忘记它们:
public class Autocancel: HTTPLoader {
private let queue = DispatchQueue(label: "AutocancelLoader")
private var currentTasks = [UUID: HTTPTask]()
public override func load(task: HTTPTask) {
queue.sync {
let id = task.id
currentTasks[id] = task
task.addCompletionHandler { _ in
self.queue.sync {
self.currentTasks[id] = nil
}
}
}
super.load(task: task)
}
}
当使命到来时,咱们会将其增加到已知使命的字典中; 咱们将根据使命的标识符查找它。 然后当使命完结时,咱们将从咱们的字典中删去它。 经过这种方式,咱们将始终对正在进行但尚未完结的使命进行最新映射。
咱们的加载器还需求对 reset() 办法做出反应:
public class Autocancel: HTTPLoader {
...
public override func reset(with group: DispatchGroup) {
group.enter() // indicate that we have work to do
queue.async {
// get the list of current tasks
let copy = self.tasks
self.tasks = [:]
DispatchQueue.global(qos: .userInitiated).async {
for task in copy.values {
// cancel the task
group.enter()
task.addCompletionHandler { _ in group.leave() }
task.cancel()
}
group.leave()
}
}
nextLoader?.reset(with: group)
}
}
这个逻辑有点微妙,所以我解说一下:
当 reset() 调用进入时,咱们当即进入 DispatchGroup 以指示咱们有一些作业要履行。 然后咱们将获取当时使命列表(即字典中的任何内容)。
关于每个使命,咱们再次进入 DispatchGroup 以将该特定使命的生命周期与整个重置恳求联系起来。 当使命“完结”时,该使命将离开组。 然后咱们指示使命撤销()。
在咱们完结指示每个使命撤销后,咱们让 DispatchGroup 正确地平衡咱们开始的 enter() 调用。
此完结是使用 DispatchGroup 作为重置协调机制的优势的首要示例。 咱们无法在编译时知道哪个使命将首要完结,或许是否有任何使命要撤销。 假如咱们使用单个完结处理程序作为发出“完结重置”信号的方式,咱们将很难正确完结此办法。 由于咱们使用的是 DispatchGroup,因而咱们所要做的就是根据需求多次履行 enter() 和 leave() 。
这两种办法意味着当这个加载器包含在咱们的链中时,咱们将主动撤销一切飞行中的恳求作为全体“重置”指令的一部分,而且直到一切飞行中的恳求完结后重置才会完结。 整洁的!
// A networking chain that:
// - prevents you from resetting while a reset command is in progress
// - automatically cancels in-flight requests when asked to reset
// - updates requests with missing server information with default or per-request server environment information
// - executes all of this on a URLSession
let chain = resetGuard --> autocancel --> applyEnvironment --> ... --> urlSessionLoader
在下一篇文章中,咱们将研究怎么主动约束传出恳求,这样咱们就不会不小心对咱们的服务器进行 DDOS。