HTTP简介

HTTP根底结构

HTTP恳求体

HTTP 加载恳求

HTTP 模仿测验

HTTP 链式加载器

HTTP 动态修改恳求

HTTP 恳求选项

HTTP 重置

HTTP 撤销

HTTP 限流

HTTP 重试

HTTP 根底鉴权

HTTP 主动鉴权设置

HTTP 主动鉴权

HTTP 复合加载器

HTTP 脑筋风暴

HTTP 总结

我曾经开发过一个应用程序,该应用程序运用Timer定时 ping 带有状态更新的服务器。 在应用程序的一个构建中,咱们留意到状态服务器开端遇到 CPU 峰值,最终导致它无法处理更多恳求。

通过查询,咱们发现对应用程序逻辑的一些简略更改导致咱们不小心使服务器的恳求过多。 计时器 A 将被设置为依据特定条件发送状态更新。 计时器将启动,更新将被发送。 引进的错误是计时器没有正确失效并且会坚持运转。 当满意另一个条件时,咱们将创立一个新的计时器 (B) 来发送状态更新。 除了现在咱们一起运转 AB,所以他们都会测验发送恳求。 然后,当恳求开端超时时,完结处理程序将设置另一个计时器重试,这对两个计时器都会发生,导致 CD。一个计时器变成两个,变成四个,变成八个,然后是 16,然后 32…咱们的服务器以指数级增长的恳求数量猛增。

当即修正是更新服务器以当即回绝所有传入恳求。 然后咱们为应用程序发布了一个紧急错误修正程序,以修正计时器失效行为。

但是……首先让应用程序阻挠这种状况发生不是很好吗? 咱们能够做到这一点,而且会十分简略。

受限恳求

让咱们看看咱们的 HTTPLoader 接口来提示咱们自己的 API 契约:

open class HTTPLoader {
    /// Load an HTTPTask that will eventually call its completion handler
    func load(task: HTTPTask)
    /// Reset the loader to its initial configuration
    func reset(with group: DispatchGroup)
}

请记住,load(task:) 方法不承诺何时履行使命。 加载程序或许会收到一个 HTTPTask 并当即开端履行它,或者它或许会等候几秒钟、几分钟、几小时或几年。 关于 API 的客户端,没有关于履行时间的承诺。

节省加载程序能够利用这一点。 当它收到 HTTPTask 时,它能够查看是否答应继续加载它。 假如是,它能够愉快地将使命传递给链中的下一个加载器。 假如不答应加载它,它能够将它放在使命列表中以便稍后履行。

咱们的整体界面看起来像这样:

public class Throttle: HTTPLoader {
    public var maximumNumberOfRequests = UInt.max
    private var executingRequests = [UUID: HTTPTask]()
    private var pendingRequests = [HTTPTask]()
    public override func load(task: HTTPTask) {
        if UInt(executingRequests.count) < maximumNumberOfRequests {
            startTask(task)
        } else {
            pendingRequests.append(task)
        }
    }
    private func startTask(_ task: HTTPTask) {
        let id = task.id
        executingRequests[id] = task
        task.addCompletionHandler {
            self.executingRequests[id] = nil
            self.startNextTasksIfAble()
        }
        super.load(task: task)
    }
    private func startNextTasksIfAble() {
        while UInt(executingRequests.count) < maximumNumberOfRequests && pendingRequests.count > 0 {
            // we have capacity for another request, and more requests to start
            let next = pendingRequests.removeFirst()
            startTask(next)
        }
    }
}

这个(不完整的)完成为咱们提供了节省怎么工作的基本概念。 当咱们收到恳求时,咱们会查看当时有多少使命在履行。 假如它小于咱们答应的最大值,那么咱们能够开端履行这个恳求。 假如咱们到达(或超出)咱们的约束,咱们会将使命放入一个“待定”恳求数组中,以表明它需求等候加载。

加载使命会将其增加到当时正在履行的使命列表中,很像咱们前次创立的 Autocancel 加载程序。 当它完结时,它会从列表中删去。 此外,当恳求完结时,加载程序会查看是否有使命等候加载,以及是否答应履行它们。 假如是,则它将它们从数组中拉出并开端履行它们。

这个简略的完成有几个缺点:

  • 它不是线程安全的。 恳求能够从任何线程加载,咱们正在修改加载器内部的许多状态,但没有保证咱们对它有独占访问权。 咱们需求一种同步类型(例如 NSLock 或 DispatchQueue)来保证咱们正确地更新状态。

  • 咱们无法对被撤销的使命做出反响。 假如一个使命在它仍然挂起时被撤销,那么咱们或许应该把它从数组中拉出来并调用它的完结处理程序。 幸运的是,增加这个十分简略:

 let id = task.id
 task.addCancelHandler {
     if let index = self.pendingRequests.firstIndex(where: { $0.id === id }) {
         let thisTask = self.pendingRequests.remove(at: index)
         let error = HTTPError(.cancelled, ...)
         thisTask.complete(.failure(error))
     }
 }
 pendingRequests.append(task)
  • 咱们缺少咱们的 reset() 逻辑。 从概念上讲,这将类似于咱们前次的 Autocancel 加载器:当咱们重置时,咱们将每个待处理使命和正在履行的使命加入 DispatchGroup。 当每个人完结后,他们别离离开小组。 咱们能够在每个使命上调用 cancel(),但理想状况下,咱们在链中有一个 Autocancel 加载器,它已经为咱们做了这件事。

不受约束的恳求

虽然咱们有能力约束恳求是件好事,但或许存在咱们永远不想约束的恳求。 这是增加另一个恳求选项的好机会:

public enum ThrottleOption: HTTPRequestOption {
    public static var defaultOptionValue: ThrottleOption { .always }
    case always
    case never
}
extension HTTPRequest {    
    public var throttle: ThrottleOption {
        get { self[option: ThrottleOption.self] }
        set { self[option: ThrottleOption.self] = newValue }
    }
}

回想一下,恳求选项答应咱们增加每个恳求的行为。 因而,默许状况下,恳求始终受到约束,但咱们能够指示单个恳求从不受到约束。 剩下的就是在咱们的加载器中寻觅它:

    public override func load(task: HTTPTask) {
        if task.request.throttle == .never {
            super.load(task: task)
            return
        }
        ...
    }

有了这个,咱们现在有了一个加载程序,咱们能够将其插入咱们的链中以约束一起传出的网络恳求的数量。 另请留意,咱们已将 maximumNumberOfRequests 声明为公共变量,这意味着咱们能够动态更新此值。 例如,咱们的应用程序或许会下载一些配置设置以指示答应加载恳求的速度。

// A networking chain that:
// - prevents you from resetting while a reset command is in progress
// - automatically cancels in-flight requests when asked to reset
// - limits the number of simultaneous requests to a maximum number
// - 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 --> throttle --> applyEnvironment --> ... --> urlSessionLoader

假如咱们有这样的东西,咱们能够远程“关闭”咱们的行为不端的应用程序,而不必力争上游地发布应用程序更新,并且咱们能够坚持咱们的服务器正常运转。

鄙人一篇文章中,咱们将研究怎么在恳求失败时主动重试恳求。