重试 URLSession dataTask 的模式?

Posted

技术标签:

【中文标题】重试 URLSession dataTask 的模式?【英文标题】:Pattern for retrying URLSession dataTask? 【发布时间】:2017-10-19 15:05:00 【问题描述】:

我是 ios/Swift 开发的新手,我正在开发一个向 REST API 发出多个请求的应用程序。以下是其中一个检索“消息”的调用示例:

func getMessages() 

    let endpoint = "/api/outgoingMessages"

    let parameters: [String: Any] = [
        "limit" : 100,
        "sortOrder" : "ASC"
    ]

    guard let url = createURLWithComponents(endpoint: endpoint, parameters: parameters) else 
        print("Failed to create URL!")
        return
    

    do 
        var request = try URLRequest(url: url, method: .get)

        let task = URLSession.shared.dataTask(with: request as URLRequest)  (data, response, error) in

            if let error = error 
                print("Request failed with error: \(error)")
                // TODO: retry failed request
             else if let data = data, let response = response as? HTTPURLResponse                 
                if response.statusCode == 200 
                    // process data here
                 else 
                    // TODO: retry failed request
                
            
        

        task.resume()

     catch 
        print("Failed to construct URL: \(error)")
    

当然,此请求失败的原因有很多(服务器无法访问、请求超时、服务器返回 200 以外的值等)。如果我的请求失败,我希望能够重试它,甚至可能在下一次尝试之前有延迟。我在 Apple 的文档中没有看到任何关于这种情况的指导,但我发现了一些关于 SO 的相关讨论。不幸的是,这两个都是几年前的,并且在我从未使用过的 Objective-C 中。在 Swift 中做这样的事情有什么常见的模式或实现吗?

【问题讨论】:

这取决于,这些请求是否与 UI 相关联? (即您是否需要来自服务器的数据来显示内容)。还是只是需要将数据从应用程序存储到服务器的后端逻辑 @TNguyen 简短的回答是两者兼而有之;一些请求仅用于填充数据库,而另一些则需要更新 UI 对于 UI 来说,有很多方法可以处理它,但我觉得一般标准是让用户知道他们没有互联网,并允许用户在何时刷新他们这样做(或者您可以使用可达性并不断收听以等待网络连接可用,然后提出您的请求,我确定还有许多其他方法)。至于后端,您是否需要尽可能地使数据库中的数据保持最新?或者如果它晚一点出现,这不是什么大不了的事。你可以看到这在重试逻辑中扮演着怎样的重要角色 @TNguyen 感谢您的建议。我现在实际上正在使用可达性来了解我是否有连接。我的问题中显示的示例请求是由服务器发送的推送通知触发的。用户可能没有查看应用程序中显示该数据的部分,但期望数据将几乎立即被提取并插入到数据库中。 我不再过多地处理远程通知,但是您不能通过远程通知推送您需要的数据而不是调用服务器吗?此外,如果您需要立即通过对服务器的请求插入数据库的部分,那么我可能会在后台使用一个计时器,就像您在 OP 中所说的那样一遍又一遍地重试它。但明显的问题是当用户离开或终止应用程序时,在这种情况下你很不走运,并且当他们再次打开应用程序时必须重新开始重试。 (也许像applicationDidBecomeActive 【参考方案1】:

这个问题是基于意见的,而且范围很广,但我敢打赌大多数都是相似的,所以就这样吧。

对于触发 UI 更改的数据更新:

(例如,填充数据的表格或加载图像)一般的经验法则是以非阻碍方式通知用户,如下所示:

然后有一个下拉刷新控件或刷新按钮。

对于不影响用户操作或行为的后台数据更新:

您可以根据代码轻松地将重试计数器添加到您的请求结果中 - 但我会小心处理这个计数器并构建一些更智能的逻辑。例如,给定以下状态代码,您可能希望以不同的方式处理事情:

5xx:您的服务器有问题。您可能希望将重试延迟 30 秒或一分钟,但如果它发生 3 或 4 次,您将希望停止重试后端。

401:经过身份验证的用户可能不再被授权调用您的 API。您根本不想重试;相反,您可能希望将用户注销,以便下次他们使用您的应用时,系统会提示他们重新进行身份验证。

网络超时/连接丢失:在重新建立连接之前重试无关紧要。您可以围绕可达性处理程序编写一些逻辑,以将后台请求排队,以便在下次网络连接可用时采取行动。

最后,正如我们在 cmets 中提到的,您可能希望了解通知驱动的后台应用程序刷新。在这里,您可以发送通知来告诉应用程序自行更新,而不是轮询您的服务器以获取更改,即使它没有在前台运行。如果您足够聪明,您可以让您的服务器重复通知您的应用,直到应用确认收到 - 这可以以一致的方式解决连接故障和无数其他服务器响应错误代码。

【讨论】:

您在这里提出的要点。但我会小心401 根本不重试。这可能会导致丢失重要数据。 同样,对于 500,您再次需要小心。仅仅因为它是 5xx 并且您重试 x 次并不意味着您应该停止重试。获得 5xx 并不总是意味着您的服务器有问题。它可能具有完美的逻辑,但有时您仍然必须返回 5xx(考虑当您尝试写入数据库并且数据库已关闭时)。还有很多其他原因导致您仍然希望始终尝试在 5xx 上重试 如果您从服务器收到 401,那么无论您重试多少次,您都可能永远无法取回数据,直到应用代表用户收到新令牌. 对于 5xx,这就是为什么我建议在更长的时间间隔后重试。例如,如果您的呼叫受到速率限制,则在 30-60 秒后尝试可能会解决它。 您已将应用设置为通过发送请求并包含通知 ID 来响应通知。当服务器收到具有相同通知 ID 的 API 调用时,您可以将该通知标记为已送达。这允许您将该重试逻辑写入您的服务器而不是客户端。【参考方案2】:

我将处理重试的三种方法分类:

    可达性重试
可达性是“网络连接发生变化时通知我”的一种奇特方式。苹果为此提供了一些 sn-ps,但它们看起来并不有趣——我的建议是使用 Ashley Mill 的 Reachability 替代品。 除了可达性之外,Apple 还提供了一个 waitsForConnectivity (iOS 11+) 属性,您可以在 URLSession 配置上设置该属性。通过设置它,当任务等待网络连接时,您会通过URLSessionDataDelegate 收到警报。您可以利用这个机会启用离线模式或向用户显示某些内容。
    手动重试
让用户决定何时重试请求。我想说这最常使用“拉动刷新”手势/UI 来实现。
    定时/自动重试
等待几秒钟,然后重试。 Apple 的 Combine 框架提供了一种重试失败的网络请求的便捷方式。见Processing URL Session Data Task Results with Combine 来自 Apple Docs:URL 会话的生命周期(已弃用)...但是,您的应用不应立即重试 [a request]。相反,它应该使用可达性 API 来确定服务器是否可达,并且只有在收到可达性已更改的通知时才应该发出新请求。

【讨论】:

好的cmets。为此,我建议添加有关何时应使用每种方法的信息。如果操作是用户发起的“获取数据”操作并且用户正在等待响应,您应该显示错误,并允许用户选择何时重试,而不是让屏幕意外刷新。如果一个动作是用户发起的“发布数据”动作,应用程序应该告诉用户它现在不能发布,但是当用户上线时它会自动发布。对于后台请求,请始终使用可达性。 我还要补充一点,可达性可能会欺骗你,所以即使之前的请求失败,也不要关闭任何关于可达性的初始请求。首先尝试,如果尝试失败,请使用可达性。在高延迟环境中,对于幂等请求,以具有指数退避的延迟并行方式尝试两个或三个请求也可能是有益的,并且仅在 [n] 次失败后使用可达性,因为丢包往往是突发性的。 在文档中没有更多关于重试生命周期的信息。 @Ramis 我更新了我的答案以表明该链接已被弃用并添加了一个新链接,该链接指的是组合框架中可用的重试

以上是关于重试 URLSession dataTask 的模式?的主要内容,如果未能解决你的问题,请参考以下文章

URLSession.shared.dataTask vs dataTaskPublisher,啥时候用哪个?

有没有办法使用 URLSession.shared.dataTask 并行请求多个不同的资源

在 URLSession.shared.dataTask 期间存储来自异步闭包的数据

Swift URLSession DataTask 在应用程序进入后台时失败

使用完成处理程序进行异步调用的多个 URLSession dataTask 导致内存上升

如何同步 URLSession 任务的串行队列?